diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9a888adbb25..3aeef82d62f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,5 @@ # web + desktop packages packages/app/ @adamdotdevin packages/tauri/ @adamdotdevin +packages/desktop/src-tauri/ @brendonovich packages/desktop/ @adamdotdevin diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 459ce25d05b..52eec90991f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - name: 💬 Discord Community url: https://discord.gg/opencode diff --git a/.github/TEAM_MEMBERS b/.github/TEAM_MEMBERS new file mode 100644 index 00000000000..22c9a923d33 --- /dev/null +++ b/.github/TEAM_MEMBERS @@ -0,0 +1,15 @@ +adamdotdevin +Brendonovich +fwang +Hona +iamdavidhill +jayair +jlongster +kitlangton +kommander +MrMushrooooom +nexxeln +R44VC0RP +rekram1-node +RhysSullivan +thdxr diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td new file mode 100644 index 00000000000..28535b57799 --- /dev/null +++ b/.github/VOUCHED.td @@ -0,0 +1,23 @@ +# Vouched contributors for this project. +# +# See https://github.com/mitchellh/vouch for details. +# +# Syntax: +# - One handle per line (without @), sorted alphabetically. +# - Optional platform prefix: platform:username (e.g., github:user). +# - Denounce with minus prefix: -username or -platform:username. +# - Optional details after a space following the handle. +adamdotdevin +-agusbasari29 AI PR slop +ariane-emory +edemaine +-florianleibert +fwang +iamdavidhill +jayair +kitlangton +kommander +r44vc0rp +rekram1-node +-spider-yamet clawdbot/llm psychosis, spam pinging the team +thdxr diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index cba04faccef..f53f20fcdb9 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -3,19 +3,42 @@ description: "Setup Bun with caching and install dependencies" runs: using: "composite" steps: + - name: Get baseline download URL + id: bun-url + shell: bash + run: | + if [ "$RUNNER_ARCH" = "X64" ]; then + V=$(node -p "require('./package.json').packageManager.split('@')[1]") + case "$RUNNER_OS" in + macOS) OS=darwin ;; + Linux) OS=linux ;; + Windows) OS=windows ;; + esac + echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT" + fi + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version-file: package.json + bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }} + bun-download-url: ${{ steps.bun-url.outputs.url }} + + - name: Get cache directory + id: cache + shell: bash + run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT" - - name: Cache ~/.bun - id: cache-bun + - name: Cache Bun dependencies uses: actions/cache@v4 with: - path: ~/.bun - key: ${{ runner.os }}-bun-${{ hashFiles('package.json') }}-${{ hashFiles('bun.lockb', 'bun.lock') }} + path: ${{ steps.cache.outputs.dir }} + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} restore-keys: | - ${{ runner.os }}-bun-${{ hashFiles('package.json') }}- + ${{ runner.os }}-bun- + + - name: Install setuptools for distutils compatibility + run: python3 -m pip install setuptools || pip install setuptools || true + shell: bash - name: Install dependencies run: bun install diff --git a/.github/actions/setup-git-committer/action.yml b/.github/actions/setup-git-committer/action.yml new file mode 100644 index 00000000000..87d2f5d0d44 --- /dev/null +++ b/.github/actions/setup-git-committer/action.yml @@ -0,0 +1,43 @@ +name: "Setup Git Committer" +description: "Create app token and configure git user" +inputs: + opencode-app-id: + description: "OpenCode GitHub App ID" + required: true + opencode-app-secret: + description: "OpenCode GitHub App private key" + required: true +outputs: + token: + description: "GitHub App token" + value: ${{ steps.apptoken.outputs.token }} + app-slug: + description: "GitHub App slug" + value: ${{ steps.apptoken.outputs.app-slug }} +runs: + using: "composite" + steps: + - name: Create app token + id: apptoken + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ inputs.opencode-app-id }} + private-key: ${{ inputs.opencode-app-secret }} + owner: ${{ github.repository_owner }} + + - name: Configure git user + run: | + slug="${{ steps.apptoken.outputs.app-slug }}" + git config --global user.name "${slug}[bot]" + git config --global user.email "${slug}[bot]@users.noreply.github.com" + shell: bash + + - name: Clear checkout auth + run: | + git config --local --unset-all http.https://github.com/.extraheader || true + shell: bash + + - name: Configure git remote + run: | + git remote set-url origin https://x-access-token:${{ steps.apptoken.outputs.token }}@github.com/${{ github.repository }} + shell: bash diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b4369fa1a43..393bf90518e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,29 @@ +### Issue for this PR + +Closes # + +### Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor / code improvement +- [ ] Documentation + ### What does this PR do? +Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. + +**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** + ### How did you verify your code works? + +### Screenshots / recordings + +_If this is a UI change, please include a screenshot or recording._ + +### Checklist + +- [ ] I have tested my changes locally +- [ ] I have not included unrelated changes in this PR + +_If you do not follow this template your PR will be automatically rejected._ diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml new file mode 100644 index 00000000000..a7106667b11 --- /dev/null +++ b/.github/workflows/beta.yml @@ -0,0 +1,37 @@ +name: beta + +on: + workflow_dispatch: + schedule: + - cron: "0 * * * *" + +jobs: + sync: + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Setup Git Committer + id: setup-git-committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + + - name: Install OpenCode + run: bun i -g opencode-ai + + - name: Sync beta branch + env: + GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + run: bun script/beta.ts diff --git a/.github/workflows/close-stale-prs.yml b/.github/workflows/close-stale-prs.yml new file mode 100644 index 00000000000..e0e571b4691 --- /dev/null +++ b/.github/workflows/close-stale-prs.yml @@ -0,0 +1,235 @@ +name: close-stale-prs + +on: + workflow_dispatch: + inputs: + dryRun: + description: "Log actions without closing PRs" + type: boolean + default: false + schedule: + - cron: "0 6 * * *" + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + close-stale-prs: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Close inactive PRs + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const DAYS_INACTIVE = 60 + const MAX_RETRIES = 3 + + // Adaptive delay: fast for small batches, slower for large to respect + // GitHub's 80 content-generating requests/minute limit + const SMALL_BATCH_THRESHOLD = 10 + const SMALL_BATCH_DELAY_MS = 1000 // 1s for daily operations (≤10 PRs) + const LARGE_BATCH_DELAY_MS = 2000 // 2s for backlog (>10 PRs) = ~30 ops/min, well under 80 limit + + const startTime = Date.now() + const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000) + const { owner, repo } = context.repo + const dryRun = context.payload.inputs?.dryRun === "true" + + core.info(`Dry run mode: ${dryRun}`) + core.info(`Cutoff date: ${cutoff.toISOString()}`) + + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) + } + + async function withRetry(fn, description = 'API call') { + let lastError + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + const result = await fn() + return result + } catch (error) { + lastError = error + const isRateLimited = error.status === 403 && + (error.message?.includes('rate limit') || error.message?.includes('secondary')) + + if (!isRateLimited) { + throw error + } + + // Parse retry-after header, default to 60 seconds + const retryAfter = error.response?.headers?.['retry-after'] + ? parseInt(error.response.headers['retry-after']) + : 60 + + // Exponential backoff: retryAfter * 2^attempt + const backoffMs = retryAfter * 1000 * Math.pow(2, attempt) + + core.warning(`${description}: Rate limited (attempt ${attempt + 1}/${MAX_RETRIES}). Waiting ${backoffMs / 1000}s before retry...`) + + await sleep(backoffMs) + } + } + core.error(`${description}: Max retries (${MAX_RETRIES}) exceeded`) + throw lastError + } + + const query = ` + query($owner: String!, $repo: String!, $cursor: String) { + repository(owner: $owner, name: $repo) { + pullRequests(first: 100, states: OPEN, after: $cursor) { + pageInfo { + hasNextPage + endCursor + } + nodes { + number + title + author { + login + } + createdAt + commits(last: 1) { + nodes { + commit { + committedDate + } + } + } + comments(last: 1) { + nodes { + createdAt + } + } + reviews(last: 1) { + nodes { + createdAt + } + } + } + } + } + } + ` + + const allPrs = [] + let cursor = null + let hasNextPage = true + let pageCount = 0 + + while (hasNextPage) { + pageCount++ + core.info(`Fetching page ${pageCount} of open PRs...`) + + const result = await withRetry( + () => github.graphql(query, { owner, repo, cursor }), + `GraphQL page ${pageCount}` + ) + + allPrs.push(...result.repository.pullRequests.nodes) + hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage + cursor = result.repository.pullRequests.pageInfo.endCursor + + core.info(`Page ${pageCount}: fetched ${result.repository.pullRequests.nodes.length} PRs (total: ${allPrs.length})`) + + // Delay between pagination requests (use small batch delay for reads) + if (hasNextPage) { + await sleep(SMALL_BATCH_DELAY_MS) + } + } + + core.info(`Found ${allPrs.length} open pull requests`) + + const stalePrs = allPrs.filter((pr) => { + const dates = [ + new Date(pr.createdAt), + pr.commits.nodes[0] ? new Date(pr.commits.nodes[0].commit.committedDate) : null, + pr.comments.nodes[0] ? new Date(pr.comments.nodes[0].createdAt) : null, + pr.reviews.nodes[0] ? new Date(pr.reviews.nodes[0].createdAt) : null, + ].filter((d) => d !== null) + + const lastActivity = dates.sort((a, b) => b.getTime() - a.getTime())[0] + + if (!lastActivity || lastActivity > cutoff) { + core.info(`PR #${pr.number} is fresh (last activity: ${lastActivity?.toISOString() || "unknown"})`) + return false + } + + core.info(`PR #${pr.number} is STALE (last activity: ${lastActivity.toISOString()})`) + return true + }) + + if (!stalePrs.length) { + core.info("No stale pull requests found.") + return + } + + core.info(`Found ${stalePrs.length} stale pull requests`) + + // ============================================ + // Close stale PRs + // ============================================ + const requestDelayMs = stalePrs.length > SMALL_BATCH_THRESHOLD + ? LARGE_BATCH_DELAY_MS + : SMALL_BATCH_DELAY_MS + + core.info(`Using ${requestDelayMs}ms delay between operations (${stalePrs.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`) + + let closedCount = 0 + let skippedCount = 0 + + for (const pr of stalePrs) { + const issue_number = pr.number + const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.` + + if (dryRun) { + core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`) + continue + } + + try { + // Add comment + await withRetry( + () => github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: closeComment, + }), + `Comment on PR #${issue_number}` + ) + + // Close PR + await withRetry( + () => github.rest.pulls.update({ + owner, + repo, + pull_number: issue_number, + state: "closed", + }), + `Close PR #${issue_number}` + ) + + closedCount++ + core.info(`Closed PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`) + + // Delay before processing next PR + await sleep(requestDelayMs) + } catch (error) { + skippedCount++ + core.error(`Failed to close PR #${issue_number}: ${error.message}`) + } + } + + const elapsed = Math.round((Date.now() - startTime) / 1000) + core.info(`\n========== Summary ==========`) + core.info(`Total open PRs found: ${allPrs.length}`) + core.info(`Stale PRs identified: ${stalePrs.length}`) + core.info(`PRs closed: ${closedCount}`) + core.info(`PRs skipped (errors): ${skippedCount}`) + core.info(`Elapsed time: ${elapsed}s`) + core.info(`=============================`) diff --git a/.github/workflows/compliance-close.yml b/.github/workflows/compliance-close.yml new file mode 100644 index 00000000000..c3bcf9f686f --- /dev/null +++ b/.github/workflows/compliance-close.yml @@ -0,0 +1,95 @@ +name: compliance-close + +on: + schedule: + # Run every 30 minutes to check for expired compliance windows + - cron: "*/30 * * * *" + workflow_dispatch: + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + close-non-compliant: + runs-on: ubuntu-latest + steps: + - name: Close non-compliant issues and PRs after 2 hours + uses: actions/github-script@v7 + with: + script: | + const { data: items } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'needs:compliance', + state: 'open', + per_page: 100, + }); + + if (items.length === 0) { + core.info('No open issues/PRs with needs:compliance label'); + return; + } + + const now = Date.now(); + const twoHours = 2 * 60 * 60 * 1000; + + for (const item of items) { + const isPR = !!item.pull_request; + const kind = isPR ? 'PR' : 'issue'; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + }); + + const complianceComment = comments.find(c => c.body.includes('')); + if (!complianceComment) continue; + + const commentAge = now - new Date(complianceComment.created_at).getTime(); + if (commentAge < twoHours) { + core.info(`${kind} #${item.number} still within 2-hour window (${Math.round(commentAge / 60000)}m elapsed)`); + continue; + } + + const closeMessage = isPR + ? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new pull request that follows our guidelines.' + : 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new issue that follows our issue templates.'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + body: closeMessage, + }); + + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + name: 'needs:compliance', + }); + } catch (e) {} + + if (isPR) { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: item.number, + state: 'closed', + }); + } else { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + state: 'closed', + state_reason: 'not_planned', + }); + } + + core.info(`Closed non-compliant ${kind} #${item.number} after 2-hour window`); + } diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml new file mode 100644 index 00000000000..c7df066d41c --- /dev/null +++ b/.github/workflows/containers.yml @@ -0,0 +1,45 @@ +name: containers + +on: + push: + branches: + - dev + paths: + - packages/containers/** + - .github/workflows/containers.yml + - package.json + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: blacksmith-4vcpu-ubuntu-2404 + env: + REGISTRY: ghcr.io/${{ github.repository_owner }} + TAG: "24.04" + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-bun + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push containers + run: bun ./packages/containers/script/build.ts --push + env: + REGISTRY: ${{ env.REGISTRY }} + TAG: ${{ env.TAG }} diff --git a/.github/workflows/daily-issues-recap.yml b/.github/workflows/daily-issues-recap.yml index a333e5365f9..31cf08233b9 100644 --- a/.github/workflows/daily-issues-recap.yml +++ b/.github/workflows/daily-issues-recap.yml @@ -1,4 +1,4 @@ -name: Daily Issues Recap +name: daily-issues-recap on: schedule: @@ -48,8 +48,12 @@ jobs: TODAY'S DATE: ${TODAY} STEP 1: Gather today's issues - Search for all issues created today (${TODAY}) using: - gh issue list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500 + Search for all OPEN issues created today (${TODAY}) using: + gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500 + + IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these: + adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr + This recap is specifically for COMMUNITY (external) issues only. STEP 2: Analyze and categorize For each issue created today, categorize it: diff --git a/.github/workflows/daily-pr-recap.yml b/.github/workflows/daily-pr-recap.yml index 7c8bab395f6..2f0f023cfd0 100644 --- a/.github/workflows/daily-pr-recap.yml +++ b/.github/workflows/daily-pr-recap.yml @@ -1,4 +1,4 @@ -name: Daily PR Recap +name: daily-pr-recap on: schedule: @@ -47,14 +47,18 @@ jobs: TODAY'S DATE: ${TODAY} STEP 1: Gather PR data - Run these commands to gather PR information. ONLY include PRs created or updated TODAY (${TODAY}): + Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}): - # PRs created today - gh pr list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100 + # Open PRs created today + gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100 - # PRs with activity today (updated today) + # Open PRs with activity today (updated today) gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100 + IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these: + adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr + This recap is specifically for COMMUNITY (external) contributions only. + STEP 2: For high-activity PRs, check comment counts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 25466a63e06..c08d7edf3b1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,6 +21,15 @@ jobs: with: node-version: "24" + # Workaround for Pulumi version conflict: + # GitHub runners have Pulumi 3.212.0+ pre-installed, which removed the -root flag + # from pulumi-language-nodejs (see https://github.com/pulumi/pulumi/pull/21065). + # SST 3.17.x uses Pulumi SDK 3.210.0 which still passes -root, causing a conflict. + # Removing the system language plugin forces SST to use its bundled compatible version. + # TODO: Remove when sst supports Pulumi >3.210.0 + - name: Fix Pulumi version conflict + run: sudo rm -f /usr/local/bin/pulumi-language-nodejs + - run: bun sst deploy --stage=${{ github.ref_name }} env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/docs-locale-sync.yml b/.github/workflows/docs-locale-sync.yml new file mode 100644 index 00000000000..fff2ec4292b --- /dev/null +++ b/.github/workflows/docs-locale-sync.yml @@ -0,0 +1,99 @@ +name: docs-locale-sync + +on: + push: + branches: + - dev + paths: + - packages/web/src/content/docs/*.mdx + +jobs: + sync-locales: + if: github.actor != 'opencode-agent[bot]' + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + + - name: Compute changed English docs + id: changes + run: | + FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'packages/web/src/content/docs/*.mdx' || true) + if [ -z "$FILES" ]; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + echo "No English docs changed in push range" + exit 0 + fi + echo "has_changes=true" >> "$GITHUB_OUTPUT" + { + echo "files<> "$GITHUB_OUTPUT" + + - name: Install OpenCode + if: steps.changes.outputs.has_changes == 'true' + run: curl -fsSL https://opencode.ai/install | bash + + - name: Sync locale docs with OpenCode + if: steps.changes.outputs.has_changes == 'true' + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + OPENCODE_CONFIG_CONTENT: | + { + "permission": { + "*": "deny", + "read": "allow", + "edit": "allow", + "glob": "allow", + "task": "allow" + } + } + run: | + opencode run --agent docs --model opencode/gpt-5.3-codex <<'EOF' + Update localized docs to match the latest English docs changes. + + Changed English doc files: + + ${{ steps.changes.outputs.files }} + + + Requirements: + 1. Update all relevant locale docs under packages/web/src/content/docs// so they reflect these English page changes. + 2. You MUST use the Task tool for translation work and launch subagents with subagent_type `translator` (defined in .opencode/agent/translator.md). + 3. Do not translate directly in the primary agent. Use translator subagent output as the source for locale text updates. + 4. Run translator subagent Task calls in parallel whenever file/locale translation work is independent. + 5. Use only the minimum tools needed for this task (read/glob, file edits, and translator Task). Do not use shell, web, search, or GitHub tools for translation work. + 6. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update. + 7. Keep locale docs structure aligned with their corresponding English pages. + 8. Do not modify English source docs in packages/web/src/content/docs/*.mdx. + 9. If no locale updates are needed, make no changes. + EOF + + - name: Commit and push locale docs updates + if: steps.changes.outputs.has_changes == 'true' + run: | + if [ -z "$(git status --porcelain)" ]; then + echo "No locale docs changes to commit" + exit 0 + fi + git add -A + git commit -m "docs(i18n): sync locale docs from english changes" + git pull --rebase --autostash origin "$GITHUB_REF_NAME" + git push origin HEAD:"$GITHUB_REF_NAME" diff --git a/.github/workflows/docs-update.yml b/.github/workflows/docs-update.yml index a8dd2ae4f2b..900ad2b0c58 100644 --- a/.github/workflows/docs-update.yml +++ b/.github/workflows/docs-update.yml @@ -1,4 +1,4 @@ -name: Docs Update +name: docs-update on: schedule: diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml index 53aa2a725eb..6c1943fe7b8 100644 --- a/.github/workflows/duplicate-issues.yml +++ b/.github/workflows/duplicate-issues.yml @@ -1,11 +1,12 @@ -name: Duplicate Issue Detection +name: duplicate-issues on: issues: - types: [opened] + types: [opened, edited] jobs: check-duplicates: + if: github.event.action == 'opened' runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read @@ -21,7 +22,7 @@ jobs: - name: Install opencode run: curl -fsSL https://opencode.ai/install | bash - - name: Check for duplicate issues + - name: Check duplicates and compliance env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -34,30 +35,143 @@ jobs: "webfetch": "deny" } run: | - opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:' + opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created: - Issue number: - ${{ github.event.issue.number }} + Issue number: ${{ github.event.issue.number }} - Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue. + Lookup this issue with gh issue view ${{ github.event.issue.number }}. + + You have TWO tasks. Perform both, then post a SINGLE comment (if needed). + + --- + + TASK 1: CONTRIBUTING GUIDELINES COMPLIANCE CHECK + + Check whether the issue follows our contributing guidelines and issue templates. + + This project has three issue templates that every issue MUST use one of: + + 1. Bug Report - requires a Description field with real content + 2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]: + 3. Question - requires the Question field with real content + + Additionally check: + - No AI-generated walls of text (long, AI-generated descriptions are not acceptable) + - The issue has real content, not just template placeholder text left unchanged + - Bug reports should include some context about how to reproduce + - Feature requests should explain the problem or need + - We want to push for having the user provide system description & information + + Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content. + + --- + + TASK 2: DUPLICATE CHECK + + Search through existing issues (excluding #${{ github.event.issue.number }}) to find potential duplicates. Consider: 1. Similar titles or descriptions 2. Same error messages or symptoms 3. Related functionality or components 4. Similar feature requests - If you find any potential duplicates, please comment on the new issue with: - - A brief explanation of why it might be a duplicate - - Links to the potentially duplicate issues - - A suggestion to check those issues first + Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, note the pinned keybinds issue #4997. + + --- + + POSTING YOUR COMMENT: + + Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows: + + If the issue is NOT compliant, start the comment with: + + Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance + + If duplicates were found, include a section about potential duplicates with links. + + If the issue mentions keybinds/keyboard shortcuts, include a note about #4997. + + If the issue IS compliant AND no duplicates were found AND no keybind reference, do NOT comment at all. Use this format for the comment: - 'This issue might be a duplicate of existing issues. Please check: + + [If not compliant:] + + This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md). + + **What needs to be fixed:** + - [specific reasons] + + Please edit this issue to address the above within **2 hours**, or it will be automatically closed. + + [If duplicates found, add:] + --- + This issue might be a duplicate of existing issues. Please check: - #[issue_number]: [brief description of similarity] - Feel free to ignore if none of these address your specific case.' + [If keybind-related, add:] + For keybind-related issues, please also check our pinned keybinds documentation: #4997 + + [End with if not compliant:] + If you believe this was flagged incorrectly, please let a maintainer know. + + Remember: post at most ONE comment combining all findings. If everything is fine, post nothing." + + recheck-compliance: + if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: ./.github/actions/setup-bun + + - name: Install opencode + run: curl -fsSL https://opencode.ai/install | bash + + - name: Recheck compliance + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENCODE_PERMISSION: | + { + "bash": { + "*": "deny", + "gh issue*": "allow" + }, + "webfetch": "deny" + } + run: | + opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited. + + Lookup this issue with gh issue view ${{ github.event.issue.number }}. + + Re-check whether the issue now follows our contributing guidelines and issue templates. + + This project has three issue templates that every issue MUST use one of: + + 1. Bug Report - requires a Description field with real content + 2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]: + 3. Question - requires the Question field with real content + + Additionally check: + - No AI-generated walls of text (long, AI-generated descriptions are not acceptable) + - The issue has real content, not just template placeholder text left unchanged + - Bug reports should include some context about how to reproduce + - Feature requests should explain the problem or need + - We want to push for having the user provide system description & information + + Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content. - Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997: - 'For keybind-related issues, please also check our pinned keybinds documentation: #4997' + If the issue is NOW compliant: + 1. Remove the needs:compliance label: gh issue edit ${{ github.event.issue.number }} --remove-label needs:compliance + 2. Find and delete the previous compliance comment (the one containing ) using: gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments --jq '.[] | select(.body | contains(\"\")) | .id' then delete it with: gh api -X DELETE repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments/{id} + 3. Post a short comment thanking them for updating the issue. - If no clear duplicates are found, do not comment." + If the issue is STILL not compliant: + Post a comment explaining what still needs to be fixed. Keep the needs:compliance label." diff --git a/.github/workflows/duplicate-prs.yml b/.github/workflows/duplicate-prs.yml deleted file mode 100644 index 32606858958..00000000000 --- a/.github/workflows/duplicate-prs.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Duplicate PR Check - -on: - pull_request_target: - types: [opened] - -jobs: - check-duplicates: - if: | - github.event.pull_request.user.login != 'actions-user' && - github.event.pull_request.user.login != 'opencode' && - github.event.pull_request.user.login != 'rekram1-node' && - github.event.pull_request.user.login != 'thdxr' && - github.event.pull_request.user.login != 'kommander' && - github.event.pull_request.user.login != 'jayair' && - github.event.pull_request.user.login != 'fwang' && - github.event.pull_request.user.login != 'adamdotdevin' && - github.event.pull_request.user.login != 'iamdavidhill' && - github.event.pull_request.user.login != 'opencode-agent[bot]' - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Install dependencies - run: bun install - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Build prompt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - { - echo "Check for duplicate PRs related to this new PR:" - echo "" - echo "CURRENT_PR_NUMBER: $PR_NUMBER" - echo "" - echo "Title: $(gh pr view "$PR_NUMBER" --json title --jq .title)" - echo "" - echo "Description:" - gh pr view "$PR_NUMBER" --json body --jq .body - } > pr_info.txt - - - name: Check for duplicate PRs - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates") - - gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_ - - $COMMENT" diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index 29cc9895393..706ab2989e1 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -4,7 +4,6 @@ on: push: branches: - dev - workflow_dispatch: jobs: generate: @@ -15,14 +14,17 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - ref: ${{ github.event.pull_request.head.ref || github.ref_name }} - name: Setup Bun uses: ./.github/actions/setup-bun + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + - name: Generate run: ./script/generate.ts @@ -32,10 +34,8 @@ jobs: echo "No changes to commit" exit 0 fi - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" git add -A - git commit -m "chore: generate" + git commit -m "chore: generate" --allow-empty git push origin HEAD:${{ github.ref_name }} --no-verify # if ! git push origin HEAD:${{ github.event.pull_request.head.ref || github.ref_name }} --no-verify; then # echo "" diff --git a/.github/workflows/nix-desktop.yml b/.github/workflows/nix-desktop.yml deleted file mode 100644 index 3d7c4803133..00000000000 --- a/.github/workflows/nix-desktop.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: nix desktop - -on: - push: - branches: [dev] - paths: - - "flake.nix" - - "flake.lock" - - "nix/**" - - "packages/app/**" - - "packages/desktop/**" - - ".github/workflows/nix-desktop.yml" - pull_request: - paths: - - "flake.nix" - - "flake.lock" - - "nix/**" - - "packages/app/**" - - "packages/desktop/**" - - ".github/workflows/nix-desktop.yml" - workflow_dispatch: - -jobs: - build-desktop: - strategy: - fail-fast: false - matrix: - os: - - blacksmith-4vcpu-ubuntu-2404 - - blacksmith-4vcpu-ubuntu-2404-arm - - macos-15-intel - - macos-latest - runs-on: ${{ matrix.os }} - timeout-minutes: 60 - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup Nix - uses: nixbuild/nix-quick-install-action@v34 - - - name: Build desktop via flake - run: | - set -euo pipefail - nix --version - nix build .#desktop -L diff --git a/.github/workflows/nix-eval.yml b/.github/workflows/nix-eval.yml new file mode 100644 index 00000000000..c76b2c97297 --- /dev/null +++ b/.github/workflows/nix-eval.yml @@ -0,0 +1,95 @@ +name: nix-eval + +on: + push: + branches: [dev] + pull_request: + branches: [dev] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + nix-eval: + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Nix + uses: nixbuild/nix-quick-install-action@v34 + + - name: Evaluate flake outputs (all systems) + run: | + set -euo pipefail + nix --version + + echo "=== Flake metadata ===" + nix flake metadata + + echo "" + echo "=== Flake structure ===" + nix flake show --all-systems + + SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" + PACKAGES="opencode" + # TODO: move 'desktop' to PACKAGES when #11755 is fixed + OPTIONAL_PACKAGES="desktop" + + echo "" + echo "=== Evaluating packages for all systems ===" + for system in $SYSTEMS; do + echo "" + echo "--- $system ---" + for pkg in $PACKAGES; do + printf " %s: " "$pkg" + if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then + echo "✓" + else + echo "✗" + echo "::error::Evaluation failed for packages.$system.$pkg" + echo "$output" + exit 1 + fi + done + done + + echo "" + echo "=== Evaluating optional packages ===" + for system in $SYSTEMS; do + echo "" + echo "--- $system ---" + for pkg in $OPTIONAL_PACKAGES; do + printf " %s: " "$pkg" + if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then + echo "✓" + else + echo "✗" + echo "::warning::Evaluation failed for packages.$system.$pkg" + echo "$output" + fi + done + done + + echo "" + echo "=== Evaluating devShells for all systems ===" + for system in $SYSTEMS; do + printf "%s: " "$system" + if output=$(nix eval ".#devShells.$system.default.drvPath" --raw 2>&1); then + echo "✓" + else + echo "✗" + echo "::error::Evaluation failed for devShells.$system.default" + echo "$output" + exit 1 + fi + done + + echo "" + echo "=== All evaluations passed ===" diff --git a/.github/workflows/nix-hashes.yml b/.github/workflows/nix-hashes.yml new file mode 100644 index 00000000000..2529c14c208 --- /dev/null +++ b/.github/workflows/nix-hashes.yml @@ -0,0 +1,148 @@ +name: nix-hashes + +permissions: + contents: write + +on: + workflow_dispatch: + push: + branches: [dev, beta] + paths: + - "bun.lock" + - "package.json" + - "packages/*/package.json" + - "flake.lock" + - "nix/node_modules.nix" + - "nix/scripts/**" + - "patches/**" + - ".github/workflows/nix-hashes.yml" + +jobs: + # Native runners required: bun install cross-compilation flags (--os/--cpu) + # do not produce byte-identical node_modules as native installs. + compute-hash: + strategy: + fail-fast: false + matrix: + include: + - system: x86_64-linux + runner: blacksmith-4vcpu-ubuntu-2404 + - system: aarch64-linux + runner: blacksmith-4vcpu-ubuntu-2404-arm + - system: x86_64-darwin + runner: macos-15-intel + - system: aarch64-darwin + runner: macos-latest + runs-on: ${{ matrix.runner }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Nix + uses: nixbuild/nix-quick-install-action@v34 + + - name: Compute node_modules hash + id: hash + env: + SYSTEM: ${{ matrix.system }} + run: | + set -euo pipefail + + BUILD_LOG=$(mktemp) + trap 'rm -f "$BUILD_LOG"' EXIT + + # Build with fakeHash to trigger hash mismatch and reveal correct hash + nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true + + # Extract hash from build log with portability + HASH="$(grep -oE 'sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)" + + if [ -z "$HASH" ]; then + echo "::error::Failed to compute hash for ${SYSTEM}" + cat "$BUILD_LOG" + exit 1 + fi + + echo "$HASH" > hash.txt + echo "Computed hash for ${SYSTEM}: $HASH" + + - name: Upload hash + uses: actions/upload-artifact@v4 + with: + name: hash-${{ matrix.system }} + path: hash.txt + retention-days: 1 + + update-hashes: + needs: compute-hash + if: github.event_name != 'pull_request' + runs-on: blacksmith-4vcpu-ubuntu-2404 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Setup git committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + + - name: Pull latest changes + run: | + git pull --rebase --autostash origin "$GITHUB_REF_NAME" + + - name: Download hash artifacts + uses: actions/download-artifact@v4 + with: + path: hashes + pattern: hash-* + + - name: Update hashes.json + run: | + set -euo pipefail + + HASH_FILE="nix/hashes.json" + + [ -f "$HASH_FILE" ] || echo '{"nodeModules":{}}' > "$HASH_FILE" + + for SYSTEM in x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin; do + FILE="hashes/hash-${SYSTEM}/hash.txt" + if [ -f "$FILE" ]; then + HASH="$(tr -d '[:space:]' < "$FILE")" + echo "${SYSTEM}: ${HASH}" + jq --arg sys "$SYSTEM" --arg h "$HASH" '.nodeModules[$sys] = $h' "$HASH_FILE" > tmp.json + mv tmp.json "$HASH_FILE" + else + echo "::warning::Missing hash for ${SYSTEM}" + fi + done + + cat "$HASH_FILE" + + - name: Commit changes + run: | + set -euo pipefail + + HASH_FILE="nix/hashes.json" + + if [ -z "$(git status --short -- "$HASH_FILE")" ]; then + echo "No changes to commit" + echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY" + echo "Status: no changes" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + git add "$HASH_FILE" + git commit -m "chore: update nix node_modules hashes" + + git pull --rebase --autostash origin "$GITHUB_REF_NAME" + git push origin HEAD:"$GITHUB_REF_NAME" + + echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY" + echo "Status: committed $(git rev-parse --short HEAD)" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/notify-discord.yml b/.github/workflows/notify-discord.yml index 62577ecf00e..b1d8053603a 100644 --- a/.github/workflows/notify-discord.yml +++ b/.github/workflows/notify-discord.yml @@ -1,4 +1,4 @@ -name: discord +name: notify-discord on: release: diff --git a/.github/workflows/pr-management.yml b/.github/workflows/pr-management.yml new file mode 100644 index 00000000000..35bd7ae36f2 --- /dev/null +++ b/.github/workflows/pr-management.yml @@ -0,0 +1,95 @@ +name: pr-management + +on: + pull_request_target: + types: [opened] + +jobs: + check-duplicates: + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Check team membership + id: team-check + run: | + LOGIN="${{ github.event.pull_request.user.login }}" + if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then + echo "is_team=true" >> "$GITHUB_OUTPUT" + echo "Skipping: $LOGIN is a team member or bot" + else + echo "is_team=false" >> "$GITHUB_OUTPUT" + fi + + - name: Setup Bun + if: steps.team-check.outputs.is_team != 'true' + uses: ./.github/actions/setup-bun + + - name: Install dependencies + if: steps.team-check.outputs.is_team != 'true' + run: bun install + + - name: Install opencode + if: steps.team-check.outputs.is_team != 'true' + run: curl -fsSL https://opencode.ai/install | bash + + - name: Build prompt + if: steps.team-check.outputs.is_team != 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + { + echo "Check for duplicate PRs related to this new PR:" + echo "" + echo "CURRENT_PR_NUMBER: $PR_NUMBER" + echo "" + echo "Title: $(gh pr view "$PR_NUMBER" --json title --jq .title)" + echo "" + echo "Description:" + gh pr view "$PR_NUMBER" --json body --jq .body + } > pr_info.txt + + - name: Check for duplicate PRs + if: steps.team-check.outputs.is_team != 'true' + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates") + + if [ "$COMMENT" != "No duplicate PRs found" ]; then + gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_ + + $COMMENT" + fi + + add-contributor-label: + runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write + steps: + - name: Add Contributor Label + uses: actions/github-script@v8 + with: + script: | + const isPR = !!context.payload.pull_request; + const issueNumber = isPR ? context.payload.pull_request.number : context.payload.issue.number; + const authorAssociation = isPR ? context.payload.pull_request.author_association : context.payload.issue.author_association; + + if (authorAssociation === 'CONTRIBUTOR') { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['contributor'] + }); + } diff --git a/.github/workflows/pr-standards.yml b/.github/workflows/pr-standards.yml index c1cf1756787..1edbd5d061d 100644 --- a/.github/workflows/pr-standards.yml +++ b/.github/workflows/pr-standards.yml @@ -1,4 +1,4 @@ -name: PR Standards +name: pr-standards on: pull_request_target: @@ -6,19 +6,9 @@ on: jobs: check-standards: - if: | - github.event.pull_request.user.login != 'actions-user' && - github.event.pull_request.user.login != 'opencode' && - github.event.pull_request.user.login != 'rekram1-node' && - github.event.pull_request.user.login != 'thdxr' && - github.event.pull_request.user.login != 'kommander' && - github.event.pull_request.user.login != 'jayair' && - github.event.pull_request.user.login != 'fwang' && - github.event.pull_request.user.login != 'adamdotdevin' && - github.event.pull_request.user.login != 'iamdavidhill' && - github.event.pull_request.user.login != 'opencode-agent[bot]' runs-on: ubuntu-latest permissions: + contents: read pull-requests: write steps: - name: Check PR standards @@ -26,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) { @@ -94,11 +108,11 @@ jobs: await removeLabel('needs:title'); - // Step 2: Check for linked issue (skip for docs/refactor PRs) - const skipIssueCheck = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); + // Step 2: Check for linked issue (skip for docs/refactor/feat PRs) + const skipIssueCheck = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); if (skipIssueCheck) { await removeLabel('needs:issue'); - console.log('Skipping issue check for docs/refactor PR'); + console.log('Skipping issue check for docs/refactor/feat PR'); return; } const query = ` @@ -137,3 +151,201 @@ jobs: await removeLabel('needs:issue'); console.log('PR meets all standards'); + + check-compliance: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Check PR template compliance + uses: actions/github-script@v7 + 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 body = pr.body || ''; + const title = pr.title; + const isDocsRefactorOrFeat = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); + + const issues = []; + + // Check: template sections exist + const hasWhatSection = /### What does this PR do\?/.test(body); + const hasTypeSection = /### Type of change/.test(body); + const hasVerifySection = /### How did you verify your code works\?/.test(body); + const hasChecklistSection = /### Checklist/.test(body); + const hasIssueSection = /### Issue for this PR/.test(body); + + if (!hasWhatSection || !hasTypeSection || !hasVerifySection || !hasChecklistSection || !hasIssueSection) { + issues.push('PR description is missing required template sections. Please use the [PR template](../blob/dev/.github/pull_request_template.md).'); + } + + // Check: "What does this PR do?" has real content (not just placeholder text) + if (hasWhatSection) { + const whatMatch = body.match(/### What does this PR do\?\s*\n([\s\S]*?)(?=###|$)/); + const whatContent = whatMatch ? whatMatch[1].trim() : ''; + const placeholder = 'Please provide a description of the issue'; + const onlyPlaceholder = whatContent.includes(placeholder) && whatContent.replace(placeholder, '').replace(/[*\s]/g, '').length < 20; + if (!whatContent || onlyPlaceholder) { + issues.push('"What does this PR do?" section is empty or only contains placeholder text. Please describe your changes.'); + } + } + + // Check: at least one "Type of change" checkbox is checked + if (hasTypeSection) { + const typeMatch = body.match(/### Type of change\s*\n([\s\S]*?)(?=###|$)/); + const typeContent = typeMatch ? typeMatch[1] : ''; + const hasCheckedBox = /- \[x\]/i.test(typeContent); + if (!hasCheckedBox) { + issues.push('No "Type of change" checkbox is checked. Please select at least one.'); + } + } + + // Check: issue reference (skip for docs/refactor/feat) + if (!isDocsRefactorOrFeat && hasIssueSection) { + const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/); + const issueContent = issueMatch ? issueMatch[1].trim() : ''; + const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent); + if (!hasIssueRef) { + issues.push('No issue referenced. Please add `Closes #` linking to the relevant issue.'); + } + } + + // Check: "How did you verify" has content + if (hasVerifySection) { + const verifyMatch = body.match(/### How did you verify your code works\?\s*\n([\s\S]*?)(?=###|$)/); + const verifyContent = verifyMatch ? verifyMatch[1].trim() : ''; + if (!verifyContent) { + issues.push('"How did you verify your code works?" section is empty. Please explain how you tested.'); + } + } + + // Check: checklist boxes are checked + if (hasChecklistSection) { + const checklistMatch = body.match(/### Checklist\s*\n([\s\S]*?)(?=###|$)/); + const checklistContent = checklistMatch ? checklistMatch[1] : ''; + const unchecked = (checklistContent.match(/- \[ \]/g) || []).length; + const checked = (checklistContent.match(/- \[x\]/gi) || []).length; + if (checked < 2) { + issues.push('Not all checklist items are checked. Please confirm you have tested locally and have not included unrelated changes.'); + } + } + + // Helper functions + async function addLabel(label) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: [label] + }); + } + + async function removeLabel(label) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: label + }); + } catch (e) {} + } + + const hasComplianceLabel = pr.labels.some(l => l.name === 'needs:compliance'); + + if (issues.length > 0) { + // Non-compliant + if (!hasComplianceLabel) { + await addLabel('needs:compliance'); + } + + const marker = ''; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number + }); + const existing = comments.find(c => c.body.includes(marker)); + + const body_text = `${marker} + This PR doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) and [PR template](../blob/dev/.github/pull_request_template.md). + + **What needs to be fixed:** + ${issues.map(i => `- ${i}`).join('\n')} + + Please edit this PR description to address the above within **2 hours**, or it will be automatically closed. + + If you believe this was flagged incorrectly, please let a maintainer know.`; + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: body_text + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: body_text + }); + } + + console.log(`PR #${pr.number} is non-compliant: ${issues.join(', ')}`); + } else if (hasComplianceLabel) { + // Was non-compliant, now fixed + await removeLabel('needs:compliance'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number + }); + const marker = ''; + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id + }); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: 'Thanks for updating your PR! It now meets our contributing guidelines. :+1:' + }); + + console.log(`PR #${pr.number} is now compliant, label removed`); + } else { + console.log(`PR #${pr.number} is compliant`); + } diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8d7a823b144..b425b32a58d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,7 +4,9 @@ run-name: "${{ format('release {0}', inputs.bump) }}" on: push: branches: + - ci - dev + - beta - snapshot-* workflow_dispatch: inputs: @@ -29,7 +31,7 @@ permissions: packages: write jobs: - publish: + version: runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.repository == 'anomalyco/opencode' steps: @@ -37,61 +39,73 @@ jobs: with: fetch-depth: 0 - - run: git fetch --force --tags - - uses: ./.github/actions/setup-bun + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + - name: Install OpenCode if: inputs.bump || inputs.version - run: bun i -g opencode-ai@1.0.169 + run: bun i -g opencode-ai - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} + - id: version + run: | + ./script/version.ts + env: + GH_TOKEN: ${{ steps.committer.outputs.token }} + OPENCODE_BUMP: ${{ inputs.bump }} + OPENCODE_VERSION: ${{ inputs.version }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GH_REPO: ${{ (github.ref_name == 'beta' && 'anomalyco/opencode-beta') || github.repository }} + outputs: + version: ${{ steps.version.outputs.version }} + release: ${{ steps.version.outputs.release }} + tag: ${{ steps.version.outputs.tag }} + repo: ${{ steps.version.outputs.repo }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + build-cli: + needs: version + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: github.repository == 'anomalyco/opencode' + steps: + - uses: actions/checkout@v3 + with: + fetch-tags: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - uses: ./.github/actions/setup-bun - - uses: actions/setup-node@v4 + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer with: - node-version: "24" - registry-url: "https://registry.npmjs.org" + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - name: Setup Git Identity + - name: Build + id: build run: | - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }} - - - name: Publish - id: publish - run: ./script/publish-start.ts + ./packages/opencode/script/build.ts env: - OPENCODE_BUMP: ${{ inputs.bump }} - OPENCODE_VERSION: ${{ inputs.version }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - AUR_KEY: ${{ secrets.AUR_KEY }} - GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} - NPM_CONFIG_PROVENANCE: false + OPENCODE_VERSION: ${{ needs.version.outputs.version }} + OPENCODE_RELEASE: ${{ needs.version.outputs.release }} + GH_REPO: ${{ needs.version.outputs.repo }} + GH_TOKEN: ${{ steps.committer.outputs.token }} - uses: actions/upload-artifact@v4 with: name: opencode-cli path: packages/opencode/dist - outputs: - release: ${{ steps.publish.outputs.release }} - tag: ${{ steps.publish.outputs.tag }} - version: ${{ steps.publish.outputs.version }} + version: ${{ needs.version.outputs.version }} - publish-tauri: - needs: publish + build-tauri: + needs: + - build-cli + - version continue-on-error: false strategy: fail-fast: false @@ -101,18 +115,20 @@ jobs: target: x86_64-apple-darwin - host: macos-latest target: aarch64-apple-darwin + # github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain + - host: windows-2025 + target: aarch64-pc-windows-msvc - host: blacksmith-4vcpu-windows-2025 target: x86_64-pc-windows-msvc - host: blacksmith-4vcpu-ubuntu-2404 target: x86_64-unknown-linux-gnu - - host: blacksmith-4vcpu-ubuntu-2404-arm + - host: blacksmith-8vcpu-ubuntu-2404-arm target: aarch64-unknown-linux-gnu runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v3 with: - fetch-depth: 0 - ref: ${{ needs.publish.outputs.tag }} + fetch-tags: true - uses: apple-actions/import-codesign-certs@v2 if: ${{ runner.os == 'macOS' }} @@ -134,15 +150,28 @@ jobs: run: | echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8 - - run: git fetch --force --tags - - uses: ./.github/actions/setup-bun + - uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Cache apt packages + if: contains(matrix.settings.host, 'ubuntu') + uses: actions/cache@v4 + with: + path: ~/apt-cache + 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: | + mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + sudo chmod -R a+rw ~/apt-cache - name: install Rust stable uses: dtolnay/rust-toolchain@stable @@ -159,42 +188,54 @@ jobs: cd packages/desktop bun ./scripts/prepare.ts env: - OPENCODE_VERSION: ${{ needs.publish.outputs.version }} - NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} - GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} - AUR_KEY: ${{ secrets.AUR_KEY }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + OPENCODE_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 }} + - name: Resolve tauri portable SHA + if: contains(matrix.settings.host, 'ubuntu') + run: echo "TAURI_PORTABLE_SHA=$(git ls-remote https://github.com/tauri-apps/tauri.git refs/heads/feat/truly-portable-appimage | cut -f1)" >> "$GITHUB_ENV" + # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released - name: Install tauri-cli from portable appimage branch + uses: taiki-e/cache-cargo-install-action@v3 if: contains(matrix.settings.host, 'ubuntu') - run: | - cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force - echo "Installed tauri-cli version:" - cargo tauri --version + with: + tool: tauri-cli + git: https://github.com/tauri-apps/tauri + # branch: feat/truly-portable-appimage + rev: ${{ env.TAURI_PORTABLE_SHA }} + + - name: Show tauri-cli version + if: contains(matrix.settings.host, 'ubuntu') + run: cargo tauri --version + + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - name: Build and upload artifacts - uses: Wandalen/wretry.action@v3 + uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a timeout-minutes: 60 with: - attempt_limit: 3 - attempt_delay: 10000 - action: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a - 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.publish.outputs.release }} - tagName: ${{ needs.publish.outputs.tag }} - releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext] - releaseDraft: true + projectPath: packages/desktop + uploadWorkflowArtifacts: true + tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} + args: --target ${{ matrix.settings.target }} --config ${{ (github.ref_name == 'beta' && './src-tauri/tauri.beta.conf.json') || './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] + repo: ${{ (github.ref_name == 'beta' && 'opencode-beta') || '' }} + releaseCommitish: ${{ github.sha }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.committer.outputs.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 }} @@ -205,20 +246,185 @@ jobs: APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8 - publish-release: + build-electron: needs: - - publish - - publish-tauri - if: needs.publish.outputs.tag - runs-on: blacksmith-4vcpu-ubuntu-2404 + - build-cli + - version + continue-on-error: false + strategy: + fail-fast: false + matrix: + settings: + - host: macos-latest + target: x86_64-apple-darwin + platform_flag: --mac --x64 + - host: macos-latest + target: aarch64-apple-darwin + platform_flag: --mac --arm64 + # github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain + - host: "windows-2025" + target: aarch64-pc-windows-msvc + platform_flag: --win --arm64 + - host: "blacksmith-4vcpu-windows-2025" + target: x86_64-pc-windows-msvc + platform_flag: --win + - host: "blacksmith-4vcpu-ubuntu-2404" + target: x86_64-unknown-linux-gnu + platform_flag: --linux + - host: "blacksmith-4vcpu-ubuntu-2404" + target: aarch64-unknown-linux-gnu + platform_flag: --linux + runs-on: ${{ matrix.settings.host }} + # if: github.ref_name == 'beta' steps: - uses: actions/checkout@v3 + + - uses: apple-actions/import-codesign-certs@v2 + if: runner.os == 'macOS' with: - fetch-depth: 0 - ref: ${{ needs.publish.outputs.tag }} + keychain: build + p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} + p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + + - 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 + - uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Cache apt packages + if: contains(matrix.settings.host, 'ubuntu') + uses: actions/cache@v4 + with: + path: ~/apt-cache + key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron-${{ hashFiles('.github/workflows/publish.yml') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron- + + - name: Install dependencies (ubuntu only) + if: contains(matrix.settings.host, 'ubuntu') + run: | + mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache + sudo apt-get update + sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" rpm + sudo chmod -R a+rw ~/apt-cache + + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + + - name: Prepare + run: bun ./scripts/prepare.ts + working-directory: packages/desktop-electron + env: + OPENCODE_VERSION: ${{ needs.version.outputs.version }} + OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} + RUST_TARGET: ${{ matrix.settings.target }} + GH_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + + - name: Build + run: bun run build + working-directory: packages/desktop-electron + env: + OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} + + - name: Package and publish + if: needs.version.outputs.release + run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish always --config electron-builder.config.ts + working-directory: packages/desktop-electron + timeout-minutes: 60 + env: + OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} + GH_TOKEN: ${{ steps.committer.outputs.token }} + CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} + CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8 + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + + - name: Package (no publish) + if: ${{ !needs.version.outputs.release }} + run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts + working-directory: packages/desktop-electron + timeout-minutes: 60 + env: + OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} + + - uses: actions/upload-artifact@v4 + with: + name: opencode-electron-${{ matrix.settings.target }} + path: packages/desktop-electron/dist/* + + - uses: actions/upload-artifact@v4 + if: needs.version.outputs.release + with: + name: latest-yml-${{ matrix.settings.target }} + path: packages/desktop-electron/dist/latest*.yml + + publish: + needs: + - version + - build-cli + - build-tauri + - build-electron + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/actions/setup-bun + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + + - uses: actions/download-artifact@v4 + with: + name: opencode-cli + path: packages/opencode/dist + + - uses: actions/download-artifact@v4 + if: needs.version.outputs.release + with: + pattern: latest-yml-* + path: /tmp/latest-yml + + - name: Cache apt packages (AUR) + uses: actions/cache@v4 + with: + path: /var/cache/apt/archives + key: ${{ runner.os }}-apt-aur-${{ hashFiles('.github/workflows/publish.yml') }} + restore-keys: | + ${{ runner.os }}-apt-aur- + - name: Setup SSH for AUR run: | sudo apt-get update @@ -230,8 +436,12 @@ jobs: git config --global user.name "opencode" ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true - - run: ./script/publish-complete.ts + - run: ./script/publish.ts env: - OPENCODE_VERSION: ${{ needs.publish.outputs.version }} + OPENCODE_VERSION: ${{ needs.version.outputs.version }} + OPENCODE_RELEASE: ${{ needs.version.outputs.release }} AUR_KEY: ${{ secrets.AUR_KEY }} - GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.committer.outputs.token }} + GH_REPO: ${{ needs.version.outputs.repo }} + NPM_CONFIG_PROVENANCE: false + LATEST_YML_DIR: /tmp/latest-yml diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 93b01bafa2b..58e73fac8fb 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -1,4 +1,4 @@ -name: Guidelines Check +name: review on: issue_comment: diff --git a/.github/workflows/sign-cli.yml b/.github/workflows/sign-cli.yml new file mode 100644 index 00000000000..d9d61fd800e --- /dev/null +++ b/.github/workflows/sign-cli.yml @@ -0,0 +1,54 @@ +name: sign-cli + +on: + push: + branches: + - brendan/desktop-signpath + workflow_dispatch: + +permissions: + contents: read + actions: read + +jobs: + sign-cli: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: github.repository == 'anomalyco/opencode' + steps: + - uses: actions/checkout@v3 + with: + fetch-tags: true + + - uses: ./.github/actions/setup-bun + + - name: Build + run: | + ./packages/opencode/script/build.ts + + - name: Upload unsigned Windows CLI + id: upload_unsigned_windows_cli + uses: actions/upload-artifact@v4 + with: + name: unsigned-opencode-windows-cli + path: packages/opencode/dist/opencode-windows-x64/bin/opencode.exe + if-no-files-found: error + + - name: Submit SignPath signing request + id: submit_signpath_signing_request + uses: signpath/github-action-submit-signing-request@v1 + with: + api-token: ${{ secrets.SIGNPATH_API_KEY }} + organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }} + artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }} + github-artifact-id: ${{ steps.upload_unsigned_windows_cli.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: signed-opencode-cli + + - name: Upload signed Windows CLI + uses: actions/upload-artifact@v4 + with: + name: signed-opencode-windows-cli + path: signed-opencode-cli/*.exe + if-no-files-found: error diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index b5378d7d527..a4b8583f928 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -1,4 +1,4 @@ -name: "Auto-close stale issues" +name: stale-issues on: schedule: diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml new file mode 100644 index 00000000000..6d143a8a22f --- /dev/null +++ b/.github/workflows/storybook.yml @@ -0,0 +1,38 @@ +name: storybook + +on: + push: + branches: [dev] + paths: + - ".github/workflows/storybook.yml" + - "package.json" + - "bun.lock" + - "packages/storybook/**" + - "packages/ui/**" + pull_request: + branches: [dev] + paths: + - ".github/workflows/storybook.yml" + - "package.json" + - "bun.lock" + - "packages/storybook/**" + - "packages/ui/**" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: storybook build + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Build Storybook + run: bun --cwd packages/storybook build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fcbf9569f57..d9eded3f175 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,9 +6,49 @@ on: - dev pull_request: workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: - test: - name: test (${{ matrix.settings.name }}) + unit: + name: unit (${{ 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 }} + defaults: + run: + shell: bash + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Configure git identity + run: | + git config --global user.email "bot@opencode.ai" + git config --global user.name "opencode" + + - name: Run unit tests + run: bun turbo test + + e2e: + name: e2e (${{ matrix.settings.name }}) + needs: unit strategy: fail-fast: false matrix: @@ -16,18 +56,12 @@ jobs: - name: linux host: blacksmith-4vcpu-ubuntu-2404 playwright: bunx playwright install --with-deps - workdir: . - command: | - git config --global user.email "bot@opencode.ai" - git config --global user.name "opencode" - bun turbo typecheck - bun turbo test - name: windows - host: windows-latest + host: blacksmith-4vcpu-windows-2025 playwright: bunx playwright install - workdir: packages/app - command: bun test:e2e:local runs-on: ${{ matrix.settings.host }} + env: + PLAYWRIGHT_BROWSERS_PATH: 0 defaults: run: shell: bash @@ -44,95 +78,10 @@ jobs: working-directory: packages/app run: ${{ matrix.settings.playwright }} - - name: Set OS-specific paths - run: | - if [ "${{ runner.os }}" = "Windows" ]; then - printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}\\opencode-e2e" >> "$GITHUB_ENV" - printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}\\opencode-e2e\\home" >> "$GITHUB_ENV" - printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}\\opencode-e2e\\share" >> "$GITHUB_ENV" - printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}\\opencode-e2e\\cache" >> "$GITHUB_ENV" - printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}\\opencode-e2e\\config" >> "$GITHUB_ENV" - printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}\\opencode-e2e\\state" >> "$GITHUB_ENV" - printf '%s\n' "MODELS_DEV_API_JSON=${{ github.workspace }}\\packages\\opencode\\test\\tool\\fixtures\\models-api.json" >> "$GITHUB_ENV" - else - printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}/opencode-e2e" >> "$GITHUB_ENV" - printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}/opencode-e2e/home" >> "$GITHUB_ENV" - printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}/opencode-e2e/share" >> "$GITHUB_ENV" - printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}/opencode-e2e/cache" >> "$GITHUB_ENV" - printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}/opencode-e2e/config" >> "$GITHUB_ENV" - printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}/opencode-e2e/state" >> "$GITHUB_ENV" - printf '%s\n' "MODELS_DEV_API_JSON=${{ github.workspace }}/packages/opencode/test/tool/fixtures/models-api.json" >> "$GITHUB_ENV" - fi - - - name: Seed opencode data - if: matrix.settings.name != 'windows' - working-directory: packages/opencode - run: bun script/seed-e2e.ts - env: - MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }} - OPENCODE_DISABLE_MODELS_FETCH: "true" - OPENCODE_DISABLE_SHARE: "true" - OPENCODE_DISABLE_LSP_DOWNLOAD: "true" - OPENCODE_DISABLE_DEFAULT_PLUGINS: "true" - OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" - OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }} - XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }} - XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }} - XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }} - XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }} - OPENCODE_E2E_PROJECT_DIR: ${{ github.workspace }} - OPENCODE_E2E_SESSION_TITLE: "E2E Session" - OPENCODE_E2E_MESSAGE: "Seeded for UI e2e" - OPENCODE_E2E_MODEL: "opencode/gpt-5-nano" - - - name: Run opencode server - if: matrix.settings.name != 'windows' - working-directory: packages/opencode - run: bun dev -- --print-logs --log-level WARN serve --port 4096 --hostname 127.0.0.1 & - env: - MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }} - OPENCODE_DISABLE_MODELS_FETCH: "true" - OPENCODE_DISABLE_SHARE: "true" - OPENCODE_DISABLE_LSP_DOWNLOAD: "true" - OPENCODE_DISABLE_DEFAULT_PLUGINS: "true" - OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" - OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }} - XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }} - XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }} - XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }} - XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }} - OPENCODE_CLIENT: "app" - - - name: Wait for opencode server - if: matrix.settings.name != 'windows' - run: | - for i in {1..120}; do - curl -fsS "http://127.0.0.1:4096/global/health" > /dev/null && exit 0 - sleep 1 - done - exit 1 - - - name: run - working-directory: ${{ matrix.settings.workdir }} - run: ${{ matrix.settings.command }} + - name: Run app e2e tests + run: bun --cwd packages/app test:e2e:local env: CI: true - MODELS_DEV_API_JSON: ${{ env.MODELS_DEV_API_JSON }} - OPENCODE_DISABLE_MODELS_FETCH: "true" - OPENCODE_DISABLE_SHARE: "true" - OPENCODE_DISABLE_LSP_DOWNLOAD: "true" - OPENCODE_DISABLE_DEFAULT_PLUGINS: "true" - OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" - OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }} - XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }} - XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }} - XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }} - XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }} - PLAYWRIGHT_SERVER_HOST: "127.0.0.1" - PLAYWRIGHT_SERVER_PORT: "4096" - VITE_OPENCODE_SERVER_HOST: "127.0.0.1" - VITE_OPENCODE_SERVER_PORT: "4096" - OPENCODE_CLIENT: "app" timeout-minutes: 30 - name: Upload Playwright artifacts diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 6e150957291..99e7b5b34fe 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -1,4 +1,4 @@ -name: Issue Triage +name: triage on: issues: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 011e23f5f6f..b247d24b40d 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -1,6 +1,8 @@ name: typecheck on: + push: + branches: [dev] pull_request: branches: [dev] workflow_dispatch: diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml deleted file mode 100644 index 7175f4fbdd6..00000000000 --- a/.github/workflows/update-nix-hashes.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: Update Nix Hashes - -permissions: - contents: write - -on: - workflow_dispatch: - push: - paths: - - "bun.lock" - - "package.json" - - "packages/*/package.json" - - "flake.lock" - - ".github/workflows/update-nix-hashes.yml" - pull_request: - paths: - - "bun.lock" - - "package.json" - - "packages/*/package.json" - - "flake.lock" - - ".github/workflows/update-nix-hashes.yml" - -jobs: - update-node-modules-hashes: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: blacksmith-4vcpu-ubuntu-2404 - env: - TITLE: node_modules hashes - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - ref: ${{ github.head_ref || github.ref_name }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - - - name: Setup Nix - uses: nixbuild/nix-quick-install-action@v34 - - - name: Configure git - run: | - git config --global user.email "action@github.com" - git config --global user.name "Github Action" - - - name: Pull latest changes - env: - TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} - run: | - BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" - git pull --rebase --autostash origin "$BRANCH" - - - name: Compute all node_modules hashes - run: | - set -euo pipefail - - HASH_FILE="nix/hashes.json" - SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" - - if [ ! -f "$HASH_FILE" ]; then - mkdir -p "$(dirname "$HASH_FILE")" - echo '{"nodeModules":{}}' > "$HASH_FILE" - fi - - for SYSTEM in $SYSTEMS; do - echo "Computing hash for ${SYSTEM}..." - BUILD_LOG=$(mktemp) - trap 'rm -f "$BUILD_LOG"' EXIT - - # The updater derivations use fakeHash, so they will fail and reveal the correct hash - UPDATER_ATTR=".#packages.x86_64-linux.${SYSTEM}_node_modules" - - nix build "$UPDATER_ATTR" --no-link 2>&1 | tee "$BUILD_LOG" || true - - CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" - - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" - fi - - if [ -z "$CORRECT_HASH" ]; then - echo "Failed to determine correct node_modules hash for ${SYSTEM}." - cat "$BUILD_LOG" - exit 1 - fi - - echo " ${SYSTEM}: ${CORRECT_HASH}" - jq --arg sys "$SYSTEM" --arg h "$CORRECT_HASH" \ - '.nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp" - mv "${HASH_FILE}.tmp" "$HASH_FILE" - done - - echo "All hashes computed:" - cat "$HASH_FILE" - - - name: Commit ${{ env.TITLE }} changes - env: - TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} - run: | - set -euo pipefail - - HASH_FILE="nix/hashes.json" - echo "Checking for changes..." - - summarize() { - local status="$1" - { - echo "### Nix $TITLE" - echo "" - echo "- ref: ${GITHUB_REF_NAME}" - echo "- status: ${status}" - } >> "$GITHUB_STEP_SUMMARY" - if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then - echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY" - fi - echo "" >> "$GITHUB_STEP_SUMMARY" - } - - FILES=("$HASH_FILE") - STATUS="$(git status --short -- "${FILES[@]}" || true)" - if [ -z "$STATUS" ]; then - echo "No changes detected." - summarize "no changes" - exit 0 - fi - - echo "Changes detected:" - echo "$STATUS" - git add "${FILES[@]}" - git commit -m "chore: update nix node_modules hashes" - - BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" - git pull --rebase --autostash origin "$BRANCH" - git push origin HEAD:"$BRANCH" - echo "Changes pushed successfully" - - summarize "committed $(git rev-parse --short HEAD)" diff --git a/.github/workflows/vouch-check-issue.yml b/.github/workflows/vouch-check-issue.yml new file mode 100644 index 00000000000..4c2aa960b2a --- /dev/null +++ b/.github/workflows/vouch-check-issue.yml @@ -0,0 +1,116 @@ +name: vouch-check-issue + +on: + issues: + types: [opened] + +permissions: + contents: read + issues: write + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Check if issue author is denounced + uses: actions/github-script@v7 + with: + script: | + const author = context.payload.issue.user.login; + const issueNumber = context.payload.issue.number; + + // Skip bots + if (author.endsWith('[bot]')) { + core.info(`Skipping bot: ${author}`); + return; + } + + // Read the VOUCHED.td file via API (no checkout needed) + let content; + try { + const response = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: '.github/VOUCHED.td', + }); + content = Buffer.from(response.data.content, 'base64').toString('utf-8'); + } catch (error) { + if (error.status === 404) { + core.info('No .github/VOUCHED.td file found, skipping check.'); + return; + } + throw error; + } + + // Parse the .td file for vouched and denounced users + const vouched = new Set(); + const denounced = new Map(); + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + + const isDenounced = trimmed.startsWith('-'); + const rest = isDenounced ? trimmed.slice(1).trim() : trimmed; + if (!rest) continue; + + const spaceIdx = rest.indexOf(' '); + const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx); + const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim(); + + // Handle platform:username or bare username + // Only match bare usernames or github: prefix (skip other platforms) + const colonIdx = handle.indexOf(':'); + if (colonIdx !== -1) { + const platform = handle.slice(0, colonIdx).toLowerCase(); + if (platform !== 'github') continue; + } + const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1); + if (!username) continue; + + if (isDenounced) { + denounced.set(username.toLowerCase(), reason); + continue; + } + + vouched.add(username.toLowerCase()); + } + + // Check if the author is denounced + const reason = denounced.get(author.toLowerCase()); + if (reason !== undefined) { + // Author is denounced — close the issue + const body = 'This issue has been automatically closed.'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body, + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + state: 'closed', + state_reason: 'not_planned', + }); + + core.info(`Closed issue #${issueNumber} from denounced user ${author}`); + return; + } + + // Author is positively vouched — add label + if (!vouched.has(author.toLowerCase())) { + core.info(`User ${author} is not denounced or vouched. Allowing issue.`); + return; + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['Vouched'], + }); + + core.info(`Added vouched label to issue #${issueNumber} from ${author}`); diff --git a/.github/workflows/vouch-check-pr.yml b/.github/workflows/vouch-check-pr.yml new file mode 100644 index 00000000000..51816dfb759 --- /dev/null +++ b/.github/workflows/vouch-check-pr.yml @@ -0,0 +1,114 @@ +name: vouch-check-pr + +on: + pull_request_target: + types: [opened] + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Check if PR author is denounced + uses: actions/github-script@v7 + with: + script: | + const author = context.payload.pull_request.user.login; + const prNumber = context.payload.pull_request.number; + + // Skip bots + if (author.endsWith('[bot]')) { + core.info(`Skipping bot: ${author}`); + return; + } + + // Read the VOUCHED.td file via API (no checkout needed) + let content; + try { + const response = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: '.github/VOUCHED.td', + }); + content = Buffer.from(response.data.content, 'base64').toString('utf-8'); + } catch (error) { + if (error.status === 404) { + core.info('No .github/VOUCHED.td file found, skipping check.'); + return; + } + throw error; + } + + // Parse the .td file for vouched and denounced users + const vouched = new Set(); + const denounced = new Map(); + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + + const isDenounced = trimmed.startsWith('-'); + const rest = isDenounced ? trimmed.slice(1).trim() : trimmed; + if (!rest) continue; + + const spaceIdx = rest.indexOf(' '); + const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx); + const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim(); + + // Handle platform:username or bare username + // Only match bare usernames or github: prefix (skip other platforms) + const colonIdx = handle.indexOf(':'); + if (colonIdx !== -1) { + const platform = handle.slice(0, colonIdx).toLowerCase(); + if (platform !== 'github') continue; + } + const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1); + if (!username) continue; + + if (isDenounced) { + denounced.set(username.toLowerCase(), reason); + continue; + } + + vouched.add(username.toLowerCase()); + } + + // Check if the author is denounced + const reason = denounced.get(author.toLowerCase()); + if (reason !== undefined) { + // Author is denounced — close the PR + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: 'This pull request has been automatically closed.', + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'closed', + }); + + core.info(`Closed PR #${prNumber} from denounced user ${author}`); + return; + } + + // Author is positively vouched — add label + if (!vouched.has(author.toLowerCase())) { + core.info(`User ${author} is not denounced or vouched. Allowing PR.`); + return; + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ['Vouched'], + }); + + core.info(`Added vouched label to PR #${prNumber} from ${author}`); diff --git a/.github/workflows/vouch-manage-by-issue.yml b/.github/workflows/vouch-manage-by-issue.yml new file mode 100644 index 00000000000..9604bf87f37 --- /dev/null +++ b/.github/workflows/vouch-manage-by-issue.yml @@ -0,0 +1,38 @@ +name: vouch-manage-by-issue + +on: + issue_comment: + types: [created] + +concurrency: + group: vouch-manage + cancel-in-progress: false + +permissions: + contents: write + issues: write + pull-requests: read + +jobs: + manage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Setup git committer + id: committer + uses: ./.github/actions/setup-git-committer + with: + opencode-app-id: ${{ vars.OPENCODE_APP_ID }} + opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + + - uses: mitchellh/vouch/action/manage-by-issue@main + with: + issue-id: ${{ github.event.issue.number }} + comment-id: ${{ github.event.comment.id }} + roles: admin,maintain + env: + GITHUB_TOKEN: ${{ steps.committer.outputs.token }} diff --git a/.gitignore b/.gitignore index 78a77f81982..bf78c046d4b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules .env .idea .vscode +.codex *~ playground tmp @@ -26,3 +27,4 @@ target opencode-dev logs/ *.bun-build +tsconfig.tsbuildinfo diff --git a/.opencode/.gitignore b/.opencode/.gitignore index 00bfdfda298..03445edaf20 100644 --- a/.opencode/.gitignore +++ b/.opencode/.gitignore @@ -1,3 +1,4 @@ plans/ bun.lock package.json +package-lock.json diff --git a/.opencode/agent/translator.md b/.opencode/agent/translator.md new file mode 100644 index 00000000000..263afbe9b5e --- /dev/null +++ b/.opencode/agent/translator.md @@ -0,0 +1,900 @@ +--- +description: Translate content for a specified locale while preserving technical terms +mode: subagent +model: opencode/gemini-3-pro +--- + +You are a professional translator and localization specialist. + +Translate the user's content into the requested target locale (language + region, e.g. fr-FR, de-DE). + +Requirements: + +- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure). +- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks. +- Also preserve every term listed in the Do-Not-Translate glossary below. +- Also apply locale-specific guidance from `.opencode/glossary/.md` when available (for example, `zh-cn.md`). +- Do not modify fenced code blocks. +- Output ONLY the translation (no commentary). + +If the target locale is missing, ask the user to provide it. +If no locale-specific glossary exists, use the global glossary only. + +--- + +# Locale-Specific Glossaries + +When a locale glossary exists, use it to: + +- Apply preferred wording for recurring UI/docs terms in that locale +- Preserve locale-specific do-not-translate terms and casing decisions +- Prefer natural phrasing over literal translation when the locale file calls it out +- If the repo uses a locale alias slug, apply that file too (for example, `pt-BR` maps to `br.md` in this repo) + +Locale guidance does not override code/command preservation rules or the global Do-Not-Translate glossary below. + +--- + +# Do-Not-Translate Terms (OpenCode Docs) + +Generated from: `packages/web/src/content/docs/*.mdx` (default English docs) +Generated on: 2026-02-10 + +Use this as a translation QA checklist / glossary. Preserve listed terms exactly (spelling, casing, punctuation). + +General rules (verbatim, even if not listed below): + +- Anything inside inline code (single backticks) or fenced code blocks (triple backticks) +- MDX/JS code in docs: `import ... from "..."`, component tags, identifiers +- CLI commands, flags, config keys/values, file paths, URLs/domains, and env vars + +## Proper nouns and product names + +Additional (not reliably captured via link text): + +```text +Astro +Bun +Chocolatey +Cursor +Docker +Git +GitHub Actions +GitLab CI +GNOME Terminal +Homebrew +Mise +Neovim +Node.js +npm +Obsidian +opencode +opencode-ai +Paru +pnpm +ripgrep +Scoop +SST +Starlight +Visual Studio Code +VS Code +VSCodium +Windsurf +Windows Terminal +Yarn +Zellij +Zed +anomalyco +``` + +Extracted from link labels in the English docs (review and prune as desired): + +```text +@openspoon/subtask2 +302.AI console +ACP progress report +Agent Client Protocol +Agent Skills +Agentic +AGENTS.md +AI SDK +Alacritty +Anthropic +Anthropic's Data Policies +Atom One +Avante.nvim +Ayu +Azure AI Foundry +Azure portal +Baseten +built-in GITHUB_TOKEN +Bun.$ +Catppuccin +Cerebras console +ChatGPT Plus or Pro +Cloudflare dashboard +CodeCompanion.nvim +CodeNomad +Configuring Adapters: Environment Variables +Context7 MCP server +Cortecs console +Deep Infra dashboard +DeepSeek console +Duo Agent Platform +Everforest +Fireworks AI console +Firmware dashboard +Ghostty +GitLab CLI agents docs +GitLab docs +GitLab User Settings > Access Tokens +Granular Rules (Object Syntax) +Grep by Vercel +Groq console +Gruvbox +Helicone +Helicone documentation +Helicone Header Directory +Helicone's Model Directory +Hugging Face Inference Providers +Hugging Face settings +install WSL +IO.NET console +JetBrains IDE +Kanagawa +Kitty +MiniMax API Console +Models.dev +Moonshot AI console +Nebius Token Factory console +Nord +OAuth +Ollama integration docs +OpenAI's Data Policies +OpenChamber +OpenCode +OpenCode config +OpenCode Config +OpenCode TUI with the opencode theme +OpenCode Web - Active Session +OpenCode Web - New Session +OpenCode Web - See Servers +OpenCode Zen +OpenCode-Obsidian +OpenRouter dashboard +OpenWork +OVHcloud panel +Pro+ subscription +SAP BTP Cockpit +Scaleway Console IAM settings +Scaleway Generative APIs +SDK documentation +Sentry MCP server +shell API +Together AI console +Tokyonight +Unified Billing +Venice AI console +Vercel dashboard +WezTerm +Windows Subsystem for Linux (WSL) +WSL +WSL (Windows Subsystem for Linux) +WSL extension +xAI console +Z.AI API console +Zed +ZenMux dashboard +Zod +``` + +## Acronyms and initialisms + +```text +ACP +AGENTS +AI +AI21 +ANSI +API +AST +AWS +BTP +CD +CDN +CI +CLI +CMD +CORS +DEBUG +EKS +ERROR +FAQ +GLM +GNOME +GPT +HTML +HTTP +HTTPS +IAM +ID +IDE +INFO +IO +IP +IRSA +JS +JSON +JSONC +K2 +LLM +LM +LSP +M2 +MCP +MR +NET +NPM +NTLM +OIDC +OS +PAT +PATH +PHP +PR +PTY +README +RFC +RPC +SAP +SDK +SKILL +SSE +SSO +TS +TTY +TUI +UI +URL +US +UX +VCS +VPC +VPN +VS +WARN +WSL +X11 +YAML +``` + +## Code identifiers used in prose (CamelCase, mixedCase) + +```text +apiKey +AppleScript +AssistantMessage +baseURL +BurntSushi +ChatGPT +ClangFormat +CodeCompanion +CodeNomad +DeepSeek +DefaultV2 +FileContent +FileDiff +FileNode +fineGrained +FormatterStatus +GitHub +GitLab +iTerm2 +JavaScript +JetBrains +macOS +mDNS +MiniMax +NeuralNomadsAI +NickvanDyke +NoeFabris +OpenAI +OpenAPI +OpenChamber +OpenCode +OpenRouter +OpenTUI +OpenWork +ownUserPermissions +PowerShell +ProviderAuthAuthorization +ProviderAuthMethod +ProviderInitError +SessionStatus +TabItem +tokenType +ToolIDs +ToolList +TypeScript +typesUrl +UserMessage +VcsInfo +WebView2 +WezTerm +xAI +ZenMux +``` + +## OpenCode CLI commands (as shown in docs) + +```text +opencode +opencode [project] +opencode /path/to/project +opencode acp +opencode agent [command] +opencode agent create +opencode agent list +opencode attach [url] +opencode attach http://10.20.30.40:4096 +opencode attach http://localhost:4096 +opencode auth [command] +opencode auth list +opencode auth login +opencode auth logout +opencode auth ls +opencode export [sessionID] +opencode github [command] +opencode github install +opencode github run +opencode import +opencode import https://opncd.ai/s/abc123 +opencode import session.json +opencode mcp [command] +opencode mcp add +opencode mcp auth [name] +opencode mcp auth list +opencode mcp auth ls +opencode mcp auth my-oauth-server +opencode mcp auth sentry +opencode mcp debug +opencode mcp debug my-oauth-server +opencode mcp list +opencode mcp logout [name] +opencode mcp logout my-oauth-server +opencode mcp ls +opencode models --refresh +opencode models [provider] +opencode models anthropic +opencode run [message..] +opencode run Explain the use of context in Go +opencode serve +opencode serve --cors http://localhost:5173 --cors https://app.example.com +opencode serve --hostname 0.0.0.0 --port 4096 +opencode serve [--port ] [--hostname ] [--cors ] +opencode session [command] +opencode session list +opencode session delete +opencode stats +opencode uninstall +opencode upgrade +opencode upgrade [target] +opencode upgrade v0.1.48 +opencode web +opencode web --cors https://example.com +opencode web --hostname 0.0.0.0 +opencode web --mdns +opencode web --mdns --mdns-domain myproject.local +opencode web --port 4096 +opencode web --port 4096 --hostname 0.0.0.0 +opencode.server.close() +``` + +## Slash commands and routes + +```text +/agent +/auth/:id +/clear +/command +/config +/config/providers +/connect +/continue +/doc +/editor +/event +/experimental/tool?provider=

&model= +/experimental/tool/ids +/export +/file?path= +/file/content?path=

+/file/status +/find?pattern= +/find/file +/find/file?query= +/find/symbol?query= +/formatter +/global/event +/global/health +/help +/init +/instance/dispose +/log +/lsp +/mcp +/mnt/ +/mnt/c/ +/mnt/d/ +/models +/oc +/opencode +/path +/project +/project/current +/provider +/provider/{id}/oauth/authorize +/provider/{id}/oauth/callback +/provider/auth +/q +/quit +/redo +/resume +/session +/session/:id +/session/:id/abort +/session/:id/children +/session/:id/command +/session/:id/diff +/session/:id/fork +/session/:id/init +/session/:id/message +/session/:id/message/:messageID +/session/:id/permissions/:permissionID +/session/:id/prompt_async +/session/:id/revert +/session/:id/share +/session/:id/shell +/session/:id/summarize +/session/:id/todo +/session/:id/unrevert +/session/status +/share +/summarize +/theme +/tui +/tui/append-prompt +/tui/clear-prompt +/tui/control/next +/tui/control/response +/tui/execute-command +/tui/open-help +/tui/open-models +/tui/open-sessions +/tui/open-themes +/tui/show-toast +/tui/submit-prompt +/undo +/Users/username +/Users/username/projects/* +/vcs +``` + +## CLI flags and short options + +```text +--agent +--attach +--command +--continue +--cors +--cwd +--days +--dir +--dry-run +--event +--file +--force +--fork +--format +--help +--hostname +--hostname 0.0.0.0 +--keep-config +--keep-data +--log-level +--max-count +--mdns +--mdns-domain +--method +--model +--models +--port +--print-logs +--project +--prompt +--refresh +--session +--share +--title +--token +--tools +--verbose +--version +--wait + +-c +-d +-f +-h +-m +-n +-s +-v +``` + +## Environment variables + +```text +AI_API_URL +AI_FLOW_CONTEXT +AI_FLOW_EVENT +AI_FLOW_INPUT +AICORE_DEPLOYMENT_ID +AICORE_RESOURCE_GROUP +AICORE_SERVICE_KEY +ANTHROPIC_API_KEY +AWS_ACCESS_KEY_ID +AWS_BEARER_TOKEN_BEDROCK +AWS_PROFILE +AWS_REGION +AWS_ROLE_ARN +AWS_SECRET_ACCESS_KEY +AWS_WEB_IDENTITY_TOKEN_FILE +AZURE_COGNITIVE_SERVICES_RESOURCE_NAME +AZURE_RESOURCE_NAME +CI_PROJECT_DIR +CI_SERVER_FQDN +CI_WORKLOAD_REF +CLOUDFLARE_ACCOUNT_ID +CLOUDFLARE_API_TOKEN +CLOUDFLARE_GATEWAY_ID +CONTEXT7_API_KEY +GITHUB_TOKEN +GITLAB_AI_GATEWAY_URL +GITLAB_HOST +GITLAB_INSTANCE_URL +GITLAB_OAUTH_CLIENT_ID +GITLAB_TOKEN +GITLAB_TOKEN_OPENCODE +GOOGLE_APPLICATION_CREDENTIALS +GOOGLE_CLOUD_PROJECT +HTTP_PROXY +HTTPS_PROXY +K2_ +MY_API_KEY +MY_ENV_VAR +MY_MCP_CLIENT_ID +MY_MCP_CLIENT_SECRET +NO_PROXY +NODE_ENV +NODE_EXTRA_CA_CERTS +NPM_AUTH_TOKEN +OC_ALLOW_WAYLAND +OPENCODE_API_KEY +OPENCODE_AUTH_JSON +OPENCODE_AUTO_SHARE +OPENCODE_CLIENT +OPENCODE_CONFIG +OPENCODE_CONFIG_CONTENT +OPENCODE_CONFIG_DIR +OPENCODE_DISABLE_AUTOCOMPACT +OPENCODE_DISABLE_AUTOUPDATE +OPENCODE_DISABLE_CLAUDE_CODE +OPENCODE_DISABLE_CLAUDE_CODE_PROMPT +OPENCODE_DISABLE_CLAUDE_CODE_SKILLS +OPENCODE_DISABLE_DEFAULT_PLUGINS +OPENCODE_DISABLE_FILETIME_CHECK +OPENCODE_DISABLE_LSP_DOWNLOAD +OPENCODE_DISABLE_MODELS_FETCH +OPENCODE_DISABLE_PRUNE +OPENCODE_DISABLE_TERMINAL_TITLE +OPENCODE_ENABLE_EXA +OPENCODE_ENABLE_EXPERIMENTAL_MODELS +OPENCODE_EXPERIMENTAL +OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS +OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT +OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER +OPENCODE_EXPERIMENTAL_EXA +OPENCODE_EXPERIMENTAL_FILEWATCHER +OPENCODE_EXPERIMENTAL_ICON_DISCOVERY +OPENCODE_EXPERIMENTAL_LSP_TOOL +OPENCODE_EXPERIMENTAL_LSP_TY +OPENCODE_EXPERIMENTAL_MARKDOWN +OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX +OPENCODE_EXPERIMENTAL_OXFMT +OPENCODE_EXPERIMENTAL_PLAN_MODE +OPENCODE_ENABLE_QUESTION_TOOL +OPENCODE_FAKE_VCS +OPENCODE_GIT_BASH_PATH +OPENCODE_MODEL +OPENCODE_MODELS_URL +OPENCODE_PERMISSION +OPENCODE_PORT +OPENCODE_SERVER_PASSWORD +OPENCODE_SERVER_USERNAME +PROJECT_ROOT +RESOURCE_NAME +RUST_LOG +VARIABLE_NAME +VERTEX_LOCATION +XDG_CONFIG_HOME +``` + +## Package/module identifiers + +```text +../../../config.mjs +@astrojs/starlight/components +@opencode-ai/plugin +@opencode-ai/sdk +path +shescape +zod + +@ +@ai-sdk/anthropic +@ai-sdk/cerebras +@ai-sdk/google +@ai-sdk/openai +@ai-sdk/openai-compatible +@File#L37-42 +@modelcontextprotocol/server-everything +@opencode +``` + +## GitHub owner/repo slugs referenced in docs + +```text +24601/opencode-zellij-namer +angristan/opencode-wakatime +anomalyco/opencode +apps/opencode-agent +athal7/opencode-devcontainers +awesome-opencode/awesome-opencode +backnotprop/plannotator +ben-vargas/ai-sdk-provider-opencode-sdk +btriapitsyn/openchamber +BurntSushi/ripgrep +Cluster444/agentic +code-yeongyu/oh-my-opencode +darrenhinde/opencode-agents +different-ai/opencode-scheduler +different-ai/openwork +features/copilot +folke/tokyonight.nvim +franlol/opencode-md-table-formatter +ggml-org/llama.cpp +ghoulr/opencode-websearch-cited.git +H2Shami/opencode-helicone-session +hosenur/portal +jamesmurdza/daytona +jenslys/opencode-gemini-auth +JRedeker/opencode-morph-fast-apply +JRedeker/opencode-shell-strategy +kdcokenny/ocx +kdcokenny/opencode-background-agents +kdcokenny/opencode-notify +kdcokenny/opencode-workspace +kdcokenny/opencode-worktree +login/device +mohak34/opencode-notifier +morhetz/gruvbox +mtymek/opencode-obsidian +NeuralNomadsAI/CodeNomad +nick-vi/opencode-type-inject +NickvanDyke/opencode.nvim +NoeFabris/opencode-antigravity-auth +nordtheme/nord +numman-ali/opencode-openai-codex-auth +olimorris/codecompanion.nvim +panta82/opencode-notificator +rebelot/kanagawa.nvim +remorses/kimaki +sainnhe/everforest +shekohex/opencode-google-antigravity-auth +shekohex/opencode-pty.git +spoons-and-mirrors/subtask2 +sudo-tee/opencode.nvim +supermemoryai/opencode-supermemory +Tarquinen/opencode-dynamic-context-pruning +Th3Whit3Wolf/one-nvim +upstash/context7 +vtemian/micode +vtemian/octto +yetone/avante.nvim +zenobi-us/opencode-plugin-template +zenobi-us/opencode-skillful +``` + +## Paths, filenames, globs, and URLs + +```text +./.opencode/themes/*.json +.//storage/ +./config/#custom-directory +./global/storage/ +.agents/skills/*/SKILL.md +.agents/skills//SKILL.md +.clang-format +.claude +.claude/skills +.claude/skills/*/SKILL.md +.claude/skills//SKILL.md +.env +.github/workflows/opencode.yml +.gitignore +.gitlab-ci.yml +.ignore +.NET SDK +.npmrc +.ocamlformat +.opencode +.opencode/ +.opencode/agents/ +.opencode/commands/ +.opencode/commands/test.md +.opencode/modes/ +.opencode/plans/*.md +.opencode/plugins/ +.opencode/skills//SKILL.md +.opencode/skills/git-release/SKILL.md +.opencode/tools/ +.well-known/opencode +{ type: "raw" \| "patch", content: string } +{file:path/to/file} +**/*.js +%USERPROFILE%/intelephense/license.txt +%USERPROFILE%\.cache\opencode +%USERPROFILE%\.config\opencode\opencode.jsonc +%USERPROFILE%\.config\opencode\plugins +%USERPROFILE%\.local\share\opencode +%USERPROFILE%\.local\share\opencode\log +/.opencode/themes/*.json +/ +/.opencode/plugins/ +~ +~/... +~/.agents/skills/*/SKILL.md +~/.agents/skills//SKILL.md +~/.aws/credentials +~/.bashrc +~/.cache/opencode +~/.cache/opencode/node_modules/ +~/.claude/CLAUDE.md +~/.claude/skills/ +~/.claude/skills/*/SKILL.md +~/.claude/skills//SKILL.md +~/.config/opencode +~/.config/opencode/AGENTS.md +~/.config/opencode/agents/ +~/.config/opencode/commands/ +~/.config/opencode/modes/ +~/.config/opencode/opencode.json +~/.config/opencode/opencode.jsonc +~/.config/opencode/plugins/ +~/.config/opencode/skills/*/SKILL.md +~/.config/opencode/skills//SKILL.md +~/.config/opencode/themes/*.json +~/.config/opencode/tools/ +~/.config/zed/settings.json +~/.local/share +~/.local/share/opencode/ +~/.local/share/opencode/auth.json +~/.local/share/opencode/log/ +~/.local/share/opencode/mcp-auth.json +~/.local/share/opencode/opencode.jsonc +~/.npmrc +~/.zshrc +~/code/ +~/Library/Application Support +~/projects/* +~/projects/personal/ +${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts +$HOME/intelephense/license.txt +$HOME/projects/* +$XDG_CONFIG_HOME/opencode/themes/*.json +agent/ +agents/ +build/ +commands/ +dist/ +http://:4096 +http://127.0.0.1:8080/callback +http://localhost: +http://localhost:4096 +http://localhost:4096/doc +https://app.example.com +https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/ +https://opencode.ai/zen/v1/chat/completions +https://opencode.ai/zen/v1/messages +https://opencode.ai/zen/v1/models/gemini-3-flash +https://opencode.ai/zen/v1/models/gemini-3-pro +https://opencode.ai/zen/v1/responses +https://RESOURCE_NAME.openai.azure.com/ +laravel/pint +log/ +model: "anthropic/claude-sonnet-4-5" +modes/ +node_modules/ +openai/gpt-4.1 +opencode.ai/config.json +opencode/ +opencode/gpt-5.1-codex +opencode/gpt-5.2-codex +opencode/kimi-k2 +openrouter/google/gemini-2.5-flash +opncd.ai/s/ +packages/*/AGENTS.md +plugins/ +project/ +provider_id/model_id +provider/model +provider/model-id +rm -rf ~/.cache/opencode +skills/ +skills/*/SKILL.md +src/**/*.ts +themes/ +tools/ +``` + +## Keybind strings + +```text +alt+b +Alt+Ctrl+K +alt+d +alt+f +Cmd+Esc +Cmd+Option+K +Cmd+Shift+Esc +Cmd+Shift+G +Cmd+Shift+P +ctrl+a +ctrl+b +ctrl+d +ctrl+e +Ctrl+Esc +ctrl+f +ctrl+g +ctrl+k +Ctrl+Shift+Esc +Ctrl+Shift+P +ctrl+t +ctrl+u +ctrl+w +ctrl+x +DELETE +Shift+Enter +WIN+R +``` + +## Model ID strings referenced + +```text +{env:OPENCODE_MODEL} +anthropic/claude-3-5-sonnet-20241022 +anthropic/claude-haiku-4-20250514 +anthropic/claude-haiku-4-5 +anthropic/claude-sonnet-4-20250514 +anthropic/claude-sonnet-4-5 +gitlab/duo-chat-haiku-4-5 +lmstudio/google/gemma-3n-e4b +openai/gpt-4.1 +openai/gpt-5 +opencode/gpt-5.1-codex +opencode/gpt-5.2-codex +opencode/kimi-k2 +openrouter/google/gemini-2.5-flash +``` diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 5d1147a8859..a77b92737bc 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: opencode/claude-haiku-4-5 +model: opencode/minimax-m2.5 color: "#44BA81" tools: "*": false @@ -12,6 +12,8 @@ You are a triage agent responsible for triaging github issues. Use your github-triage tool to triage issues. +This file is the source of truth for ownership/routing rules. + ## Labels ### windows @@ -43,12 +45,34 @@ Desktop app issues: **Only** add if the issue explicitly mentions nix. +If the issue does not mention nix, do not add nix. + +If the issue mentions nix, assign to `rekram1-node`. + #### zen **Only** add if the issue mentions "zen" or "opencode zen" or "opencode black". If the issue doesn't have "zen" or "opencode black" in it then don't add zen label +#### core + +Use for core server issues in `packages/opencode/`, excluding `packages/opencode/src/cli/cmd/tui/`. + +Examples: + +- LSP server behavior +- Harness behavior (agent + tools) +- Feature requests for server behavior +- Agent context construction +- API endpoints +- Provider integration issues +- New, broken, or poor-quality models + +#### acp + +If the issue mentions acp support, assign acp label. + #### docs Add if the issue requests better documentation or docs updates. @@ -66,13 +90,51 @@ TUI issues potentially caused by our underlying TUI library: When assigning to people here are the following rules: -adamdotdev: -ONLY assign adam if the issue will have the "desktop" label. +Desktop / Web: +Use for desktop-labeled issues only. + +- adamdotdevin +- iamdavidhill +- Brendonovich +- nexxeln + +Zen: +ONLY assign if the issue will have the "zen" label. + +- fwang +- MrMushrooooom + +TUI (`packages/opencode/src/cli/cmd/tui/...`): + +- thdxr for TUI UX/UI product decisions and interaction flow +- kommander for OpenTUI engine issues: rendering artifacts, keybind handling, terminal compatibility, SSH behavior, and low-level perf bottlenecks +- rekram1-node for TUI bugs that are not clearly OpenTUI engine issues + +Core (`packages/opencode/...`, excluding TUI subtree): + +- thdxr for sqlite/snapshot/memory bugs and larger architectural core features +- jlongster for opencode server + API feature work (tool currently remaps jlongster -> thdxr until assignable) +- rekram1-node for harness issues, provider issues, and other bug-squashing + +For core bugs that do not clearly map, either thdxr or rekram1-node is acceptable. + +Docs: + +- R44VC0RP + +Windows: + +- Hona (assign any issue that mentions Windows or is likely Windows-specific) + +Determinism rules: + +- If title + body does not contain "zen", do not add the "zen" label +- If "nix" label is added but title + body does not mention nix/nixos, the tool will drop "nix" +- If title + body mentions nix/nixos, assign to `rekram1-node` +- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner -fwang: -ONLY assign fwang if the issue will have the "zen" label. +In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random. -jayair: -ONLY assign jayair if the issue will have the "docs" label. +ACP: -In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node. +- rekram1-node (assign any acp issues to rekram1-node) diff --git a/.opencode/command/ai-deps.md b/.opencode/command/ai-deps.md index 4d23c76a4d5..83783d5b9be 100644 --- a/.opencode/command/ai-deps.md +++ b/.opencode/command/ai-deps.md @@ -7,7 +7,7 @@ Please read @package.json and @packages/opencode/package.json. Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes). I want a report of every dependency and the version that can be upgraded to. -What would be even better is if you can give me links to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added. +What would be even better is if you can give me brief summary of the changes for each dep and a link to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added. Consider using subagents for each dep to save your context window. diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index 8e9346ebc88..e88932a2448 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -1,6 +1,6 @@ --- description: git commit and push -model: opencode/glm-4.6 +model: opencode/kimi-k2.5 subtask: true --- @@ -16,13 +16,22 @@ wip: For anything in the packages/web use the docs: prefix. -For anything in the packages/app use the ignore: prefix. - prefer to explain WHY something was done from an end user perspective instead of WHAT was done. do not do generic messages like "improved agent experience" be very specific about what user facing changes were made -if there are changes do a git pull --rebase if there are conflicts DO NOT FIX THEM. notify me and I will fix them + +## GIT DIFF + +!`git diff` + +## GIT DIFF --cached + +!`git diff --cached` + +## GIT STATUS --short + +!`git status --short` diff --git a/.opencode/command/learn.md b/.opencode/command/learn.md new file mode 100644 index 00000000000..fe4965a5887 --- /dev/null +++ b/.opencode/command/learn.md @@ -0,0 +1,42 @@ +--- +description: Extract non-obvious learnings from session to AGENTS.md files to build codebase understanding +--- + +Analyze this session and extract non-obvious learnings to add to AGENTS.md files. + +AGENTS.md files can exist at any directory level, not just the project root. When an agent reads a file, any AGENTS.md in parent directories are automatically loaded into the context of the tool read. Place learnings as close to the relevant code as possible: + +- Project-wide learnings → root AGENTS.md +- Package/module-specific → packages/foo/AGENTS.md +- Feature-specific → src/auth/AGENTS.md + +What counts as a learning (non-obvious discoveries only): + +- Hidden relationships between files or modules +- Execution paths that differ from how code appears +- Non-obvious configuration, env vars, or flags +- Debugging breakthroughs when error messages were misleading +- API/tool quirks and workarounds +- Build/test commands not in README +- Architectural decisions and constraints +- Files that must change together + +What NOT to include: + +- Obvious facts from documentation +- Standard language/framework behavior +- Things already in an AGENTS.md +- Verbose explanations +- Session-specific details + +Process: + +1. Review session for discoveries, errors that took multiple attempts, unexpected connections +2. Determine scope - what directory does each learning apply to? +3. Read existing AGENTS.md files at relevant levels +4. Create or update AGENTS.md at the appropriate level +5. Keep entries to 1-3 lines per insight + +After updating, summarize which AGENTS.md files were created/updated and how many learnings per file. + +$ARGUMENTS diff --git a/.opencode/glossary/README.md b/.opencode/glossary/README.md new file mode 100644 index 00000000000..983900381ca --- /dev/null +++ b/.opencode/glossary/README.md @@ -0,0 +1,63 @@ +# Locale Glossaries + +Use this folder for locale-specific translation guidance that supplements `.opencode/agent/translator.md`. + +The global glossary in `translator.md` remains the source of truth for shared do-not-translate terms (commands, code, paths, product names, etc.). These locale files capture community learnings about phrasing and terminology preferences. + +## File Naming + +- One file per locale +- Use lowercase locale slugs that match docs locales when possible (for example, `zh-cn.md`, `zh-tw.md`) +- If only language-level guidance exists, use the language code (for example, `fr.md`) +- Some repo locale slugs may be aliases/non-BCP47 for consistency (for example, `br` for Brazilian Portuguese / `pt-BR`) + +## What To Put In A Locale File + +- **Sources**: PRs/issues/discussions that motivated the guidance +- **Do Not Translate (Locale Additions)**: locale-specific terms or casing decisions +- **Preferred Terms**: recurring UI/docs words with preferred translations +- **Guidance**: tone, style, and consistency notes +- **Avoid** (optional): common literal translations or wording we should avoid +- If the repo uses a locale alias slug, document the alias in **Guidance** (for example, prose may mention `pt-BR` while config/examples use `br`) + +Prefer guidance that is: + +- Repeated across multiple docs/screens +- Easy to apply consistently +- Backed by a community contribution or review discussion + +## Template + +```md +# Glossary + +## Sources + +- PR #12345: https://github.com/anomalyco/opencode/pull/12345 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing) + +## Preferred Terms + +| English | Preferred | Notes | +| ------- | --------- | --------- | +| prompt | ... | preferred | +| session | ... | preferred | + +## Guidance + +- Prefer natural phrasing over literal translation + +## Avoid + +- Avoid ... when ... +``` + +## Contribution Notes + +- Mark entries as preferred when they may evolve +- Keep examples short +- Add or update the `Sources` section whenever you add a new rule +- Prefer PR-backed guidance over invented term mappings; start with general guidance if no term-level corrections exist yet diff --git a/.opencode/glossary/ar.md b/.opencode/glossary/ar.md new file mode 100644 index 00000000000..37355522a0a --- /dev/null +++ b/.opencode/glossary/ar.md @@ -0,0 +1,28 @@ +# ar Glossary + +## Sources + +- PR #9947: https://github.com/anomalyco/opencode/pull/9947 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Arabic phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- For RTL text, treat code, commands, and paths as LTR artifacts and keep their character order unchanged + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Arabic terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/glossary/br.md b/.opencode/glossary/br.md new file mode 100644 index 00000000000..fd3e7251cd9 --- /dev/null +++ b/.opencode/glossary/br.md @@ -0,0 +1,34 @@ +# br Glossary + +## Sources + +- PR #10086: https://github.com/anomalyco/opencode/pull/10086 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Locale code `br` in repo config, code, and paths (repo alias for Brazilian Portuguese) + +## Preferred Terms + +These are PR-backed locale naming preferences and may evolve. + +| English / Context | Preferred | Notes | +| ---------------------------------------- | ------------------------------ | ------------------------------------------------------------- | +| Brazilian Portuguese (prose locale name) | `pt-BR` | Use standard locale naming in prose when helpful | +| Repo locale slug (code/config) | `br` | PR #10086 uses `br` for consistency/simplicity | +| Browser locale detection | `pt`, `pt-br`, `pt-BR` -> `br` | Preserve this mapping in docs/examples about locale detection | + +## Guidance + +- This file covers Brazilian Portuguese (`pt-BR`), but the repo locale code is `br` +- Use natural Brazilian Portuguese phrasing over literal translation +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- Keep repo locale identifiers as implemented in code/config (`br`) even when prose mentions `pt-BR` + +## Avoid + +- Avoid changing repo locale code references from `br` to `pt-br` in code snippets, paths, or config examples +- Avoid mixing Portuguese variants when a Brazilian Portuguese form is established diff --git a/.opencode/glossary/bs.md b/.opencode/glossary/bs.md new file mode 100644 index 00000000000..aa3bd96f6f9 --- /dev/null +++ b/.opencode/glossary/bs.md @@ -0,0 +1,33 @@ +# bs Glossary + +## Sources + +- PR #12283: https://github.com/anomalyco/opencode/pull/12283 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +These are PR-backed locale naming preferences and may evolve. + +| English / Context | Preferred | Notes | +| ---------------------------------- | ---------- | ------------------------------------------------- | +| Bosnian language label (UI) | `Bosanski` | PR #12283 tested switching language to `Bosanski` | +| Repo locale slug (code/config) | `bs` | Preserve in code, config, paths, and examples | +| Browser locale detection (Bosnian) | `bs` | PR #12283 added `bs` locale auto-detection | + +## Guidance + +- Use natural Bosnian phrasing over literal translation +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- Keep repo locale references as `bs` in code/config, and use `Bosanski` for the user-facing language name when applicable + +## Avoid + +- Avoid changing repo locale references from `bs` to another slug in code snippets or config examples +- Avoid translating product and protocol names that are fixed identifiers diff --git a/.opencode/glossary/da.md b/.opencode/glossary/da.md new file mode 100644 index 00000000000..e6322217010 --- /dev/null +++ b/.opencode/glossary/da.md @@ -0,0 +1,27 @@ +# da Glossary + +## Sources + +- PR #9821: https://github.com/anomalyco/opencode/pull/9821 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Danish phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Danish terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/glossary/de.md b/.opencode/glossary/de.md new file mode 100644 index 00000000000..0d2c49facea --- /dev/null +++ b/.opencode/glossary/de.md @@ -0,0 +1,27 @@ +# de Glossary + +## Sources + +- PR #9817: https://github.com/anomalyco/opencode/pull/9817 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural German phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple German terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/glossary/es.md b/.opencode/glossary/es.md new file mode 100644 index 00000000000..dc9b977ecff --- /dev/null +++ b/.opencode/glossary/es.md @@ -0,0 +1,27 @@ +# es Glossary + +## Sources + +- PR #9817: https://github.com/anomalyco/opencode/pull/9817 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Spanish phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Spanish terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/glossary/fr.md b/.opencode/glossary/fr.md new file mode 100644 index 00000000000..074c4de110a --- /dev/null +++ b/.opencode/glossary/fr.md @@ -0,0 +1,27 @@ +# fr Glossary + +## Sources + +- PR #9821: https://github.com/anomalyco/opencode/pull/9821 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural French phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple French terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/glossary/ja.md b/.opencode/glossary/ja.md new file mode 100644 index 00000000000..f0159ca9669 --- /dev/null +++ b/.opencode/glossary/ja.md @@ -0,0 +1,33 @@ +# ja Glossary + +## Sources + +- PR #9821: https://github.com/anomalyco/opencode/pull/9821 +- PR #13160: https://github.com/anomalyco/opencode/pull/13160 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +These are PR-backed wording preferences and may evolve. + +| English / Context | Preferred | Notes | +| --------------------------- | ----------------------- | ------------------------------------- | +| WSL integration (UI label) | `WSL連携` | PR #13160 prefers this over `WSL統合` | +| WSL integration description | `WindowsのWSL環境で...` | PR #13160 improved phrasing naturally | + +## Guidance + +- Prefer natural Japanese phrasing over literal translation +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- In WSL integration text, follow PR #13160 wording direction for more natural Japanese phrasing + +## Avoid + +- Avoid `WSL統合` in the WSL integration UI context where `WSL連携` is the reviewed wording +- Avoid translating product and protocol names that are fixed identifiers diff --git a/.opencode/glossary/ko.md b/.opencode/glossary/ko.md new file mode 100644 index 00000000000..71385c8a10a --- /dev/null +++ b/.opencode/glossary/ko.md @@ -0,0 +1,27 @@ +# ko Glossary + +## Sources + +- PR #9817: https://github.com/anomalyco/opencode/pull/9817 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Korean phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Korean terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/glossary/no.md b/.opencode/glossary/no.md new file mode 100644 index 00000000000..d7159dca410 --- /dev/null +++ b/.opencode/glossary/no.md @@ -0,0 +1,38 @@ +# no Glossary + +## Sources + +- PR #10018: https://github.com/anomalyco/opencode/pull/10018 +- PR #12935: https://github.com/anomalyco/opencode/pull/12935 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Sound names (PR #10018 notes these were intentionally left untranslated) + +## Preferred Terms + +These are PR-backed corrections and may evolve. + +| English / Context | Preferred | Notes | +| ----------------------------------- | ------------ | ----------------------------- | +| Save (data persistence action) | `Lagre` | Prefer over `Spare` | +| Disabled (feature/state) | `deaktivert` | Prefer over `funksjonshemmet` | +| API keys | `API Nøkler` | Prefer over `API Taster` | +| Cost (noun) | `Kostnad` | Prefer over verb form `Koste` | +| Show/View (imperative button label) | `Vis` | Prefer over `Utsikt` | + +## Guidance + +- Prefer natural Norwegian Bokmal (Bokmål) wording over literal translation +- Keep tone clear and practical in UI labels +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- Keep recurring UI terms consistent once a preferred term is chosen + +## Avoid + +- Avoid `Spare` for save actions in persistence contexts +- Avoid `funksjonshemmet` for disabled feature states +- Avoid `API Taster`, `Koste`, and `Utsikt` in the corrected contexts above diff --git a/.opencode/glossary/pl.md b/.opencode/glossary/pl.md new file mode 100644 index 00000000000..e9bad7a5156 --- /dev/null +++ b/.opencode/glossary/pl.md @@ -0,0 +1,27 @@ +# pl Glossary + +## Sources + +- PR #9884: https://github.com/anomalyco/opencode/pull/9884 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Polish phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Polish terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/glossary/ru.md b/.opencode/glossary/ru.md new file mode 100644 index 00000000000..6fee0f94c06 --- /dev/null +++ b/.opencode/glossary/ru.md @@ -0,0 +1,27 @@ +# ru Glossary + +## Sources + +- PR #9882: https://github.com/anomalyco/opencode/pull/9882 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +No PR-backed term mappings yet. Add entries here when review PRs introduce repeated wording corrections. + +## Guidance + +- Prefer natural Russian phrasing over literal translation +- Keep tone clear and direct in UI labels and docs prose +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths + +## Avoid + +- Avoid translating product and protocol names that are fixed identifiers +- Avoid mixing multiple Russian terms for the same recurring UI action once a preferred term is established diff --git a/.opencode/glossary/th.md b/.opencode/glossary/th.md new file mode 100644 index 00000000000..7b5a31d16bf --- /dev/null +++ b/.opencode/glossary/th.md @@ -0,0 +1,34 @@ +# th Glossary + +## Sources + +- PR #10809: https://github.com/anomalyco/opencode/pull/10809 +- PR #11496: https://github.com/anomalyco/opencode/pull/11496 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only in commands, package names, paths, or code) +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +These are PR-backed preferences and may evolve. + +| English / Context | Preferred | Notes | +| ------------------------------------- | --------------------- | -------------------------------------------------------------------------------- | +| Thai language label in language lists | `ไทย` | PR #10809 standardized this across locales | +| Language names in language pickers | Native names (static) | PR #11496: keep names like `English`, `Deutsch`, `ไทย` consistent across locales | + +## Guidance + +- Prefer natural Thai phrasing over literal translation +- Keep tone short and clear for buttons and labels +- Preserve technical artifacts exactly: commands, flags, code, URLs, model IDs, and file paths +- Keep language names static/native in language pickers instead of translating them per current locale (PR #11496) + +## Avoid + +- Avoid translating language names differently per current locale in language lists +- Avoid changing `ไทย` to another display form for the Thai language option unless the product standard changes diff --git a/.opencode/glossary/tr.md b/.opencode/glossary/tr.md new file mode 100644 index 00000000000..72b1cdfb40b --- /dev/null +++ b/.opencode/glossary/tr.md @@ -0,0 +1,38 @@ +# tr Glossary + +## Sources + +- PR #15835: https://github.com/anomalyco/opencode/pull/15835 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose, docs, and UI copy) +- Keep lowercase `opencode` in commands, package names, paths, URLs, and other exact identifiers +- `` stays the literal key token in code blocks; use `Tab` for the nearby explanatory label in prose +- Commands, flags, file paths, and code literals (keep exactly as written) + +## Preferred Terms + +These are PR-backed wording preferences and may evolve. + +| English / Context | Preferred | Notes | +| ------------------------- | --------------------------------------- | ------------------------------------------------------------- | +| available in beta | `beta olarak mevcut` | Prefer this over `beta olarak kullanılabilir` | +| privacy-first | `Gizlilik öncelikli tasarlandı` | Prefer this over `Önce gizlilik için tasarlandı` | +| connect your local models | `yerel modellerinizi bağlayabilirsiniz` | Use the fuller, more direct action phrase | +| `` key label | `Tab` | Use `Tab` in prose; keep `` in literal UI or code blocks | +| cross-platform | `cross-platform (tüm platformlarda)` | Keep the English term, add a short clarification when helpful | + +## Guidance + +- Prefer natural Turkish phrasing over literal translation +- Merge broken sentence fragments into one clear sentence when the source is a single thought +- Keep product naming consistent: `OpenCode` in prose, `opencode` only for exact technical identifiers +- When an English technical term is intentionally kept, add a short Turkish clarification only if it improves readability + +## Avoid + +- Avoid `beta olarak kullanılabilir` when `beta olarak mevcut` fits +- Avoid `Önce gizlilik için tasarlandı`; use the more natural reviewed wording instead +- Avoid `Sekme` for the translated key label in prose when referring to `` +- Avoid changing `opencode` to `OpenCode` inside commands, URLs, package names, or code literals diff --git a/.opencode/glossary/zh-cn.md b/.opencode/glossary/zh-cn.md new file mode 100644 index 00000000000..054e94b7e83 --- /dev/null +++ b/.opencode/glossary/zh-cn.md @@ -0,0 +1,42 @@ +# zh-cn Glossary + +## Sources + +- PR #13942: https://github.com/anomalyco/opencode/pull/13942 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only when it is part of commands, package names, paths, or code) +- `OpenCode Zen` +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- `Model Context Protocol` (prefer the English expansion when introducing `MCP`) + +## Preferred Terms + +These are preferred terms for docs/UI prose and may evolve. + +| English | Preferred | Notes | +| ----------------------- | --------- | ------------------------------------------- | +| prompt | 提示词 | Keep `--prompt` unchanged in flags/code | +| session | 会话 | | +| provider | 提供商 | | +| share link / shared URL | 分享链接 | Prefer `分享` for user-facing share actions | +| headless (server) | 无界面 | Docs wording | +| authentication | 认证 | Prefer in auth/OAuth contexts | +| cache | 缓存 | | +| keybind / shortcut | 快捷键 | User-facing docs wording | +| workflow | 工作流 | e.g. GitHub Actions workflow | + +## Guidance + +- Prefer natural, concise phrasing over literal translation +- Keep the tone direct and friendly (PR #13942 consistently moved wording in this direction) +- Preserve technical artifacts exactly: commands, flags, code, inline code, URLs, file paths, model IDs +- Keep enum-like values in English when they are literals (for example, `default`, `json`) +- Prefer consistent terminology across pages once a term is chosen (`会话`, `提供商`, `提示词`, etc.) + +## Avoid + +- Avoid `opencode` in prose when referring to the product name; use `OpenCode` +- Avoid mixing alternative terms for the same concept across docs when a preferred term is already established diff --git a/.opencode/glossary/zh-tw.md b/.opencode/glossary/zh-tw.md new file mode 100644 index 00000000000..283660e1219 --- /dev/null +++ b/.opencode/glossary/zh-tw.md @@ -0,0 +1,42 @@ +# zh-tw Glossary + +## Sources + +- PR #13942: https://github.com/anomalyco/opencode/pull/13942 + +## Do Not Translate (Locale Additions) + +- `OpenCode` (preserve casing in prose; keep `opencode` only when it is part of commands, package names, paths, or code) +- `OpenCode Zen` +- `OpenCode CLI` +- `CLI`, `TUI`, `MCP`, `OAuth` +- `Model Context Protocol` (prefer the English expansion when introducing `MCP`) + +## Preferred Terms + +These are preferred terms for docs/UI prose and may evolve. + +| English | Preferred | Notes | +| ----------------------- | --------- | ------------------------------------------- | +| prompt | 提示詞 | Keep `--prompt` unchanged in flags/code | +| session | 工作階段 | | +| provider | 供應商 | | +| share link / shared URL | 分享連結 | Prefer `分享` for user-facing share actions | +| headless (server) | 無介面 | Docs wording | +| authentication | 認證 | Prefer in auth/OAuth contexts | +| cache | 快取 | | +| keybind / shortcut | 快捷鍵 | User-facing docs wording | +| workflow | 工作流程 | e.g. GitHub Actions workflow | + +## Guidance + +- Prefer natural, concise phrasing over literal translation +- Keep the tone direct and friendly (PR #13942 consistently moved wording in this direction) +- Preserve technical artifacts exactly: commands, flags, code, inline code, URLs, file paths, model IDs +- Keep enum-like values in English when they are literals (for example, `default`, `json`) +- Prefer consistent terminology across pages once a term is chosen (`工作階段`, `供應商`, `提示詞`, etc.) + +## Avoid + +- Avoid `opencode` in prose when referring to the product name; use `OpenCode` +- Avoid mixing alternative terms for the same concept across docs when a preferred term is already established diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 5d2dec625c6..8380f7f719e 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -1,21 +1,16 @@ { "$schema": "https://opencode.ai/config.json", - // "plugin": ["opencode-openai-codex-auth"], - // "enterprise": { - // "url": "https://enterprise.dev.opencode.ai", - // }, - "instructions": ["STYLE_GUIDE.md"], "provider": { "opencode": { "options": {}, }, }, - "mcp": { - "context7": { - "type": "remote", - "url": "https://mcp.context7.com/mcp", + "permission": { + "edit": { + "packages/opencode/migration/*": "deny", }, }, + "mcp": {}, "tools": { "github-triage": false, "github-pr-search": false, diff --git a/.opencode/skill/bun-file-io/SKILL.md b/.opencode/skill/bun-file-io/SKILL.md deleted file mode 100644 index ea39507d269..00000000000 --- a/.opencode/skill/bun-file-io/SKILL.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: bun-file-io -description: Use this when you are working on file operations like reading, writing, scanning, or deleting files. It summarizes the preferred file APIs and patterns used in this repo. It also notes when to use filesystem helpers for directories. ---- - -## Use this when - -- Editing file I/O or scans in `packages/opencode` -- Handling directory operations or external tools - -## Bun file APIs (from Bun docs) - -- `Bun.file(path)` is lazy; call `text`, `json`, `stream`, `arrayBuffer`, `bytes`, `exists` to read. -- Metadata: `file.size`, `file.type`, `file.name`. -- `Bun.write(dest, input)` writes strings, buffers, Blobs, Responses, or files. -- `Bun.file(...).delete()` deletes a file. -- `file.writer()` returns a FileSink for incremental writes. -- `Bun.Glob` + `Array.fromAsync(glob.scan({ cwd, absolute, onlyFiles, dot }))` for scans. -- Use `Bun.which` to find a binary, then `Bun.spawn` to run it. -- `Bun.readableStreamToText/Bytes/JSON` for stream output. - -## When to use node:fs - -- Use `node:fs/promises` for directories (`mkdir`, `readdir`, recursive operations). - -## Repo patterns - -- Prefer Bun APIs over Node `fs` for file access. -- Check `Bun.file(...).exists()` before reading. -- For binary/large files use `arrayBuffer()` and MIME checks via `file.type`. -- Use `Bun.Glob` + `Array.fromAsync` for scans. -- Decode tool stderr with `Bun.readableStreamToText`. -- For large writes, use `Bun.write(Bun.file(path), text)`. - -## Quick checklist - -- Use Bun APIs first. -- Use `path.join`/`path.resolve` for paths. -- Prefer promise `.catch(...)` over `try/catch` when possible. diff --git a/.opencode/tool/github-pr-search.txt b/.opencode/tool/github-pr-search.txt index 28d8643f13c..1b658e71c43 100644 --- a/.opencode/tool/github-pr-search.txt +++ b/.opencode/tool/github-pr-search.txt @@ -1,6 +1,6 @@ Use this tool to search GitHub pull requests by title and description. -This tool searches PRs in the sst/opencode repository and returns LLM-friendly results including: +This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including: - PR number and title - Author - State (open/closed/merged) diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts index 1e216f1c8da..ed80f49d541 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/tool/github-triage.ts @@ -1,8 +1,22 @@ /// -// import { Octokit } from "@octokit/rest" import { tool } from "@opencode-ai/plugin" import DESCRIPTION from "./github-triage.txt" +const TEAM = { + desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"], + zen: ["fwang", "MrMushrooooom"], + tui: ["thdxr", "kommander", "rekram1-node"], + core: ["thdxr", "rekram1-node", "jlongster"], + docs: ["R44VC0RP"], + windows: ["Hona"], +} as const + +const ASSIGNEES = [...new Set(Object.values(TEAM).flat())] + +function pick(items: readonly T[]) { + return items[Math.floor(Math.random() * items.length)]! +} + function getIssueNumber(): number { const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10) if (!issue) throw new Error("ISSUE_NUMBER env var not set") @@ -29,60 +43,69 @@ export default tool({ description: DESCRIPTION, args: { assignee: tool.schema - .enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"]) + .enum(ASSIGNEES as [string, ...string[]]) .describe("The username of the assignee") .default("rekram1-node"), labels: tool.schema - .array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs", "windows"])) + .array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"])) .describe("The labels(s) to add to the issue") .default([]), }, async execute(args) { const issue = getIssueNumber() - // const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) const owner = "anomalyco" const repo = "opencode" const results: string[] = [] + let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))] + const web = labels.includes("web") + const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase() + const zen = /\bzen\b/.test(text) || text.includes("opencode black") + const nix = /\bnix(os)?\b/.test(text) + + if (labels.includes("nix") && !nix) { + labels = labels.filter((x) => x !== "nix") + results.push("Dropped label: nix (issue does not mention nix)") + } + + const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee - if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) { - throw new Error("Only desktop issues should be assigned to adamdotdevin") + if (labels.includes("zen") && !zen) { + throw new Error("Only add the zen label when issue title/body contains 'zen'") } - if (args.assignee === "fwang" && !args.labels.includes("zen")) { - throw new Error("Only zen issues should be assigned to fwang") + if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) { + throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln") } - if (args.assignee === "kommander" && !args.labels.includes("opentui")) { + if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) { + throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom") + } + + if (assignee === "Hona" && !labels.includes("windows")) { + throw new Error("Only windows issues should be assigned to Hona") + } + + if (assignee === "R44VC0RP" && !labels.includes("docs")) { + throw new Error("Only docs issues should be assigned to R44VC0RP") + } + + if (assignee === "kommander" && !labels.includes("opentui")) { throw new Error("Only opentui issues should be assigned to kommander") } - // await octokit.rest.issues.addAssignees({ - // owner, - // repo, - // issue_number: issue, - // assignees: [args.assignee], - // }) await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, { method: "POST", - body: JSON.stringify({ assignees: [args.assignee] }), + body: JSON.stringify({ assignees: [assignee] }), }) - results.push(`Assigned @${args.assignee} to issue #${issue}`) - - const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label)) + results.push(`Assigned @${assignee} to issue #${issue}`) if (labels.length > 0) { - // await octokit.rest.issues.addLabels({ - // owner, - // repo, - // issue_number: issue, - // labels, - // }) await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, { method: "POST", body: JSON.stringify({ labels }), }) - results.push(`Added labels: ${args.labels.join(", ")}`) + results.push(`Added labels: ${labels.join(", ")}`) } return results.join("\n") diff --git a/.opencode/tool/github-triage.txt b/.opencode/tool/github-triage.txt index 4c46a72c162..4369ed23512 100644 --- a/.opencode/tool/github-triage.txt +++ b/.opencode/tool/github-triage.txt @@ -1,88 +1,6 @@ -Use this tool to assign and/or label a Github issue. +Use this tool to assign and/or label a GitHub issue. -You can assign the following users: -- thdxr -- adamdotdevin -- fwang -- jayair -- kommander -- rekram1-node +Choose labels and assignee using the current triage policy and ownership rules. +Pick the most fitting labels for the issue and assign one owner. - -You can use the following labels: -- nix -- opentui -- perf -- web -- zen -- docs - -Always try to assign an issue, if in doubt, assign rekram1-node to it. - -## Breakdown of responsibilities: - -### thdxr - -Dax is responsible for managing core parts of the application, for large feature requests, api changes, or things that require significant changes to the codebase assign him. - -This relates to OpenCode server primarily but has overlap with just about anything - -### adamdotdevin - -Adam is responsible for managing the Desktop/Web app. If there is an issue relating to the desktop app or `opencode web` command. Assign him. - - -### fwang - -Frank is responsible for managing Zen, if you see complaints about OpenCode Zen, maybe it's the dashboard, the model quality, billing issues, etc. Assign him to the issue. - -### jayair - -Jay is responsible for documentation. If there is an issue relating to documentation assign him. - -### kommander - -Sebastian is responsible for managing an OpenTUI (a library for building terminal user interfaces). OpenCode's TUI is built with OpenTUI. If there are issues about: -- random characters on screen -- keybinds not working on different terminals -- general terminal stuff -Then assign the issue to Him. - -### rekram1-node - -ALL BUGS SHOULD BE assigned to rekram1-node unless they have the `opentui` label. - -Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things. -If no one else makes sense to assign, assign rekram1-node to it. - -Always assign to aiden if the issue mentions "acp", "zed", or model performance issues - -## Breakdown of Labels: - -### nix - -Any issue that mentions nix, or nixos should have a nix label - -### opentui - -Anything relating to the TUI itself should have an opentui label - -### perf - -Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label - -### desktop - -Anything related to `opencode web` command or the desktop app should have a desktop label. Never add this label for anything terminal/tui related - -### zen - -Anything related to OpenCode Zen, billing, or model quality from Zen should have a zen label - -### docs - -Anything related to the documentation should have a docs label - -### windows - -Use for any issue that involves the windows OS +If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random. diff --git a/.prettierignore b/.prettierignore index aa3a7ce2381..a2a27765969 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -sst-env.d.ts \ No newline at end of file +sst-env.d.ts +packages/desktop/src/bindings.ts diff --git a/.signpath/policies/opencode/test-signing.yml b/.signpath/policies/opencode/test-signing.yml new file mode 100644 index 00000000000..683b27adb75 --- /dev/null +++ b/.signpath/policies/opencode/test-signing.yml @@ -0,0 +1,5 @@ +github-policies: + runners: + allowed_groups: + - "GitHub Actions" + - "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt" diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000000..a3a5e1e2b21 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,9 @@ +{ + "format_on_save": "on", + "formatter": { + "external": { + "command": "bunx", + "arguments": ["prettier", "--stdin-filepath", "{buffer_path}"] + } + } +} diff --git a/AGENTS.md b/AGENTS.md index 3138f6c5ece..0b080ac4e26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,128 @@ -- To test opencode in `packages/opencode`, run `bun dev`. - To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`. - ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. - The default branch in this repo is `dev`. +- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs. +- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility. + +## Style Guide + +### General Principles + +- Keep things in one function unless composable or reusable +- Avoid `try`/`catch` where possible +- Avoid using the `any` type +- Prefer single word variable names where possible +- Use Bun APIs when possible, like `Bun.file()` +- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity +- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream + +### Naming + +Prefer single word names for variables and functions. Only use multiple words if necessary. + +### Naming Enforcement (Read This) + +THIS RULE IS MANDATORY FOR AGENT WRITTEN CODE. + +- Use single word names by default for new locals, params, and helper functions. +- Multi-word names are allowed only when a single word would be unclear or ambiguous. +- Do not introduce new camelCase compounds when a short single-word alternative is clear. +- Before finishing edits, review touched lines and shorten newly introduced identifiers where possible. +- Good short names to prefer: `pid`, `cfg`, `err`, `opts`, `dir`, `root`, `child`, `state`, `timeout`. +- Examples to avoid unless truly required: `inputPID`, `existingClient`, `connectTimeout`, `workerPath`. + +```ts +// Good +const foo = 1 +function journal(dir: string) {} + +// Bad +const fooBar = 1 +function prepareJournal(dir: string) {} +``` + +Reduce total variable count by inlining when a value is only used once. + +```ts +// Good +const journal = await Bun.file(path.join(dir, "journal.json")).json() + +// Bad +const journalPath = path.join(dir, "journal.json") +const journal = await Bun.file(journalPath).json() +``` + +### Destructuring + +Avoid unnecessary destructuring. Use dot notation to preserve context. + +```ts +// Good +obj.a +obj.b + +// Bad +const { a, b } = obj +``` + +### Variables + +Prefer `const` over `let`. Use ternaries or early returns instead of reassignment. + +```ts +// Good +const foo = condition ? 1 : 2 + +// Bad +let foo +if (condition) foo = 1 +else foo = 2 +``` + +### Control Flow + +Avoid `else` statements. Prefer early returns. + +```ts +// Good +function foo() { + if (condition) return 1 + return 2 +} + +// Bad +function foo() { + if (condition) return 1 + else return 2 +} +``` + +### Schema Definitions (Drizzle) + +Use snake_case for field names so column names don't need to be redefined as strings. + +```ts +// Good +const table = sqliteTable("session", { + id: text().primaryKey(), + project_id: text().notNull(), + created_at: integer().notNull(), +}) + +// Bad +const table = sqliteTable("session", { + id: text("id").primaryKey(), + projectID: text("project_id").notNull(), + createdAt: integer("created_at").notNull(), +}) +``` + +## Testing + +- Avoid mocks as much as possible +- Test actual implementation, do not duplicate logic into tests +- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`. + +## Type Checking + +- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a32504b22f8..2ae3fc6f2fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,11 @@ If you are unsure if a PR would be accepted, feel free to ask a maintainer or lo Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on. +## Adding New Providers + +New providers shouldn't require many if ANY code changes, but if you want to add support for a new provider first make a PR to: +https://github.com/anomalyco/models.dev + ## Developing OpenCode - Requirements: Bun 1.3+ @@ -148,7 +153,7 @@ This runs `bun run --cwd packages/desktop build` automatically via Tauri’s `be > [!NOTE] > If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files. -Please try to follow the [style guide](./STYLE_GUIDE.md) +Please try to follow the [style guide](./AGENTS.md) ### Setting up a Debugger @@ -258,3 +263,49 @@ These are not strictly enforced, they are just general guidelines: ## Feature Requests For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly. + +## Trust & Vouch System + +This project uses [vouch](https://github.com/mitchellh/vouch) to manage contributor trust. The vouch list is maintained in [`.github/VOUCHED.td`](.github/VOUCHED.td). + +### How it works + +- **Vouched users** are explicitly trusted contributors. +- **Denounced users** are explicitly blocked. Issues and pull requests from denounced users are automatically closed. If you have been denounced, you can request to be unvouched by reaching out to a maintainer on [Discord](https://opencode.ai/discord) +- **Everyone else** can participate normally — you don't need to be vouched to open issues or PRs. + +### For maintainers + +Collaborators with write access can manage the vouch list by commenting on any issue: + +- `vouch` — vouch for the issue author +- `vouch @username` — vouch for a specific user +- `denounce` — denounce the issue author +- `denounce @username` — denounce a specific user +- `denounce @username ` — denounce with a reason +- `unvouch` / `unvouch @username` — remove someone from the list + +Changes are committed automatically to `.github/VOUCHED.td`. + +### Denouncement policy + +Denouncement is reserved for users who repeatedly submit low-quality AI-generated contributions, spam, or otherwise act in bad faith. It is not used for disagreements or honest mistakes. + +## Issue Requirements + +All issues **must** use one of our issue templates: + +- **Bug report** — for reporting bugs (requires a description) +- **Feature request** — for suggesting enhancements (requires verification checkbox and description) +- **Question** — for asking questions (requires the question) + +Blank issues are not allowed. When a new issue is opened, an automated check verifies that it follows a template and meets our contributing guidelines. If an issue doesn't meet the requirements, you'll receive a comment explaining what needs to be fixed and have **2 hours** to edit the issue. After that, it will be automatically closed. + +Issues may be flagged for: + +- Not using a template +- Required fields left empty or filled with placeholder text +- AI-generated walls of text +- Missing meaningful content + +If you believe your issue was incorrectly flagged, let a maintainer know. diff --git a/README.ar.md b/README.ar.md new file mode 100644 index 00000000000..beb44589e62 --- /dev/null +++ b/README.ar.md @@ -0,0 +1,141 @@ +

+ + + + + شعار OpenCode + + +

+

وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### التثبيت + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# مديري الحزم +npm i -g opencode-ai@latest # او bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS و Linux (موصى به، دائما محدث) +brew install opencode # macOS و Linux (صيغة brew الرسمية، تحديث اقل) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # اي نظام +nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث فرع dev +``` + +> [!TIP] +> احذف الاصدارات الاقدم من 0.1.x قبل التثبيت. + +### تطبيق سطح المكتب (BETA) + +يتوفر OpenCode ايضا كتطبيق سطح مكتب. قم بالتنزيل مباشرة من [صفحة الاصدارات](https://github.com/anomalyco/opencode/releases) او من [opencode.ai/download](https://opencode.ai/download). + +| المنصة | التنزيل | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb` او `.rpm` او AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### مجلد التثبيت + +يحترم سكربت التثبيت ترتيب الاولوية التالي لمسار التثبيت: + +1. `$OPENCODE_INSTALL_DIR` - مجلد تثبيت مخصص +2. `$XDG_BIN_DIR` - مسار متوافق مع مواصفات XDG Base Directory +3. `$HOME/bin` - مجلد الثنائيات القياسي للمستخدم (ان وجد او امكن انشاؤه) +4. `$HOME/.opencode/bin` - المسار الافتراضي الاحتياطي + +```bash +# امثلة +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +يتضمن OpenCode وكيليْن (Agents) مدمجين يمكنك التبديل بينهما باستخدام زر `Tab`. + +- **build** - الافتراضي، وكيل بصلاحيات كاملة لاعمال التطوير +- **plan** - وكيل للقراءة فقط للتحليل واستكشاف الكود + - يرفض تعديل الملفات افتراضيا + - يطلب الاذن قبل تشغيل اوامر bash + - مثالي لاستكشاف قواعد كود غير مألوفة او لتخطيط التغييرات + +بالاضافة الى ذلك يوجد وكيل فرعي **general** للبحث المعقد والمهام متعددة الخطوات. +يستخدم داخليا ويمكن استدعاؤه بكتابة `@general` في الرسائل. + +تعرف على المزيد حول [agents](https://opencode.ai/docs/agents). + +### التوثيق + +لمزيد من المعلومات حول كيفية ضبط OpenCode، [**راجع التوثيق**](https://opencode.ai/docs). + +### المساهمة + +اذا كنت مهتما بالمساهمة في OpenCode، يرجى قراءة [contributing docs](./CONTRIBUTING.md) قبل ارسال pull request. + +### البناء فوق OpenCode + +اذا كنت تعمل على مشروع مرتبط بـ OpenCode ويستخدم "opencode" كجزء من اسمه (مثل "opencode-dashboard" او "opencode-mobile")، يرجى اضافة ملاحظة في README توضح انه ليس مبنيا بواسطة فريق OpenCode ولا يرتبط بنا بأي شكل. + +### FAQ + +#### ما الفرق عن Claude Code؟ + +هو مشابه جدا لـ Claude Code من حيث القدرات. هذه هي الفروقات الاساسية: + +- 100% مفتوح المصدر +- غير مقترن بمزود معين. نوصي بالنماذج التي نوفرها عبر [OpenCode Zen](https://opencode.ai/zen)؛ لكن يمكن استخدام OpenCode مع Claude او OpenAI او Google او حتى نماذج محلية. مع تطور النماذج ستتقلص الفجوات وستنخفض الاسعار، لذا من المهم ان يكون مستقلا عن المزود. +- دعم LSP جاهز للاستخدام +- تركيز على TUI. تم بناء OpenCode بواسطة مستخدمي neovim ومنشئي [terminal.shop](https://terminal.shop)؛ وسندفع حدود ما هو ممكن داخل الطرفية. +- معمارية عميل/خادم. على سبيل المثال، يمكن تشغيل OpenCode على جهازك بينما تقوده عن بعد من تطبيق جوال. هذا يعني ان واجهة TUI هي واحدة فقط من العملاء الممكنين. + +--- + +**انضم الى مجتمعنا** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.bn.md b/README.bn.md new file mode 100644 index 00000000000..c7abc7346a2 --- /dev/null +++ b/README.bn.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

ওপেন সোর্স এআই কোডিং এজেন্ট।

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### ইনস্টলেশন (Installation) + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Package managers +npm i -g opencode-ai@latest # or bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date) +brew install opencode # macOS and Linux (official brew formula, updated less) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # Any OS +nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch +``` + +> [!TIP] +> ইনস্টল করার আগে ০.১.x এর চেয়ে পুরোনো ভার্সনগুলো মুছে ফেলুন। + +### ডেস্কটপ অ্যাপ (BETA) + +OpenCode ডেস্কটপ অ্যাপ্লিকেশন হিসেবেও উপলব্ধ। সরাসরি [রিলিজ পেজ](https://github.com/anomalyco/opencode/releases) অথবা [opencode.ai/download](https://opencode.ai/download) থেকে ডাউনলোড করুন। + +| প্ল্যাটফর্ম | ডাউনলোড | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, or AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### ইনস্টলেশন ডিরেক্টরি (Installation Directory) + +ইনস্টল স্ক্রিপ্টটি ইনস্টলেশন পাতের জন্য নিম্নলিখিত অগ্রাধিকার ক্রম মেনে চলে: + +1. `$OPENCODE_INSTALL_DIR` - কাস্টম ইনস্টলেশন ডিরেক্টরি +2. `$XDG_BIN_DIR` - XDG বেস ডিরেক্টরি স্পেসিফিকেশন সমর্থিত পাথ +3. `$HOME/bin` - সাধারণ ব্যবহারকারী বাইনারি ডিরেক্টরি (যদি বিদ্যমান থাকে বা তৈরি করা যায়) +4. `$HOME/.opencode/bin` - ডিফল্ট ফলব্যাক + +```bash +# উদাহরণ +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### এজেন্টস (Agents) + +OpenCode এ দুটি বিল্ট-ইন এজেন্ট রয়েছে যা আপনি `Tab` কি(key) দিয়ে পরিবর্তন করতে পারবেন। + +- **build** - ডিফল্ট, ডেভেলপমেন্টের কাজের জন্য সম্পূর্ণ অ্যাক্সেসযুক্ত এজেন্ট +- **plan** - বিশ্লেষণ এবং কোড এক্সপ্লোরেশনের জন্য রিড-ওনলি এজেন্ট + - ডিফল্টভাবে ফাইল এডিট করতে দেয় না + - ব্যাশ কমান্ড চালানোর আগে অনুমতি চায় + - অপরিচিত কোডবেস এক্সপ্লোর করা বা পরিবর্তনের পরিকল্পনা করার জন্য আদর্শ + +এছাড়াও জটিল অনুসন্ধান এবং মাল্টিস্টেপ টাস্কের জন্য একটি **general** সাবএজেন্ট অন্তর্ভুক্ত রয়েছে। +এটি অভ্যন্তরীণভাবে ব্যবহৃত হয় এবং মেসেজে `@general` লিখে ব্যবহার করা যেতে পারে। + +এজেন্টদের সম্পর্কে আরও জানুন: [docs](https://opencode.ai/docs/agents)। + +### ডকুমেন্টেশন (Documentation) + +কিভাবে OpenCode কনফিগার করবেন সে সম্পর্কে আরও তথ্যের জন্য, [**আমাদের ডকস দেখুন**](https://opencode.ai/docs)। + +### অবদান (Contributing) + +আপনি যদি OpenCode এ অবদান রাখতে চান, অনুগ্রহ করে একটি পুল রিকোয়েস্ট সাবমিট করার আগে আমাদের [কন্ট্রিবিউটিং ডকস](./CONTRIBUTING.md) পড়ে নিন। + +### OpenCode এর উপর বিল্ডিং (Building on OpenCode) + +আপনি যদি এমন প্রজেক্টে কাজ করেন যা OpenCode এর সাথে সম্পর্কিত এবং প্রজেক্টের নামের অংশ হিসেবে "opencode" ব্যবহার করেন, উদাহরণস্বরূপ "opencode-dashboard" বা "opencode-mobile", তবে দয়া করে আপনার README তে একটি নোট যোগ করে স্পষ্ট করুন যে এই প্রজেক্টটি OpenCode দল দ্বারা তৈরি হয়নি এবং আমাদের সাথে এর কোনো সরাসরি সম্পর্ক নেই। + +### সচরাচর জিজ্ঞাসিত প্রশ্নাবলী (FAQ) + +#### এটি ক্লড কোড (Claude Code) থেকে কীভাবে আলাদা? + +ক্যাপাবিলিটির দিক থেকে এটি ক্লড কোডের (Claude Code) মতই। এখানে মূল পার্থক্যগুলো দেওয়া হলো: + +- ১০০% ওপেন সোর্স +- কোনো প্রোভাইডারের সাথে আবদ্ধ নয়। যদিও আমরা [OpenCode Zen](https://opencode.ai/zen) এর মাধ্যমে মডেলসমূহ ব্যবহারের পরামর্শ দিই, OpenCode ক্লড (Claude), ওপেনএআই (OpenAI), গুগল (Google), অথবা লোকাল মডেলগুলোর সাথেও ব্যবহার করা যেতে পারে। যেমন যেমন মডেলগুলো উন্নত হবে, তাদের মধ্যকার পার্থক্য কমে আসবে এবং দামও কমবে, তাই প্রোভাইডার-অজ্ঞাস্টিক হওয়া খুবই গুরুত্বপূর্ণ। +- আউট-অফ-দ্য-বক্স LSP সাপোর্ট +- TUI এর উপর ফোকাস। OpenCode নিওভিম (neovim) ব্যবহারকারী এবং [terminal.shop](https://terminal.shop) এর নির্মাতাদের দ্বারা তৈরি; আমরা টার্মিনালে কী কী সম্ভব তার সীমাবদ্ধতা ছাড়িয়ে যাওয়ার চেষ্টা করছি। +- ক্লায়েন্ট/সার্ভার আর্কিটেকচার। এটি যেমন OpenCode কে আপনার কম্পিউটারে চালানোর সুযোগ দেয়, তেমনি আপনি মোবাইল অ্যাপ থেকে রিমোটলি এটি নিয়ন্ত্রণ করতে পারবেন, অর্থাৎ TUI ফ্রন্টএন্ড কেবল সম্ভাব্য ক্লায়েন্টগুলোর মধ্যে একটি। + +--- + +**আমাদের কমিউনিটিতে যুক্ত হোন** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.br.md b/README.br.md new file mode 100644 index 00000000000..6d1de21562c --- /dev/null +++ b/README.br.md @@ -0,0 +1,141 @@ +

+ + + + + Logo do OpenCode + + +

+

O agente de programação com IA de código aberto.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Instalação + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Gerenciadores de pacotes +npm i -g opencode-ai@latest # ou bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS e Linux (recomendado, sempre atualizado) +brew install opencode # macOS e Linux (fórmula oficial do brew, atualiza menos) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # qualquer sistema +nix run nixpkgs#opencode # ou github:anomalyco/opencode para a branch dev mais recente +``` + +> [!TIP] +> Remova versões anteriores a 0.1.x antes de instalar. + +### App desktop (BETA) + +O OpenCode também está disponível como aplicativo desktop. Baixe diretamente pela [página de releases](https://github.com/anomalyco/opencode/releases) ou em [opencode.ai/download](https://opencode.ai/download). + +| Plataforma | Download | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` ou AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Diretório de instalação + +O script de instalação respeita a seguinte ordem de prioridade para o caminho de instalação: + +1. `$OPENCODE_INSTALL_DIR` - Diretório de instalação personalizado +2. `$XDG_BIN_DIR` - Caminho compatível com a especificação XDG Base Directory +3. `$HOME/bin` - Diretório binário padrão do usuário (se existir ou puder ser criado) +4. `$HOME/.opencode/bin` - Fallback padrão + +```bash +# Exemplos +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +O OpenCode inclui dois agents integrados, que você pode alternar com a tecla `Tab`. + +- **build** - Padrão, agent com acesso total para trabalho de desenvolvimento +- **plan** - Agent somente leitura para análise e exploração de código + - Nega edições de arquivos por padrão + - Pede permissão antes de executar comandos bash + - Ideal para explorar codebases desconhecidas ou planejar mudanças + +Também há um subagent **general** para buscas complexas e tarefas em várias etapas. +Ele é usado internamente e pode ser invocado com `@general` nas mensagens. + +Saiba mais sobre [agents](https://opencode.ai/docs/agents). + +### Documentação + +Para mais informações sobre como configurar o OpenCode, [**veja nossa documentação**](https://opencode.ai/docs). + +### Contribuir + +Se você tem interesse em contribuir com o OpenCode, leia os [contributing docs](./CONTRIBUTING.md) antes de enviar um pull request. + +### Construindo com OpenCode + +Se você estiver trabalhando em um projeto relacionado ao OpenCode e estiver usando "opencode" como parte do nome (por exemplo, "opencode-dashboard" ou "opencode-mobile"), adicione uma nota no README para deixar claro que não foi construído pela equipe do OpenCode e não é afiliado a nós de nenhuma forma. + +### FAQ + +#### Como isso é diferente do Claude Code? + +É muito parecido com o Claude Code em termos de capacidade. Aqui estão as principais diferenças: + +- 100% open source +- Não está acoplado a nenhum provedor. Embora recomendemos os modelos que oferecemos pelo [OpenCode Zen](https://opencode.ai/zen); o OpenCode pode ser usado com Claude, OpenAI, Google ou até modelos locais. À medida que os modelos evoluem, as diferenças diminuem e os preços caem, então ser provider-agnostic é importante. +- Suporte a LSP pronto para uso +- Foco em TUI. O OpenCode é construído por usuários de neovim e pelos criadores do [terminal.shop](https://terminal.shop); vamos levar ao limite o que é possível no terminal. +- Arquitetura cliente/servidor. Isso, por exemplo, permite executar o OpenCode no seu computador enquanto você o controla remotamente por um aplicativo mobile. Isso significa que o frontend TUI é apenas um dos possíveis clientes. + +--- + +**Junte-se à nossa comunidade** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.bs.md b/README.bs.md new file mode 100644 index 00000000000..2cff8e0279c --- /dev/null +++ b/README.bs.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

OpenCode je open source AI agent za programiranje.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Instalacija + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Package manageri +npm i -g opencode-ai@latest # ili bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS i Linux (preporučeno, uvijek ažurno) +brew install opencode # macOS i Linux (zvanična brew formula, rjeđe se ažurira) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # Bilo koji OS +nix run nixpkgs#opencode # ili github:anomalyco/opencode za najnoviji dev branch +``` + +> [!TIP] +> Ukloni verzije starije od 0.1.x prije instalacije. + +### Desktop aplikacija (BETA) + +OpenCode je dostupan i kao desktop aplikacija. Preuzmi je direktno sa [stranice izdanja](https://github.com/anomalyco/opencode/releases) ili sa [opencode.ai/download](https://opencode.ai/download). + +| Platforma | Preuzimanje | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, ili AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Instalacijski direktorij + +Instalacijska skripta koristi sljedeći redoslijed prioriteta za putanju instalacije: + +1. `$OPENCODE_INSTALL_DIR` - Prilagođeni instalacijski direktorij +2. `$XDG_BIN_DIR` - Putanja usklađena sa XDG Base Directory specifikacijom +3. `$HOME/bin` - Standardni korisnički bin direktorij (ako postoji ili se može kreirati) +4. `$HOME/.opencode/bin` - Podrazumijevana rezervna lokacija + +```bash +# Primjeri +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agenti + +OpenCode uključuje dva ugrađena agenta između kojih možeš prebacivati tasterom `Tab`. + +- **build** - Podrazumijevani agent sa punim pristupom za razvoj +- **plan** - Agent samo za čitanje za analizu i istraživanje koda + - Podrazumijevano zabranjuje izmjene datoteka + - Traži dozvolu prije pokretanja bash komandi + - Idealan za istraživanje nepoznatih codebase-ova ili planiranje izmjena + +Uključen je i **general** pod-agent za složene pretrage i višekoračne zadatke. +Koristi se interno i može se pozvati pomoću `@general` u porukama. + +Saznaj više o [agentima](https://opencode.ai/docs/agents). + +### Dokumentacija + +Za više informacija o konfiguraciji OpenCode-a, [**pogledaj dokumentaciju**](https://opencode.ai/docs). + +### Doprinosi + +Ako želiš doprinositi OpenCode-u, pročitaj [upute za doprinošenje](./CONTRIBUTING.md) prije slanja pull requesta. + +### Gradnja na OpenCode-u + +Ako radiš na projektu koji je povezan s OpenCode-om i koristi "opencode" kao dio naziva, npr. "opencode-dashboard" ili "opencode-mobile", dodaj napomenu u svoj README da projekat nije napravio OpenCode tim i da nije povezan s nama. + +### FAQ + +#### Po čemu se razlikuje od Claude Code-a? + +Po mogućnostima je vrlo sličan Claude Code-u. Ključne razlike su: + +- 100% open source +- Nije vezan za jednog provajdera. Iako preporučujemo modele koje nudimo kroz [OpenCode Zen](https://opencode.ai/zen), OpenCode možeš koristiti s Claude, OpenAI, Google ili čak lokalnim modelima. Kako modeli napreduju, razlike među njima će se smanjivati, a cijene padati, zato je nezavisnost od provajdera važna. +- LSP podrška odmah po instalaciji +- Fokus na TUI. OpenCode grade neovim korisnici i kreatori [terminal.shop](https://terminal.shop); pomjeraćemo granice onoga što je moguće u terminalu. +- Klijent/server arhitektura. To, recimo, omogućava da OpenCode radi na tvom računaru dok ga daljinski koristiš iz mobilne aplikacije, što znači da je TUI frontend samo jedan od mogućih klijenata. + +--- + +**Pridruži se našoj zajednici** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.da.md b/README.da.md new file mode 100644 index 00000000000..ac522f29c49 --- /dev/null +++ b/README.da.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

Den open source AI-kodeagent.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installation + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Pakkehåndteringer +npm i -g opencode-ai@latest # eller bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS og Linux (anbefalet, altid up to date) +brew install opencode # macOS og Linux (officiel brew formula, opdateres sjældnere) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # alle OS +nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch +``` + +> [!TIP] +> Fjern versioner ældre end 0.1.x før installation. + +### Desktop-app (BETA) + +OpenCode findes også som desktop-app. Download direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download). + +| Platform | Download | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, eller AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Installationsmappe + +Installationsscriptet bruger følgende prioriteringsrækkefølge for installationsstien: + +1. `$OPENCODE_INSTALL_DIR` - Tilpasset installationsmappe +2. `$XDG_BIN_DIR` - Sti der følger XDG Base Directory Specification +3. `$HOME/bin` - Standard bruger-bin-mappe (hvis den findes eller kan oprettes) +4. `$HOME/.opencode/bin` - Standard fallback + +```bash +# Eksempler +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode har to indbyggede agents, som du kan skifte mellem med `Tab`-tasten. + +- **build** - Standard, agent med fuld adgang til udviklingsarbejde +- **plan** - Skrivebeskyttet agent til analyse og kodeudforskning + - Afviser filredigering som standard + - Spørger om tilladelse før bash-kommandoer + - Ideel til at udforske ukendte kodebaser eller planlægge ændringer + +Derudover findes der en **general**-subagent til komplekse søgninger og flertrinsopgaver. +Den bruges internt og kan kaldes via `@general` i beskeder. + +Læs mere om [agents](https://opencode.ai/docs/agents). + +### Dokumentation + +For mere info om konfiguration af OpenCode, [**se vores docs**](https://opencode.ai/docs). + +### Bidrag + +Hvis du vil bidrage til OpenCode, så læs vores [contributing docs](./CONTRIBUTING.md) før du sender en pull request. + +### Bygget på OpenCode + +Hvis du arbejder på et projekt der er relateret til OpenCode og bruger "opencode" som en del af navnet; f.eks. "opencode-dashboard" eller "opencode-mobile", så tilføj en note i din README, der tydeliggør at projektet ikke er bygget af OpenCode-teamet og ikke er tilknyttet os på nogen måde. + +### FAQ + +#### Hvordan adskiller dette sig fra Claude Code? + +Det minder meget om Claude Code i forhold til funktionalitet. Her er de vigtigste forskelle: + +- 100% open source +- Ikke låst til en udbyder. Selvom vi anbefaler modellerne via [OpenCode Zen](https://opencode.ai/zen); kan OpenCode bruges med Claude, OpenAI, Google eller endda lokale modeller. Efterhånden som modeller udvikler sig vil forskellene mindskes og priserne falde, så det er vigtigt at være provider-agnostic. +- LSP-support out of the box +- Fokus på TUI. OpenCode er bygget af neovim-brugere og skaberne af [terminal.shop](https://terminal.shop); vi vil skubbe grænserne for hvad der er muligt i terminalen. +- Klient/server-arkitektur. Det kan f.eks. lade OpenCode køre på din computer, mens du styrer den eksternt fra en mobilapp. Det betyder at TUI-frontend'en kun er en af de mulige clients. + +--- + +**Bliv en del af vores community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.de.md b/README.de.md new file mode 100644 index 00000000000..87a670f3fce --- /dev/null +++ b/README.de.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

Der Open-Source KI-Coding-Agent.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installation + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Paketmanager +npm i -g opencode-ai@latest # oder bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS und Linux (empfohlen, immer aktuell) +brew install opencode # macOS und Linux (offizielle Brew-Formula, seltener aktualisiert) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # jedes Betriebssystem +nix run nixpkgs#opencode # oder github:anomalyco/opencode für den neuesten dev-Branch +``` + +> [!TIP] +> Entferne Versionen älter als 0.1.x vor der Installation. + +### Desktop-App (BETA) + +OpenCode ist auch als Desktop-Anwendung verfügbar. Lade sie direkt von der [Releases-Seite](https://github.com/anomalyco/opencode/releases) oder [opencode.ai/download](https://opencode.ai/download) herunter. + +| Plattform | Download | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` oder AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Installationsverzeichnis + +Das Installationsskript beachtet die folgende Prioritätsreihenfolge für den Installationspfad: + +1. `$OPENCODE_INSTALL_DIR` - Benutzerdefiniertes Installationsverzeichnis +2. `$XDG_BIN_DIR` - XDG Base Directory Specification-konformer Pfad +3. `$HOME/bin` - Standard-Binärverzeichnis des Users (falls vorhanden oder erstellbar) +4. `$HOME/.opencode/bin` - Standard-Fallback + +```bash +# Beispiele +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode enthält zwei eingebaute Agents, zwischen denen du mit der `Tab`-Taste wechseln kannst. + +- **build** - Standard-Agent mit vollem Zugriff für Entwicklungsarbeit +- **plan** - Nur-Lese-Agent für Analyse und Code-Exploration + - Verweigert Datei-Edits standardmäßig + - Fragt vor dem Ausführen von bash-Befehlen nach + - Ideal zum Erkunden unbekannter Codebases oder zum Planen von Änderungen + +Außerdem ist ein **general**-Subagent für komplexe Suchen und mehrstufige Aufgaben enthalten. +Dieser wird intern genutzt und kann in Nachrichten mit `@general` aufgerufen werden. + +Mehr dazu unter [Agents](https://opencode.ai/docs/agents). + +### Dokumentation + +Mehr Infos zur Konfiguration von OpenCode findest du in unseren [**Docs**](https://opencode.ai/docs). + +### Beitragen + +Wenn du zu OpenCode beitragen möchtest, lies bitte unsere [Contributing Docs](./CONTRIBUTING.md), bevor du einen Pull Request einreichst. + +### Auf OpenCode aufbauen + +Wenn du an einem Projekt arbeitest, das mit OpenCode zusammenhängt und "opencode" als Teil seines Namens verwendet (z.B. "opencode-dashboard" oder "opencode-mobile"), füge bitte einen Hinweis in deine README ein, dass es nicht vom OpenCode-Team gebaut wird und nicht in irgendeiner Weise mit uns verbunden ist. + +### FAQ + +#### Worin unterscheidet sich das von Claude Code? + +In Bezug auf die Fähigkeiten ist es Claude Code sehr ähnlich. Hier sind die wichtigsten Unterschiede: + +- 100% open source +- Nicht an einen Anbieter gekoppelt. Wir empfehlen die Modelle aus [OpenCode Zen](https://opencode.ai/zen); OpenCode kann aber auch mit Claude, OpenAI, Google oder sogar lokalen Modellen genutzt werden. Mit der Weiterentwicklung der Modelle werden die Unterschiede kleiner und die Preise sinken, deshalb ist Provider-Unabhängigkeit wichtig. +- LSP-Unterstützung direkt nach dem Start +- Fokus auf TUI. OpenCode wird von Neovim-Nutzern und den Machern von [terminal.shop](https://terminal.shop) gebaut; wir treiben die Grenzen dessen, was im Terminal möglich ist. +- Client/Server-Architektur. Das ermöglicht z.B., OpenCode auf deinem Computer laufen zu lassen, während du es von einer mobilen App aus fernsteuerst. Das TUI-Frontend ist nur einer der möglichen Clients. + +--- + +**Tritt unserer Community bei** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.es.md b/README.es.md new file mode 100644 index 00000000000..9e456af1c0b --- /dev/null +++ b/README.es.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

El agente de programación con IA de código abierto.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Instalación + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Gestores de paquetes +npm i -g opencode-ai@latest # o bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS y Linux (recomendado, siempre al día) +brew install opencode # macOS y Linux (fórmula oficial de brew, se actualiza menos) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # cualquier sistema +nix run nixpkgs#opencode # o github:anomalyco/opencode para la rama dev más reciente +``` + +> [!TIP] +> Elimina versiones anteriores a 0.1.x antes de instalar. + +### App de escritorio (BETA) + +OpenCode también está disponible como aplicación de escritorio. Descárgala directamente desde la [página de releases](https://github.com/anomalyco/opencode/releases) o desde [opencode.ai/download](https://opencode.ai/download). + +| Plataforma | Descarga | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, o AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Directorio de instalación + +El script de instalación respeta el siguiente orden de prioridad para la ruta de instalación: + +1. `$OPENCODE_INSTALL_DIR` - Directorio de instalación personalizado +2. `$XDG_BIN_DIR` - Ruta compatible con la especificación XDG Base Directory +3. `$HOME/bin` - Directorio binario estándar del usuario (si existe o se puede crear) +4. `$HOME/.opencode/bin` - Alternativa por defecto + +```bash +# Ejemplos +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode incluye dos agents integrados que puedes alternar con la tecla `Tab`. + +- **build** - Por defecto, agent con acceso completo para trabajo de desarrollo +- **plan** - Agent de solo lectura para análisis y exploración de código + - Niega ediciones de archivos por defecto + - Pide permiso antes de ejecutar comandos bash + - Ideal para explorar codebases desconocidas o planificar cambios + +Además, incluye un subagent **general** para búsquedas complejas y tareas de varios pasos. +Se usa internamente y se puede invocar con `@general` en los mensajes. + +Más información sobre [agents](https://opencode.ai/docs/agents). + +### Documentación + +Para más información sobre cómo configurar OpenCode, [**ve a nuestra documentación**](https://opencode.ai/docs). + +### Contribuir + +Si te interesa contribuir a OpenCode, lee nuestras [docs de contribución](./CONTRIBUTING.md) antes de enviar un pull request. + +### Construyendo sobre OpenCode + +Si estás trabajando en un proyecto relacionado con OpenCode y usas "opencode" como parte del nombre; por ejemplo, "opencode-dashboard" u "opencode-mobile", agrega una nota en tu README para aclarar que no está construido por el equipo de OpenCode y que no está afiliado con nosotros de ninguna manera. + +### FAQ + +#### ¿En qué se diferencia de Claude Code? + +Es muy similar a Claude Code en cuanto a capacidades. Estas son las diferencias clave: + +- 100% open source +- No está acoplado a ningún proveedor. Aunque recomendamos los modelos que ofrecemos a través de [OpenCode Zen](https://opencode.ai/zen); OpenCode se puede usar con Claude, OpenAI, Google o incluso modelos locales. A medida que evolucionan los modelos, las brechas se cerrarán y los precios bajarán, por lo que ser agnóstico al proveedor es importante. +- Soporte LSP listo para usar +- Un enfoque en la TUI. OpenCode está construido por usuarios de neovim y los creadores de [terminal.shop](https://terminal.shop); vamos a empujar los límites de lo que es posible en la terminal. +- Arquitectura cliente/servidor. Esto, por ejemplo, permite ejecutar OpenCode en tu computadora mientras lo controlas de forma remota desde una app móvil. Esto significa que el frontend TUI es solo uno de los posibles clientes. + +--- + +**Únete a nuestra comunidad** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 00000000000..c1fca23376d --- /dev/null +++ b/README.fr.md @@ -0,0 +1,141 @@ +

+ + + + + Logo OpenCode + + +

+

L'agent de codage IA open source.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installation + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Gestionnaires de paquets +npm i -g opencode-ai@latest # ou bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS et Linux (recommandé, toujours à jour) +brew install opencode # macOS et Linux (formule officielle brew, mise à jour moins fréquente) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # n'importe quel OS +nix run nixpkgs#opencode # ou github:anomalyco/opencode pour la branche dev la plus récente +``` + +> [!TIP] +> Supprimez les versions antérieures à 0.1.x avant d'installer. + +### Application de bureau (BETA) + +OpenCode est aussi disponible en application de bureau. Téléchargez-la directement depuis la [page des releases](https://github.com/anomalyco/opencode/releases) ou [opencode.ai/download](https://opencode.ai/download). + +| Plateforme | Téléchargement | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, ou AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Répertoire d'installation + +Le script d'installation respecte l'ordre de priorité suivant pour le chemin d'installation : + +1. `$OPENCODE_INSTALL_DIR` - Répertoire d'installation personnalisé +2. `$XDG_BIN_DIR` - Chemin conforme à la spécification XDG Base Directory +3. `$HOME/bin` - Répertoire binaire utilisateur standard (s'il existe ou peut être créé) +4. `$HOME/.opencode/bin` - Repli par défaut + +```bash +# Exemples +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode inclut deux agents intégrés que vous pouvez basculer avec la touche `Tab`. + +- **build** - Par défaut, agent avec accès complet pour le travail de développement +- **plan** - Agent en lecture seule pour l'analyse et l'exploration du code + - Refuse les modifications de fichiers par défaut + - Demande l'autorisation avant d'exécuter des commandes bash + - Idéal pour explorer une base de code inconnue ou planifier des changements + +Un sous-agent **general** est aussi inclus pour les recherches complexes et les tâches en plusieurs étapes. +Il est utilisé en interne et peut être invoqué via `@general` dans les messages. + +En savoir plus sur les [agents](https://opencode.ai/docs/agents). + +### Documentation + +Pour plus d'informations sur la configuration d'OpenCode, [**consultez notre documentation**](https://opencode.ai/docs). + +### Contribuer + +Si vous souhaitez contribuer à OpenCode, lisez nos [docs de contribution](./CONTRIBUTING.md) avant de soumettre une pull request. + +### Construire avec OpenCode + +Si vous travaillez sur un projet lié à OpenCode et que vous utilisez "opencode" dans le nom du projet (par exemple, "opencode-dashboard" ou "opencode-mobile"), ajoutez une note dans votre README pour préciser qu'il n'est pas construit par l'équipe OpenCode et qu'il n'est pas affilié à nous. + +### FAQ + +#### En quoi est-ce différent de Claude Code ? + +C'est très similaire à Claude Code en termes de capacités. Voici les principales différences : + +- 100% open source +- Pas couplé à un fournisseur. Nous recommandons les modèles proposés via [OpenCode Zen](https://opencode.ai/zen) ; OpenCode peut être utilisé avec Claude, OpenAI, Google ou même des modèles locaux. Au fur et à mesure que les modèles évoluent, les écarts se réduiront et les prix baisseront, donc être agnostique au fournisseur est important. +- Support LSP prêt à l'emploi +- Un focus sur la TUI. OpenCode est construit par des utilisateurs de neovim et les créateurs de [terminal.shop](https://terminal.shop) ; nous allons repousser les limites de ce qui est possible dans le terminal. +- Architecture client/serveur. Cela permet par exemple de faire tourner OpenCode sur votre ordinateur tout en le pilotant à distance depuis une application mobile. Cela signifie que la TUI n'est qu'un des clients possibles. + +--- + +**Rejoignez notre communauté** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.gr.md b/README.gr.md new file mode 100644 index 00000000000..2b2c2679d8e --- /dev/null +++ b/README.gr.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

Ο πράκτορας τεχνητής νοημοσύνης ανοικτού κώδικα για προγραμματισμό.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Εγκατάσταση + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Διαχειριστές πακέτων +npm i -g opencode-ai@latest # ή bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS και Linux (προτείνεται, πάντα ενημερωμένο) +brew install opencode # macOS και Linux (επίσημος τύπος brew, λιγότερο συχνές ενημερώσεις) +sudo pacman -S opencode # Arch Linux (Σταθερό) +paru -S opencode-bin # Arch Linux (Τελευταία έκδοση από AUR) +mise use -g opencode # Οποιοδήποτε λειτουργικό σύστημα +nix run nixpkgs#opencode # ή github:anomalyco/opencode με βάση την πιο πρόσφατη αλλαγή από το dev branch +``` + +> [!TIP] +> Αφαίρεσε παλαιότερες εκδόσεις από τη 0.1.x πριν από την εγκατάσταση. + +### Εφαρμογή Desktop (BETA) + +Το OpenCode είναι επίσης διαθέσιμο ως εφαρμογή. Κατέβασε το απευθείας από τη [σελίδα εκδόσεων](https://github.com/anomalyco/opencode/releases) ή το [opencode.ai/download](https://opencode.ai/download). + +| Πλατφόρμα | Λήψη | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, ή AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Κατάλογος Εγκατάστασης + +Το script εγκατάστασης τηρεί την ακόλουθη σειρά προτεραιότητας για τη διαδρομή εγκατάστασης: + +1. `$OPENCODE_INSTALL_DIR` - Προσαρμοσμένος κατάλογος εγκατάστασης +2. `$XDG_BIN_DIR` - Διαδρομή συμβατή με τις προδιαγραφές XDG Base Directory +3. `$HOME/bin` - Τυπικός κατάλογος εκτελέσιμων αρχείων χρήστη (εάν υπάρχει ή μπορεί να δημιουργηθεί) +4. `$HOME/.opencode/bin` - Προεπιλεγμένη εφεδρική διαδρομή + +```bash +# Παραδείγματα +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Πράκτορες + +Το OpenCode περιλαμβάνει δύο ενσωματωμένους πράκτορες μεταξύ των οποίων μπορείτε να εναλλάσσεστε με το πλήκτρο `Tab`. + +- **build** - Προεπιλεγμένος πράκτορας με πλήρη πρόσβαση για εργασία πάνω σε κώδικα +- **plan** - Πράκτορας μόνο ανάγνωσης για ανάλυση και εξερεύνηση κώδικα + - Αρνείται την επεξεργασία αρχείων από προεπιλογή + - Ζητά άδεια πριν εκτελέσει εντολές bash + - Ιδανικός για εξερεύνηση άγνωστων αρχείων πηγαίου κώδικα ή σχεδιασμό αλλαγών + +Περιλαμβάνεται επίσης ένας **general** υποπράκτορας για σύνθετες αναζητήσεις και πολυβηματικές διεργασίες. +Χρησιμοποιείται εσωτερικά και μπορεί να κληθεί χρησιμοποιώντας `@general` στα μηνύματα. + +Μάθετε περισσότερα για τους [πράκτορες](https://opencode.ai/docs/agents). + +### Οδηγός Χρήσης + +Για περισσότερες πληροφορίες σχετικά με τη ρύθμιση του OpenCode, [**πλοηγήσου στον οδηγό χρήσης μας**](https://opencode.ai/docs). + +### Συνεισφορά + +Εάν ενδιαφέρεσαι να συνεισφέρεις στο OpenCode, διαβάστε τα [οδηγό χρήσης συνεισφοράς](./CONTRIBUTING.md) πριν υποβάλεις ένα pull request. + +### Δημιουργία πάνω στο OpenCode + +Εάν εργάζεσαι σε ένα έργο σχετικό με το OpenCode και χρησιμοποιείτε το "opencode" ως μέρος του ονόματός του, για παράδειγμα "opencode-dashboard" ή "opencode-mobile", πρόσθεσε μια σημείωση στο README σας για να διευκρινίσεις ότι δεν είναι κατασκευασμένο από την ομάδα του OpenCode και δεν έχει καμία σχέση με εμάς. + +### Συχνές Ερωτήσεις + +#### Πώς διαφέρει αυτό από το Claude Code; + +Είναι πολύ παρόμοιο με το Claude Code ως προς τις δυνατότητες. Ακολουθούν οι βασικές διαφορές: + +- 100% ανοιχτού κώδικα +- Δεν είναι συνδεδεμένο με κανέναν πάροχο. Αν και συνιστούμε τα μοντέλα που παρέχουμε μέσω του [OpenCode Zen](https://opencode.ai/zen), το OpenCode μπορεί να χρησιμοποιηθεί με Claude, OpenAI, Google, ή ακόμα και τοπικά μοντέλα. Καθώς τα μοντέλα εξελίσσονται, τα κενά μεταξύ τους θα κλείσουν και οι τιμές θα μειωθούν, οπότε είναι σημαντικό να είσαι ανεξάρτητος από τον πάροχο. +- Out-of-the-box υποστήριξη LSP +- Εστίαση στο TUI. Το OpenCode είναι κατασκευασμένο από χρήστες που χρησιμοποιούν neovim και τους δημιουργούς του [terminal.shop](https://terminal.shop)· θα εξαντλήσουμε τα όρια του τι είναι δυνατό στο terminal. +- Αρχιτεκτονική client/server. Αυτό, για παράδειγμα, μπορεί να επιτρέψει στο OpenCode να τρέχει στον υπολογιστή σου ενώ το χειρίζεσαι εξ αποστάσεως από μια εφαρμογή κινητού, που σημαίνει ότι το TUI frontend είναι μόνο ένας από τους πιθανούς clients. + +--- + +**Γίνε μέλος της κοινότητάς μας** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.it.md b/README.it.md new file mode 100644 index 00000000000..3e516a90270 --- /dev/null +++ b/README.it.md @@ -0,0 +1,141 @@ +

+ + + + + Logo OpenCode + + +

+

L’agente di coding AI open source.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installazione + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Package manager +npm i -g opencode-ai@latest # oppure bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS e Linux (consigliato, sempre aggiornato) +brew install opencode # macOS e Linux (formula brew ufficiale, aggiornata meno spesso) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # Qualsiasi OS +nix run nixpkgs#opencode # oppure github:anomalyco/opencode per l’ultima branch di sviluppo +``` + +> [!TIP] +> Rimuovi le versioni precedenti alla 0.1.x prima di installare. + +### App Desktop (BETA) + +OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla direttamente dalla [pagina delle release](https://github.com/anomalyco/opencode/releases) oppure da [opencode.ai/download](https://opencode.ai/download). + +| Piattaforma | Download | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, oppure AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Directory di installazione + +Lo script di installazione rispetta il seguente ordine di priorità per il percorso di installazione: + +1. `$OPENCODE_INSTALL_DIR` – Directory di installazione personalizzata +2. `$XDG_BIN_DIR` – Percorso conforme alla XDG Base Directory Specification +3. `$HOME/bin` – Directory binaria standard dell’utente (se esiste o può essere creata) +4. `$HOME/.opencode/bin` – Fallback predefinito + +```bash +# Esempi +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agenti + +OpenCode include due agenti integrati tra cui puoi passare usando il tasto `Tab`. + +- **build** – Predefinito, agente con accesso completo per il lavoro di sviluppo +- **plan** – Agente in sola lettura per analisi ed esplorazione del codice + - Nega le modifiche ai file per impostazione predefinita + - Chiede il permesso prima di eseguire comandi bash + - Ideale per esplorare codebase sconosciute o pianificare modifiche + +È inoltre incluso un sotto-agente **general** per ricerche complesse e attività multi-step. +Viene utilizzato internamente e può essere invocato usando `@general` nei messaggi. + +Scopri di più sugli [agenti](https://opencode.ai/docs/agents). + +### Documentazione + +Per maggiori informazioni su come configurare OpenCode, [**consulta la nostra documentazione**](https://opencode.ai/docs). + +### Contribuire + +Se sei interessato a contribuire a OpenCode, leggi la nostra [guida alla contribuzione](./CONTRIBUTING.md) prima di inviare una pull request. + +### Costruire su OpenCode + +Se stai lavorando a un progetto correlato a OpenCode e che utilizza “opencode” come parte del nome (ad esempio “opencode-dashboard” o “opencode-mobile”), aggiungi una nota nel tuo README per chiarire che non è sviluppato dal team OpenCode e che non è affiliato in alcun modo con noi. + +### FAQ + +#### In cosa è diverso da Claude Code? + +È molto simile a Claude Code in termini di funzionalità. Ecco le principali differenze: + +- 100% open source +- Non è legato a nessun provider. Anche se consigliamo i modelli forniti tramite [OpenCode Zen](https://opencode.ai/zen), OpenCode può essere utilizzato con Claude, OpenAI, Google o persino modelli locali. Con l’evoluzione dei modelli, le differenze tra di essi si ridurranno e i prezzi scenderanno, quindi essere indipendenti dal provider è importante. +- Supporto LSP pronto all’uso +- Forte attenzione alla TUI. OpenCode è sviluppato da utenti neovim e dai creatori di [terminal.shop](https://terminal.shop); spingeremo al limite ciò che è possibile fare nel terminale. +- Architettura client/server. Questo, ad esempio, permette a OpenCode di girare sul tuo computer mentre lo controlli da remoto tramite un’app mobile. La frontend TUI è quindi solo uno dei possibili client. + +--- + +**Unisciti alla nostra community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 00000000000..144dc7b6f8a --- /dev/null +++ b/README.ja.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

オープンソースのAIコーディングエージェント。

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### インストール + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# パッケージマネージャー +npm i -g opencode-ai@latest # bun/pnpm/yarn でもOK +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS と Linux(推奨。常に最新) +brew install opencode # macOS と Linux(公式 brew formula。更新頻度は低め) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # どのOSでも +nix run nixpkgs#opencode # または github:anomalyco/opencode で最新 dev ブランチ +``` + +> [!TIP] +> インストール前に 0.1.x より古いバージョンを削除してください。 + +### デスクトップアプリ (BETA) + +OpenCode はデスクトップアプリとしても利用できます。[releases page](https://github.com/anomalyco/opencode/releases) から直接ダウンロードするか、[opencode.ai/download](https://opencode.ai/download) を利用してください。 + +| プラットフォーム | ダウンロード | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`、`.rpm`、または AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### インストールディレクトリ + +インストールスクリプトは、インストール先パスを次の優先順位で決定します。 + +1. `$OPENCODE_INSTALL_DIR` - カスタムのインストールディレクトリ +2. `$XDG_BIN_DIR` - XDG Base Directory Specification に準拠したパス +3. `$HOME/bin` - 標準のユーザー用バイナリディレクトリ(存在する場合、または作成できる場合) +4. `$HOME/.opencode/bin` - デフォルトのフォールバック + +```bash +# 例 +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode には組み込みの Agent が2つあり、`Tab` キーで切り替えられます。 + +- **build** - デフォルト。開発向けのフルアクセス Agent +- **plan** - 分析とコード探索向けの読み取り専用 Agent + - デフォルトでファイル編集を拒否 + - bash コマンド実行前に確認 + - 未知のコードベース探索や変更計画に最適 + +また、複雑な検索やマルチステップのタスク向けに **general** サブ Agent も含まれています。 +内部的に使用されており、メッセージで `@general` と入力して呼び出せます。 + +[agents](https://opencode.ai/docs/agents) の詳細はこちら。 + +### ドキュメント + +OpenCode の設定については [**ドキュメント**](https://opencode.ai/docs) を参照してください。 + +### コントリビュート + +OpenCode に貢献したい場合は、Pull Request を送る前に [contributing docs](./CONTRIBUTING.md) を読んでください。 + +### OpenCode の上に構築する + +OpenCode に関連するプロジェクトで、名前に "opencode"(例: "opencode-dashboard" や "opencode-mobile")を含める場合は、そのプロジェクトが OpenCode チームによって作られたものではなく、いかなる形でも関係がないことを README に明記してください。 + +### FAQ + +#### Claude Code との違いは? + +機能面では Claude Code と非常に似ています。主な違いは次のとおりです。 + +- 100% オープンソース +- 特定のプロバイダーに依存しません。[OpenCode Zen](https://opencode.ai/zen) で提供しているモデルを推奨しますが、OpenCode は Claude、OpenAI、Google、またはローカルモデルでも利用できます。モデルが進化すると差は縮まり価格も下がるため、provider-agnostic であることが重要です。 +- そのまま使える LSP サポート +- TUI にフォーカス。OpenCode は neovim ユーザーと [terminal.shop](https://terminal.shop) の制作者によって作られており、ターミナルで可能なことの限界を押し広げます。 +- クライアント/サーバー構成。例えば OpenCode をあなたのPCで動かし、モバイルアプリからリモート操作できます。TUI フロントエンドは複数あるクライアントの1つにすぎません。 + +--- + +**コミュニティに参加** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 00000000000..32defc0a5e0 --- /dev/null +++ b/README.ko.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

오픈 소스 AI 코딩 에이전트.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### 설치 + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# 패키지 매니저 +npm i -g opencode-ai@latest # bun/pnpm/yarn 도 가능 +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS 및 Linux (권장, 항상 최신) +brew install opencode # macOS 및 Linux (공식 brew formula, 업데이트 빈도 낮음) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # 어떤 OS든 +nix run nixpkgs#opencode # 또는 github:anomalyco/opencode 로 최신 dev 브랜치 +``` + +> [!TIP] +> 설치 전에 0.1.x 보다 오래된 버전을 제거하세요. + +### 데스크톱 앱 (BETA) + +OpenCode 는 데스크톱 앱으로도 제공됩니다. [releases page](https://github.com/anomalyco/opencode/releases) 에서 직접 다운로드하거나 [opencode.ai/download](https://opencode.ai/download) 를 이용하세요. + +| 플랫폼 | 다운로드 | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, 또는 AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### 설치 디렉터리 + +설치 스크립트는 설치 경로를 다음 우선순위로 결정합니다. + +1. `$OPENCODE_INSTALL_DIR` - 사용자 지정 설치 디렉터리 +2. `$XDG_BIN_DIR` - XDG Base Directory Specification 준수 경로 +3. `$HOME/bin` - 표준 사용자 바이너리 디렉터리 (존재하거나 생성 가능할 경우) +4. `$HOME/.opencode/bin` - 기본 폴백 + +```bash +# 예시 +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode 에는 내장 에이전트 2개가 있으며 `Tab` 키로 전환할 수 있습니다. + +- **build** - 기본값, 개발 작업을 위한 전체 권한 에이전트 +- **plan** - 분석 및 코드 탐색을 위한 읽기 전용 에이전트 + - 기본적으로 파일 편집을 거부 + - bash 명령 실행 전에 권한을 요청 + - 낯선 코드베이스를 탐색하거나 변경을 계획할 때 적합 + +또한 복잡한 검색과 여러 단계 작업을 위한 **general** 서브 에이전트가 포함되어 있습니다. +내부적으로 사용되며, 메시지에서 `@general` 로 호출할 수 있습니다. + +[agents](https://opencode.ai/docs/agents) 에 대해 더 알아보세요. + +### 문서 + +OpenCode 설정에 대한 자세한 내용은 [**문서**](https://opencode.ai/docs) 를 참고하세요. + +### 기여하기 + +OpenCode 에 기여하고 싶다면, Pull Request 를 제출하기 전에 [contributing docs](./CONTRIBUTING.md) 를 읽어주세요. + +### OpenCode 기반으로 만들기 + +OpenCode 와 관련된 프로젝트를 진행하면서 이름에 "opencode"(예: "opencode-dashboard" 또는 "opencode-mobile") 를 포함한다면, README 에 해당 프로젝트가 OpenCode 팀이 만든 것이 아니며 어떤 방식으로도 우리와 제휴되어 있지 않다는 점을 명시해 주세요. + +### FAQ + +#### Claude Code 와는 무엇이 다른가요? + +기능 면에서는 Claude Code 와 매우 유사합니다. 주요 차이점은 다음과 같습니다. + +- 100% 오픈 소스 +- 특정 제공자에 묶여 있지 않습니다. [OpenCode Zen](https://opencode.ai/zen) 을 통해 제공하는 모델을 권장하지만, OpenCode 는 Claude, OpenAI, Google 또는 로컬 모델과도 사용할 수 있습니다. 모델이 발전하면서 격차는 줄고 가격은 내려가므로 provider-agnostic 인 것이 중요합니다. +- 기본으로 제공되는 LSP 지원 +- TUI 에 집중. OpenCode 는 neovim 사용자와 [terminal.shop](https://terminal.shop) 제작자가 만들었으며, 터미널에서 가능한 것의 한계를 밀어붙입니다. +- 클라이언트/서버 아키텍처. 예를 들어 OpenCode 를 내 컴퓨터에서 실행하면서 모바일 앱으로 원격 조작할 수 있습니다. 즉, TUI 프런트엔드는 가능한 여러 클라이언트 중 하나일 뿐입니다. + +--- + +**커뮤니티에 참여하기** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.md b/README.md index 64ca1ef7a6f..79ccf8b3491 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,31 @@ Build status

+

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ [![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) --- @@ -30,7 +55,8 @@ scoop install opencode # Windows choco install opencode # Windows brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date) brew install opencode # macOS and Linux (official brew formula, updated less) -paru -S opencode-bin # Arch Linux +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) mise use -g opencode # Any OS nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch ``` @@ -62,7 +88,7 @@ The install script respects the following priority order for the installation pa 1. `$OPENCODE_INSTALL_DIR` - Custom installation directory 2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path -3. `$HOME/bin` - Standard user binary directory (if exists or can be created) +3. `$HOME/bin` - Standard user binary directory (if it exists or can be created) 4. `$HOME/.opencode/bin` - Default fallback ```bash @@ -75,20 +101,20 @@ XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash OpenCode includes two built-in agents you can switch between with the `Tab` key. -- **build** - Default, full access agent for development work +- **build** - Default, full-access agent for development work - **plan** - Read-only agent for analysis and code exploration - Denies file edits by default - Asks permission before running bash commands - Ideal for exploring unfamiliar codebases or planning changes -Also, included is a **general** subagent for complex searches and multistep tasks. +Also included is a **general** subagent for complex searches and multistep tasks. This is used internally and can be invoked using `@general` in messages. Learn more about [agents](https://opencode.ai/docs/agents). ### Documentation -For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs). +For more info on how to configure OpenCode, [**head over to our docs**](https://opencode.ai/docs). ### Contributing @@ -96,7 +122,7 @@ If you're interested in contributing to OpenCode, please read our [contributing ### Building on OpenCode -If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way. +If you are working on a project that's related to OpenCode and is using "opencode" as part of its name, for example "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way. ### FAQ @@ -105,10 +131,10 @@ If you are working on a project that's related to OpenCode and is using "opencod It's very similar to Claude Code in terms of capability. Here are the key differences: - 100% open source -- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important. -- Out of the box LSP support +- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important. +- Out-of-the-box LSP support - A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal. -- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients. +- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients. --- diff --git a/README.no.md b/README.no.md new file mode 100644 index 00000000000..c3348286b29 --- /dev/null +++ b/README.no.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

AI-kodeagent med åpen kildekode.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Installasjon + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Pakkehåndterere +npm i -g opencode-ai@latest # eller bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS og Linux (anbefalt, alltid oppdatert) +brew install opencode # macOS og Linux (offisiell brew-formel, oppdateres sjeldnere) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # alle OS +nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch +``` + +> [!TIP] +> Fjern versjoner eldre enn 0.1.x før du installerer. + +### Desktop-app (BETA) + +OpenCode er også tilgjengelig som en desktop-app. Last ned direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download). + +| Plattform | Nedlasting | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` eller AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Installasjonsmappe + +Installasjonsskriptet bruker følgende prioritet for installasjonsstien: + +1. `$OPENCODE_INSTALL_DIR` - Egendefinert installasjonsmappe +2. `$XDG_BIN_DIR` - Sti som følger XDG Base Directory Specification +3. `$HOME/bin` - Standard brukerbinar-mappe (hvis den finnes eller kan opprettes) +4. `$HOME/.opencode/bin` - Standard fallback + +```bash +# Eksempler +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode har to innebygde agents du kan bytte mellom med `Tab`-tasten. + +- **build** - Standard, agent med full tilgang for utviklingsarbeid +- **plan** - Skrivebeskyttet agent for analyse og kodeutforsking + - Nekter filendringer som standard + - Spør om tillatelse før bash-kommandoer + - Ideell for å utforske ukjente kodebaser eller planlegge endringer + +Det finnes også en **general**-subagent for komplekse søk og flertrinnsoppgaver. +Den brukes internt og kan kalles via `@general` i meldinger. + +Les mer om [agents](https://opencode.ai/docs/agents). + +### Dokumentasjon + +For mer info om hvordan du konfigurerer OpenCode, [**se dokumentasjonen**](https://opencode.ai/docs). + +### Bidra + +Hvis du vil bidra til OpenCode, les [contributing docs](./CONTRIBUTING.md) før du sender en pull request. + +### Bygge på OpenCode + +Hvis du jobber med et prosjekt som er relatert til OpenCode og bruker "opencode" som en del av navnet; for eksempel "opencode-dashboard" eller "opencode-mobile", legg inn en merknad i README som presiserer at det ikke er bygget av OpenCode-teamet og ikke er tilknyttet oss på noen måte. + +### FAQ + +#### Hvordan er dette forskjellig fra Claude Code? + +Det er veldig likt Claude Code når det gjelder funksjonalitet. Her er de viktigste forskjellene: + +- 100% open source +- Ikke knyttet til en bestemt leverandør. Selv om vi anbefaler modellene vi tilbyr gjennom [OpenCode Zen](https://opencode.ai/zen); kan OpenCode brukes med Claude, OpenAI, Google eller til og med lokale modeller. Etter hvert som modellene utvikler seg vil gapene lukkes og prisene gå ned, så det er viktig å være provider-agnostic. +- LSP-støtte rett ut av boksen +- Fokus på TUI. OpenCode er bygget av neovim-brukere og skaperne av [terminal.shop](https://terminal.shop); vi kommer til å presse grensene for hva som er mulig i terminalen. +- Klient/server-arkitektur. Dette kan for eksempel la OpenCode kjøre på maskinen din, mens du styrer den eksternt fra en mobilapp. Det betyr at TUI-frontend'en bare er en av de mulige klientene. + +--- + +**Bli med i fellesskapet** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.pl.md b/README.pl.md new file mode 100644 index 00000000000..4c5a0766561 --- /dev/null +++ b/README.pl.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

Otwartoźródłowy agent kodujący AI.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Instalacja + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Menedżery pakietów +npm i -g opencode-ai@latest # albo bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS i Linux (polecane, zawsze aktualne) +brew install opencode # macOS i Linux (oficjalna formuła brew, rzadziej aktualizowana) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # dowolny system +nix run nixpkgs#opencode # lub github:anomalyco/opencode dla najnowszej gałęzi dev +``` + +> [!TIP] +> Przed instalacją usuń wersje starsze niż 0.1.x. + +### Aplikacja desktopowa (BETA) + +OpenCode jest także dostępny jako aplikacja desktopowa. Pobierz ją bezpośrednio ze strony [releases](https://github.com/anomalyco/opencode/releases) lub z [opencode.ai/download](https://opencode.ai/download). + +| Platforma | Pobieranie | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` lub AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Katalog instalacji + +Skrypt instalacyjny stosuje następujący priorytet wyboru ścieżki instalacji: + +1. `$OPENCODE_INSTALL_DIR` - Własny katalog instalacji +2. `$XDG_BIN_DIR` - Ścieżka zgodna ze specyfikacją XDG Base Directory +3. `$HOME/bin` - Standardowy katalog binarny użytkownika (jeśli istnieje lub można go utworzyć) +4. `$HOME/.opencode/bin` - Domyślny fallback + +```bash +# Przykłady +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode zawiera dwóch wbudowanych agentów, między którymi możesz przełączać się klawiszem `Tab`. + +- **build** - Domyślny agent z pełnym dostępem do pracy developerskiej +- **plan** - Agent tylko do odczytu do analizy i eksploracji kodu + - Domyślnie odmawia edycji plików + - Pyta o zgodę przed uruchomieniem komend bash + - Idealny do poznawania nieznanych baz kodu lub planowania zmian + +Dodatkowo jest subagent **general** do złożonych wyszukiwań i wieloetapowych zadań. +Jest używany wewnętrznie i można go wywołać w wiadomościach przez `@general`. + +Dowiedz się więcej o [agents](https://opencode.ai/docs/agents). + +### Dokumentacja + +Więcej informacji o konfiguracji OpenCode znajdziesz w [**dokumentacji**](https://opencode.ai/docs). + +### Współtworzenie + +Jeśli chcesz współtworzyć OpenCode, przeczytaj [contributing docs](./CONTRIBUTING.md) przed wysłaniem pull requesta. + +### Budowanie na OpenCode + +Jeśli pracujesz nad projektem związanym z OpenCode i używasz "opencode" jako części nazwy (na przykład "opencode-dashboard" lub "opencode-mobile"), dodaj proszę notatkę do swojego README, aby wyjaśnić, że projekt nie jest tworzony przez zespół OpenCode i nie jest z nami w żaden sposób powiązany. + +### FAQ + +#### Czym to się różni od Claude Code? + +Jest bardzo podobne do Claude Code pod względem możliwości. Oto kluczowe różnice: + +- 100% open source +- Niezależne od dostawcy. Chociaż polecamy modele oferowane przez [OpenCode Zen](https://opencode.ai/zen); OpenCode może być używany z Claude, OpenAI, Google, a nawet z modelami lokalnymi. W miarę jak modele ewoluują, różnice będą się zmniejszać, a ceny spadać, więc ważna jest niezależność od dostawcy. +- Wbudowane wsparcie LSP +- Skupienie na TUI. OpenCode jest budowany przez użytkowników neovim i twórców [terminal.shop](https://terminal.shop); przesuwamy granice tego, co jest możliwe w terminalu. +- Architektura klient/serwer. Pozwala np. uruchomić OpenCode na twoim komputerze, a sterować nim zdalnie z aplikacji mobilnej. To znaczy, że frontend TUI jest tylko jednym z możliwych klientów. + +--- + +**Dołącz do naszej społeczności** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 00000000000..e507be70e65 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

Открытый AI-агент для программирования.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Установка + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Менеджеры пакетов +npm i -g opencode-ai@latest # или bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS и Linux (рекомендуем, всегда актуально) +brew install opencode # macOS и Linux (официальная формула brew, обновляется реже) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # любая ОС +nix run nixpkgs#opencode # или github:anomalyco/opencode для самой свежей ветки dev +``` + +> [!TIP] +> Перед установкой удалите версии старше 0.1.x. + +### Десктопное приложение (BETA) + +OpenCode также доступен как десктопное приложение. Скачайте его со [страницы релизов](https://github.com/anomalyco/opencode/releases) или с [opencode.ai/download](https://opencode.ai/download). + +| Платформа | Загрузка | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` или AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Каталог установки + +Скрипт установки выбирает путь установки в следующем порядке приоритета: + +1. `$OPENCODE_INSTALL_DIR` - Пользовательский каталог установки +2. `$XDG_BIN_DIR` - Путь, совместимый со спецификацией XDG Base Directory +3. `$HOME/bin` - Стандартный каталог пользовательских бинарников (если существует или можно создать) +4. `$HOME/.opencode/bin` - Fallback по умолчанию + +```bash +# Примеры +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +В OpenCode есть два встроенных агента, между которыми можно переключаться клавишей `Tab`. + +- **build** - По умолчанию, агент с полным доступом для разработки +- **plan** - Агент только для чтения для анализа и изучения кода + - По умолчанию запрещает редактирование файлов + - Запрашивает разрешение перед выполнением bash-команд + - Идеален для изучения незнакомых кодовых баз или планирования изменений + +Также включен сабагент **general** для сложных поисков и многошаговых задач. +Он используется внутренне и может быть вызван в сообщениях через `@general`. + +Подробнее об [agents](https://opencode.ai/docs/agents). + +### Документация + +Больше информации о том, как настроить OpenCode: [**наши docs**](https://opencode.ai/docs). + +### Вклад + +Если вы хотите внести вклад в OpenCode, прочитайте [contributing docs](./CONTRIBUTING.md) перед тем, как отправлять pull request. + +### Разработка на базе OpenCode + +Если вы делаете проект, связанный с OpenCode, и используете "opencode" как часть имени (например, "opencode-dashboard" или "opencode-mobile"), добавьте примечание в README, чтобы уточнить, что проект не создан командой OpenCode и не аффилирован с нами. + +### FAQ + +#### Чем это отличается от Claude Code? + +По возможностям это очень похоже на Claude Code. Вот ключевые отличия: + +- 100% open source +- Не привязано к одному провайдеру. Мы рекомендуем модели из [OpenCode Zen](https://opencode.ai/zen); но OpenCode можно использовать с Claude, OpenAI, Google или даже локальными моделями. По мере развития моделей разрыв будет сокращаться, а цены падать, поэтому важна независимость от провайдера. +- Поддержка LSP из коробки +- Фокус на TUI. OpenCode построен пользователями neovim и создателями [terminal.shop](https://terminal.shop); мы будем раздвигать границы того, что возможно в терминале. +- Архитектура клиент/сервер. Например, это позволяет запускать OpenCode на вашем компьютере, а управлять им удаленно из мобильного приложения. Это значит, что TUI-фронтенд - лишь один из возможных клиентов. + +--- + +**Присоединяйтесь к нашему сообществу** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.th.md b/README.th.md new file mode 100644 index 00000000000..4a4ea62c957 --- /dev/null +++ b/README.th.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

เอเจนต์การเขียนโค้ดด้วย AI แบบโอเพนซอร์ส

+

+ Discord + npm + สถานะการสร้าง +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### การติดตั้ง + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# ตัวจัดการแพ็กเกจ +npm i -g opencode-ai@latest # หรือ bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS และ Linux (แนะนำ อัปเดตเสมอ) +brew install opencode # macOS และ Linux (brew formula อย่างเป็นทางการ อัปเดตน้อยกว่า) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # ระบบปฏิบัติการใดก็ได้ +nix run nixpkgs#opencode # หรือ github:anomalyco/opencode สำหรับสาขาพัฒนาล่าสุด +``` + +> [!TIP] +> ลบเวอร์ชันที่เก่ากว่า 0.1.x ก่อนติดตั้ง + +### แอปพลิเคชันเดสก์ท็อป (เบต้า) + +OpenCode มีให้ใช้งานเป็นแอปพลิเคชันเดสก์ท็อป ดาวน์โหลดโดยตรงจาก [หน้ารุ่น](https://github.com/anomalyco/opencode/releases) หรือ [opencode.ai/download](https://opencode.ai/download) + +| แพลตฟอร์ม | ดาวน์โหลด | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, หรือ AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### ไดเรกทอรีการติดตั้ง + +สคริปต์การติดตั้งจะใช้ลำดับความสำคัญตามเส้นทางการติดตั้ง: + +1. `$OPENCODE_INSTALL_DIR` - ไดเรกทอรีการติดตั้งที่กำหนดเอง +2. `$XDG_BIN_DIR` - เส้นทางที่สอดคล้องกับ XDG Base Directory Specification +3. `$HOME/bin` - ไดเรกทอรีไบนารีผู้ใช้มาตรฐาน (หากมีอยู่หรือสามารถสร้างได้) +4. `$HOME/.opencode/bin` - ค่าสำรองเริ่มต้น + +```bash +# ตัวอย่าง +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### เอเจนต์ + +OpenCode รวมเอเจนต์ในตัวสองตัวที่คุณสามารถสลับได้ด้วยปุ่ม `Tab` + +- **build** - เอเจนต์เริ่มต้น มีสิทธิ์เข้าถึงแบบเต็มสำหรับงานพัฒนา +- **plan** - เอเจนต์อ่านอย่างเดียวสำหรับการวิเคราะห์และการสำรวจโค้ด + - ปฏิเสธการแก้ไขไฟล์โดยค่าเริ่มต้น + - ขอสิทธิ์ก่อนเรียกใช้คำสั่ง bash + - เหมาะสำหรับสำรวจโค้ดเบสที่ไม่คุ้นเคยหรือวางแผนการเปลี่ยนแปลง + +นอกจากนี้ยังมีเอเจนต์ย่อย **general** สำหรับการค้นหาที่ซับซ้อนและงานหลายขั้นตอน +ใช้ภายในและสามารถเรียกใช้ได้โดยใช้ `@general` ในข้อความ + +เรียนรู้เพิ่มเติมเกี่ยวกับ [เอเจนต์](https://opencode.ai/docs/agents) + +### เอกสารประกอบ + +สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีกำหนดค่า OpenCode [**ไปที่เอกสารของเรา**](https://opencode.ai/docs) + +### การมีส่วนร่วม + +หากคุณสนใจที่จะมีส่วนร่วมใน OpenCode โปรดอ่าน [เอกสารการมีส่วนร่วม](./CONTRIBUTING.md) ก่อนส่ง Pull Request + +### การสร้างบน OpenCode + +หากคุณทำงานในโปรเจกต์ที่เกี่ยวข้องกับ OpenCode และใช้ "opencode" เป็นส่วนหนึ่งของชื่อ เช่น "opencode-dashboard" หรือ "opencode-mobile" โปรดเพิ่มหมายเหตุใน README ของคุณเพื่อชี้แจงว่าไม่ได้สร้างโดยทีม OpenCode และไม่ได้เกี่ยวข้องกับเราในทางใด + +### คำถามที่พบบ่อย + +#### ต่างจาก Claude Code อย่างไร? + +คล้ายกับ Claude Code มากในแง่ความสามารถ นี่คือความแตกต่างหลัก: + +- โอเพนซอร์ส 100% +- ไม่ผูกมัดกับผู้ให้บริการใดๆ แม้ว่าเราจะแนะนำโมเดลที่เราจัดหาให้ผ่าน [OpenCode Zen](https://opencode.ai/zen) OpenCode สามารถใช้กับ Claude, OpenAI, Google หรือแม้กระทั่งโมเดลในเครื่องได้ เมื่อโมเดลพัฒนาช่องว่างระหว่างพวกมันจะปิดลงและราคาจะลดลง ดังนั้นการไม่ผูกมัดกับผู้ให้บริการจึงสำคัญ +- รองรับ LSP ใช้งานได้ทันทีหลังการติดตั้งโดยไม่ต้องปรับแต่งหรือเปลี่ยนแปลงฟังก์ชันการทำงานใด ๆ +- เน้นที่ TUI OpenCode สร้างโดยผู้ใช้ neovim และผู้สร้าง [terminal.shop](https://terminal.shop) เราจะผลักดันขีดจำกัดของสิ่งที่เป็นไปได้ในเทอร์มินัล +- สถาปัตยกรรมไคลเอนต์/เซิร์ฟเวอร์ ตัวอย่างเช่น อาจอนุญาตให้ OpenCode ทำงานบนคอมพิวเตอร์ของคุณ ในขณะที่คุณสามารถขับเคลื่อนจากระยะไกลผ่านแอปมือถือ หมายความว่า TUI frontend เป็นหนึ่งในไคลเอนต์ที่เป็นไปได้เท่านั้น + +--- + +**ร่วมชุมชนของเรา** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.tr.md b/README.tr.md new file mode 100644 index 00000000000..e88b40f8751 --- /dev/null +++ b/README.tr.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

Açık kaynaklı yapay zeka kodlama asistanı.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Kurulum + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Paket yöneticileri +npm i -g opencode-ai@latest # veya bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS ve Linux (önerilir, her zaman güncel) +brew install opencode # macOS ve Linux (resmi brew formülü, daha az güncellenir) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # Tüm işletim sistemleri +nix run nixpkgs#opencode # veya en güncel geliştirme dalı için github:anomalyco/opencode +``` + +> [!TIP] +> Kurulumdan önce 0.1.x'ten eski sürümleri kaldırın. + +### Masaüstü Uygulaması (BETA) + +OpenCode ayrıca masaüstü uygulaması olarak da mevcuttur. Doğrudan [sürüm sayfasından](https://github.com/anomalyco/opencode/releases) veya [opencode.ai/download](https://opencode.ai/download) adresinden indirebilirsiniz. + +| Platform | İndirme | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` veya AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Kurulum Dizini (Installation Directory) + +Kurulum betiği (install script), kurulum yolu (installation path) için aşağıdaki öncelik sırasını takip eder: + +1. `$OPENCODE_INSTALL_DIR` - Özel kurulum dizini +2. `$XDG_BIN_DIR` - XDG Base Directory Specification uyumlu yol +3. `$HOME/bin` - Standart kullanıcı binary dizini (varsa veya oluşturulabiliyorsa) +4. `$HOME/.opencode/bin` - Varsayılan yedek konum + +```bash +# Örnekler +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Ajanlar + +OpenCode, `Tab` tuşuyla aralarında geçiş yapabileceğiniz iki yerleşik (built-in) ajan içerir. + +- **build** - Varsayılan, geliştirme çalışmaları için tam erişimli ajan +- **plan** - Analiz ve kod keşfi için salt okunur ajan + - Varsayılan olarak dosya düzenlemelerini reddeder + - Bash komutlarını çalıştırmadan önce izin ister + - Tanımadığınız kod tabanlarını keşfetmek veya değişiklikleri planlamak için ideal + +Ayrıca, karmaşık aramalar ve çok adımlı görevler için bir **genel** alt ajan bulunmaktadır. +Bu dahili olarak kullanılır ve mesajlarda `@general` ile çağrılabilir. + +[Ajanlar](https://opencode.ai/docs/agents) hakkında daha fazla bilgi edinin. + +### Dokümantasyon + +OpenCode'u nasıl yapılandıracağınız hakkında daha fazla bilgi için [**dokümantasyonumuza göz atın**](https://opencode.ai/docs). + +### Katkıda Bulunma + +OpenCode'a katkıda bulunmak istiyorsanız, lütfen bir pull request göndermeden önce [katkıda bulunma dokümanlarımızı](./CONTRIBUTING.md) okuyun. + +### OpenCode Üzerine Geliştirme + +OpenCode ile ilgili bir proje üzerinde çalışıyorsanız ve projenizin adının bir parçası olarak "opencode" kullanıyorsanız (örneğin, "opencode-dashboard" veya "opencode-mobile"), lütfen README dosyanıza projenin OpenCode ekibi tarafından geliştirilmediğini ve bizimle hiçbir şekilde bağlantılı olmadığını belirten bir not ekleyin. + +### SSS + +#### Bu Claude Code'dan nasıl farklı? + +Yetenekler açısından Claude Code'a çok benzer. İşte temel farklar: + +- %100 açık kaynak +- Herhangi bir sağlayıcıya bağlı değil. [OpenCode Zen](https://opencode.ai/zen) üzerinden sunduğumuz modelleri önermekle birlikte; OpenCode, Claude, OpenAI, Google veya hatta yerel modellerle kullanılabilir. Modeller geliştikçe aralarındaki farklar kapanacak ve fiyatlar düşecek, bu nedenle sağlayıcıdan bağımsız olmak önemlidir. +- Kurulum gerektirmeyen hazır LSP desteği +- TUI odaklı yaklaşım. OpenCode, neovim kullanıcıları ve [terminal.shop](https://terminal.shop)'un geliştiricileri tarafından geliştirilmektedir; terminalde olabileceklerin sınırlarını zorlayacağız. +- İstemci/sunucu (client/server) mimarisi. Bu, örneğin OpenCode'un bilgisayarınızda çalışması ve siz onu bir mobil uygulamadan uzaktan yönetmenizi sağlar. TUI arayüzü olası istemcilerden sadece biridir. + +--- + +**Topluluğumuza katılın** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.uk.md b/README.uk.md new file mode 100644 index 00000000000..a1a0259b6d0 --- /dev/null +++ b/README.uk.md @@ -0,0 +1,142 @@ +

+ + + + + OpenCode logo + + +

+

AI-агент для програмування з відкритим кодом.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Встановлення + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Менеджери пакетів +npm i -g opencode-ai@latest # або bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS і Linux (рекомендовано, завжди актуально) +brew install opencode # macOS і Linux (офіційна формула Homebrew, оновлюється рідше) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # Будь-яка ОС +nix run nixpkgs#opencode # або github:anomalyco/opencode для найновішої dev-гілки +``` + +> [!TIP] +> Перед встановленням видаліть версії старші за 0.1.x. + +### Десктопний застосунок (BETA) + +OpenCode також доступний як десктопний застосунок. Завантажуйте напряму зі [сторінки релізів](https://github.com/anomalyco/opencode/releases) або [opencode.ai/download](https://opencode.ai/download). + +| Платформа | Завантаження | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` або AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Каталог встановлення + +Скрипт встановлення дотримується такого порядку пріоритету для шляху встановлення: + +1. `$OPENCODE_INSTALL_DIR` - Користувацький каталог встановлення +2. `$XDG_BIN_DIR` - Шлях, сумісний зі специфікацією XDG Base Directory +3. `$HOME/bin` - Стандартний каталог користувацьких бінарників (якщо існує або його можна створити) +4. `$HOME/.opencode/bin` - Резервний варіант за замовчуванням + +```bash +# Приклади +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Агенти + +OpenCode містить два вбудовані агенти, між якими можна перемикатися клавішею `Tab`. + +- **build** - Агент за замовчуванням із повним доступом для завдань розробки +- **plan** - Агент лише для читання для аналізу та дослідження коду + - За замовчуванням забороняє редагування файлів + - Запитує дозвіл перед запуском bash-команд + - Ідеально підходить для дослідження незнайомих кодових баз або планування змін + +Також доступний допоміжний агент **general** для складного пошуку та багатокрокових завдань. +Він використовується всередині системи й може бути викликаний у повідомленнях через `@general`. + +Дізнайтеся більше про [agents](https://opencode.ai/docs/agents). + +### Документація + +Щоб дізнатися більше про налаштування OpenCode, [**перейдіть до нашої документації**](https://opencode.ai/docs). + +### Внесок + +Якщо ви хочете зробити внесок в OpenCode, будь ласка, прочитайте нашу [документацію для контриб'юторів](./CONTRIBUTING.md) перед надсиланням pull request. + +### Проєкти на базі OpenCode + +Якщо ви працюєте над проєктом, пов'язаним з OpenCode, і використовуєте "opencode" у назві, наприклад "opencode-dashboard" або "opencode-mobile", додайте примітку до свого README. +Уточніть, що цей проєкт не створений командою OpenCode і жодним чином не афілійований із нами. + +### FAQ + +#### Чим це відрізняється від Claude Code? + +За можливостями це дуже схоже на Claude Code. Ось ключові відмінності: + +- 100% open source +- Немає прив'язки до конкретного провайдера. Ми рекомендуємо моделі, які надаємо через [OpenCode Zen](https://opencode.ai/zen), але OpenCode також працює з Claude, OpenAI, Google і навіть локальними моделями. З розвитком моделей різниця між ними зменшуватиметься, а ціни падатимуть, тому незалежність від провайдера має значення. +- Підтримка LSP з коробки +- Фокус на TUI. OpenCode створено користувачами neovim та авторами [terminal.shop](https://terminal.shop); ми й надалі розширюватимемо межі можливого в терміналі. +- Клієнт-серверна архітектура. Наприклад, це дає змогу запускати OpenCode на вашому комп'ютері й керувати ним віддалено з мобільного застосунку, тобто TUI-фронтенд - лише один із можливих клієнтів. + +--- + +**Приєднуйтеся до нашої спільноти** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.vi.md b/README.vi.md new file mode 100644 index 00000000000..0932c50f78a --- /dev/null +++ b/README.vi.md @@ -0,0 +1,141 @@ +

+ + + + + OpenCode logo + + +

+

Trợ lý lập trình AI mã nguồn mở.

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### Cài đặt + +```bash +# YOLO +curl -fsSL https://opencode.ai/install | bash + +# Các trình quản lý gói (Package managers) +npm i -g opencode-ai@latest # hoặc bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS và Linux (khuyên dùng, luôn cập nhật) +brew install opencode # macOS và Linux (công thức brew chính thức, ít cập nhật hơn) +sudo pacman -S opencode # Arch Linux (Bản ổn định) +paru -S opencode-bin # Arch Linux (Bản mới nhất từ AUR) +mise use -g opencode # Mọi hệ điều hành +nix run nixpkgs#opencode # hoặc github:anomalyco/opencode cho nhánh dev mới nhất +``` + +> [!TIP] +> Hãy xóa các phiên bản cũ hơn 0.1.x trước khi cài đặt. + +### Ứng dụng Desktop (BETA) + +OpenCode cũng có sẵn dưới dạng ứng dụng desktop. Tải trực tiếp từ [trang releases](https://github.com/anomalyco/opencode/releases) hoặc [opencode.ai/download](https://opencode.ai/download). + +| Nền tảng | Tải xuống | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, hoặc AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### Thư mục cài đặt + +Tập lệnh cài đặt tuân theo thứ tự ưu tiên sau cho đường dẫn cài đặt: + +1. `$OPENCODE_INSTALL_DIR` - Thư mục cài đặt tùy chỉnh +2. `$XDG_BIN_DIR` - Đường dẫn tuân thủ XDG Base Directory Specification +3. `$HOME/bin` - Thư mục nhị phân tiêu chuẩn của người dùng (nếu tồn tại hoặc có thể tạo) +4. `$HOME/.opencode/bin` - Mặc định dự phòng + +```bash +# Ví dụ +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents (Đại diện) + +OpenCode bao gồm hai agent được tích hợp sẵn mà bạn có thể chuyển đổi bằng phím `Tab`. + +- **build** - Agent mặc định, có toàn quyền truy cập cho công việc lập trình +- **plan** - Agent chỉ đọc dùng để phân tích và khám phá mã nguồn + - Mặc định từ chối việc chỉnh sửa tệp + - Hỏi quyền trước khi chạy các lệnh bash + - Lý tưởng để khám phá các codebase lạ hoặc lên kế hoạch thay đổi + +Ngoài ra còn có một subagent **general** dùng cho các tìm kiếm phức tạp và tác vụ nhiều bước. +Agent này được sử dụng nội bộ và có thể gọi bằng cách dùng `@general` trong tin nhắn. + +Tìm hiểu thêm về [agents](https://opencode.ai/docs/agents). + +### Tài liệu + +Để biết thêm thông tin về cách cấu hình OpenCode, [**hãy truy cập tài liệu của chúng tôi**](https://opencode.ai/docs). + +### Đóng góp + +Nếu bạn muốn đóng góp cho OpenCode, vui lòng đọc [tài liệu hướng dẫn đóng góp](./CONTRIBUTING.md) trước khi gửi pull request. + +### Xây dựng trên nền tảng OpenCode + +Nếu bạn đang làm việc trên một dự án liên quan đến OpenCode và sử dụng "opencode" như một phần của tên dự án, ví dụ "opencode-dashboard" hoặc "opencode-mobile", vui lòng thêm một ghi chú vào README của bạn để làm rõ rằng dự án đó không được xây dựng bởi đội ngũ OpenCode và không liên kết với chúng tôi dưới bất kỳ hình thức nào. + +### Các câu hỏi thường gặp (FAQ) + +#### OpenCode khác biệt thế nào so với Claude Code? + +Về mặt tính năng, nó rất giống Claude Code. Dưới đây là những điểm khác biệt chính: + +- 100% mã nguồn mở +- Không bị ràng buộc với bất kỳ nhà cung cấp nào. Mặc dù chúng tôi khuyên dùng các mô hình được cung cấp qua [OpenCode Zen](https://opencode.ai/zen), OpenCode có thể được sử dụng với Claude, OpenAI, Google, hoặc thậm chí các mô hình chạy cục bộ. Khi các mô hình phát triển, khoảng cách giữa chúng sẽ thu hẹp lại và giá cả sẽ giảm, vì vậy việc không phụ thuộc vào nhà cung cấp là rất quan trọng. +- Hỗ trợ LSP ngay từ đầu +- Tập trung vào TUI (Giao diện người dùng dòng lệnh). OpenCode được xây dựng bởi những người dùng neovim và đội ngũ tạo ra [terminal.shop](https://terminal.shop); chúng tôi sẽ đẩy giới hạn của những gì có thể làm được trên terminal lên mức tối đa. +- Kiến trúc client/server. Chẳng hạn, điều này cho phép OpenCode chạy trên máy tính của bạn trong khi bạn điều khiển nó từ xa qua một ứng dụng di động, nghĩa là frontend TUI chỉ là một trong những client có thể dùng. + +--- + +**Tham gia cộng đồng của chúng tôi** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.zh-CN.md b/README.zh-CN.md deleted file mode 100644 index 4b56e0fb0b0..00000000000 --- a/README.zh-CN.md +++ /dev/null @@ -1,118 +0,0 @@ -

- - - - - OpenCode logo - - -

-

开源的 AI Coding Agent。

-

- Discord - npm - Build status -

- -[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) - ---- - -### 安装 - -```bash -# 直接安装 (YOLO) -curl -fsSL https://opencode.ai/install | bash - -# 软件包管理器 -npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn -scoop install opencode # Windows -choco install opencode # Windows -brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新) -brew install opencode # macOS 和 Linux(官方 brew formula,更新频率较低) -paru -S opencode-bin # Arch Linux -mise use -g opencode # 任意系统 -nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最新 dev 分支 -``` - -> [!TIP] -> 安装前请先移除 0.1.x 之前的旧版本。 - -### 桌面应用程序 (BETA) - -OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下载。 - -| 平台 | 下载文件 | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`、`.rpm` 或 AppImage | - -```bash -# macOS (Homebrew Cask) -brew install --cask opencode-desktop -# Windows (Scoop) -scoop bucket add extras; scoop install extras/opencode-desktop -``` - -#### 安装目录 - -安装脚本按照以下优先级决定安装路径: - -1. `$OPENCODE_INSTALL_DIR` - 自定义安装目录 -2. `$XDG_BIN_DIR` - 符合 XDG 基础目录规范的路径 -3. `$HOME/bin` - 如果存在或可创建的用户二进制目录 -4. `$HOME/.opencode/bin` - 默认备用路径 - -```bash -# 示例 -OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash -XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash -``` - -### Agents - -OpenCode 内置两种 Agent,可用 `Tab` 键快速切换: - -- **build** - 默认模式,具备完整权限,适合开发工作 -- **plan** - 只读模式,适合代码分析与探索 - - 默认拒绝修改文件 - - 运行 bash 命令前会询问 - - 便于探索未知代码库或规划改动 - -另外还包含一个 **general** 子 Agent,用于复杂搜索和多步任务,内部使用,也可在消息中输入 `@general` 调用。 - -了解更多 [Agents](https://opencode.ai/docs/agents) 相关信息。 - -### 文档 - -更多配置说明请查看我们的 [**官方文档**](https://opencode.ai/docs)。 - -### 参与贡献 - -如有兴趣贡献代码,请在提交 PR 前阅读 [贡献指南 (Contributing Docs)](./CONTRIBUTING.md)。 - -### 基于 OpenCode 进行开发 - -如果你在项目名中使用了 “opencode”(如 “opencode-dashboard” 或 “opencode-mobile”),请在 README 里注明该项目不是 OpenCode 团队官方开发,且不存在隶属关系。 - -### 常见问题 (FAQ) - -#### 这和 Claude Code 有什么不同? - -功能上很相似,关键差异: - -- 100% 开源。 -- 不绑定特定提供商。推荐使用 [OpenCode Zen](https://opencode.ai/zen) 的模型,但也可搭配 Claude、OpenAI、Google 甚至本地模型。模型迭代会缩小差异、降低成本,因此保持 provider-agnostic 很重要。 -- 内置 LSP 支持。 -- 聚焦终端界面 (TUI)。OpenCode 由 Neovim 爱好者和 [terminal.shop](https://terminal.shop) 的创建者打造,会持续探索终端的极限。 -- 客户端/服务器架构。可在本机运行,同时用移动设备远程驱动。TUI 只是众多潜在客户端之一。 - -#### 另一个同名的仓库是什么? - -另一个名字相近的仓库与本项目无关。[点击这里了解背后故事](https://x.com/thdxr/status/1933561254481666466)。 - ---- - -**加入我们的社区** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.zh-TW.md b/README.zh-TW.md deleted file mode 100644 index 66664a70305..00000000000 --- a/README.zh-TW.md +++ /dev/null @@ -1,118 +0,0 @@ -

- - - - - OpenCode logo - - -

-

開源的 AI Coding Agent。

-

- Discord - npm - Build status -

- -[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) - ---- - -### 安裝 - -```bash -# 直接安裝 (YOLO) -curl -fsSL https://opencode.ai/install | bash - -# 套件管理員 -npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn -scoop install opencode # Windows -choco install opencode # Windows -brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新) -brew install opencode # macOS 與 Linux(官方 brew formula,更新頻率較低) -paru -S opencode-bin # Arch Linux -mise use -g github:anomalyco/opencode # 任何作業系統 -nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支 -``` - -> [!TIP] -> 安裝前請先移除 0.1.x 以前的舊版本。 - -### 桌面應用程式 (BETA) - -OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。 - -| 平台 | 下載連結 | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, 或 AppImage | - -```bash -# macOS (Homebrew Cask) -brew install --cask opencode-desktop -# Windows (Scoop) -scoop bucket add extras; scoop install extras/opencode-desktop -``` - -#### 安裝目錄 - -安裝腳本會依據以下優先順序決定安裝路徑: - -1. `$OPENCODE_INSTALL_DIR` - 自定義安裝目錄 -2. `$XDG_BIN_DIR` - 符合 XDG 基礎目錄規範的路徑 -3. `$HOME/bin` - 標準使用者執行檔目錄 (若存在或可建立) -4. `$HOME/.opencode/bin` - 預設備用路徑 - -```bash -# 範例 -OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash -XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash -``` - -### Agents - -OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。 - -- **build** - 預設模式,具備完整權限的 Agent,適用於開發工作。 -- **plan** - 唯讀模式,適用於程式碼分析與探索。 - - 預設禁止修改檔案。 - - 執行 bash 指令前會詢問權限。 - - 非常適合用來探索陌生的程式碼庫或規劃變更。 - -此外,OpenCode 還包含一個 **general** 子 Agent,用於處理複雜搜尋與多步驟任務。此 Agent 供系統內部使用,亦可透過在訊息中輸入 `@general` 來呼叫。 - -了解更多關於 [Agents](https://opencode.ai/docs/agents) 的資訊。 - -### 線上文件 - -關於如何設定 OpenCode 的詳細資訊,請參閱我們的 [**官方文件**](https://opencode.ai/docs)。 - -### 參與貢獻 - -如果您有興趣參與 OpenCode 的開發,請在提交 Pull Request 前先閱讀我們的 [貢獻指南 (Contributing Docs)](./CONTRIBUTING.md)。 - -### 基於 OpenCode 進行開發 - -如果您正在開發與 OpenCode 相關的專案,並在名稱中使用了 "opencode"(例如 "opencode-dashboard" 或 "opencode-mobile"),請在您的 README 中加入聲明,說明該專案並非由 OpenCode 團隊開發,且與我們沒有任何隸屬關係。 - -### 常見問題 (FAQ) - -#### 這跟 Claude Code 有什麼不同? - -在功能面上與 Claude Code 非常相似。以下是關鍵差異: - -- 100% 開源。 -- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。 -- 內建 LSP (語言伺服器協定) 支援。 -- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。 -- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。 - -#### 另一個同名的 Repo 是什麼? - -另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。 - ---- - -**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/README.zh.md b/README.zh.md new file mode 100644 index 00000000000..0859ed11d03 --- /dev/null +++ b/README.zh.md @@ -0,0 +1,140 @@ +

+ + + + + OpenCode logo + + +

+

开源的 AI Coding Agent。

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### 安装 + +```bash +# 直接安装 (YOLO) +curl -fsSL https://opencode.ai/install | bash + +# 软件包管理器 +npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新) +brew install opencode # macOS 和 Linux(官方 brew formula,更新频率较低) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # 任意系统 +nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最新 dev 分支 +``` + +> [!TIP] +> 安装前请先移除 0.1.x 之前的旧版本。 + +### 桌面应用程序 (BETA) + +OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下载。 + +| 平台 | 下载文件 | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`、`.rpm` 或 AppImage | + +```bash +# macOS (Homebrew Cask) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### 安装目录 + +安装脚本按照以下优先级决定安装路径: + +1. `$OPENCODE_INSTALL_DIR` - 自定义安装目录 +2. `$XDG_BIN_DIR` - 符合 XDG 基础目录规范的路径 +3. `$HOME/bin` - 如果存在或可创建的用户二进制目录 +4. `$HOME/.opencode/bin` - 默认备用路径 + +```bash +# 示例 +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode 内置两种 Agent,可用 `Tab` 键快速切换: + +- **build** - 默认模式,具备完整权限,适合开发工作 +- **plan** - 只读模式,适合代码分析与探索 + - 默认拒绝修改文件 + - 运行 bash 命令前会询问 + - 便于探索未知代码库或规划改动 + +另外还包含一个 **general** 子 Agent,用于复杂搜索和多步任务,内部使用,也可在消息中输入 `@general` 调用。 + +了解更多 [Agents](https://opencode.ai/docs/agents) 相关信息。 + +### 文档 + +更多配置说明请查看我们的 [**官方文档**](https://opencode.ai/docs)。 + +### 参与贡献 + +如有兴趣贡献代码,请在提交 PR 前阅读 [贡献指南 (Contributing Docs)](./CONTRIBUTING.md)。 + +### 基于 OpenCode 进行开发 + +如果你在项目名中使用了 “opencode”(如 “opencode-dashboard” 或 “opencode-mobile”),请在 README 里注明该项目不是 OpenCode 团队官方开发,且不存在隶属关系。 + +### 常见问题 (FAQ) + +#### 这和 Claude Code 有什么不同? + +功能上很相似,关键差异: + +- 100% 开源。 +- 不绑定特定提供商。推荐使用 [OpenCode Zen](https://opencode.ai/zen) 的模型,但也可搭配 Claude、OpenAI、Google 甚至本地模型。模型迭代会缩小差异、降低成本,因此保持 provider-agnostic 很重要。 +- 内置 LSP 支持。 +- 聚焦终端界面 (TUI)。OpenCode 由 Neovim 爱好者和 [terminal.shop](https://terminal.shop) 的创建者打造,会持续探索终端的极限。 +- 客户端/服务器架构。可在本机运行,同时用移动设备远程驱动。TUI 只是众多潜在客户端之一。 + +--- + +**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode) diff --git a/README.zht.md b/README.zht.md new file mode 100644 index 00000000000..b7d8b8fc476 --- /dev/null +++ b/README.zht.md @@ -0,0 +1,140 @@ +

+ + + + + OpenCode logo + + +

+

開源的 AI Coding Agent。

+

+ Discord + npm + Build status +

+ +

+ English | + 简体中文 | + 繁體中文 | + 한국어 | + Deutsch | + Español | + Français | + Italiano | + Dansk | + 日本語 | + Polski | + Русский | + Bosanski | + العربية | + Norsk | + Português (Brasil) | + ไทย | + Türkçe | + Українська | + বাংলা | + Ελληνικά | + Tiếng Việt +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### 安裝 + +```bash +# 直接安裝 (YOLO) +curl -fsSL https://opencode.ai/install | bash + +# 套件管理員 +npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn +scoop install opencode # Windows +choco install opencode # Windows +brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新) +brew install opencode # macOS 與 Linux(官方 brew formula,更新頻率較低) +sudo pacman -S opencode # Arch Linux (Stable) +paru -S opencode-bin # Arch Linux (Latest from AUR) +mise use -g opencode # 任何作業系統 +nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支 +``` + +> [!TIP] +> 安裝前請先移除 0.1.x 以前的舊版本。 + +### 桌面應用程式 (BETA) + +OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。 + +| 平台 | 下載連結 | +| --------------------- | ------------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | +| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, 或 AppImage | + +```bash +# macOS (Homebrew Cask) +brew install --cask opencode-desktop +# Windows (Scoop) +scoop bucket add extras; scoop install extras/opencode-desktop +``` + +#### 安裝目錄 + +安裝腳本會依據以下優先順序決定安裝路徑: + +1. `$OPENCODE_INSTALL_DIR` - 自定義安裝目錄 +2. `$XDG_BIN_DIR` - 符合 XDG 基礎目錄規範的路徑 +3. `$HOME/bin` - 標準使用者執行檔目錄 (若存在或可建立) +4. `$HOME/.opencode/bin` - 預設備用路徑 + +```bash +# 範例 +OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash +XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash +``` + +### Agents + +OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。 + +- **build** - 預設模式,具備完整權限的 Agent,適用於開發工作。 +- **plan** - 唯讀模式,適用於程式碼分析與探索。 + - 預設禁止修改檔案。 + - 執行 bash 指令前會詢問權限。 + - 非常適合用來探索陌生的程式碼庫或規劃變更。 + +此外,OpenCode 還包含一個 **general** 子 Agent,用於處理複雜搜尋與多步驟任務。此 Agent 供系統內部使用,亦可透過在訊息中輸入 `@general` 來呼叫。 + +了解更多關於 [Agents](https://opencode.ai/docs/agents) 的資訊。 + +### 線上文件 + +關於如何設定 OpenCode 的詳細資訊,請參閱我們的 [**官方文件**](https://opencode.ai/docs)。 + +### 參與貢獻 + +如果您有興趣參與 OpenCode 的開發,請在提交 Pull Request 前先閱讀我們的 [貢獻指南 (Contributing Docs)](./CONTRIBUTING.md)。 + +### 基於 OpenCode 進行開發 + +如果您正在開發與 OpenCode 相關的專案,並在名稱中使用了 "opencode"(例如 "opencode-dashboard" 或 "opencode-mobile"),請在您的 README 中加入聲明,說明該專案並非由 OpenCode 團隊開發,且與我們沒有任何隸屬關係。 + +### 常見問題 (FAQ) + +#### 這跟 Claude Code 有什麼不同? + +在功能面上與 Claude Code 非常相似。以下是關鍵差異: + +- 100% 開源。 +- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。 +- 內建 LSP (語言伺服器協定) 支援。 +- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造。我們將不斷挑戰終端機介面的極限。 +- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。 + +--- + +**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode) diff --git a/SECURITY.md b/SECURITY.md index 93c7341cef6..e7e59f4a27a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,11 @@ # Security +## IMPORTANT + +We do not accept AI generated security reports. We receive a large number of +these and we absolutely do not have the resources to review them all. If you +submit one that will be an automatic ban from the project. + ## Threat Model ### Overview diff --git a/STATS.md b/STATS.md index 5cfa76f16db..44819a6eb8c 100644 --- a/STATS.md +++ b/STATS.md @@ -1,210 +1,217 @@ # Download Stats -| Date | GitHub Downloads | npm Downloads | Total | -| ---------- | -------------------- | -------------------- | -------------------- | -| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) | -| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) | -| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) | -| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) | -| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) | -| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) | -| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) | -| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) | -| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) | -| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) | -| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) | -| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) | -| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) | -| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) | -| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) | -| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) | -| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) | -| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) | -| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) | -| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) | -| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) | -| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) | -| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) | -| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) | -| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) | -| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) | -| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) | -| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) | -| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) | -| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) | -| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) | -| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) | -| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) | -| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) | -| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) | -| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) | -| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) | -| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) | -| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) | -| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) | -| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) | -| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) | -| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) | -| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) | -| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) | -| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) | -| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) | -| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) | -| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) | -| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) | -| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) | -| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) | -| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) | -| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) | -| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) | -| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) | -| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) | -| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) | -| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) | -| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) | -| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) | -| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) | -| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) | -| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) | -| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) | -| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) | -| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) | -| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) | -| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) | -| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) | -| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) | -| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) | -| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) | -| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) | -| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) | -| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) | -| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) | -| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) | -| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) | -| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) | -| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) | -| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) | -| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) | -| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) | -| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) | -| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) | -| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) | -| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) | -| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) | -| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) | -| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) | -| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) | -| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) | -| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) | -| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) | -| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) | -| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) | -| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) | -| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) | -| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) | -| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) | -| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) | -| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) | -| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) | -| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) | -| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) | -| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) | -| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) | -| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) | -| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) | -| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) | -| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) | -| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) | -| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) | -| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) | -| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) | -| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) | -| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) | -| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) | -| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) | -| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) | -| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) | -| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) | -| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) | -| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) | -| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) | -| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) | -| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) | -| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) | -| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) | -| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) | -| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) | -| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) | -| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) | -| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) | -| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) | -| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) | -| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) | -| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) | -| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) | -| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) | -| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) | -| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) | -| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) | -| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) | -| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) | -| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) | -| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) | -| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) | -| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) | -| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) | -| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) | -| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) | -| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) | -| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) | -| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) | -| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) | -| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) | -| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) | -| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) | -| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) | -| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) | -| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) | -| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) | -| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) | -| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) | -| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) | -| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) | -| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) | -| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) | -| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) | -| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) | -| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) | -| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) | -| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) | -| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) | -| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) | -| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) | -| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) | -| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) | -| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) | -| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) | -| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) | -| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) | -| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) | -| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) | -| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) | -| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) | -| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) | -| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) | -| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) | -| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) | -| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) | -| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) | -| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) | -| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) | -| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) | -| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) | -| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) | -| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) | -| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) | -| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) | -| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) | -| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) | -| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) | -| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) | +| Date | GitHub Downloads | npm Downloads | Total | +| ---------- | -------------------- | -------------------- | --------------------- | +| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) | +| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) | +| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) | +| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) | +| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) | +| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) | +| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) | +| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) | +| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) | +| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) | +| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) | +| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) | +| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) | +| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) | +| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) | +| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) | +| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) | +| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) | +| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) | +| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) | +| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) | +| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) | +| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) | +| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) | +| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) | +| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) | +| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) | +| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) | +| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) | +| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) | +| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) | +| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) | +| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) | +| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) | +| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) | +| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) | +| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) | +| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) | +| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) | +| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) | +| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) | +| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) | +| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) | +| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) | +| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) | +| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) | +| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) | +| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) | +| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) | +| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) | +| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) | +| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) | +| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) | +| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) | +| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) | +| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) | +| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) | +| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) | +| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) | +| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) | +| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) | +| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) | +| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) | +| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) | +| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) | +| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) | +| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) | +| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) | +| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) | +| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) | +| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) | +| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) | +| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) | +| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) | +| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) | +| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) | +| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) | +| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) | +| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) | +| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) | +| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) | +| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) | +| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) | +| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) | +| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) | +| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) | +| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) | +| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) | +| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) | +| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) | +| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) | +| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) | +| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) | +| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) | +| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) | +| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) | +| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) | +| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) | +| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) | +| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) | +| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) | +| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) | +| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) | +| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) | +| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) | +| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) | +| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) | +| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) | +| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) | +| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) | +| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) | +| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) | +| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) | +| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) | +| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) | +| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) | +| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) | +| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) | +| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) | +| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) | +| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) | +| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) | +| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) | +| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) | +| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) | +| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) | +| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) | +| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) | +| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) | +| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) | +| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) | +| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) | +| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) | +| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) | +| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) | +| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) | +| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) | +| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) | +| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) | +| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) | +| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) | +| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) | +| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) | +| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) | +| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) | +| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) | +| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) | +| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) | +| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) | +| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) | +| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) | +| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) | +| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) | +| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) | +| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) | +| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) | +| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) | +| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) | +| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) | +| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) | +| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) | +| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) | +| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) | +| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) | +| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) | +| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) | +| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) | +| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) | +| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) | +| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) | +| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) | +| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) | +| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) | +| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) | +| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) | +| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) | +| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) | +| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) | +| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) | +| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) | +| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) | +| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) | +| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) | +| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) | +| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) | +| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) | +| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) | +| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) | +| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) | +| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) | +| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) | +| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) | +| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) | +| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) | +| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) | +| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) | +| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) | +| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) | +| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) | +| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) | +| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) | +| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) | +| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) | +| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) | +| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) | +| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) | +| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) | +| 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) | +| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) | +| 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) | +| 2026-01-27 | 7,208,093 (+266,473) | 2,280,762 (+48,647) | 9,488,855 (+315,120) | +| 2026-01-28 | 7,489,370 (+281,277) | 2,314,849 (+34,087) | 9,804,219 (+315,364) | +| 2026-01-29 | 7,815,471 (+326,101) | 2,374,982 (+60,133) | 10,190,453 (+386,234) | diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md deleted file mode 100644 index 52d012fcb97..00000000000 --- a/STYLE_GUIDE.md +++ /dev/null @@ -1,71 +0,0 @@ -## Style Guide - -- Keep things in one function unless composable or reusable -- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context -- Avoid `try`/`catch` where possible -- Avoid using the `any` type -- Prefer single word variable names where possible -- Use Bun APIs when possible, like `Bun.file()` - -# Avoid let statements - -We don't like `let` statements, especially combined with if/else statements. -Prefer `const`. - -Good: - -```ts -const foo = condition ? 1 : 2 -``` - -Bad: - -```ts -let foo - -if (condition) foo = 1 -else foo = 2 -``` - -# Avoid else statements - -Prefer early returns or using an `iife` to avoid else statements. - -Good: - -```ts -function foo() { - if (condition) return 1 - return 2 -} -``` - -Bad: - -```ts -function foo() { - if (condition) return 1 - else return 2 -} -``` - -# Prefer single word naming - -Try your best to find a single word name for your variables, functions, etc. -Only use multiple words if you cannot. - -Good: - -```ts -const foo = 1 -const bar = 2 -const baz = 3 -``` - -Bad: - -```ts -const fooBar = 1 -const barBaz = 2 -const bazFoo = 3 -``` diff --git a/bun.lock b/bun.lock index 68929677a61..6140c3497a5 100644 --- a/bun.lock +++ b/bun.lock @@ -14,16 +14,19 @@ "devDependencies": { "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", + "@types/mime-types": "3.0.1", + "@typescript/native-preview": "catalog:", + "glob": "13.0.5", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", - "sst": "3.17.23", - "turbo": "2.5.6", + "sst": "3.18.10", + "turbo": "2.8.13", }, }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -43,8 +46,9 @@ "@solidjs/router": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", "diff": "catalog:", + "effect": "4.0.0-beta.31", "fuzzysort": "catalog:", - "ghostty-web": "0.3.0", + "ghostty-web": "github:anomalyco/ghostty-web#main", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", @@ -73,7 +77,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -107,7 +111,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -115,7 +119,7 @@ "@opencode-ai/console-resource": "workspace:*", "@planetscale/database": "1.19.0", "aws4fetch": "1.0.20", - "drizzle-orm": "0.41.0", + "drizzle-orm": "catalog:", "postgres": "3.4.7", "stripe": "18.0.0", "ulid": "catalog:", @@ -127,14 +131,14 @@ "@types/bun": "1.3.0", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", - "drizzle-kit": "0.30.5", + "drizzle-kit": "catalog:", "mysql2": "3.14.4", "typescript": "catalog:", }, }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -158,7 +162,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -182,12 +186,16 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.33", + "version": "1.2.24", "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", @@ -209,9 +217,40 @@ "vite": "catalog:", }, }, + "packages/desktop-electron": { + "name": "@opencode-ai/desktop-electron", + "version": "1.2.24", + "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", + "effect": "4.0.0-beta.31", + "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", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "electron": "40.4.1", + "electron-builder": "^26", + "electron-vite": "^5", + "typescript": "~5.6.2", + "vite": "catalog:", + }, + }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -240,7 +279,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -256,35 +295,37 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.33", + "version": "1.2.24", "bin": { "opencode": "./bin/opencode", }, "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.12.0", - "@ai-sdk/amazon-bedrock": "3.0.73", - "@ai-sdk/anthropic": "2.0.57", + "@agentclientprotocol/sdk": "0.14.1", + "@ai-sdk/amazon-bedrock": "3.0.82", + "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/azure": "2.0.91", - "@ai-sdk/cerebras": "1.0.34", + "@ai-sdk/cerebras": "1.0.36", "@ai-sdk/cohere": "2.0.22", - "@ai-sdk/deepinfra": "1.0.31", - "@ai-sdk/gateway": "2.0.25", - "@ai-sdk/google": "2.0.52", - "@ai-sdk/google-vertex": "3.0.97", + "@ai-sdk/deepinfra": "1.0.36", + "@ai-sdk/gateway": "2.0.30", + "@ai-sdk/google": "2.0.54", + "@ai-sdk/google-vertex": "3.0.106", "@ai-sdk/groq": "2.0.34", "@ai-sdk/mistral": "2.0.27", "@ai-sdk/openai": "2.0.89", - "@ai-sdk/openai-compatible": "1.0.30", + "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/perplexity": "2.0.23", "@ai-sdk/provider": "2.0.1", - "@ai-sdk/provider-utils": "3.0.20", - "@ai-sdk/togetherai": "1.0.31", - "@ai-sdk/vercel": "1.0.31", + "@ai-sdk/provider-utils": "3.0.21", + "@ai-sdk/togetherai": "1.0.34", + "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", + "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.2.0", + "@gitlab/gitlab-ai-provider": "3.6.0", + "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", @@ -295,9 +336,9 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", - "@openrouter/ai-sdk-provider": "1.5.2", - "@opentui/core": "0.1.74", - "@opentui/solid": "0.1.74", + "@openrouter/ai-sdk-provider": "1.5.4", + "@opentui/core": "0.1.87", + "@opentui/solid": "0.1.87", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -305,23 +346,30 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", + "ai-gateway-provider": "2.3.1", "bonjour-service": "1.3.0", - "bun-pty": "0.4.4", + "bun-pty": "0.4.8", "chokidar": "4.0.3", "clipboardy": "4.0.0", "decimal.js": "10.5.0", "diff": "catalog:", + "drizzle-orm": "1.0.0-beta.16-ea816b6", + "effect": "catalog:", "fuzzysort": "3.1.0", + "glob": "13.0.5", + "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "hono": "catalog:", "hono-openapi": "catalog:", "ignore": "7.0.5", "jsonc-parser": "3.3.1", + "mime-types": "3.0.2", "minimatch": "10.0.3", "open": "10.1.2", "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", + "semver": "^7.6.3", "solid-js": "catalog:", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", @@ -329,6 +377,7 @@ "ulid": "catalog:", "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", + "which": "6.0.1", "xdg-basedir": "5.1.0", "yargs": "18.0.0", "zod": "catalog:", @@ -336,6 +385,7 @@ }, "devDependencies": { "@babel/core": "7.28.4", + "@effect/language-service": "0.79.0", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/script": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", @@ -344,14 +394,20 @@ "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1", "@standard-schema/spec": "1.0.0", "@tsconfig/bun": "catalog:", "@types/babel__core": "7.20.5", "@types/bun": "catalog:", + "@types/mime-types": "3.0.1", + "@types/semver": "^7.5.8", "@types/turndown": "5.0.5", + "@types/which": "3.0.4", "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", + "drizzle-kit": "1.0.0-beta.16-ea816b6", + "drizzle-orm": "1.0.0-beta.16-ea816b6", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -360,7 +416,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -374,15 +430,19 @@ }, "packages/script": { "name": "@opencode-ai/script", + "dependencies": { + "semver": "^7.6.3", + }, "devDependencies": { "@types/bun": "catalog:", + "@types/semver": "^7.5.8", }, }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.33", + "version": "1.2.24", "devDependencies": { - "@hey-api/openapi-ts": "0.90.4", + "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", @@ -391,7 +451,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -402,9 +462,31 @@ "typescript": "catalog:", }, }, + "packages/storybook": { + "name": "@opencode-ai/storybook", + "devDependencies": { + "@opencode-ai/ui": "workspace:*", + "@solidjs/meta": "catalog:", + "@storybook/addon-a11y": "^10.2.13", + "@storybook/addon-docs": "^10.2.13", + "@storybook/addon-links": "^10.2.13", + "@storybook/addon-onboarding": "^10.2.13", + "@storybook/addon-vitest": "^10.2.13", + "@tailwindcss/vite": "catalog:", + "@tsconfig/node22": "catalog:", + "@types/node": "catalog:", + "@types/react": "18.0.25", + "react": "18.2.0", + "solid-js": "catalog:", + "storybook": "^10.2.13", + "storybook-solidjs-vite": "^10.0.9", + "typescript": "catalog:", + "vite": "catalog:", + }, + }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -415,7 +497,7 @@ "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", - "@typescript/native-preview": "catalog:", + "@solidjs/router": "catalog:", "dompurify": "3.3.1", "fuzzysort": "catalog:", "katex": "0.16.27", @@ -423,6 +505,10 @@ "marked": "catalog:", "marked-katex-extension": "5.1.6", "marked-shiki": "catalog:", + "morphdom": "2.7.8", + "motion": "12.34.5", + "motion-dom": "12.34.3", + "motion-utils": "12.29.2", "remeda": "catalog:", "shiki": "catalog:", "solid-js": "catalog:", @@ -436,6 +522,7 @@ "@types/bun": "catalog:", "@types/katex": "0.16.7", "@types/luxon": "catalog:", + "@typescript/native-preview": "catalog:", "tailwindcss": "catalog:", "typescript": "catalog:", "vite": "catalog:", @@ -445,7 +532,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "zod": "catalog:", }, @@ -456,14 +543,14 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.33", + "version": "1.2.24", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", "@astrojs/solid-js": "5.1.0", "@astrojs/starlight": "0.34.3", "@fontsource/ibm-plex-mono": "5.2.5", - "@shikijs/transformers": "3.4.2", + "@shikijs/transformers": "3.20.0", "@types/luxon": "catalog:", "ai": "catalog:", "astro": "5.7.13", @@ -478,8 +565,10 @@ "shiki": "catalog:", "solid-js": "catalog:", "toolbeam-docs-theme": "0.4.8", + "vscode-languageserver-types": "3.17.5", }, "devDependencies": { + "@astrojs/check": "0.9.6", "@types/node": "catalog:", "opencode": "workspace:*", "typescript": "catalog:", @@ -487,12 +576,14 @@ }, }, "trustedDependencies": [ + "electron", "esbuild", "web-tree-sitter", "tree-sitter-bash", ], "patchedDependencies": { - "ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch", + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", }, "overrides": { "@types/bun": "catalog:", @@ -504,7 +595,7 @@ "@kobalte/core": "0.13.11", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/diffs": "1.0.2", + "@pierre/diffs": "1.1.0-beta.18", "@playwright/test": "1.51.0", "@solid-primitives/storage": "4.3.3", "@solidjs/meta": "0.29.4", @@ -513,14 +604,17 @@ "@tailwindcss/vite": "4.1.11", "@tsconfig/bun": "1.0.9", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.5", + "@types/bun": "1.3.9", "@types/luxon": "3.7.1", "@types/node": "22.13.9", "@types/semver": "7.7.1", "@typescript/native-preview": "7.0.0-dev.20251207.1", - "ai": "5.0.119", + "ai": "5.0.124", "diff": "8.0.2", "dompurify": "3.3.1", + "drizzle-kit": "1.0.0-beta.16-ea816b6", + "drizzle-orm": "1.0.0-beta.16-ea816b6", + "effect": "4.0.0-beta.31", "fuzzysort": "3.1.0", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -540,6 +634,8 @@ "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=="], @@ -548,31 +644,39 @@ "@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="], - "@actions/http-client": ["@actions/http-client@3.0.0", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.28.5" } }, "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ=="], + "@actions/http-client": ["@actions/http-client@3.0.2", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^6.23.0" } }, "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA=="], "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.12.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.73", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EAAGJ/dfbAZaqIhK3w52hq6cftSLZwXdC6uHKh8Cls1T0N4MxS6ykDf54UyFO3bZWkQxR+Mdw1B3qireGOxtJQ=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.82", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yb1EkRCMWex0tnpHPLGQxoJEiJvMGOizuxzlXFOpuGFiYgE679NsWE/F8pHwtoAWsqLlylgGAJvJDIJ8us8LEw=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="], "@ai-sdk/azure": ["@ai-sdk/azure@2.0.91", "", { "dependencies": { "@ai-sdk/openai": "2.0.89", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9tznVSs6LGQNKKxb8pKd7CkBV9yk+a/ENpFicHCj2CmBUKefxzwJ9JbUqrlK3VF6dGZw3LXq0dWxt7/Yekaj1w=="], - "@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.34", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XOK0dJsAGoPYi/lfR4KFBi8xhvJ46oCpAxUD6FmJAuJ4eh0qlj5zDt+myvzM8gvN7S6K7zHD+mdWlOPKGQT8Vg=="], + "@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.36", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zoJYL33+ieyd86FSP0Whm86D79d1lKPR7wUzh1SZ1oTxwYmsGyvIrmMf2Ll0JA9Ds2Es6qik4VaFCrjwGYRTIQ=="], "@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="], - "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-87qFcYNvDF/89hB//MQjYTb3tlsAfmgeZrZ34RESeBTZpSgs0EzYOMqPMwFTHUNp4wteoifikDJbaS/9Da8cfw=="], + "@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-E+wzGPSa/XHmajO3WtX8mtq0ewy04tsHSpU6/SGwqbiykwWba/emi7ayZ4ir89s5OzbAen2g7T9zZiEchMfkHQ=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.25", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rq+FX55ne7lMiqai7NcvvDZj4HLsr+hg77WayqmySqc6zhw3tIOLxd4Ty6OpwNj0C0bVMi3iCl2zvJIEirh9XA=="], + "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.36", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.33", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LndvRktEgY2IFu4peDJMEXcjhHEEFtM0upLx/J64kCpFHCifalXpK4PPSX3PVndnn0bJzvamO5+fc0z2ooqBZw=="], - "@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="], + "@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.35", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qvh2yxL5zJS9RO/Bf12pyYBIDmn+9GR1hT6e28IYWQWnt2Xq0h9XGps6XagLAv3VYYFg8c/ozkWVd4kXLZ25HA=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.97", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.57", "@ai-sdk/google": "2.0.52", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-s4tI7Z15i6FlbtCvS4SBRal8wRfkOXJzKxlS6cU4mJW/QfUfoVy4b22836NVNJwDvkG/HkDSfzwm/X8mn46MhA=="], + "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@1.0.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ee2At5jgV+SqC6nrtPq20iH7N/aN+O36LrA4gkzVM4cmhM7bvQKVkOXhC1XxG+wsYG6UZi3Nekoi8MEjNWuRrw=="], + + "@ai-sdk/fireworks": ["@ai-sdk/fireworks@1.0.35", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.34", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-inUq29XvSVDer6JIeOkwAmCFxOtHPU0OZEhwaWoe3PI59naHIW4RIFA9wppLLV5fJI9WQcAfDKy0ZHW9nV3UJw=="], + + "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5Nrkj8B4MzkkOfjjA+Cs5pamkbkK4lI11bx80QV7TFcen/hWA8wEC+UVzwuM5H2zpekoNMjvl6GonHnR62XIZw=="], + + "@ai-sdk/google": ["@ai-sdk/google@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VKguP0x/PUYpdQyuA/uy5pDGJy6reL0X/yDKxHfL207aCUXpFIBmyMhVs4US39dkEVhtmIFSwXauY0Pt170JRw=="], + + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.106", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/google": "2.0.54", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-f9sA66bmhgJoTwa+pHWFSdYxPa0lgdQ/MgYNxZptzVyGptoziTf1a9EIXEL3jiCD0qIBAg+IhDAaYalbvZaDqQ=="], "@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="], @@ -586,11 +690,11 @@ "@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - "@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RlYubjStoZQxna4Ng91Vvo8YskvL7lW9zj68IwZfCnaDBSAp1u6Nhc5BR4ZtKnY6PA3XEtu4bATIQl7yiiQ+Lw=="], + "@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.34", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jjJmJms6kdEc4nC3MDGFJfhV8F1ifY4nolV2dbnT7BM4ab+Wkskc0GwCsJ7G7WdRMk7xDbFh4he3DPL8KJ/cyA=="], - "@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ggvwAMt/KsbqcdR6ILQrjwrRONLV/8aG6rOLbjcOGvV0Ai+WdZRRKQj5nOeQ06PvwVQtKdkp7S4IinpXIhCiHg=="], + "@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qwjm+HdwKasu7L9bDUryBMGKDMscIEzMUkjw/33uGdJpktzyNW13YaNIObOZ2HkskqDMIQJSd4Ao2BBT8fEYLw=="], "@ai-sdk/xai": ["@ai-sdk/xai@2.0.51", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AI3le03qiegkZvn9hpnpDwez49lOvQLj4QUBT8H41SMbrdTYOxn3ktTwrsSu90cNDdzKGMvoH0u2GHju1EdnCg=="], @@ -602,19 +706,23 @@ "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], + "@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="], + "@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="], - "@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="], + "@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.1", "", {}, "sha512-7dwEVigz9vUWDw3nRwLQ/yH/xYovlUA0ZD86xoeKEBmkz9O6iELG1yri67PgAPW6VLL/xInA4t7H0CK6VmtkKQ=="], + "@astrojs/language-server": ["@astrojs/language-server@2.16.3", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.27", "@volar/language-core": "~2.4.27", "@volar/language-server": "~2.4.27", "@volar/language-service": "~2.4.27", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.15", "volar-service-css": "0.0.68", "volar-service-emmet": "0.0.68", "volar-service-html": "0.0.68", "volar-service-prettier": "0.0.68", "volar-service-typescript": "0.0.68", "volar-service-typescript-twoslash-queries": "0.0.68", "volar-service-yaml": "0.0.68", "vscode-html-languageservice": "^5.6.1", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-yO5K7RYCMXUfeDlnU6UnmtnoXzpuQc0yhlaCNZ67k1C/MiwwwvMZz+LGa+H35c49w5QBfvtr4w4Zcf5PcH8uYA=="], + "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.2.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^3.0.0", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg=="], - "@astrojs/mdx": ["@astrojs/mdx@4.3.12", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.9", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-pL3CVPtuQrPnDhWjy7zqbOibNyPaxP4VpQS8T8spwKqKzauJ4yoKyNkVTD8jrP7EAJHmBhZ7PTmUGZqOpKKp8g=="], + "@astrojs/mdx": ["@astrojs/mdx@4.3.13", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.10", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q=="], "@astrojs/prism": ["@astrojs/prism@3.2.0", "", { "dependencies": { "prismjs": "^1.29.0" } }, "sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw=="], - "@astrojs/sitemap": ["@astrojs/sitemap@3.6.0", "", { "dependencies": { "sitemap": "^8.0.0", "stream-replace-string": "^2.0.0", "zod": "^3.25.76" } }, "sha512-4aHkvcOZBWJigRmMIAJwRQXBS+ayoP5z40OklTXYXhUDhwusz+DyDl+nSshY6y9DvkVEavwNcFO8FD81iGhXjg=="], + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.0", "", { "dependencies": { "sitemap": "^8.0.2", "stream-replace-string": "^2.0.0", "zod": "^3.25.76" } }, "sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA=="], "@astrojs/solid-js": ["@astrojs/solid-js@5.1.0", "", { "dependencies": { "vite": "^6.3.5", "vite-plugin-solid": "^2.11.6" }, "peerDependencies": { "solid-devtools": "^0.30.1", "solid-js": "^1.8.5" }, "optionalPeers": ["solid-devtools"] }, "sha512-VmPHOU9k7m6HHCT2Y1mNzifilUnttlowBM36frGcfj5wERJE9Ci0QtWJbzdf6AlcoIirb7xVw+ByupU011Di9w=="], @@ -624,6 +732,8 @@ "@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.0", "", {}, "sha512-qZxHwVnmb5FXuvRsaIGaqWgnftjCuMY+GSbaVZdBmE4j8AfgPqKPxYp8SUERyJcjpKCEmO4wD6ybuGH8A2kVRQ=="], + "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.2", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], @@ -638,6 +748,8 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="], "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zwGLSiK48z3PzKpQiDMKP85+fpIrPMF1qQOQW9OW7BGj5AuBZIisT2O4VzIgYJeh+t47MLU7VgBQL7muc+MJDg=="], @@ -646,19 +758,25 @@ "@aws-sdk/core": ["@aws-sdk/core@3.932.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-AS8gypYQCbNojwgjvZGkJocC2CoEICDx9ZJ15ILsv+MlcCVLtUJSRSx3VzJOUY2EEIaGLRrPNlIqyn/9/fySvA=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ozge/c7NdHUDyHqro6+P5oHt8wfKSUBN+olttiVfBe9Mw3wBMpPa3gQ0pZnG+gwBkKskBuip2bMR16tqYvUSEA=="], + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.11", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.996.8", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-yHBjinYauxSvikf15EtgXyZ9TBIMVHUSWFPycQtPltTINpK+uv6K22zKkVsbxpB0gvsdRdIWP0UG5gejM+jPuQ=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-b6N9Nnlg8JInQwzBkUq5spNaXssM3h3zLxGzpPrnw0nHSIWPJPTbZzA5Ca285fcDUFuKP+qf3qkuqlAjGOdWhg=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MBAMW6YELzE1SdkOniqr51mrjapQUv8JXSGxtwRjQV0mwVDutVsn22OPAUt4RcLRvdiHQmNBDEFP9iTeSVCOlA=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HygGyKuMG5AaGXsmM0d81miWDon55xwalRHB3UmDg3QBhtunbNIoIaWUbNTKuBZXcIN6emeeEZw/YgSMqLc0YA=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-9EJROO8LXll5a7eUFqu48k6BChrtokbmgeMWmsH7lBb6lVbtjslUYz/ShLi+SHkYzTomiGBhmzTW7y+H4BxsnA=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/credential-provider-env": "^3.972.17", "@aws-sdk/credential-provider-http": "^3.972.19", "@aws-sdk/credential-provider-login": "^3.972.18", "@aws-sdk/credential-provider-process": "^3.972.17", "@aws-sdk/credential-provider-sso": "^3.972.18", "@aws-sdk/credential-provider-web-identity": "^3.972.18", "@aws-sdk/nested-clients": "^3.996.8", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vthIAXJISZnj2576HeyLBj4WTeX+I7PwWeRkbOa0mVX39K13SCGxCgOFuKj2ytm9qTlLOmXe4cdEnroteFtJfw=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.8", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-kINzc5BBxdYBkPZ0/i1AMPMOk5b5QaFNbYMElVw5QTX13AKj6jcxnv/YNl9oW9mg+Y08ti19hh01HhyEAxsSJQ=="], "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.933.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-ini": "3.933.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-L2dE0Y7iMLammQewPKNeEh1z/fdJyYEU+/QsLBD9VEh+SXcN/FIyTi21Isw8wPZN6lMB9PDVtISzBnF8HuSFrw=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-BodZYKvT4p/Dkm28Ql/FhDdS1+p51bcZeMMu2TRtU8PoMDHnVDhHz27zASEKSZwmhvquxHrZHB0IGuVqjZUtSQ=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-c8G8wT1axpJDgaP3xzcy+q8Y1fTi9A2eIQJvyhQ9xuXrUZhlCfXbC0vM9bM1CUXiZppFQ1p7g0tuUMvil/gCPg=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.933.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.933.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/token-providers": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-/R1DBR7xNcuZIhS2RirU+P2o8E8/fOk+iLAhbqeSTq+g09fP/F6W7ouFpS5eVE2NIfWG7YBFoVddOhvuqpn51g=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.8", "@aws-sdk/token-providers": "3.1005.0", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-YHYEfj5S2aqInRt5ub8nDOX8vAxgMvd84wm2Y3WVNfFa/53vOv9T7WOAqXI25qjj3uEcV46xxfqdDQk04h5XQA=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-c7Eccw2lhFx2/+qJn3g+uIDWRuWi2A6Sz3PVvckFUEzPsP0dPUo19hlvtarwP5GzrsXn0yEPRVhpewsIaSCGaQ=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.8", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-OqlEQpJ+J3T5B96qtC1zLLwkBloechP+fezKbCH0sbd2cCc0Ra55XpxWpk/hRj69xAOYtHvoC4orx6eTa4zU7g=="], + + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="], "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA=="], @@ -680,13 +798,13 @@ "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-9BGTbJyA/4PTdwQWE9hAFIJGpsYkyEW20WON3i15aDqo5oRZwZmqaVageOD57YYqG8JDJjvcwKyDdR4cc38dvg=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iOq86f2H67924kQUIPOAvlmMaOAvOLoDOIb66I2YqSUpMYB6ufiuJW3RlREgskxv86S5qKzMnfy/X6CqMjK6XQ=="], "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw=="], "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.932.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-NCIRJvoRc9246RZHIusY1+n/neeG2yGhBGdKhghmrNdM+mLLN6Ii7CKFZjx3DhxtpHMpl1HWLTMhdVrGwP2upw=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qzq7zj9yXUgAAJEbbmqRhm0jmUndl8nHG0AbxFEfCfQRVZWL96Qzx0mf8lYwT9hIMrXncLwy31HOthmbXwFRwQ=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1005.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.8", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw=="], "@aws-sdk/types": ["@aws-sdk/types@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A=="], @@ -694,7 +812,7 @@ "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" } }, "sha512-M2oEKBzzNAYr136RRc6uqw3aWlwCxqTP1Lawps9E1d2abRPvl1p1ztQmmXp1Ak4rv8eByIZ+yQyKQ3zPdRG5dw=="], - "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.893.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg=="], + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-q6lCRm6UAe+e1LguM5E4EqM9brQlDem4XDcQ87NzEvlTW6GzmNCO0w1jS0XgCFXQHjDxjdlNFX+5sRbHijwklg=="], @@ -702,7 +820,9 @@ "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], - "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.1", "", {}, "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww=="], + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], + + "@azure-rest/core-client": ["@azure-rest/core-client@2.5.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", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="], "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], @@ -712,13 +832,13 @@ "@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.3.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g=="], + "@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="], "@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=="], "@azure/core-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="], - "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg=="], + "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.23.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ=="], "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], @@ -726,39 +846,51 @@ "@azure/core-xml": ["@azure/core-xml@1.5.0", "", { "dependencies": { "fast-xml-parser": "^5.0.7", "tslib": "^2.8.1" } }, "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw=="], + "@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + + "@azure/keyvault-common": ["@azure/keyvault-common@2.0.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w=="], + + "@azure/keyvault-keys": ["@azure/keyvault-keys@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag=="], + "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], - "@azure/storage-blob": ["@azure/storage-blob@12.29.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.1.1", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-7ktyY0rfTM0vo7HvtK6E3UvYnI9qfd6Oz6z/+92VhGRveWng3kJwMKeUpqmW/NmwcDNbxHpSlldG+vsUnRFnBg=="], + "@azure/msal-browser": ["@azure/msal-browser@4.29.0", "", { "dependencies": { "@azure/msal-common": "15.15.0" } }, "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w=="], + + "@azure/msal-common": ["@azure/msal-common@15.15.0", "", {}, "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw=="], - "@azure/storage-common": ["@azure/storage-common@12.1.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-eIOH1pqFwI6UmVNnDQvmFeSg0XppuzDLFeUNO/Xht7ODAzRLgGDh7h550pSxoA+lPDxBl1+D2m/KG3jWzCUjTg=="], + "@azure/msal-node": ["@azure/msal-node@3.8.8", "", { "dependencies": { "@azure/msal-common": "15.15.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg=="], - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@azure/storage-blob": ["@azure/storage-blob@12.31.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.3.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg=="], - "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + "@azure/storage-common": ["@azure/storage-common@12.3.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], @@ -768,35 +900,37 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], + "@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-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], + "@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-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="], + "@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=="], "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], - "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@bufbuild/protobuf": ["@bufbuild/protobuf@2.10.1", "", {}, "sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="], - "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.10.1", "", { "dependencies": { "@bufbuild/protobuf": "2.10.1", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-imB8dKEjrOnG5+XqVS+CeYn924WGLU/g3wogKhk11XtX9y9NJ7432OS6h24asuBbLrQcPdEZ6QkfM7KeOCeeyQ=="], + "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.11.0", "", { "dependencies": { "@bufbuild/protobuf": "2.11.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-lyZVNFUHArIOt4W0+dwYBe5GBwbKzbOy8ObaloEqsw9Mmiwv2O48TwddDoHN4itylC+BaEGqFdI1W8WQt2vWJQ=="], "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], @@ -828,15 +962,49 @@ "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], + "@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=="], "@dot/log": ["@dot/log@0.1.5", "", { "dependencies": { "chalk": "^4.1.2", "loglevelnext": "^6.0.0", "p-defer": "^3.0.0" } }, "sha512-ECraEVJWv2f2mWK93lYiefUkphStVlKD6yKDzisuoEmxuLKrxO9iGetHK2DoEAkj7sxjE886n0OUVVCUx0YPNg=="], - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], + + "@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="], + + "@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=="], - "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + "@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=="], - "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@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=="], + + "@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="], + + "@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="], + + "@emmetio/css-parser": ["@emmetio/css-parser@0.4.1", "", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ=="], + + "@emmetio/html-matcher": ["@emmetio/html-matcher@1.3.0", "", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="], + + "@emmetio/scanner": ["@emmetio/scanner@1.0.4", "", {}, "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA=="], + + "@emmetio/stream-reader": ["@emmetio/stream-reader@2.2.0", "", {}, "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw=="], + + "@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="], + + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], @@ -844,97 +1012,111 @@ "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], - "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@expressive-code/core": ["@expressive-code/core@0.41.3", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-9qzohqU7O0+JwMEEgQhnBPOw5DtsQRBXhW++5fvEywsuX44vCGGof1SL5OvPElvNgaWZ4pFZAFSlkNOkGyLwSQ=="], + "@expressive-code/core": ["@expressive-code/core@0.41.7", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-ck92uZYZ9Wba2zxkiZLsZGi9N54pMSAVdrI9uW3Oo9AtLglD5RmrdTwbYPCT2S/jC36JGB2i+pnQtBm/Ib2+dg=="], - "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3" } }, "sha512-rFQtmf/3N2CK3Cq/uERweMTYZnBu+CwxBdHuOftEmfA9iBE7gTVvwpbh82P9ZxkPLvc40UMhYt7uNuAZexycRQ=="], + "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7" } }, "sha512-diKtxjQw/979cTglRFaMCY/sR6hWF0kSMg8jsKLXaZBSfGS0I/Hoe7Qds3vVEgeoW+GHHQzMcwvgx/MOIXhrTA=="], - "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3", "shiki": "^3.2.2" } }, "sha512-RlTARoopzhFJIOVHLGvuXJ8DCEme/hjV+ZnRJBIxzxsKVpGPW4Oshqg9xGhWTYdHstTsxO663s0cdBLzZj9TQA=="], + "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "shiki": "^3.2.2" } }, "sha512-DL605bLrUOgqTdZ0Ot5MlTaWzppRkzzqzeGEu7ODnHF39IkEBbFdsC7pbl3LbUQ1DFtnfx6rD54k/cdofbW6KQ=="], - "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3" } }, "sha512-SN8tkIzDpA0HLAscEYD2IVrfLiid6qEdE9QLlGVSxO1KEw7qYvjpbNBQjUjMr5/jvTJ7ys6zysU2vLPHE0sb2g=="], + "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7" } }, "sha512-Ewpwuc5t6eFdZmWlFyeuy3e1PTQC0jFvw2Q+2bpcWXbOZhPLsT7+h8lsSIJxb5mS7wZko7cKyQ2RLYDyK6Fpmw=="], + + "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], - "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="], + + "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + "@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], + "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="], - "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="], + + "@fastify/rate-limit": ["@fastify/rate-limit@10.3.0", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="], "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], - "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.2.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-sqP34jDSWWEHygmYbM2rzIcRjhA+1FHVHj8mxUvVz1s7o2Cgb1NnOaUXU7eWTI0AGhO+tPYHDTqI/mRC4cdjlQ=="], + "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="], + + "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], "@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.2", "", { "dependencies": { "ansi-colors": "4.1.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA=="], + "@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=="], - "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.4", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-9l++kjcb0ui4JqPlueZ6OZ9zKn6eK/8//Z2jHcIXb5MRwDRgubOOSpTU5llEv3uvWfT10VzcMp99dySWq0AASw=="], + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.10", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.5", "@hey-api/json-schema-ref-parser": "1.2.2", "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-o0wlFxuLt1bcyIV/ZH8DQ1wrgODTnUYj/VfCHOOYgXUQlLp9Dm2PjihOz+WYrZLowhqUhSKeJRArOGzvLuOTsg=="], - "@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="], + "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], + + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="], @@ -942,7 +1124,7 @@ "@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="], - "@ibm/telemetry-js": ["@ibm/telemetry-js@1.10.2", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-F8+/NNUwtm8BuFz18O9KPvIFTFDo8GUSoyhPxPjEpk7nEyEzWGfhIiEPhL00B2NdHRLDSljh3AiCfSnL/tutiQ=="], + "@ibm/telemetry-js": ["@ibm/telemetry-js@1.11.0", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], @@ -982,17 +1164,15 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - "@internationalized/date": ["@internationalized/date@3.10.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw=="], + "@internationalized/date": ["@internationalized/date@3.12.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ=="], "@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="], - "@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="], - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="], - "@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=="], + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -1052,6 +1232,8 @@ "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], + "@joshwooding/vite-plugin-react-docgen-typescript": ["@joshwooding/vite-plugin-react-docgen-typescript@0.6.4", "", { "dependencies": { "glob": "^13.0.1", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "typescript": ">= 4.3.x", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-6PyZBYKnnVNqOSB0YFly+62R7dmov8segT27A+RVTBVd4iAE6kbW9QBJGlyR2yG4D4ohzhZSTIu7BK1UTtmFFA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -1064,6 +1246,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], + + "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], @@ -1116,8 +1302,16 @@ "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], + "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], + + "@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=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], @@ -1134,7 +1328,19 @@ "@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1142,6 +1348,10 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@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=="], + + "@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], + "@octokit/auth-app": ["@octokit/auth-app@8.0.1", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.1", "@octokit/auth-oauth-user": "^6.0.0", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" } }, "sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg=="], "@octokit/auth-oauth-app": ["@octokit/auth-oauth-app@9.0.3", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg=="], @@ -1200,6 +1410,8 @@ "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], + "@opencode-ai/desktop-electron": ["@opencode-ai/desktop-electron@workspace:packages/desktop-electron"], + "@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"], "@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"], @@ -1212,33 +1424,35 @@ "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], + "@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"], + "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], "@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"], "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], - "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.5.2", "", { "dependencies": { "@openrouter/sdk": "^0.1.27" }, "peerDependencies": { "@toon-format/toon": "^2.0.0", "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" }, "optionalPeers": ["@toon-format/toon"] }, "sha512-3Th0vmJ9pjnwcPc2H1f59Mb0LFvwaREZAScfOQIpUxAHjZ7ZawVKDP27qgsteZPmMYqccNMy4r4Y3kgUnNcKAg=="], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.5.4", "", { "dependencies": { "@openrouter/sdk": "^0.1.27" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw=="], "@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.74", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.74", "@opentui/core-darwin-x64": "0.1.74", "@opentui/core-linux-arm64": "0.1.74", "@opentui/core-linux-x64": "0.1.74", "@opentui/core-win32-arm64": "0.1.74", "@opentui/core-win32-x64": "0.1.74", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g4W16ymv12JdgZ+9B4t7mpIICvzWy2+eHERfmDf80ALduOQCUedKQdULcBFhVCYUXIkDRtIy6CID5thMAah3FA=="], + "@opentui/core": ["@opentui/core@0.1.87", "", { "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.87", "@opentui/core-darwin-x64": "0.1.87", "@opentui/core-linux-arm64": "0.1.87", "@opentui/core-linux-x64": "0.1.87", "@opentui/core-win32-arm64": "0.1.87", "@opentui/core-win32-x64": "0.1.87", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-dhsmMv0IqKftwG7J/pBrLBj2armsYIg5R3LBvciRQI/6X89GufP4l1u0+QTACAx6iR4SYJJNVNQ2tdX8LM9rMw=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.87", "", { "os": "darwin", "cpu": "arm64" }, "sha512-G8oq85diOfkU6n0T1CxCle7oDmpKxwhcdhZ9khBMU5IrfLx9ZDuCM3F6MsiRQWdvPPCq2oomNbd64bYkPamYgw=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.87", "", { "os": "darwin", "cpu": "x64" }, "sha512-MYTFQfOHm6qO7YaY4GHK9u/oJlXY6djaaxl5I+k4p2mk3vvuFIl/AP1ypITwBFjyV5gyp7PRWFp4nGfY9oN8bw=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.87", "", { "os": "linux", "cpu": "arm64" }, "sha512-he8o1h5M6oskRJ7wE+xKJgmWnv5ZwN6gB3M/Z+SeHtOMPa5cZmi3TefTjG54llEgFfx0F9RcqHof7TJ/GNxRkw=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.74", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.87", "", { "os": "linux", "cpu": "x64" }, "sha512-aiUwjPlH4yDcB8/6YDKSmMkaoGAAltL0Xo0AzXyAtJXWK5tkCSaYjEVwzJ/rYRkr4Magnad+Mjth4AQUWdR2AA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.87", "", { "os": "win32", "cpu": "arm64" }, "sha512-cmP0pOyREjWGniHqbDmaMY7U+1AyagrD8VseJbU0cGpNgVpG2/gbrJUGdfdLB0SNb+mzLdx6SOjdxtrElwRCQA=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.74", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.87", "", { "os": "win32", "cpu": "x64" }, "sha512-N2GErAAP8iODf2RPp86pilPaVKiD6G4pkpZL5nLGbKsl0bndrVTpSqZcn8+/nQwFZDPD/AsiRTYNOfWOblhzOw=="], - "@opentui/solid": ["@opentui/solid@0.1.74", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.74", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-Vz82cI8T9YeJjGsVg4ULp6ral4N+xyt1j9A6Tbu3aaQgEKiB74LW03EXREehfjPr1irOFxtKfWPbx5NKH0Upag=="], + "@opentui/solid": ["@opentui/solid@0.1.87", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.87", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-lRT9t30l8+FtgOjjWJcdb2MT6hP8/RKqwGgYwTI7fXrOqdhxxwdP2SM+rH2l3suHeASheiTdlvPAo230iUcsvg=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1352,9 +1566,11 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], - "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], + "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.18", "", { "dependencies": { "@pierre/theme": "0.0.22", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-7ZF3YD9fxdbYsPnltz5cUqHacN7ztp8RX/fJLxwv8wIEORpP4+7dHz1h/qx3o4EW2xUrIhmbM8ImywLasB787Q=="], + + "@pierre/theme": ["@pierre/theme@0.0.22", "", {}, "sha512-ePUIdQRNGjrveELTU7fY89Xa7YGHHEy5Po5jQy/18lm32eRn96+tnYJEtFooGdffrx55KBUtOXfvVy/7LDFFhA=="], - "@pierre/diffs": ["@pierre/diffs@1.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -1362,11 +1578,11 @@ "@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="], - "@poppinss/colors": ["@poppinss/colors@4.1.5", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw=="], + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], - "@poppinss/exception": ["@poppinss/exception@1.2.2", "", {}, "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg=="], + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], "@protobuf-ts/plugin": ["@protobuf-ts/plugin@2.11.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.4.0", "@bufbuild/protoplugin": "^2.4.0", "@protobuf-ts/protoc": "^2.11.1", "@protobuf-ts/runtime": "^2.11.1", "@protobuf-ts/runtime-rpc": "^2.11.1", "typescript": "^3.9" }, "bin": { "protoc-gen-ts": "bin/protoc-gen-ts", "protoc-gen-dump": "bin/protoc-gen-dump" } }, "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A=="], @@ -1444,49 +1660,55 @@ "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], @@ -1506,7 +1728,7 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - "@sindresorhus/is": ["@sindresorhus/is@7.1.1", "", {}, "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ=="], + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], "@slack/bolt": ["@slack/bolt@3.22.0", "", { "dependencies": { "@slack/logger": "^4.0.0", "@slack/oauth": "^2.6.3", "@slack/socket-mode": "^1.3.6", "@slack/types": "^2.13.0", "@slack/web-api": "^6.13.0", "@types/express": "^4.16.1", "@types/promise.allsettled": "^1.0.3", "@types/tsscmp": "^1.0.0", "axios": "^1.7.4", "express": "^4.21.0", "path-to-regexp": "^8.1.0", "promise.allsettled": "^1.0.2", "raw-body": "^2.3.3", "tsscmp": "^1.0.6" } }, "sha512-iKDqGPEJDnrVwxSVlFW6OKTkijd7s4qLBeSufoBsTM0reTyfdp/5izIQVkxNfzjHi3o6qjdYbRXkYad5HBsBog=="], @@ -1516,111 +1738,111 @@ "@slack/socket-mode": ["@slack/socket-mode@1.3.6", "", { "dependencies": { "@slack/logger": "^3.0.0", "@slack/web-api": "^6.12.1", "@types/node": ">=12.0.0", "@types/ws": "^7.4.7", "eventemitter3": "^5", "finity": "^0.5.4", "ws": "^7.5.3" } }, "sha512-G+im7OP7jVqHhiNSdHgv2VVrnN5U7KY845/5EZimZkrD4ZmtV0P3BiWkgeJhPtdLuM7C7i6+M6h6Bh+S4OOalA=="], - "@slack/types": ["@slack/types@2.19.0", "", {}, "sha512-7+QZ38HGcNh/b/7MpvPG6jnw7mliV6UmrquJLqgdxkzJgQEYUcEztvFWRU49z0x4vthF0ixL5lTK601AXrS8IA=="], + "@slack/types": ["@slack/types@2.20.0", "", {}, "sha512-PVF6P6nxzDMrzPC8fSCsnwaI+kF8YfEpxf3MqXmdyjyWTYsZQURpkK7WWUWvP5QpH55pB7zyYL9Qem/xSgc5VA=="], "@slack/web-api": ["@slack/web-api@6.13.0", "", { "dependencies": { "@slack/logger": "^3.0.0", "@slack/types": "^2.11.0", "@types/is-stream": "^1.1.0", "@types/node": ">=12.0.0", "axios": "^1.7.4", "eventemitter3": "^3.1.0", "form-data": "^2.5.0", "is-electron": "2.2.2", "is-stream": "^1.1.0", "p-queue": "^6.6.1", "p-retry": "^4.0.0" } }, "sha512-dv65crIgdh9ZYHrevLU6XFHTQwTyDmNqEqzuIrV+Vqe/vgiG6w37oex5ePDU1RGm2IJ90H8iOvHFvzdEO/vB+g=="], - "@smithy/abort-controller": ["@smithy/abort-controller@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], - "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="], + "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], - "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.1", "", { "dependencies": { "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ=="], + "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.4.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.10", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg=="], - "@smithy/core": ["@smithy/core@3.18.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.6", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-6gnIz3h+PEPQGDj8MnRSjDvKBah042jEoPgjFGJ4iJLBE78L4lY/n98x14XyPF4u3lN179Ub/ZKFY5za9GeLQw=="], + "@smithy/core": ["@smithy/core@3.23.9", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.12", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw=="], - "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.5", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q=="], + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.11", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ=="], - "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.6", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8P//tA8DVPk+3XURk2rwcKgYwFvwGwmJH/wJqQiSKwXZtf/LiZK+hbUZmPj/9KzM+OVSwe4o85KTp5x9DUZTjw=="], + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.12", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ=="], - "@smithy/hash-node": ["@smithy/hash-node@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA=="], + "@smithy/hash-node": ["@smithy/hash-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A=="], - "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6+do24VnEyvWcGdHXomlpd0m8bfZePpUKBy7m311n+JuRwug8J4dCanJdTymx//8mi0nlkflZBvJe+dEO/O12Q=="], + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g=="], - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], - "@smithy/md5-js": ["@smithy/md5-js@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg=="], + "@smithy/md5-js": ["@smithy/md5-js@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.12", "", { "dependencies": { "@smithy/core": "^3.18.5", "@smithy/middleware-serde": "^4.2.6", "@smithy/node-config-provider": "^4.3.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-9pAX/H+VQPzNbouhDhkW723igBMLgrI8OtX+++M7iKJgg/zY/Ig3i1e6seCcx22FWhE6Q/S61BRdi2wXBORT+A=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.23", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-serde": "^4.2.12", "@smithy/node-config-provider": "^4.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/service-error-classification": "^4.2.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-S4kWNKFowYd0lID7/DBqWHOQxmxlsf0jBaos9chQZUWTVOjSW1Ogyh8/ib5tM+agFDJ/TCxuCTvrnlc+9cIBcQ=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.40", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/service-error-classification": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.5", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.5", "", { "dependencies": { "@smithy/abort-controller": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.14", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A=="], - "@smithy/property-provider": ["@smithy/property-provider@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg=="], + "@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.3.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg=="], + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ=="], + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0" } }, "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ=="], + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.3.5", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.9.8", "", { "dependencies": { "@smithy/core": "^3.18.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-stack": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-8xgq3LgKDEFoIrLWBho/oYKyWByw9/corz7vuh1upv7ZBm0ZMjGYBhbn6v643WoIqA9UTcx5A5htEp/YatUwMA=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.3", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-stack": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw=="], - "@smithy/types": ["@smithy/types@4.9.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA=="], + "@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], - "@smithy/url-parser": ["@smithy/url-parser@4.2.5", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ=="], + "@smithy/url-parser": ["@smithy/url-parser@4.2.11", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing=="], - "@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], - "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="], + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], - "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-yHv+r6wSQXEXTPVCIQTNmXVWs7ekBTpMVErjqZoWkYN75HIFN5y9+/+sYOejfAuvxWGvgzgxbTHa/oz61YTbKw=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.39", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.14", "", { "dependencies": { "@smithy/config-resolver": "^4.4.3", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ljZN3iRvaJUgulfvobIuG97q1iUuCMrvXAlkZ4msY+ZuVHQHDIqn7FKZCEj+bx8omz6kF5yQXms/xhzjIO5XiA=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.42", "", { "dependencies": { "@smithy/config-resolver": "^4.4.10", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.2", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA=="], - "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], - "@smithy/util-retry": ["@smithy/util-retry@4.2.5", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg=="], + "@smithy/util-retry": ["@smithy/util-retry@4.2.11", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw=="], - "@smithy/util-stream": ["@smithy/util-stream@4.5.6", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ=="], + "@smithy/util-stream": ["@smithy/util-stream@4.5.17", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ=="], - "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], "@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.2.5", "", { "dependencies": { "@smithy/abort-controller": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.11", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-x7Rh2azQPs3XxbvCzcttRErKKvLnbZfqRf/gOjw2pb+ZscX88e5UkRPCB67bVnsFHxayvMvmePfKTqsRb+is1A=="], - "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], @@ -1632,35 +1854,35 @@ "@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.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], + "@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.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg=="], + "@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA=="], "@solid-primitives/map": ["@solid-primitives/map@0.4.13", "", { "dependencies": { "@solid-primitives/trigger": "^1.1.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew=="], "@solid-primitives/media": ["@solid-primitives/media@2.3.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-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA=="], - "@solid-primitives/props": ["@solid-primitives/props@3.2.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-lZOTwFJajBrshSyg14nBMEP0h8MXzPowGO0s3OeiR3z6nXHTfj0FhzDtJMv+VYoRJKQHG2QRnJTgCzK6erARAw=="], + "@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="], - "@solid-primitives/refs": ["@solid-primitives/refs@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg=="], + "@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="], "@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=="], - "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="], + "@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/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.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="], + "@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/trigger": ["@solid-primitives/trigger@1.2.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-IWoptVc0SWYgmpBPpCMehS5b07+tpFcvw15tOQ3QbXedSYn6KP8zCjPkHNzMxcOvOicTneleeZDP7lqmz+PQ6g=="], + "@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.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="], + "@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=="], @@ -1668,9 +1890,9 @@ "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="], - "@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@dfb2020", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite": "7.1.10", "vite-plugin-solid": "^2.11.9", "vitest": "^4.0.10" } }], + "@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@dfb2020", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite": "7.1.10", "vite-plugin-solid": "^2.11.9", "vitest": "^4.0.10" } }, "sha512-7JjjA49VGNOsMRI8QRUhVudZmv0CnJ18SliSgK1ojszs/c3ijftgVkzvXdkSLN4miDTzbkXewf65D6ZBo6W+GQ=="], - "@speed-highlight/core": ["@speed-highlight/core@1.2.12", "", {}, "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA=="], + "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], @@ -1678,9 +1900,31 @@ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@storybook/addon-a11y": ["@storybook/addon-a11y@10.2.17", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.2.17" } }, "sha512-J0ogEc4/XFC+Ytz+X1we6TOKreEk/shgUs/mtxdsLa0xJ6bp2n2OQPSjNtQHH/nK4SRBSfHWPm8ztfcXTzeG9w=="], + + "@storybook/addon-docs": ["@storybook/addon-docs@10.2.17", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.2.17", "@storybook/icons": "^2.0.1", "@storybook/react-dom-shim": "10.2.17", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.17" } }, "sha512-c414xi7rxlaHn92qWOxtEkcOMm0/+cvBui0gUsgiWOZOM8dHChGZ/RjMuf1pPDyOrSsybLsPjZhP0WthsMDkdQ=="], + + "@storybook/addon-links": ["@storybook/addon-links@10.2.17", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.2.17" }, "optionalPeers": ["react"] }, "sha512-KY2usxhPpt9AAzD22uBEfdPj1NZyCNyaYXgKkr8r/UeCNt7E7OdVBLNA1QMYZZ5dtIWj9EtY8c55OPuBM7aUkQ=="], + + "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.2.17", "", { "peerDependencies": { "storybook": "^10.2.17" } }, "sha512-+WQC4RJlIFXF+ww2aNsq0pg8JaM5el29WQ9Hd2VZrB9LdjTqckuJI+5oQBZ1GFQNQDPxlif2TJfRGgI3m1wSpA=="], + + "@storybook/addon-vitest": ["@storybook/addon-vitest@10.2.17", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.2.17", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-47mo952M/dHZQn1yTVMEUnri5KuIwWynPqamv6Q9KFXrSPOnBt/8IdrTcPUXFo5XO1ZmIWclgQjJtIvGe4z+ag=="], + + "@storybook/builder-vite": ["@storybook/builder-vite@10.2.17", "", { "dependencies": { "@storybook/csf-plugin": "10.2.17", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.17", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-m/OBveTLm5ds/tUgHmmbKzgSi/oeCpQwm5rZa49vP2BpAd41Q7ER6TzkOoISzPoNNMAcbVmVc5vn7k6hdbPSHw=="], + + "@storybook/csf-plugin": ["@storybook/csf-plugin@10.2.17", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.2.17", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-crHH8i/4mwzeXpWRPgwvwX2vjytW42zyzTRySUax5dTU8o9sjk4y+Z9hkGx3Nmu1TvqseS8v1Z20saZr/tQcWw=="], + + "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], + + "@storybook/icons": ["@storybook/icons@2.0.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg=="], + + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.2.17", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.2.17" } }, "sha512-x9Kb7eUSZ1zGsEw/TtWrvs1LwWIdNp8qoOQCgPEjdB07reSJcE8R3+ASWHJThmd4eZf66ZALPJyerejake4Osw=="], + "@stripe/stripe-js": ["@stripe/stripe-js@8.6.1", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="], - "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], + "@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="], + + "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], @@ -1718,52 +1962,64 @@ "@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.134.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.134.5", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw=="], - "@tauri-apps/api": ["@tauri-apps/api@2.9.0", "", {}, "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw=="], + "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="], - "@tauri-apps/cli": ["@tauri-apps/cli@2.9.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.4", "@tauri-apps/cli-darwin-x64": "2.9.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.4", "@tauri-apps/cli-linux-arm64-gnu": "2.9.4", "@tauri-apps/cli-linux-arm64-musl": "2.9.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.4", "@tauri-apps/cli-linux-x64-gnu": "2.9.4", "@tauri-apps/cli-linux-x64-musl": "2.9.4", "@tauri-apps/cli-win32-arm64-msvc": "2.9.4", "@tauri-apps/cli-win32-ia32-msvc": "2.9.4", "@tauri-apps/cli-win32-x64-msvc": "2.9.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-pvylWC9QckrOS9ATWXIXcgu7g2hKK5xTL5ZQyZU/U0n9l88SEFGcWgLQNa8WZmd+wWIOWhkxOFcOl3i6ubDNNw=="], + "@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.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw=="], + "@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.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ=="], + "@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.9.4", "", { "os": "linux", "cpu": "arm" }, "sha512-tTWkEPig+2z3Rk0zqZYfjUYcgD+aSm72wdrIhdYobxbQZOBw0zfn50YtWv+av7bm0SHvv75f0l7JuwgZM1HFow=="], + "@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.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-ql6vJ611qoqRYHxkKPnb2vHa27U+YRKRmIpLMMBeZnfFtZ938eao7402AQCH1mO2+/8ioUhbpy9R/ZcLTXVmkg=="], + "@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.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-vg7yNn7ICTi6hRrcA/6ff2UpZQP7un3xe3SEld5QM0prgridbKAiXGaCKr3BnUBx/rGXegQlD/wiLcWdiiraSw=="], + "@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.9.4", "", { "os": "linux", "cpu": "none" }, "sha512-l8L+3VxNk6yv5T/Z/gv5ysngmIpsai40B9p6NQQyqYqxImqYX37pqREoEBl1YwG7szGnDibpWhidPrWKR59OJA=="], + "@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.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-PepPhCXc/xVvE3foykNho46OmCyx47E/aG676vKTVp+mqin5d+IBqDL6wDKiGNT5OTTxKEyNlCQ81Xs2BQhhqA=="], + "@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.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw=="], + "@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.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-/7ZhnP6PY04bEob23q8MH/EoDISdmR1wuNm0k9d5HV7TDMd2GGCDa8dPXA4vJuglJKXIfXqxFmZ4L+J+MO42+w=="], + "@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.9.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-1LmAfaC4Cq+3O1Ir1ksdhczhdtFSTIV51tbAGtbV/mr348O+M52A/xwCCXQank0OcdBxy5BctqkMtuZnQvA8uQ=="], + "@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.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="], + "@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-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="], + "@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-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="], + "@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-K0FQlLM6BoV7Ws2xfkh+Tnwi5VZVdkI4Vw/3AGLSf0Xvu2y86AMBzd9w/SpzKhw9ai2B6ES8di/OoGDCExkOzg=="], + + "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="], + + "@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A=="], "@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.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="], + "@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.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="], + "@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.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ckGSEzZ5Ii4Hf2D5x25Oqnm2Zf9MfDWAzR+volY0z/OOBz6aucPKEY0F649JvQ0Vupku6UJo7ugpGRDOFOunkA=="], + "@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.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="], + "@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.10.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ=="], "@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=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -1774,6 +2030,8 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -1786,7 +2044,9 @@ "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@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=="], @@ -1802,12 +2062,16 @@ "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="], - "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="], + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="], "@types/fontkit": ["@types/fontkit@2.0.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew=="], + "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], + "@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/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], "@types/is-stream": ["@types/is-stream@1.1.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg=="], @@ -1820,6 +2084,8 @@ "@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/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -1830,30 +2096,42 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="], + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], "@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], + "@types/promise.allsettled": ["@types/promise.allsettled@1.0.6", "", {}, "sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg=="], "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + "@types/qs": ["@types/qs@6.15.0", "", {}, "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], "@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], + "@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/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], "@types/scheduler": ["@types/scheduler@0.26.0", "", {}, "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA=="], + "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], @@ -1868,14 +2146,20 @@ "@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/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], - "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], + "@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], "@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/native-preview": ["@typescript/native-preview@7.0.0-dev.20251207.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251207.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-4QcRnzB0pi9rS0AOvg8kWbmuwHv5X7B2EXHbgcms9+56hsZ8SZrZjNgBJb2rUIodJ4kU5mrkj/xlTTT4r9VcpQ=="], "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-waWJnuuvkXh4WdpbTjYf7pyahJzx0ycesV2BylyHrE9OxU9FSKcD/cRLQYvbq3YcBSdF7sZwRLDBer7qTeLsYA=="], @@ -1892,41 +2176,61 @@ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="], - "@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="], + "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], - "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.4", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], + "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="], + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + + "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], - "@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="], + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="], + "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], - "@vitest/runner": ["@vitest/runner@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" } }, "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q=="], + "@volar/kit": ["@volar/kit@2.4.28", "", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="], - "@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="], + "@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="], - "@vitest/spy": ["@vitest/spy@4.0.16", "", {}, "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw=="], + "@volar/language-server": ["@volar/language-server@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw=="], - "@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="], + "@volar/language-service": ["@volar/language-service@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw=="], + + "@volar/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="], + + "@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="], + + "@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="], + + "@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="], "@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + "@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="], "abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -1936,12 +2240,18 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@5.0.119", "", { "dependencies": { "@ai-sdk/gateway": "2.0.25", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HUOwhc17fl2SZTJGZyA/99aNu706qKfXaUBCy9vgZiXBwrxg2eTzn2BCz7kmYDsfx6Fg2ACBy2icm41bsDXCTw=="], + "ai": ["ai@5.0.124", "", { "dependencies": { "@ai-sdk/gateway": "2.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Li6Jw9F9qsvFJXZPBfxj38ddP2iURCnMs96f9Q3OeQzrDVcl1hvtwSEAuxA/qmfh6SDV2ERqFUOFzigvr0697g=="], + + "ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], "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-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -1958,6 +2268,10 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "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=="], @@ -1984,24 +2298,40 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "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=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], "astro": ["astro@5.7.13", "", { "dependencies": { "@astrojs/compiler": "^2.11.0", "@astrojs/internal-helpers": "0.6.1", "@astrojs/markdown-remark": "6.3.1", "@astrojs/telemetry": "3.2.1", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-cRGq2llKOhV3XMcYwQpfBIUcssN6HEK5CRbcMxAfd9OcFhvWE7KUy50zLioAZVVl3AqgUTJoNTlmZfD2eG0G1w=="], - "astro-expressive-code": ["astro-expressive-code@0.41.3", "", { "dependencies": { "rehype-expressive-code": "^0.41.3" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" } }, "sha512-u+zHMqo/QNLE2eqYRCrK3+XMlKakv33Bzuz+56V1gs8H0y6TZ0hIi3VNbIxeTn51NLn+mJfUV/A0kMNfE4rANw=="], + "astro-expressive-code": ["astro-expressive-code@0.41.7", "", { "dependencies": { "rehype-expressive-code": "^0.41.7" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" } }, "sha512-hUpogGc6DdAd+I7pPXsctyYPRBJDK7Q7d06s4cyP0Vz3OcbziP3FNzN0jZci1BpCvLn9675DvS7B9ctKKX64JQ=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "autoprefixer": ["autoprefixer@10.4.22", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-lite": "^1.0.30001754", "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg=="], + "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.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "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-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="], + "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], "aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="], @@ -2010,15 +2340,17 @@ "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], + "axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="], + + "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], + "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="], - "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.10", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA=="], + "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.3", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.5", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg=="], "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], @@ -2026,15 +2358,25 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], + "bare-fs": ["bare-fs@4.5.5", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w=="], + + "bare-os": ["bare-os@3.7.1", "", {}, "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA=="], + + "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], + + "bare-stream": ["bare-stream@2.8.0", "", { "dependencies": { "streamx": "^2.21.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA=="], + + "bare-url": ["bare-url@2.3.2", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw=="], + "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.8.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], @@ -2048,31 +2390,35 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], "bmp-ts": ["bmp-ts@1.0.9", "", {}, "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw=="], - "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], "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=="], - "bowser": ["bowser@2.12.1", "", {}, "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], - "browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="], @@ -2084,21 +2430,25 @@ "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.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="], + "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], - "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], + "bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="], - "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eDgLN9teKTfmvrCqgwwmWNsNszxYs7IZdCqk0S1DCarvMhr4wcajoSBlA/nQA0/owwLduPTS8xxCnQp4/N/gDg=="], + "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qM7W5IaFpWYGPDcNiQ8DOng3noQ97gxpH2MFH1mGsdKwI0T4oy++egSh5Z7s6AQx8WKgc9GzAsTUM4KZkFdacw=="], - "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-X+PjwJUWenUmdQBP8EtdItMyieQ6Nlpn+BH518oaouDiSnWj5+b0Y7DNDZJq7Ezom4EaxmqL/uGYZK3aCQ7CXg=="], + "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-oVoIsme27pcXB68YxnQSAgdNGCa4A3PGWYIBUewOh9VnJaoik4JenGb5Yy+svGE+ETFhQXV9nhHqgMPsDRrO6A=="], - "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zMLs2YIGB+/jxrYFXaFhVKX/GBt05UTF45lc9srcHc9JXGjEj+12CIo1CHLTAWatXMTqt0Jsu6ukWEoWVT/ayA=="], + "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+SYt09k+xDEl/GfcU7L1zdNgm7IlvAFKV5Xl/auBwuprKG5UwXNhjRlRAWfhTMCUZWN+NDf8E+ZQx0cQi9K2/g=="], - "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Z5yAK28xrcm8Wb5k7TZ8FJKpOI/r+aVCRdlHYAqI2SDJFN3nD4mJs900X6kNVmG/xFzb5yOuKVYWGg+6ZXWbyA=="], + "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-zvnUl4EAsQbKsmZVu+lEJcH8axQ7MiCfqg2OmnHd6uw1THABmHaX0GbpKiHshdgadNN2Nf+4zDyTJB5YMcAdrA=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -2106,6 +2456,14 @@ "c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "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=="], + + "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.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "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=="], @@ -2118,11 +2476,11 @@ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001756", "", {}, "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A=="], + "caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="], @@ -2138,6 +2496,8 @@ "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + "cheerio": ["cheerio@1.0.0-rc.12", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "htmlparser2": "^8.0.1", "parse5": "^7.0.0", "parse5-htmlparser2-tree-adapter": "^7.0.0" } }, "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q=="], "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], @@ -2146,7 +2506,9 @@ "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], + + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], @@ -2156,7 +2518,11 @@ "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - "cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="], + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + + "cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], "clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="], @@ -2164,12 +2530,12 @@ "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + "cloudflare": ["cloudflare@5.2.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-dVzqDpPFYR9ApEC9e+JJshFJZXcw4HzM8W+3DHzO5oy9+8rLC53G7x6fEf9A7/gSuSCxuvndzui5qJKftfIM9A=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], @@ -2190,11 +2556,17 @@ "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "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=="], + "condense-newlines": ["condense-newlines@0.2.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-whitespace": "^0.3.0", "kind-of": "^3.0.2" } }, "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg=="], - "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + "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=="], "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], @@ -2206,34 +2578,40 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], - "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "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=="], - "crossws": ["crossws@0.4.1", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-E7WKBcHVhAVrY6JYD5kteNqVq1GSZxqGrdSiwXR9at+XHi43HJoCQKXcCczR5LBnBquFZPsB3o7HklulKoBU5w=="], + "crossws": ["crossws@0.4.4", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg=="], "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], - "css-selector-parser": ["css-selector-parser@3.2.0", "", {}, "sha512-L1bdkNKUP5WYxiW5dW6vA2hd3sL8BdRNLy2FCX0rLVise4eNw9nBdeBuJHxlELieSE2H1f6bYQFfwVUwWCV9rQ=="], + "css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="], - "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -2248,18 +2626,28 @@ "db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="], + "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=="], "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], - "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "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=="], @@ -2284,11 +2672,13 @@ "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=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], - "devalue": ["devalue@5.5.0", "", {}, "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w=="], + "devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], @@ -2298,14 +2688,22 @@ "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "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=="], "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "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=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -2320,11 +2718,13 @@ "dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="], - "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + "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@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="], + "drizzle-kit": ["drizzle-kit@1.0.0-beta.16-ea816b6", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GiJQqCNPZP8Kk+i7/sFa3rtXbq26tLDNi3LbMx9aoLuwF2ofk8CS7cySUGdI+r4J3q0a568quC8FZeaFTCw4IA=="], - "drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@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", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], + "drizzle-orm": ["drizzle-orm@1.0.0-beta.16-ea816b6", "", { "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", "@prisma/client": "*", "@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", "@prisma/client", "@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-k9gT4f0O9Qvah5YK/zL+FZonQ8TPyVxcG/ojN4dzO0fHP8hs8tBno8lqmJo53g0JLWv3Q2nsTUoyBRKM2TljFw=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -2334,11 +2734,37 @@ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - "editorconfig": ["editorconfig@1.0.4", "", { "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", "minimatch": "9.0.1", "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" } }, "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q=="], + "editorconfig": ["editorconfig@1.0.7", "", { "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", "minimatch": "^9.0.1", "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" } }, "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.259", "", {}, "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ=="], + "effect": ["effect@4.0.0-beta.31", "", { "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-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="], + + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + + "electron": ["electron@40.4.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-N1ZXybQZL8kYemO8vAeh9nrk4mSvqlAO8xs0QCHkXIvRnuB/7VGwEehjvQbsU5/f4bmTKpG+2GQERe/zmKpudQ=="], + + "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-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=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], + + "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=="], + + "emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -2346,21 +2772,27 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], + + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], - "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], "es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="], @@ -2378,16 +2810,16 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], - "esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], + "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=="], "esbuild-plugin-copy": ["esbuild-plugin-copy@2.1.1", "", { "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.3", "fs-extra": "^10.0.1", "globby": "^11.0.3" }, "peerDependencies": { "esbuild": ">= 0.14.0" } }, "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw=="], - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -2414,7 +2846,7 @@ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], @@ -2432,11 +2864,13 @@ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], - "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + + "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], - "expressive-code": ["expressive-code@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3", "@expressive-code/plugin-frames": "^0.41.3", "@expressive-code/plugin-shiki": "^0.41.3", "@expressive-code/plugin-text-markers": "^0.41.3" } }, "sha512-YLnD62jfgBZYrXIPQcJ0a51Afv9h8VlWqEGK9uU2T5nL/5rb8SnA86+7+mgCZe5D34Tff5RNEA5hjNVJYHzrFg=="], + "expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], @@ -2444,19 +2878,41 @@ "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + "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.6.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA=="], + "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-json-stringify": ["fast-json-stringify@6.3.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA=="], + + "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-xml-builder": ["fast-xml-builder@1.0.0", "", {}, "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ=="], + "fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="], - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fastify": ["fastify@5.8.2", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg=="], + + "fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -2464,12 +2920,18 @@ "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@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], + "find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="], + + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="], @@ -2504,6 +2966,8 @@ "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=="], + "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -2516,12 +2980,10 @@ "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], - "gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], - "gel": ["gel@2.2.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="], - "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], @@ -2530,7 +2992,7 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -2540,13 +3002,11 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - - "ghostty-web": ["ghostty-web@0.3.0", "", {}, "sha512-SAdSHWYF20GMZUB0n8kh1N6Z4ljMnuUqT8iTB2n5FAPswEV10MejEpLlhW/769GL5+BQa1NYwEg9y/XCckV5+A=="], + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], @@ -2554,12 +3014,14 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "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=="], + "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + "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=="], + "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.0.4", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.1.1", "ignore": "^5.1.4", "merge2": "^1.3.0", "slash": "^3.0.0" } }, "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg=="], @@ -2570,9 +3032,11 @@ "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.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], + "graphql": ["graphql@16.13.1", "", {}, "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ=="], "graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="], @@ -2582,7 +3046,7 @@ "h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="], - "happy-dom": ["happy-dom@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g=="], + "happy-dom": ["happy-dom@20.8.3", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -2630,7 +3094,7 @@ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw=="], + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], @@ -2648,6 +3112,8 @@ "hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="], + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], @@ -2666,10 +3132,12 @@ "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "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-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], @@ -2680,7 +3148,9 @@ "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], - "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + "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=="], @@ -2692,15 +3162,21 @@ "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "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@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - "ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -2758,6 +3234,8 @@ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], @@ -2786,6 +3264,8 @@ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], @@ -2796,13 +3276,15 @@ "is-whitespace": ["is-whitespace@0.3.0", "", {}, "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg=="], - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], "is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="], "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + + "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], @@ -2810,7 +3292,9 @@ "iterate-value": ["iterate-value@1.0.2", "", { "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" } }, "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ=="], - "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], + "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=="], @@ -2828,52 +3312,72 @@ "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], "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=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="], + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "json-with-bigint": ["json-with-bigint@3.5.7", "", {}, "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - "jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], "jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="], "katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + "lang-map": ["lang-map@0.4.0", "", { "dependencies": { "language-map": "^1.1.0" } }, "sha512-oiSqZIEUnWdFeDNsp4HId4tAxdFbx5iMBOwA3666Fn2L8Khj8NiD9xRvMsGmKXopPVkaDFtSv3CJOmXFUB0Hcg=="], "language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="], + "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=="], "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], + "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], @@ -2902,16 +3406,16 @@ "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], - "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=="], @@ -2922,6 +3426,8 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -2930,20 +3436,28 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - "lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="], + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + "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=="], + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], @@ -2954,6 +3468,8 @@ "marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "md-to-react-email": ["md-to-react-email@5.0.0", "", { "dependencies": { "marked": "7.0.4" }, "peerDependencies": { "react": "18.x" } }, "sha512-GdBrBUbAAJHypnuyofYGfVos8oUslxHx69hs3CW9P0L8mS1sT6GnJuMBTlz/Fw+2widiwdavcu9UwyLF/BzZ4w=="], @@ -2964,7 +3480,7 @@ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], @@ -2988,13 +3504,13 @@ "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="], + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], @@ -3090,36 +3606,72 @@ "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + "miniflare": ["miniflare@4.20251118.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251118.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-uLSAE/DvOm392fiaig4LOaatxLjM7xzIniFRG5Y3yF9IduOYLLK/pkCPQNCgKQH3ou0YJRHnTN+09LPfqYNTQQ=="], "minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + + "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=="], + + "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], + + "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], + + "motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="], + + "motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], + + "mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="], + + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], + "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], "mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="], + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], @@ -3132,33 +3684,41 @@ "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + "node-abi": ["node-abi@4.26.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw=="], + "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=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "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=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - "node-html-parser": ["node-html-parser@7.0.1", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + + "node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="], - "node-mock-http": ["node-mock-http@1.0.3", "", {}, "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog=="], + "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], "nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], + "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -3180,6 +3740,8 @@ "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -3204,12 +3766,16 @@ "opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="], + "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=="], + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="], "oxc-transform": ["oxc-transform@0.96.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.96.0", "@oxc-transform/binding-darwin-arm64": "0.96.0", "@oxc-transform/binding-darwin-x64": "0.96.0", "@oxc-transform/binding-freebsd-x64": "0.96.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.96.0", "@oxc-transform/binding-linux-arm64-gnu": "0.96.0", "@oxc-transform/binding-linux-arm64-musl": "0.96.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.96.0", "@oxc-transform/binding-linux-s390x-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-musl": "0.96.0", "@oxc-transform/binding-wasm32-wasi": "0.96.0", "@oxc-transform/binding-win32-arm64-msvc": "0.96.0", "@oxc-transform/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ=="], + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], + "p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="], "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], @@ -3218,6 +3784,8 @@ "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], + "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], @@ -3228,7 +3796,7 @@ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - "package-manager-detector": ["package-manager-detector@1.5.0", "", {}, "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "pagefind": ["pagefind@1.4.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.4.0", "@pagefind/darwin-x64": "1.4.0", "@pagefind/freebsd-x64": "1.4.0", "@pagefind/linux-arm64": "1.4.0", "@pagefind/linux-x64": "1.4.0", "@pagefind/windows-x64": "1.4.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g=="], @@ -3258,13 +3826,17 @@ "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "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=="], - "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], @@ -3272,11 +3844,17 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], + "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], - "perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="], + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], @@ -3286,6 +3864,12 @@ "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], + + "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], + + "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], @@ -3298,17 +3882,19 @@ "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], - "planck": ["planck@1.4.2", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-mNbhnV3g8X2rwGxzcesjmN8BDA6qfXgQxXVMkWau9MCRlQY0RLNEkyHlVp6yFy/X6qrzAXyNONCnZ1cGDLrNew=="], + "planck": ["planck@1.4.3", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-B+lHKhRSeg7vZOfEyEzyQVu7nx8JHcX3QgnAcHXrPW0j04XYKX5eXSiUrxH2Z5QR8OoqvjD6zKIaPMdMYAd0uA=="], "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], "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=="], + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "postcss-css-variables": ["postcss-css-variables@0.18.0", "", { "dependencies": { "balanced-match": "^1.0.0", "escape-string-regexp": "^1.0.3", "extend": "^3.0.1" }, "peerDependencies": { "postcss": "^8.2.6" } }, "sha512-lYS802gHbzn1GI+lXvy9MYIYDuGnl1WB4FTKoqMQqJ3Mab09A7a/1wZvGTkCEZJTM8mSbIyb1mJYn8f0aPye0Q=="], @@ -3326,22 +3912,36 @@ "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], + "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=="], "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "pretty": ["pretty@2.0.0", "", { "dependencies": { "condense-newlines": "^0.2.1", "extend-shallow": "^2.0.1", "js-beautify": "^1.6.12" } }, "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w=="], + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + "proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + + "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + "promise.allsettled": ["promise.allsettled@1.0.7", "", { "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "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=="], "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], @@ -3350,9 +3950,13 @@ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + "punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "pure-rand": ["pure-rand@8.0.0", "", {}, "sha512-7rgWlxG2gAvFPIQfUreo1XYlNvrQ9VnQPFWdncPkdl3icucLK0InOxsaafbvxGTnI6Bk/Rxmslg0lQlRCuzOXw=="], + + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -3360,18 +3964,26 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], + "react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="], + "react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], "react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], @@ -3384,6 +3996,8 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "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-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], @@ -3394,6 +4008,10 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], @@ -3402,13 +4020,11 @@ "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], - "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], - - "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], - "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -3420,7 +4036,7 @@ "rehype-autolink-headings": ["rehype-autolink-headings@7.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-is-element": "^3.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw=="], - "rehype-expressive-code": ["rehype-expressive-code@0.41.3", "", { "dependencies": { "expressive-code": "^0.41.3" } }, "sha512-8d9Py4c/V6I/Od2VIXFAdpiO2kc0SV2qTJsRAaqSIcM9aruW4ASLNe2kOEo1inXAAkIhpFzAHTc358HKbvpNUg=="], + "rehype-expressive-code": ["rehype-expressive-code@0.41.7", "", { "dependencies": { "expressive-code": "^0.41.7" } }, "sha512-25f8ZMSF1d9CMscX7Cft0TSQIqdwjce2gDOvQ+d/w0FovsMwrSt3ODP4P3Z7wO1jsIJ4eYyaDRnIR/27bd/EMQ=="], "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], @@ -3450,20 +4066,32 @@ "remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="], + "request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "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.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "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@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], + "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], @@ -3472,15 +4100,19 @@ "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], - "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], + "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], - "rou3": ["rou3@0.7.10", "", {}, "sha512-aoFj6f7MJZ5muJ+Of79nrhs9N3oLGqi2VEMe94Zbkjb6Wupha46EuoYgpWSOZlXww3bbd8ojgXTAA2mzimX5Ww=="], + "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=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -3498,27 +4130,41 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safe-regex2": ["safe-regex2@5.0.0", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="], + "sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], + "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -3534,8 +4180,6 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "shiki": ["shiki@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="], "shikiji": ["shikiji@0.6.13", "", { "dependencies": { "hast-util-to-html": "^9.0.0" } }, "sha512-4T7X39csvhT0p7GDnq9vysWddf2b6BeioiN3Ymhnt3xcy9tXmDcnsEFVxX18Z4YcQgEE/w48dLJ4pPPUcG9KkA=="], @@ -3554,20 +4198,30 @@ "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + "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.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "sitemap": ["sitemap@8.0.2", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ=="], + "sitemap": ["sitemap@8.0.3", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-9Ew1tR2WYw8RGE2XLy7GjkusvYXy8Rg6y8TYuBuQMfIEdGcWoJpY2Wr5DzsEiL/TKCw56+YKTCCUHglorEYK+A=="], "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="], + "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=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], "socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], "socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="], + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="], "solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="], @@ -3582,7 +4236,9 @@ "solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -3590,39 +4246,43 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], - "srvx": ["srvx@0.9.6", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-5L4rT6qQqqb+xcoDoklUgCNdmzqJ6vbcDRwPVGRXewF55IJH0pqh0lQlrJ266ZWTKJ4mfeioqHQJeAYesS+RrQ=="], + "srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="], + + "ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], - "sst": ["sst@3.17.23", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.23", "sst-darwin-x64": "3.17.23", "sst-linux-arm64": "3.17.23", "sst-linux-x64": "3.17.23", "sst-linux-x86": "3.17.23", "sst-win32-arm64": "3.17.23", "sst-win32-x64": "3.17.23", "sst-win32-x86": "3.17.23" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-TwKgUgDnZdc1Swe+bvCNeyO4dQnYz5cTodMpYj3jlXZdK9/KNz0PVxT1f0u5E76i1pmilXrUBL/f7iiMPw4RDg=="], + "sst": ["sst@3.18.10", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.18.10", "sst-darwin-x64": "3.18.10", "sst-linux-arm64": "3.18.10", "sst-linux-x64": "3.18.10", "sst-linux-x86": "3.18.10", "sst-win32-arm64": "3.18.10", "sst-win32-x64": "3.18.10", "sst-win32-x86": "3.18.10" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-SY+ldeJ9K5E9q+DhjXA3e2W3BEOzBwkE3IyLSD71uA3/5nRhUAST31iOWEpW36LbIvSQ9uOVDFcebztoLJ8s7w=="], - "sst-darwin-arm64": ["sst-darwin-arm64@3.17.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R6kvmF+rUideOoU7KBs2SdvrIupoE+b+Dor/eq9Uo4Dojj7KvYDZI/EDm8sSCbbcx/opiWeyNqKtlnLEdCxE6g=="], + "sst-darwin-arm64": ["sst-darwin-arm64@3.18.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3MwIpMZhhdZKDqLp9ZQNlwkWix5+q+N0PWstuTomYwgZOxCCe6u9IIsoIszSk+GAJJN/jvGZyLiXKeV4iiQvw=="], - "sst-darwin-x64": ["sst-darwin-x64@3.17.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-WW4P1S35iYCifQXxD+sE3wuzcN+LHLpuKMaNoaBqEcWGZnH3IPaDJ7rpLF0arkDAo/z3jZmWWzOCkr0JuqJ8vQ=="], + "sst-darwin-x64": ["sst-darwin-x64@3.18.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-nQ0jMKkPOa+kj6Ygz8+kYhBua/vgNTLkd+4r8NSmk7v+Zs78lKnx3T//kEzS0yik6Q6QwGfokwrTcA1Jii2xSw=="], - "sst-linux-arm64": ["sst-linux-arm64@3.17.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-TjtNqgIh7RlAWgPLFCAt0mXvIB+J7WjmRvIRrAdX0mXsndOiBJ/DMOgXSLVsIWHCfPj8MIEot/hWpnJgXgIeag=="], + "sst-linux-arm64": ["sst-linux-arm64@3.18.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-mj9VNj3SvLS+HaXx2PhCX0aTA7CwJNoM6JhRc0s/zCilqchcvqDjbhpYBJO4brEPv6aOaaa7T3WvIQqtYauK4Q=="], - "sst-linux-x64": ["sst-linux-x64@3.17.23", "", { "os": "linux", "cpu": "x64" }, "sha512-qdqJiEbYfCjZlI3F/TA6eoIU7JXVkEEI/UMILNf2JWhky0KQdCW2Xyz+wb6c0msVJCWdUM/uj+1DaiP2eXvghw=="], + "sst-linux-x64": ["sst-linux-x64@3.18.10", "", { "os": "linux", "cpu": "x64" }, "sha512-7iy1Eq2eqnT9Ag/8OVgC04vRjV7AAQyf/BvzLc+6Sz+GvRiKA8VEuPnbXNYQF+NIvEqsawfcd7MknSTtImpsvQ=="], - "sst-linux-x86": ["sst-linux-x86@3.17.23", "", { "os": "linux", "cpu": "none" }, "sha512-aGmUujIvoNlmAABEGsOgfY1rxD9koC6hN8bnTLbDI+oI/u/zjHYh50jsbL0p3TlaHpwF/lxP3xFSuT6IKp+KgA=="], + "sst-linux-x86": ["sst-linux-x86@3.18.10", "", { "os": "linux", "cpu": "none" }, "sha512-77qZSuPZeQ5bdRCiq1pQEdY8EcGNHboKrx4P2yFid2FBDKJsXxOXtIxJdloyx+ljBn0+nxl/g040QBmXxdc9tA=="], - "sst-win32-arm64": ["sst-win32-arm64@3.17.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZxdkGqYDrrZGz98rijDCN+m5yuCcwD6Bc9/6hubLsvdpNlVorUqzpg801Ec97xSK0nIC9g6pNiRyxAcsQQstUg=="], + "sst-win32-arm64": ["sst-win32-arm64@3.18.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aY+FhMxvYs8crlrKALpLn/kKmud8YQj6LkMHsrOAAIJhfNyxhCja2vrYQaY+bcqdsS5W2LMVcS2hyaMqKXZKcg=="], - "sst-win32-x64": ["sst-win32-x64@3.17.23", "", { "os": "win32", "cpu": "x64" }, "sha512-yc9cor4MS49Ccy2tQCF1tf6M81yLeSGzGL+gjhUxpVKo2pN3bxl3w70eyU/mTXSEeyAmG9zEfbt6FNu4sy5cUA=="], + "sst-win32-x64": ["sst-win32-x64@3.18.10", "", { "os": "win32", "cpu": "x64" }, "sha512-rY+yJXOpG+P5xXnaQRpCvBK2zwwLhjzpYidGkp6F+cGgiVdh2Wre/CIQNRaVHr20ncj8lLe/RsHWa9QCNM48jg=="], - "sst-win32-x86": ["sst-win32-x86@3.17.23", "", { "os": "win32", "cpu": "none" }, "sha512-DIp3s54IpNAfdYjSRt6McvkbEPQDMxUu6RUeRAd2C+FcTJgTloon/ghAPQBaDgu2VoVgymjcJARO/XyfKcCLOQ=="], + "sst-win32-x86": ["sst-win32-x86@3.18.10", "", { "os": "win32", "cpu": "none" }, "sha512-pq8SmV0pIjBFMY6DraUZ4akyTxHnfjIKCRbBLdMxFUZK8TzA1NK2YdjRt1AwrgXRYGRyctrz/mt4WyO0SMOVQQ=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], - "stage-js": ["stage-js@1.0.0-alpha.17", "", {}, "sha512-AzlMO+t51v6cFvKZ+Oe9DJnL1OXEH5s9bEy6di5aOrUpcP7PCzI/wIeXF0u3zg0L89gwnceoKxrLId0ZpYnNXw=="], + "stage-js": ["stage-js@1.0.1", "", {}, "sha512-cz14aPp/wY0s3bkb/B93BPP5ZAEhgBbRmAT3CCDqert8eCAqIpQ0RB2zpK8Ksxf+Pisl5oTzvPHtL4CVzzeHcw=="], - "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.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], @@ -3630,6 +4290,10 @@ "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], + "storybook": ["storybook@10.2.17", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-yueTpl5YJqLzQqs3CanxNdAAfFU23iP0j+JVJURE4ghfEtRmWfWoZWLGkVcyjmgum7UmjwAlqRuOjQDNvH89kw=="], + + "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-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], @@ -3656,18 +4320,26 @@ "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="], "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], + "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=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], + "superstruct": ["superstruct@1.0.4", "", {}, "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -3680,28 +4352,44 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - "tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="], + "tar": ["tar@7.5.11", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ=="], + + "tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="], + + "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], + + "tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], + + "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], - "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], - "terracotta": ["terracotta@1.0.6", "", { "dependencies": { "solid-use": "^0.9.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-yVrmT/Lg6a3tEbeYEJH8ksb1PYkR5FA9k5gr1TchaSNIiA2ZWs5a+koEbePXwlBP0poaV7xViZ/v50bQFcMgqw=="], + "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=="], - "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], + "terracotta": ["terracotta@1.1.0", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww=="], - "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], + "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], + + "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], + "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], + "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "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=="], @@ -3712,8 +4400,14 @@ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "titleize": ["titleize@4.0.0", "", {}, "sha512-ZgUJ1K83rhdu7uh7EHAC2BgY5DzoX8V5rTvoWI4vFysggi6YjLe5gUXABPWAU7VkvGP7P/0YiWq+dcPeYDsf1g=="], + "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-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], @@ -3722,20 +4416,28 @@ "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + "toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "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=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "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-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], @@ -3746,19 +4448,19 @@ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], - "turbo": ["turbo@2.5.6", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.6", "turbo-darwin-arm64": "2.5.6", "turbo-linux-64": "2.5.6", "turbo-linux-arm64": "2.5.6", "turbo-windows-64": "2.5.6", "turbo-windows-arm64": "2.5.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w=="], + "turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="], - "turbo-darwin-64": ["turbo-darwin-64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A=="], + "turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="], - "turbo-linux-64": ["turbo-linux-64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA=="], + "turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="], - "turbo-windows-64": ["turbo-windows-64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg=="], + "turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="], "turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="], @@ -3776,9 +4478,15 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="], + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], - "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + "typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="], + + "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=="], @@ -3788,7 +4496,7 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], @@ -3802,6 +4510,10 @@ "unifont": ["unifont@0.5.2", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg=="], + "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-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -3816,7 +4528,7 @@ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], @@ -3830,11 +4542,15 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "unstorage": ["unstorage@2.0.0-alpha.4", "", { "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4.0.3", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "lru-cache": "^11.2.2", "mongodb": "^6.20.0", "ofetch": "*", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-ywXZMZRfrvmO1giJeMTCw6VUn0ALYxVl8pFqJPStiyQUvgJImejtAHrKvXPj4QGJAoS/iLGcVGF6ljN/lkh1bw=="], + "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=="], + + "unstorage": ["unstorage@2.0.0-alpha.6", "", { "peerDependencies": { "@azure/app-configuration": "^1.11.0", "@azure/cosmos": "^4.9.1", "@azure/data-tables": "^13.3.2", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.31.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.13.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.36.2", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.9.3", "lru-cache": "^11.2.6", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-w5vLYCJtnSx3OBtDk7cG4c1p3dfAnHA4WSZq9Xsurjbl2wMj7zqfOIjaHQI1Bl7yKzUxXAi+kbMr8iO2RhJmBA=="], "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.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="], @@ -3842,6 +4558,10 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "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=="], @@ -3850,10 +4570,12 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "uuid": ["uuid@8.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], @@ -3870,14 +4592,46 @@ "vite-plugin-solid": ["vite-plugin-solid@2.11.10", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw=="], - "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + "vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="], + + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + + "volar-service-css": ["volar-service-css@0.0.68", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-lJSMh6f3QzZ1tdLOZOzovLX0xzAadPhx8EKwraDLPxBndLCYfoTvnNuiFFV8FARrpAlW5C0WkH+TstPaCxr00Q=="], + + "volar-service-emmet": ["volar-service-emmet@0.0.68", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-nHvixrRQ83EzkQ4G/jFxu9Y4eSsXS/X2cltEPDM+K9qZmIv+Ey1w0tg1+6caSe8TU5Hgw4oSTwNMf/6cQb3LzQ=="], - "vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="], + "volar-service-html": ["volar-service-html@0.0.68", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-fru9gsLJxy33xAltXOh4TEdi312HP80hpuKhpYQD4O5hDnkNPEBdcQkpB+gcX0oK0VxRv1UOzcGQEUzWCVHLfA=="], + + "volar-service-prettier": ["volar-service-prettier@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-grUmWHkHlebMOd6V8vXs2eNQUw/bJGJMjekh/EPf/p2ZNTK0Uyz7hoBRngcvGfJHMsSXZH8w/dZTForIW/4ihw=="], + + "volar-service-typescript": ["volar-service-typescript@0.0.68", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-z7B/7CnJ0+TWWFp/gh2r5/QwMObHNDiQiv4C9pTBNI2Wxuwymd4bjEORzrJ/hJ5Yd5+OzeYK+nFCKevoGEEeKw=="], + + "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-NugzXcM0iwuZFLCJg47vI93su5YhTIweQuLmZxvz5ZPTaman16JCvmDZexx2rd5T/75SNuvvZmrTOTNYUsfe5w=="], + + "volar-service-yaml": ["volar-service-yaml@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.19.2" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-84XgE02LV0OvTcwfqhcSwVg4of3MLNUWPMArO6Aj8YXqyEVnPu8xTEMY2btKSq37mVAPuaEVASI4e3ptObmqcA=="], + + "vscode-css-languageservice": ["vscode-css-languageservice@6.3.10", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA=="], + + "vscode-html-languageservice": ["vscode-html-languageservice@5.6.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg=="], + + "vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="], "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + "vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], @@ -3886,11 +4640,15 @@ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "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-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -3900,7 +4658,7 @@ "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], - "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], @@ -3918,7 +4676,7 @@ "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], - "wsl-utils": ["wsl-utils@0.3.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ=="], + "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], @@ -3936,12 +4694,16 @@ "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "yaml-language-server": ["yaml-language-server@1.19.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "lodash": "4.17.21", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg=="], "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], "yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="], @@ -3964,7 +4726,7 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@actions/artifact/@actions/core": ["@actions/core@2.0.1", "", { "dependencies": { "@actions/exec": "^2.0.0", "@actions/http-client": "^3.0.0" } }, "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg=="], + "@actions/artifact/@actions/core": ["@actions/core@2.0.3", "", { "dependencies": { "@actions/exec": "^2.0.0", "@actions/http-client": "^3.0.2" } }, "sha512-Od9Thc3T1mQJYddvVPM4QGiLUewdh+3txmDYHHxoNdkqysR1MbCT+rFOtNUxYAz+7+6RIsqipVahY2GJqGPyxA=="], "@actions/core/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], @@ -3976,11 +4738,9 @@ "@actions/github/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], + "@actions/http-client/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], - "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -3988,11 +4748,33 @@ "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], - "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], + "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], + + "@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], + + "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], + + "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], - "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], + "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AnGoxVNZ/E3EU4lW12rrufI6riqL2cEv4jk3OrjJ/i54XwR0CJU1V26jXAwxb+Pc+uZmYG++HM+gzXxPQZkMNQ=="], + + "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], + + "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], + + "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -4002,32 +4784,76 @@ "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], - "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], + "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], + + "@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], - "@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], + "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], + "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@astrojs/check/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=="], + "@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], - "@astrojs/markdown-remark/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], + "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.19.0", "smol-toml": "^1.5.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A=="], - "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.9", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.13.0", "smol-toml": "^1.4.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hX2cLC/KW74Io1zIbn92kI482j9J7LleBLGCVU9EP3BeH5MVrnFawOnqD0t/q6D1Z+ZNeQG2gNKMslCcO36wng=="], + "@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "@astrojs/sitemap/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@astrojs/solid-js/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-crypto/sha1-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.775.0", "", { "dependencies": { "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZoGKwa4C9fC9Av6bdfqcW6Ix5ot05F/S4VxWR2nHuMv7hzfmAjTOcUiWT7UR4hM/U0whf84VhDtXN/DWAk52KA=="], + "@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-crypto/util/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.19", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.17", "@aws-sdk/credential-provider-http": "^3.972.19", "@aws-sdk/credential-provider-ini": "^3.972.18", "@aws-sdk/credential-provider-process": "^3.972.17", "@aws-sdk/credential-provider-sso": "^3.972.18", "@aws-sdk/credential-provider-web-identity": "^3.972.18", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-yDWQ9dFTr+IMxwanFe7+tbN5++q8psZBjlUwOiCXn1EzANoBgtqBwcpYcHaMGtn0Wlfj4NuXdf2JaEx1lz5RaQ=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.9", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.5", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw=="], + + "@aws-sdk/client-sso/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/client-sts/@aws-sdk/core": ["@aws-sdk/core@3.775.0", "", { "dependencies": { "@aws-sdk/types": "3.775.0", "@smithy/core": "^3.2.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.0.2", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.782.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-ini": "3.782.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-HZiAF+TCEyKjju9dgysjiPIWgt/+VerGaeEp18mvKLNfgKz1d+/82A2USEpNKTze7v3cMFASx3CvL8yYyF7mJw=="], @@ -4050,6 +4876,90 @@ "@aws-sdk/client-sts/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.782.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-dMFkUBgh2Bxuw8fYZQoH/u3H4afQ12VSkzEi//qFiDTwbKYq+u+RYjc8GLDM6JSK1BShMu5AVR7HD4ap1TYUnA=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.8", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.9", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-retry": "^4.4.40", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.39", "@smithy/util-defaults-mode-node": "^4.2.42", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.8", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.9", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-retry": "^4.4.40", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.39", "@smithy/util-defaults-mode-node": "^4.2.42", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.8", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.9", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-retry": "^4.4.40", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.39", "@smithy/util-defaults-mode-node": "^4.2.42", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ozge/c7NdHUDyHqro6+P5oHt8wfKSUBN+olttiVfBe9Mw3wBMpPa3gQ0pZnG+gwBkKskBuip2bMR16tqYvUSEA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-b6N9Nnlg8JInQwzBkUq5spNaXssM3h3zLxGzpPrnw0nHSIWPJPTbZzA5Ca285fcDUFuKP+qf3qkuqlAjGOdWhg=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HygGyKuMG5AaGXsmM0d81miWDon55xwalRHB3UmDg3QBhtunbNIoIaWUbNTKuBZXcIN6emeeEZw/YgSMqLc0YA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-BodZYKvT4p/Dkm28Ql/FhDdS1+p51bcZeMMu2TRtU8PoMDHnVDhHz27zASEKSZwmhvquxHrZHB0IGuVqjZUtSQ=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.933.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.933.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/token-providers": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-/R1DBR7xNcuZIhS2RirU+P2o8E8/fOk+iLAhbqeSTq+g09fP/F6W7ouFpS5eVE2NIfWG7YBFoVddOhvuqpn51g=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-c7Eccw2lhFx2/+qJn3g+uIDWRuWi2A6Sz3PVvckFUEzPsP0dPUo19hlvtarwP5GzrsXn0yEPRVhpewsIaSCGaQ=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.8", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.9", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-retry": "^4.4.40", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.39", "@smithy/util-defaults-mode-node": "^4.2.42", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.8", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.9", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-retry": "^4.4.40", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.39", "@smithy/util-defaults-mode-node": "^4.2.42", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/credential-providers/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.19", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.17", "@aws-sdk/credential-provider-http": "^3.972.19", "@aws-sdk/credential-provider-ini": "^3.972.18", "@aws-sdk/credential-provider-process": "^3.972.17", "@aws-sdk/credential-provider-sso": "^3.972.18", "@aws-sdk/credential-provider-web-identity": "^3.972.18", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-yDWQ9dFTr+IMxwanFe7+tbN5++q8psZBjlUwOiCXn1EzANoBgtqBwcpYcHaMGtn0Wlfj4NuXdf2JaEx1lz5RaQ=="], + + "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], + + "@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], + + "@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + + "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.9", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA=="], + + "@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + + "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], + + "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + + "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.5", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw=="], + + "@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.8", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.9", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-retry": "^4.4.40", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.39", "@smithy/util-defaults-mode-node": "^4.2.42", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA=="], + + "@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], "@azure/core-http/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], @@ -4062,6 +4972,10 @@ "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@azure/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=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -4074,25 +4988,49 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@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=="], + "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], - "@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], + "@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=="], - "@gitlab/gitlab-ai-provider/openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="], + "@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@electron/fuses/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "@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=="], - "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + "@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=="], - "@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@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/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], + + "@electron/rebuild/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "@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=="], - "@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/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "@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/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=="], + + "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], + + "@gitlab/gitlab-ai-provider/openai": ["openai@6.27.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ=="], + + "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + + "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -4136,27 +5074,33 @@ "@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], - "@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "@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=="], + + "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "@modelcontextprotocol/sdk/jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], "@octokit/auth-app/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], - "@octokit/auth-oauth-app/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/auth-oauth-app/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], "@octokit/auth-oauth-app/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/auth-oauth-device/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/auth-oauth-device/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], "@octokit/auth-oauth-device/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/auth-oauth-user/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/auth-oauth-user/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], "@octokit/auth-oauth-user/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], @@ -4170,11 +5114,11 @@ "@octokit/endpoint/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], - "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], "@octokit/graphql/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="], - "@octokit/oauth-methods/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/oauth-methods/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], "@octokit/oauth-methods/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -4208,7 +5152,13 @@ "@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], - "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="], + "@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/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], @@ -4216,13 +5166,11 @@ "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], - "@pierre/diffs/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], - - "@pierre/diffs/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ=="], + "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], - "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/types": "3.19.0" } }, "sha512-e6vwrsyw+wx4OkcrDbL+FVCxwx8jgKiCoXzakVur++mIWVcgpzIi8vxf4/b4dVTYrV/nUx5RjinMf4tq8YV8Fw=="], + "@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - "@pierre/diffs/shiki": ["shiki@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/engine-oniguruma": "3.19.0", "@shikijs/langs": "3.19.0", "@shikijs/themes": "3.19.0", "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA=="], + "@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], @@ -4244,6 +5192,8 @@ "@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], + "@slack/socket-mode/@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], + "@slack/socket-mode/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "@slack/web-api/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], @@ -4254,11 +5204,21 @@ "@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], - "@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], + "@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], + + "@smithy/hash-node/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@smithy/hash-stream-node/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="], + "@smithy/md5-js/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@solidjs/start/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=="], + "@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@smithy/util-base64/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], "@solidjs/start/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], @@ -4266,11 +5226,15 @@ "@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], + "@standard-community/standard-json/effect": ["effect@4.0.0-beta.29", "", { "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-7UoBAEiktoS81XLMX/39Mq/Ymq8whxmqFpsI0MEYdMlbDcbytzQlyuyhvrwEIdrd9qrqa8DZ5mKblWasamryqw=="], + + "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.29", "", { "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-7UoBAEiktoS81XLMX/39Mq/Ymq8whxmqFpsI0MEYdMlbDcbytzQlyuyhvrwEIdrd9qrqa8DZ5mKblWasamryqw=="], + "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], @@ -4280,63 +5244,145 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@tanstack/directive-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@tanstack/router-utils/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "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=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], - "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], + "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - "astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="], + "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - "astro/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - "astro/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], + "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], - "astro/unstorage": ["unstorage@1.17.3", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q=="], + "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], - "astro/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], + + "ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="], + + "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], + + "ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AnGoxVNZ/E3EU4lW12rrufI6riqL2cEv4jk3OrjJ/i54XwR0CJU1V26jXAwxb+Pc+uZmYG++HM+gzXxPQZkMNQ=="], + + "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=="], + + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "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/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "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=="], + + "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], + + "astro/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], + + "astro/unstorage": ["unstorage@1.17.4", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="], + + "astro/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "aws-sdk/events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="], + "aws-sdk/uuid": ["uuid@8.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "bl/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "body-parser/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], - "bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="], + "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-runtime/sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], + + "bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "c12/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + + "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=="], + + "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "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=="], + + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "condense-newlines/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + "conf/dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="], + + "conf/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + + "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "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=="], - "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "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=="], + + "dmg-builder/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "drizzle-kit/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + "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=="], + + "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], "editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], - "editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="], + "editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "editorconfig/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "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/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + + "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], @@ -4346,48 +5392,66 @@ "esbuild-plugin-copy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], - "express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - "gel/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "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=="], - "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + "happy-dom/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], - "js-beautify/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=="], + "html-minifier-terser/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "jsonwebtoken/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], + + "js-beautify/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=="], "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -4400,19 +5464,35 @@ "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], - "named-placeholders/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "motion/framer-motion": ["framer-motion@12.35.2", "", { "dependencies": { "motion-dom": "^12.35.2", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA=="], + + "mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], + "node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], + + "node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + + "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="], + "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], "opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], - "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], + "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], @@ -4426,33 +5506,59 @@ "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "ora/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "path-scurry/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], - "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "postcss-css-variables/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + + "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=="], + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "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=="], "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], @@ -4462,26 +5568,26 @@ "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], - "sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "shiki/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], "shiki/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - "sitemap/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], - - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "sitemap/sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], "sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], + "storybook/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=="], + + "storybook/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -4492,11 +5598,15 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "tree-sitter-bash/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "tree-sitter-bash/node-addon-api": ["node-addon-api@8.6.0", "", {}, "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q=="], "tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -4506,26 +5616,44 @@ "unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "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/@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + + "vitest/@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], "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=="], + "vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + "which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "wrap-ansi-cjs/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=="], "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "xml2js/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], + "yaml-language-server/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], + + "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], "yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + "yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + "zod-to-json-schema/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -4538,30 +5666,66 @@ "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], - "@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], + "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/anthropic/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + + "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/gateway/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], + "@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA=="], + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@astrojs/markdown-remark/shiki/@shikijs/langs": ["@shikijs/langs@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A=="], + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - "@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ=="], + "@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="], + "@ai-sdk/openai/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + + "@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@astrojs/check/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=="], + + "@astrojs/check/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=="], "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="], "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], - "@astrojs/mdx/@astrojs/markdown-remark/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], - "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww=="], @@ -4574,73 +5738,193 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.782.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xCna0opVPaueEbJoclj5C6OpDNi0Gynj+4d7tnuXGgQhTHPyAz8ZyClkVqpi5qvHTgxROdUEDxWqEO5jqRHZHQ=="], - "@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], - "@azure/core-http/xml2js/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], - "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], - "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.9", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.5", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.9", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.5", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.9", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.5", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + "@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qzq7zj9yXUgAAJEbbmqRhm0jmUndl8nHG0AbxFEfCfQRVZWL96Qzx0mf8lYwT9hIMrXncLwy31HOthmbXwFRwQ=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/langs": ["@shikijs/langs@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.9", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.5", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.9", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.5", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.9", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.5", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw=="], + + "@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], + + "@azure/core-http/xml2js/sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], + + "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], + + "@azure/identity/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/themes": ["@shikijs/themes@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ=="], + "@develar/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="], + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "@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=="], - "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@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/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/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/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@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=="], "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], @@ -4698,11 +5982,15 @@ "@jsx-email/cli/vite/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@jsx-email/cli/vite/rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="], + "@jsx-email/cli/vite/rollup": ["rollup@3.30.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA=="], + + "@jsx-email/doiuse-email/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "@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=="], "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], @@ -4710,43 +5998,37 @@ "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.0", "", { "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-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "@modelcontextprotocol/sdk/express/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=="], "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "@modelcontextprotocol/sdk/express/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "@modelcontextprotocol/sdk/express/send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + "@modelcontextprotocol/sdk/express/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=="], - "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], - - "@modelcontextprotocol/sdk/express/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], "@octokit/auth-app/@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-app/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], "@octokit/auth-oauth-app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-device/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], "@octokit/auth-oauth-device/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-user/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -4756,7 +6038,7 @@ "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - "@octokit/graphql/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/graphql/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/graphql/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -4764,7 +6046,7 @@ "@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], - "@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -4772,7 +6054,7 @@ "@octokit/plugin-paginate-rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], - "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -4786,7 +6068,7 @@ "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -4806,7 +6088,7 @@ "@octokit/rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], - "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], "@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -4814,251 +6096,259 @@ "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@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/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], + "@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], - "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], + "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@pierre/diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], - "@pierre/diffs/@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="], + "@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], + "@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - "@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="], + "@slack/web-api/p-queue/p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="], - "@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg=="], + "@solidjs/start/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="], - "@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg=="], + "@solidjs/start/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="], - "@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A=="], + "@solidjs/start/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="], - "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="], + "@solidjs/start/shiki/@shikijs/langs": ["@shikijs/langs@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ=="], - "@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@solidjs/start/shiki/@shikijs/themes": ["@shikijs/themes@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g=="], - "@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], - "@slack/web-api/p-queue/p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="], + "@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@solidjs/start/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@solidjs/start/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "@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=="], - "@solidjs/start/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "@solidjs/start/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "@solidjs/start/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="], - "@solidjs/start/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="], - "@solidjs/start/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], - "@solidjs/start/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="], - "@solidjs/start/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - "@solidjs/start/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], - "@solidjs/start/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@solidjs/start/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "@solidjs/start/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "@solidjs/start/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@solidjs/start/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "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=="], - "@solidjs/start/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@solidjs/start/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - "@solidjs/start/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "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=="], - "@solidjs/start/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "archiver-utils/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "@solidjs/start/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "@solidjs/start/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "astro/unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "@solidjs/start/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "astro/unstorage/h3": ["h3@1.15.6", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ=="], - "@solidjs/start/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], - "@solidjs/start/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], - "@solidjs/start/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], - "@solidjs/start/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="], + "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "@solidjs/start/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="], + "bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "@solidjs/start/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="], + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "@solidjs/start/shiki/@shikijs/langs": ["@shikijs/langs@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "@solidjs/start/shiki/@shikijs/themes": ["@shikijs/themes@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g=="], + "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=="], - "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], + "cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "@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=="], + "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=="], - "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "cli-truncate/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "cli-truncate/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "crc/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "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=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "archiver-utils/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "dir-compare/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "astro/shiki/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], + "dmg-license/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "astro/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], + "editorconfig/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "astro/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA=="], + "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=="], - "astro/shiki/@shikijs/langs": ["@shikijs/langs@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A=="], + "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=="], - "astro/shiki/@shikijs/themes": ["@shikijs/themes@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ=="], + "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "astro/shiki/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="], + "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "astro/unstorage/h3": ["h3@1.15.4", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.2", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ=="], + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "astro/unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "js-beautify/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "js-beautify/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=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + "motion/framer-motion/motion-dom": ["motion-dom@12.35.2", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg=="], - "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + "node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], - "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + "node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + "opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], - "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + "opencontrol/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + "opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + "ora/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + "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=="], - "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + "ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + "parse-bmfont-xml/xml2js/sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], - "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + "readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "tw-to-css/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "tw-to-css/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "tw-to-css/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "tw-to-css/tailwindcss/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], - "js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "js-beautify/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], - "js-beautify/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=="], + "vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "jsonwebtoken/jws/jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], + "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], - "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], - "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="], - "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], + "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="], - "opencontrol/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="], - "opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="], - "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], + "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="], - "parse-bmfont-xml/xml2js/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], + "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="], - "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="], - "readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="], - "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="], - "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="], - "rimraf/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=="], + "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="], - "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="], - "tw-to-css/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="], - "tw-to-css/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="], - "tw-to-css/tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="], - "tw-to-css/tailwindcss/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="], - "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="], + + "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="], + + "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="], - "vitest/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=="], + "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="], + + "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -5070,17 +6360,13 @@ "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], - "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], - - "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], + "@astrojs/check/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA=="], + "@astrojs/check/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=="], - "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/langs": ["@shikijs/langs@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A=="], + "@astrojs/check/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ=="], - - "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="], + "@astrojs/check/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], @@ -5088,6 +6374,8 @@ "@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=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-5GlJBejo8wqMpSSEKb45WE82YxI2k73YuebjLH/eWDNQeE6VI5Bh9lA1YQ7xNkLLH8hIsb0pSfKVuwh0VEzVrg=="], @@ -5096,6 +6384,46 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@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=="], + "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5146,55 +6474,99 @@ "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "@modelcontextprotocol/sdk/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/plugin-paginate-rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@opencode-ai/desktop-electron/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], + "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "astro/unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "astro/unstorage/h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], "astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "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=="], + + "cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "editorconfig/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=="], "esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "js-beautify/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=="], + + "js-beautify/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "opencode/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "opencontrol/@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], @@ -5202,102 +6574,128 @@ "opencontrol/@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "opencontrol/@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.0", "", { "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-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "opencontrol/@modelcontextprotocol/sdk/express/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=="], "opencontrol/@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "opencontrol/@modelcontextprotocol/sdk/express/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "opencontrol/@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "opencontrol/@modelcontextprotocol/sdk/express/send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], - - "opencontrol/@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + "opencontrol/@modelcontextprotocol/sdk/express/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=="], - "opencontrol/@modelcontextprotocol/sdk/express/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "opencontrol/@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "opencontrol/@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "opencontrol/@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "ora/bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "@astrojs/check/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@astrojs/check/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], + + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], - "vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], - "vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], - "vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "@electron/rebuild/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "@electron/rebuild/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "vitest/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], - "vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="], - "vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - "vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - "vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "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=="], - "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + "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=="], - "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], + "electron-builder/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="], + "electron-builder/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "js-beautify/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=="], - "opencontrol/@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "js-beautify/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=="], - "opencontrol/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "js-beautify/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "opencontrol/@modelcontextprotocol/sdk/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "opencontrol/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], + + "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "js-beautify/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "js-beautify/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], } } diff --git a/flake.lock b/flake.lock index 16fb71c0a5a..59eb118fa46 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1768393167, - "narHash": "sha256-n2063BRjHde6DqAz2zavhOOiLUwA3qXt7jQYHyETjX8=", + "lastModified": 1772091128, + "narHash": "sha256-TnrYykX8Mf/Ugtkix6V+PjW7miU2yClA6uqWl/v6KWM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2f594d5af95d4fdac67fba60376ec11e482041cb", + "rev": "3f0336406035444b4a24b942788334af5f906259", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e4d214a0b93..40e9d337f58 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,26 @@ }; }); + overlays = { + default = + final: _prev: + let + node_modules = final.callPackage ./nix/node_modules.nix { + inherit rev; + }; + opencode = final.callPackage ./nix/opencode.nix { + inherit node_modules; + }; + desktop = final.callPackage ./nix/desktop.nix { + inherit opencode; + }; + in + { + inherit opencode; + opencode-desktop = desktop; + }; + }; + packages = forEachSystem ( pkgs: let @@ -42,28 +62,15 @@ desktop = pkgs.callPackage ./nix/desktop.nix { inherit opencode; }; - # nixpkgs cpu naming to bun cpu naming - cpuMap = { x86_64 = "x64"; aarch64 = "arm64"; }; - # matrix of node_modules builds - these will always fail due to fakeHash usage - # but allow computation of the correct hash from any build machine for any cpu/os - # see the update-nix-hashes workflow for usage - moduleUpdaters = pkgs.lib.listToAttrs ( - pkgs.lib.concatMap (cpu: - map (os: { - name = "${cpu}-${os}_node_modules"; - value = node_modules.override { - bunCpu = cpuMap.${cpu}; - bunOs = os; - hash = pkgs.lib.fakeHash; - }; - }) [ "linux" "darwin" ] - ) [ "x86_64" "aarch64" ] - ); in { default = opencode; inherit opencode desktop; - } // moduleUpdaters + # Updater derivation with fakeHash - build fails and reveals correct hash + node_modules_updater = node_modules.override { + hash = pkgs.lib.fakeHash; + }; + } ); }; } diff --git a/github/action.yml b/github/action.yml index 8652bb8c151..3d983a16099 100644 --- a/github/action.yml +++ b/github/action.yml @@ -30,6 +30,10 @@ inputs: description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/opencode,/oc'" required: false + variant: + description: "Model variant for provider-specific reasoning effort (e.g., high, max, minimal)" + required: false + oidc_base_url: description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.opencode.ai" required: false @@ -71,4 +75,5 @@ runs: PROMPT: ${{ inputs.prompt }} USE_GITHUB_TOKEN: ${{ inputs.use_github_token }} MENTIONS: ${{ inputs.mentions }} + VARIANT: ${{ inputs.variant }} OIDC_BASE_URL: ${{ inputs.oidc_base_url }} diff --git a/github/index.ts b/github/index.ts index 73378894cd3..1a0a9926224 100644 --- a/github/index.ts +++ b/github/index.ts @@ -8,6 +8,7 @@ import type { Context as GitHubContext } from "@actions/github/lib/context" import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types" import { createOpencodeClient } from "@opencode-ai/sdk" import { spawn } from "node:child_process" +import { setTimeout as sleep } from "node:timers/promises" type GitHubAuthor = { login: string @@ -275,13 +276,13 @@ async function assertOpencodeConnected() { body: { service: "github-workflow", level: "info", - message: "Prepare to react to Github Workflow event", + message: "Prepare to react to GitHub Workflow event", }, }) connected = true break } catch (e) {} - await Bun.sleep(300) + await sleep(300) } while (retry++ < 30) if (!connected) { diff --git a/github/sst-env.d.ts b/github/sst-env.d.ts index f742a120044..3b8cffd4fd6 100644 --- a/github/sst-env.d.ts +++ b/github/sst-env.d.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ /* deno-fmt-ignore-file */ +/* biome-ignore-all lint: auto-generated */ /// diff --git a/infra/console.ts b/infra/console.ts index 3d482160d02..c7889c587f9 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -77,6 +77,8 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint", "checkout.session.expired", "charge.refunded", "invoice.payment_succeeded", + "invoice.payment_failed", + "invoice.payment_action_required", "customer.created", "customer.deleted", "customer.updated", @@ -98,29 +100,54 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint", ], }) -const zenProduct = new stripe.Product("ZenBlack", { +const zenLiteProduct = new stripe.Product("ZenLite", { + name: "OpenCode Go", +}) +const zenLiteCouponFirstMonth50 = new stripe.Coupon("ZenLiteCouponFirstMonth50", { + name: "First month 50% off", + percentOff: 50, + appliesToProducts: [zenLiteProduct.id], + duration: "once", +}) +const zenLitePrice = new stripe.Price("ZenLitePrice", { + product: zenLiteProduct.id, + currency: "usd", + recurring: { + interval: "month", + intervalCount: 1, + }, + unitAmount: 1000, +}) +const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", { + properties: { + product: zenLiteProduct.id, + price: zenLitePrice.id, + firstMonth50Coupon: zenLiteCouponFirstMonth50.id, + }, +}) + +const zenBlackProduct = new stripe.Product("ZenBlack", { name: "OpenCode Black", }) -const zenPriceProps = { - product: zenProduct.id, +const zenBlackPriceProps = { + product: zenBlackProduct.id, currency: "usd", recurring: { interval: "month", intervalCount: 1, }, } -const zenPrice200 = new stripe.Price("ZenBlackPrice", { ...zenPriceProps, unitAmount: 20000 }) -const zenPrice100 = new stripe.Price("ZenBlack100Price", { ...zenPriceProps, unitAmount: 10000 }) -const zenPrice20 = new stripe.Price("ZenBlack20Price", { ...zenPriceProps, unitAmount: 2000 }) +const zenBlackPrice200 = new stripe.Price("ZenBlackPrice", { ...zenBlackPriceProps, unitAmount: 20000 }) +const zenBlackPrice100 = new stripe.Price("ZenBlack100Price", { ...zenBlackPriceProps, unitAmount: 10000 }) +const zenBlackPrice20 = new stripe.Price("ZenBlack20Price", { ...zenBlackPriceProps, unitAmount: 2000 }) const ZEN_BLACK_PRICE = new sst.Linkable("ZEN_BLACK_PRICE", { properties: { - product: zenProduct.id, - plan200: zenPrice200.id, - plan100: zenPrice100.id, - plan20: zenPrice20.id, + product: zenBlackProduct.id, + plan200: zenBlackPrice200.id, + plan100: zenBlackPrice100.id, + plan20: zenBlackPrice20.id, }, }) -const ZEN_BLACK_LIMITS = new sst.Secret("ZEN_BLACK_LIMITS") const ZEN_MODELS = [ new sst.Secret("ZEN_MODELS1"), @@ -131,6 +158,28 @@ const ZEN_MODELS = [ new sst.Secret("ZEN_MODELS6"), new sst.Secret("ZEN_MODELS7"), new sst.Secret("ZEN_MODELS8"), + new sst.Secret("ZEN_MODELS9"), + new sst.Secret("ZEN_MODELS10"), + new sst.Secret("ZEN_MODELS11"), + new sst.Secret("ZEN_MODELS12"), + new sst.Secret("ZEN_MODELS13"), + new sst.Secret("ZEN_MODELS14"), + new sst.Secret("ZEN_MODELS15"), + new sst.Secret("ZEN_MODELS16"), + new sst.Secret("ZEN_MODELS17"), + new sst.Secret("ZEN_MODELS18"), + new sst.Secret("ZEN_MODELS19"), + new sst.Secret("ZEN_MODELS20"), + new sst.Secret("ZEN_MODELS21"), + new sst.Secret("ZEN_MODELS22"), + new sst.Secret("ZEN_MODELS23"), + new sst.Secret("ZEN_MODELS24"), + new sst.Secret("ZEN_MODELS25"), + new sst.Secret("ZEN_MODELS26"), + new sst.Secret("ZEN_MODELS27"), + new sst.Secret("ZEN_MODELS28"), + new sst.Secret("ZEN_MODELS29"), + new sst.Secret("ZEN_MODELS30"), ] const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY") const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY") @@ -152,14 +201,10 @@ const bucketNew = new sst.cloudflare.Bucket("ZenDataNew") const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID") const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY") -let logProcessor -if ($app.stage === "production" || $app.stage === "frank") { - const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY") - logProcessor = new sst.cloudflare.Worker("LogProcessor", { - handler: "packages/console/function/src/log-processor.ts", - link: [HONEYCOMB_API_KEY], - }) -} +const logProcessor = new sst.cloudflare.Worker("LogProcessor", { + handler: "packages/console/function/src/log-processor.ts", + link: [new sst.Secret("HONEYCOMB_API_KEY")], +}) new sst.cloudflare.x.SolidStart("Console", { domain, @@ -175,7 +220,8 @@ new sst.cloudflare.x.SolidStart("Console", { AWS_SES_ACCESS_KEY_ID, AWS_SES_SECRET_ACCESS_KEY, ZEN_BLACK_PRICE, - ZEN_BLACK_LIMITS, + ZEN_LITE_PRICE, + new sst.Secret("ZEN_LIMITS"), new sst.Secret("ZEN_SESSION_SECRET"), ...ZEN_MODELS, ...($dev @@ -194,10 +240,10 @@ new sst.cloudflare.x.SolidStart("Console", { }, transform: { server: { + placement: { region: "aws:us-east-1" }, transform: { worker: { - placement: { mode: "smart" }, - tailConsumers: logProcessor ? [{ service: logProcessor.nodes.worker.scriptName }] : [], + tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }], }, }, }, diff --git a/install b/install index 22b7ca39ed7..b0716d53208 100755 --- a/install +++ b/install @@ -130,7 +130,7 @@ else needs_baseline=false if [ "$arch" = "x64" ]; then if [ "$os" = "linux" ]; then - if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then + if ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then needs_baseline=true fi fi @@ -141,6 +141,20 @@ else needs_baseline=true fi fi + + if [ "$os" = "windows" ]; then + ps="(Add-Type -MemberDefinition \"[DllImport(\"\"kernel32.dll\"\")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);\" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)" + out="" + if command -v powershell.exe >/dev/null 2>&1; then + out=$(powershell.exe -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true) + elif command -v pwsh >/dev/null 2>&1; then + out=$(pwsh -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true) + fi + out=$(echo "$out" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') + if [ "$out" != "true" ] && [ "$out" != "1" ]; then + needs_baseline=true + fi + fi fi target="$os-$arch" diff --git a/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json b/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json deleted file mode 100644 index 41cb01a2b83..00000000000 --- a/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "keep": { - "days": true, - "amount": 14 - }, - "auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json", - "files": [ - { - "date": 1759827172859, - "name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log", - "hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16" - } - ], - "hashType": "sha256" -} diff --git a/logs/mcp-puppeteer-2025-10-07.log b/logs/mcp-puppeteer-2025-10-07.log deleted file mode 100644 index 77053569666..00000000000 --- a/logs/mcp-puppeteer-2025-10-07.log +++ /dev/null @@ -1,48 +0,0 @@ -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"} -{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"} -{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.120"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.121"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.490"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.491"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.524"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.525"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.126"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.127"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.175"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.176"} diff --git a/nix/desktop.nix b/nix/desktop.nix index 9625f75c271..efdc2bd72e2 100644 --- a/nix/desktop.nix +++ b/nix/desktop.nix @@ -45,8 +45,7 @@ rustPlatform.buildRustPackage (finalAttrs: { rustc jq makeWrapper - ] - ++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ]; + ] ++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ]; buildInputs = lib.optionals stdenv.isLinux [ dbus @@ -61,6 +60,7 @@ rustPlatform.buildRustPackage (finalAttrs: { gst_all_1.gstreamer gst_all_1.gst-plugins-base gst_all_1.gst-plugins-good + gst_all_1.gst-plugins-bad ]; strictDeps = true; @@ -97,4 +97,4 @@ rustPlatform.buildRustPackage (finalAttrs: { mainProgram = "opencode-desktop"; inherit (opencode.meta) platforms; }; -}) \ No newline at end of file +}) diff --git a/nix/hashes.json b/nix/hashes.json index 80e646d45dd..06f54dc950f 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-wSkJcUnS0ODOYkbkjRnxnjfWYKQOVXwkDNB8qrikuLA=", - "aarch64-linux": "sha256-4BlpH/oIXRJEjkQydXDv1oi1Yx7li3k1dKHUy2/Gb10=", - "aarch64-darwin": "sha256-awW0ooZo/QfB2xmRdZ9XLNzQ9sP/mbN+rTg215id6nc=", - "x86_64-darwin": "sha256-CHrE2z+LqY2WXTQeGWG5LNMF1AY4UGSwViJAy4IwIVw=" + "x86_64-linux": "sha256-WJgo6UclmtQOEubnKMZybdIEhZ1uRTucF61yojjd+l0=", + "aarch64-linux": "sha256-QfZ/g7EZFpe6ndR3dG8WvVfMj5Kyd/R/4kkTJfGJxL4=", + "aarch64-darwin": "sha256-ezr/R70XJr9eN5l3mgb7HzLF6QsofNEKUOtuxbfli80=", + "x86_64-darwin": "sha256-MbsBGS415uEU/n1RQ/5H5pqh+udLY3+oimJ+eS5uJVI=" } } diff --git a/nix/node_modules.nix b/nix/node_modules.nix index 981a60ef9ba..6c188c07cf7 100644 --- a/nix/node_modules.nix +++ b/nix/node_modules.nix @@ -2,8 +2,6 @@ lib, stdenvNoCC, bun, - bunCpu ? if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64", - bunOs ? if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin", rev ? "dirty", hash ? (lib.pipe ./hashes.json [ @@ -16,6 +14,9 @@ let builtins.readFile builtins.fromJSON ]; + platform = stdenvNoCC.hostPlatform; + bunCpu = if platform.isAarch64 then "arm64" else "x64"; + bunOs = if platform.isLinux then "linux" else "darwin"; in stdenvNoCC.mkDerivation { pname = "opencode-node_modules"; @@ -29,7 +30,8 @@ stdenvNoCC.mkDerivation { ../bun.lock ../package.json ../patches - ../install + ../install # required by desktop build (cli.rs include_str!) + ../.github/TEAM_MEMBERS # required by @opencode-ai/script ] ); }; @@ -39,23 +41,22 @@ stdenvNoCC.mkDerivation { "SOCKS_SERVER" ]; - nativeBuildInputs = [ - bun - ]; + nativeBuildInputs = [ bun ]; dontConfigure = true; buildPhase = '' runHook preBuild - export HOME=$(mktemp -d) export BUN_INSTALL_CACHE_DIR=$(mktemp -d) bun install \ --cpu="${bunCpu}" \ --os="${bunOs}" \ + --filter '!./' \ + --filter './packages/opencode' \ + --filter './packages/desktop' \ --frozen-lockfile \ --ignore-scripts \ - --no-progress \ - --linker=isolated + --no-progress bun --bun ${./scripts/canonicalize-node-modules.ts} bun --bun ${./scripts/normalize-bun-binaries.ts} runHook postBuild @@ -63,10 +64,8 @@ stdenvNoCC.mkDerivation { installPhase = '' runHook preInstall - mkdir -p $out find . -type d -name node_modules -exec cp -R --parents {} $out \; - runHook postInstall ''; diff --git a/nix/opencode.nix b/nix/opencode.nix index 23d9fbe34e0..b7d6f95947c 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -34,6 +34,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { ''; env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json"; + env.OPENCODE_DISABLE_MODELS_FETCH = true; env.OPENCODE_VERSION = finalAttrs.version; env.OPENCODE_CHANNEL = "local"; @@ -79,7 +80,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { writableTmpDirAsHomeHook ]; doInstallCheck = true; - versionCheckKeepEnvironment = [ "HOME" ]; + versionCheckKeepEnvironment = [ "HOME" "OPENCODE_DISABLE_MODELS_FETCH" ]; versionCheckProgramArg = "--version"; passthru = { diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts index faa6f63402e..7997a3cd232 100644 --- a/nix/scripts/canonicalize-node-modules.ts +++ b/nix/scripts/canonicalize-node-modules.ts @@ -1,27 +1,32 @@ import { lstat, mkdir, readdir, rm, symlink } from "fs/promises" import { join, relative } from "path" -type SemverLike = { - valid: (value: string) => string | null - rcompare: (left: string, right: string) => number -} - type Entry = { dir: string version: string - label: string } +async function isDirectory(path: string) { + try { + const info = await lstat(path) + return info.isDirectory() + } catch { + return false + } +} + +const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x") + const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") const linkRoot = join(bunRoot, "node_modules") const directories = (await readdir(bunRoot)).sort() + const versions = new Map() for (const entry of directories) { const full = join(bunRoot, entry) - const info = await lstat(full) - if (!info.isDirectory()) { + if (!(await isDirectory(full))) { continue } const parsed = parseEntry(entry) @@ -29,37 +34,23 @@ for (const entry of directories) { continue } const list = versions.get(parsed.name) ?? [] - list.push({ dir: full, version: parsed.version, label: entry }) + list.push({ dir: full, version: parsed.version }) versions.set(parsed.name, list) } -const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as - | SemverLike - | { - default: SemverLike - } -const semver = "default" in semverModule ? semverModule.default : semverModule const selections = new Map() for (const [slug, list] of versions) { list.sort((a, b) => { - const left = semver.valid(a.version) - const right = semver.valid(b.version) - if (left && right) { - const delta = semver.rcompare(left, right) - if (delta !== 0) { - return delta - } - } - if (left && !right) { - return -1 - } - if (!left && right) { - return 1 - } + const aValid = isValidSemver(a.version) + const bValid = isValidSemver(b.version) + if (aValid && bValid) return -Bun.semver.order(a.version, b.version) + if (aValid) return -1 + if (bValid) return 1 return b.version.localeCompare(a.version) }) - selections.set(slug, list[0]) + const first = list[0] + if (first) selections.set(slug, first) } await rm(linkRoot, { recursive: true, force: true }) @@ -77,10 +68,7 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0] await mkdir(parent, { recursive: true }) const linkPath = join(parent, leaf) const desired = join(entry.dir, "node_modules", slug) - const exists = await lstat(desired) - .then((info) => info.isDirectory()) - .catch(() => false) - if (!exists) { + if (!(await isDirectory(desired))) { continue } const relativeTarget = relative(parent, desired) diff --git a/nix/scripts/normalize-bun-binaries.ts b/nix/scripts/normalize-bun-binaries.ts index 531d8fd0567..978ab325b7b 100644 --- a/nix/scripts/normalize-bun-binaries.ts +++ b/nix/scripts/normalize-bun-binaries.ts @@ -8,7 +8,7 @@ type PackageManifest = { const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") -const bunEntries = (await safeReadDir(bunRoot)).sort() +const bunEntries = (await readdir(bunRoot)).sort() let rewritten = 0 for (const entry of bunEntries) { @@ -45,11 +45,11 @@ for (const entry of bunEntries) { } } -console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`) +console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`) async function collectPackages(modulesRoot: string) { const found: string[] = [] - const topLevel = (await safeReadDir(modulesRoot)).sort() + const topLevel = (await readdir(modulesRoot)).sort() for (const name of topLevel) { if (name === ".bin" || name === ".bun") { continue @@ -59,7 +59,7 @@ async function collectPackages(modulesRoot: string) { continue } if (name.startsWith("@")) { - const scoped = (await safeReadDir(full)).sort() + const scoped = (await readdir(full)).sort() for (const child of scoped) { const scopedDir = join(full, child) if (await isDirectory(scopedDir)) { @@ -121,14 +121,6 @@ async function isDirectory(path: string) { } } -async function safeReadDir(path: string) { - try { - return await readdir(path) - } catch { - return [] - } -} - function normalizeBinName(name: string) { const slash = name.lastIndexOf("/") if (slash >= 0) { diff --git a/package.json b/package.json index 4267ef64566..97087c0e76f 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,12 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.5", + "packageManager": "bun@1.3.10", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", + "dev:desktop": "bun --cwd packages/desktop tauri dev", + "dev:web": "bun --cwd packages/app dev", + "dev:storybook": "bun --cwd packages/storybook storybook", "typecheck": "bun turbo typecheck", "prepare": "husky", "random": "echo 'Random script'", @@ -21,7 +24,7 @@ "packages/slack" ], "catalog": { - "@types/bun": "1.3.5", + "@types/bun": "1.3.9", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", "ulid": "3.0.1", @@ -33,12 +36,15 @@ "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/diffs": "1.0.2", + "@pierre/diffs": "1.1.0-beta.18", "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "dompurify": "3.3.1", - "ai": "5.0.119", + "drizzle-kit": "1.0.0-beta.16-ea816b6", + "drizzle-orm": "1.0.0-beta.16-ea816b6", + "effect": "4.0.0-beta.31", + "ai": "5.0.124", "hono": "4.10.7", "hono-openapi": "1.1.2", "fuzzysort": "3.1.0", @@ -65,11 +71,14 @@ "devDependencies": { "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", + "@types/mime-types": "3.0.1", + "@typescript/native-preview": "catalog:", + "glob": "13.0.5", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", - "sst": "3.17.23", - "turbo": "2.5.6" + "sst": "3.18.10", + "turbo": "2.8.13" }, "dependencies": { "@aws-sdk/client-s3": "3.933.0", @@ -92,13 +101,15 @@ "protobufjs", "tree-sitter", "tree-sitter-bash", - "web-tree-sitter" + "web-tree-sitter", + "electron" ], "overrides": { "@types/bun": "catalog:", "@types/node": "catalog:" }, "patchedDependencies": { - "ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch" + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch" } } diff --git a/packages/app/bunfig.toml b/packages/app/bunfig.toml index 36399045119..f1caabbcce9 100644 --- a/packages/app/bunfig.toml +++ b/packages/app/bunfig.toml @@ -1,2 +1,3 @@ [test] +root = "./src" preload = ["./happydom.ts"] diff --git a/packages/app/create-effect-simplification-spec.md b/packages/app/create-effect-simplification-spec.md new file mode 100644 index 00000000000..cc101ab0592 --- /dev/null +++ b/packages/app/create-effect-simplification-spec.md @@ -0,0 +1,515 @@ +# 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/AGENTS.md b/packages/app/e2e/AGENTS.md new file mode 100644 index 00000000000..cb8080fb252 --- /dev/null +++ b/packages/app/e2e/AGENTS.md @@ -0,0 +1,197 @@ +# E2E Testing Guide + +## Build/Lint/Test Commands + +```bash +# Run all e2e tests +bun test:e2e + +# Run specific test file +bun test:e2e -- app/home.spec.ts + +# Run single test by title +bun test:e2e -- -g "home renders and shows core entrypoints" + +# Run tests with UI mode (for debugging) +bun test:e2e:ui + +# Run tests locally with full server setup +bun test:e2e:local + +# View test report +bun test:e2e:report + +# Typecheck +bun typecheck +``` + +## Test Structure + +All tests live in `packages/app/e2e/`: + +``` +e2e/ +├── fixtures.ts # Test fixtures (test, expect, gotoSession, sdk) +├── actions.ts # Reusable action helpers +├── selectors.ts # DOM selectors +├── utils.ts # Utilities (serverUrl, modKey, path helpers) +└── [feature]/ + └── *.spec.ts # Test files +``` + +## Test Patterns + +### Basic Test Structure + +```typescript +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { withSession } from "../actions" + +test("test description", async ({ page, sdk, gotoSession }) => { + await gotoSession() // or gotoSession(sessionID) + + // Your test code + await expect(page.locator(promptSelector)).toBeVisible() +}) +``` + +### Using Fixtures + +- `page` - Playwright page +- `sdk` - OpenCode SDK client for API calls +- `gotoSession(sessionID?)` - Navigate to session + +### Helper Functions + +**Actions** (`actions.ts`): + +- `openPalette(page)` - Open command palette +- `openSettings(page)` - Open settings dialog +- `closeDialog(page, dialog)` - Close any dialog +- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar +- `waitTerminalReady(page, { term? })` - Wait for a mounted terminal to connect and finish rendering output +- `runTerminal(page, { cmd, token, term?, timeout? })` - Type into the terminal via the browser and wait for rendered output +- `withSession(sdk, title, callback)` - Create temp session +- `withProject(...)` - Create temp project/workspace +- `sessionIDFromUrl(url)` - Read session ID from URL +- `slugFromUrl(url)` - Read workspace slug from URL +- `waitSlug(page, skip?)` - Wait for resolved workspace slug +- `trackSession(sessionID, directory?)` - Register session for fixture cleanup +- `trackDirectory(directory)` - Register directory for fixture cleanup +- `clickListItem(container, filter)` - Click list item by key/text + +**Selectors** (`selectors.ts`): + +- `promptSelector` - Prompt input +- `terminalSelector` - Terminal panel +- `sessionItemSelector(id)` - Session in sidebar +- `listItemSelector` - Generic list items + +**Utils** (`utils.ts`): + +- `modKey` - Meta (Mac) or Control (Linux/Win) +- `serverUrl` - Backend server URL +- `sessionPath(dir, id?)` - Build session URL + +## Code Style Guidelines + +### Imports + +Always import from `../fixtures`, not `@playwright/test`: + +```typescript +// ✅ Good +import { test, expect } from "../fixtures" + +// ❌ Bad +import { test, expect } from "@playwright/test" +``` + +### Naming Conventions + +- Test files: `feature-name.spec.ts` +- Test names: lowercase, descriptive: `"sidebar can be toggled"` +- Variables: camelCase +- Constants: SCREAMING_SNAKE_CASE + +### Error Handling + +Tests should clean up after themselves. Prefer fixture-managed cleanup: + +```typescript +test("test with cleanup", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, "test session", async (session) => { + await gotoSession(session.id) + // Test code... + }) // Auto-deletes session +}) +``` + +- Prefer `withSession(...)` for temp sessions +- In `withProject(...)` tests that create sessions or extra workspaces, call `trackSession(sessionID, directory?)` and `trackDirectory(directory)` +- This lets fixture teardown abort, wait for idle, and clean up safely under CI concurrency +- Avoid calling `sdk.session.delete(...)` directly + +### Timeouts + +Default: 60s per test, 10s per assertion. Override when needed: + +```typescript +test.setTimeout(120_000) // For long LLM operations +test("slow test", async () => { + await expect.poll(() => check(), { timeout: 90_000 }).toBe(true) +}) +``` + +### Selectors + +Use `data-component`, `data-action`, or semantic roles: + +```typescript +// ✅ Good +await page.locator('[data-component="prompt-input"]').click() +await page.getByRole("button", { name: "Open settings" }).click() + +// ❌ Bad +await page.locator(".css-class-name").click() +await page.locator("#id-name").click() +``` + +### Keyboard Shortcuts + +Use `modKey` for cross-platform compatibility: + +```typescript +import { modKey } from "../utils" + +await page.keyboard.press(`${modKey}+B`) // Toggle sidebar +await page.keyboard.press(`${modKey}+Comma`) // Open settings +``` + +### Terminal Tests + +- In terminal tests, type through the browser. Do not write to the PTY through the SDK. +- Use `waitTerminalReady(page, { term? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`. +- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles. +- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks. + +## Writing New Tests + +1. Choose appropriate folder or create new one +2. Import from `../fixtures` +3. Use helper functions from `../actions` and `../selectors` +4. When validating routing, use shared helpers from `../actions`. Workspace URL slugs can be canonicalized on Windows, so assert against canonical or resolved workspace slugs. +5. Clean up any created resources +6. Use specific selectors (avoid CSS classes) +7. Test one feature per test file + +## Local Development + +For UI debugging, use: + +```bash +bun test:e2e:ui +``` + +This opens Playwright's interactive UI for step-through debugging. diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts new file mode 100644 index 00000000000..a56001248d3 --- /dev/null +++ b/packages/app/e2e/actions.ts @@ -0,0 +1,762 @@ +import { expect, type Locator, type Page } from "@playwright/test" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { execSync } from "node:child_process" +import { terminalAttr, type E2EWindow } from "../src/testing/terminal" +import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils" +import { + dropdownMenuTriggerSelector, + dropdownMenuContentSelector, + projectMenuTriggerSelector, + projectCloseMenuSelector, + projectWorkspacesToggleSelector, + titlebarRightSelector, + popoverBodySelector, + listItemSelector, + listItemKeySelector, + listItemKeyStartsWithSelector, + terminalSelector, + workspaceItemSelector, + workspaceMenuTriggerSelector, +} from "./selectors" + +export async function defocus(page: Page) { + await page + .evaluate(() => { + const el = document.activeElement + if (el instanceof HTMLElement) el.blur() + }) + .catch(() => undefined) +} + +async function terminalID(term: Locator) { + const id = await term.getAttribute(terminalAttr) + if (id) return id + throw new Error(`Active terminal missing ${terminalAttr}`) +} + +async function terminalReady(page: Page, term?: Locator) { + const next = term ?? page.locator(terminalSelector).first() + const id = await terminalID(next) + return page.evaluate((id) => { + const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id] + return !!state?.connected && (state.settled ?? 0) > 0 + }, id) +} + +async function terminalHas(page: Page, input: { term?: Locator; token: string }) { + const next = input.term ?? page.locator(terminalSelector).first() + const id = await terminalID(next) + return page.evaluate( + (input) => { + const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[input.id] + return state?.rendered.includes(input.token) ?? false + }, + { id, token: input.token }, + ) +} + +export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) { + const term = input?.term ?? page.locator(terminalSelector).first() + const timeout = input?.timeout ?? 10_000 + await expect(term).toBeVisible() + await expect(term.locator("textarea")).toHaveCount(1) + await expect.poll(() => terminalReady(page, term), { timeout }).toBe(true) +} + +export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) { + const term = input.term ?? page.locator(terminalSelector).first() + const timeout = input.timeout ?? 10_000 + await waitTerminalReady(page, { term, timeout }) + const textarea = term.locator("textarea") + await term.click() + await expect(textarea).toBeFocused() + await page.keyboard.type(input.cmd) + await page.keyboard.press("Enter") + await expect.poll(() => terminalHas(page, { term, token: input.token }), { timeout }).toBe(true) +} + +export async function openPalette(page: Page) { + await defocus(page) + await page.keyboard.press(`${modKey}+P`) + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + await expect(dialog.getByRole("textbox").first()).toBeVisible() + return dialog +} + +export async function closeDialog(page: Page, dialog: Locator) { + await page.keyboard.press("Escape") + const closed = await dialog + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (closed) return + + await page.keyboard.press("Escape") + const closedSecond = await dialog + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (closedSecond) return + + await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) + await expect(dialog).toHaveCount(0) +} + +export async function isSidebarClosed(page: Page) { + const button = page.getByRole("button", { name: /toggle sidebar/i }).first() + await expect(button).toBeVisible() + return (await button.getAttribute("aria-expanded")) !== "true" +} + +export async function toggleSidebar(page: Page) { + await defocus(page) + await page.keyboard.press(`${modKey}+B`) +} + +export async function openSidebar(page: Page) { + if (!(await isSidebarClosed(page))) return + + const button = page.getByRole("button", { name: /toggle sidebar/i }).first() + await button.click() + + const opened = await expect(button) + .toHaveAttribute("aria-expanded", "true", { timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (opened) return + + await toggleSidebar(page) + await expect(button).toHaveAttribute("aria-expanded", "true") +} + +export async function closeSidebar(page: Page) { + if (await isSidebarClosed(page)) return + + const button = page.getByRole("button", { name: /toggle sidebar/i }).first() + await button.click() + + const closed = await expect(button) + .toHaveAttribute("aria-expanded", "false", { timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (closed) return + + await toggleSidebar(page) + await expect(button).toHaveAttribute("aria-expanded", "false") +} + +export async function openSettings(page: Page) { + await defocus(page) + + const dialog = page.getByRole("dialog") + await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined) + + const opened = await dialog + .waitFor({ state: "visible", timeout: 3000 }) + .then(() => true) + .catch(() => false) + + if (opened) return dialog + + await page.getByRole("button", { name: "Settings" }).first().click() + await expect(dialog).toBeVisible() + return dialog +} + +export async function seedProjects(page: Page, input: { directory: string; extra?: string[] }) { + await page.addInitScript( + (args: { directory: string; serverUrl: string; extra: string[] }) => { + const key = "opencode.global.dat:server" + const raw = localStorage.getItem(key) + const parsed = (() => { + if (!raw) return undefined + try { + return JSON.parse(raw) as unknown + } catch { + return undefined + } + })() + + const store = parsed && typeof parsed === "object" ? (parsed as Record) : {} + const list = Array.isArray(store.list) ? store.list : [] + const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {} + const projects = store.projects && typeof store.projects === "object" ? store.projects : {} + const nextProjects = { ...(projects as Record) } + + const add = (origin: string, directory: string) => { + const current = nextProjects[origin] + const items = Array.isArray(current) ? current : [] + const existing = items.filter( + (p): p is { worktree: string; expanded?: boolean } => + !!p && + typeof p === "object" && + "worktree" in p && + typeof (p as { worktree?: unknown }).worktree === "string", + ) + + if (existing.some((p) => p.worktree === directory)) return + nextProjects[origin] = [{ worktree: directory, expanded: true }, ...existing] + } + + const directories = [args.directory, ...args.extra] + for (const directory of directories) { + add("local", directory) + add(args.serverUrl, directory) + } + + localStorage.setItem( + key, + JSON.stringify({ + list, + projects: nextProjects, + lastProject, + }), + ) + }, + { directory: input.directory, serverUrl, extra: input.extra ?? [] }, + ) +} + +export async function createTestProject() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-")) + + await fs.writeFile(path.join(root, "README.md"), "# e2e\n") + + execSync("git init", { cwd: root, stdio: "ignore" }) + execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" }) + execSync("git add -A", { cwd: root, stdio: "ignore" }) + execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', { + cwd: root, + stdio: "ignore", + }) + + return resolveDirectory(root) +} + +export async function cleanupTestProject(directory: string) { + try { + execSync("git fsmonitor--daemon stop", { cwd: directory, stdio: "ignore" }) + } catch {} + await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined) +} + +export function slugFromUrl(url: string) { + return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? "" +} + +export async function waitSlug(page: Page, skip: string[] = []) { + let prev = "" + let next = "" + await expect + .poll( + () => { + const slug = slugFromUrl(page.url()) + if (!slug) return "" + if (skip.includes(slug)) return "" + if (slug !== prev) { + prev = slug + next = "" + return "" + } + next = slug + return slug + }, + { timeout: 45_000 }, + ) + .not.toBe("") + return next +} + +export function sessionIDFromUrl(url: string) { + const match = /\/session\/([^/?#]+)/.exec(url) + return match?.[1] +} + +export async function hoverSessionItem(page: Page, sessionID: string) { + const sessionEl = page.locator(`[data-session-id="${sessionID}"]`).last() + await expect(sessionEl).toBeVisible() + await sessionEl.hover() + return sessionEl +} + +export async function openSessionMoreMenu(page: Page, sessionID: string) { + await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`)) + + const scroller = page.locator(".scroll-view__viewport").first() + await expect(scroller).toBeVisible() + await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 }) + + const menu = page + .locator(dropdownMenuContentSelector) + .filter({ has: page.getByRole("menuitem", { name: /rename/i }) }) + .filter({ has: page.getByRole("menuitem", { name: /archive/i }) }) + .filter({ has: page.getByRole("menuitem", { name: /delete/i }) }) + .first() + + const opened = await menu + .isVisible() + .then((x) => x) + .catch(() => false) + + if (opened) return menu + + const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first() + await expect(menuTrigger).toBeVisible() + await menuTrigger.click() + + await expect(menu).toBeVisible() + return menu +} + +export async function clickMenuItem(menu: Locator, itemName: string | RegExp, options?: { force?: boolean }) { + const item = menu.getByRole("menuitem").filter({ hasText: itemName }).first() + await expect(item).toBeVisible() + await item.click({ force: options?.force }) +} + +export async function confirmDialog(page: Page, buttonName: string | RegExp) { + const dialog = page.getByRole("dialog").first() + await expect(dialog).toBeVisible() + + const button = dialog.getByRole("button").filter({ hasText: buttonName }).first() + await expect(button).toBeVisible() + await button.click() +} + +export async function openSharePopover(page: Page) { + const rightSection = page.locator(titlebarRightSelector) + const shareButton = rightSection.getByRole("button", { name: "Share" }).first() + await expect(shareButton).toBeVisible() + + const popoverBody = page + .locator(popoverBodySelector) + .filter({ has: page.getByRole("button", { name: /^(Publish|Unpublish)$/ }) }) + .first() + + const opened = await popoverBody + .isVisible() + .then((x) => x) + .catch(() => false) + + if (!opened) { + await shareButton.click() + await expect(popoverBody).toBeVisible() + } + return { rightSection, popoverBody } +} + +export async function clickPopoverButton(page: Page, buttonName: string | RegExp) { + const button = page.getByRole("button").filter({ hasText: buttonName }).first() + await expect(button).toBeVisible() + await button.click() +} + +export async function clickListItem( + container: Locator | Page, + filter: string | RegExp | { key?: string; text?: string | RegExp; keyStartsWith?: string }, +): Promise { + let item: Locator + + if (typeof filter === "string" || filter instanceof RegExp) { + item = container.locator(listItemSelector).filter({ hasText: filter }).first() + } else if (filter.keyStartsWith) { + item = container.locator(listItemKeyStartsWithSelector(filter.keyStartsWith)).first() + } else if (filter.key) { + item = container.locator(listItemKeySelector(filter.key)).first() + } else if (filter.text) { + item = container.locator(listItemSelector).filter({ hasText: filter.text }).first() + } else { + throw new Error("Invalid filter provided to clickListItem") + } + + await expect(item).toBeVisible() + await item.click() + return item +} + +async function status(sdk: ReturnType, sessionID: string) { + const data = await sdk.session + .status() + .then((x) => x.data ?? {}) + .catch(() => undefined) + return data?.[sessionID] +} + +async function stable(sdk: ReturnType, sessionID: string, timeout = 10_000) { + let prev = "" + await expect + .poll( + async () => { + const info = await sdk.session + .get({ sessionID }) + .then((x) => x.data) + .catch(() => undefined) + if (!info) return true + const next = `${info.title}:${info.time.updated ?? info.time.created}` + if (next !== prev) { + prev = next + return false + } + return true + }, + { timeout }, + ) + .toBe(true) +} + +export async function waitSessionIdle(sdk: ReturnType, sessionID: string, timeout = 30_000) { + await expect.poll(() => status(sdk, sessionID).then((x) => !x || x.type === "idle"), { timeout }).toBe(true) +} + +export async function cleanupSession(input: { + sessionID: string + directory?: string + sdk?: ReturnType +}) { + const sdk = input.sdk ?? (input.directory ? createSdk(input.directory) : undefined) + if (!sdk) throw new Error("cleanupSession requires sdk or directory") + await waitSessionIdle(sdk, input.sessionID, 5_000).catch(() => undefined) + const current = await status(sdk, input.sessionID).catch(() => undefined) + if (current && current.type !== "idle") { + await sdk.session.abort({ sessionID: input.sessionID }).catch(() => undefined) + await waitSessionIdle(sdk, input.sessionID).catch(() => undefined) + } + await stable(sdk, input.sessionID).catch(() => undefined) + await sdk.session.delete({ sessionID: input.sessionID }).catch(() => undefined) +} + +export async function withSession( + sdk: ReturnType, + title: string, + callback: (session: { id: string; title: string }) => Promise, +): Promise { + const session = await sdk.session.create({ title }).then((r) => r.data) + if (!session?.id) throw new Error("Session create did not return an id") + + try { + return await callback(session) + } finally { + await cleanupSession({ sdk, sessionID: session.id }) + } +} + +const seedSystem = [ + "You are seeding deterministic e2e UI state.", + "Follow the user's instruction exactly.", + "When asked to call a tool, call exactly that tool exactly once with the exact JSON input.", + "Do not call any extra tools.", +].join(" ") + +const wait = async (input: { probe: () => Promise; timeout?: number }) => { + const timeout = input.timeout ?? 30_000 + const end = Date.now() + timeout + while (Date.now() < end) { + const value = await input.probe() + if (value !== undefined) return value + await new Promise((resolve) => setTimeout(resolve, 250)) + } +} + +const seed = async (input: { + sessionID: string + prompt: string + sdk: ReturnType + probe: () => Promise + timeout?: number + attempts?: number +}) => { + for (let i = 0; i < (input.attempts ?? 2); i++) { + await input.sdk.session.promptAsync({ + sessionID: input.sessionID, + agent: "build", + system: seedSystem, + parts: [{ type: "text", text: input.prompt }], + }) + const value = await wait({ probe: input.probe, timeout: input.timeout }) + if (value !== undefined) return value + } +} + +export async function seedSessionQuestion( + sdk: ReturnType, + input: { + sessionID: string + questions: Array<{ + header: string + question: string + options: Array<{ label: string; description: string }> + multiple?: boolean + custom?: boolean + }> + }, +) { + const first = input.questions[0] + if (!first) throw new Error("Question seed requires at least one question") + + const text = [ + "Your only valid response is one question tool call.", + `Use this JSON input: ${JSON.stringify({ questions: input.questions })}`, + "Do not output plain text.", + "After calling the tool, wait for the user response.", + ].join("\n") + + const result = await seed({ + sdk, + sessionID: input.sessionID, + prompt: text, + timeout: 30_000, + probe: async () => { + const list = await sdk.question.list().then((x) => x.data ?? []) + return list.find((item) => item.sessionID === input.sessionID && item.questions[0]?.header === first.header) + }, + }) + + if (!result) throw new Error("Timed out seeding question request") + return { id: result.id } +} + +export async function seedSessionPermission( + sdk: ReturnType, + input: { + sessionID: string + permission: string + patterns: string[] + description?: string + }, +) { + const text = [ + "Your only valid response is one bash tool call.", + `Use this JSON input: ${JSON.stringify({ + command: input.patterns[0] ? `ls ${JSON.stringify(input.patterns[0])}` : "pwd", + workdir: "/", + description: input.description ?? `seed ${input.permission} permission request`, + })}`, + "Do not output plain text.", + ].join("\n") + + const result = await seed({ + sdk, + sessionID: input.sessionID, + prompt: text, + timeout: 30_000, + probe: async () => { + const list = await sdk.permission.list().then((x) => x.data ?? []) + return list.find((item) => item.sessionID === input.sessionID) + }, + }) + + if (!result) throw new Error("Timed out seeding permission request") + return { id: result.id } +} + +export async function seedSessionTask( + sdk: ReturnType, + input: { + sessionID: string + description: string + prompt: string + subagentType?: string + }, +) { + const text = [ + "Your only valid response is one task tool call.", + `Use this JSON input: ${JSON.stringify({ + description: input.description, + prompt: input.prompt, + subagent_type: input.subagentType ?? "general", + })}`, + "Do not output plain text.", + "Wait for the task to start and return the child session id.", + ].join("\n") + + const result = await seed({ + sdk, + sessionID: input.sessionID, + prompt: text, + timeout: 90_000, + probe: async () => { + const messages = await sdk.session.messages({ sessionID: input.sessionID, limit: 50 }).then((x) => x.data ?? []) + const part = messages + .flatMap((message) => message.parts) + .find((part) => { + if (part.type !== "tool" || part.tool !== "task") return false + if (part.state.input?.description !== input.description) return false + return typeof part.state.metadata?.sessionId === "string" && part.state.metadata.sessionId.length > 0 + }) + + if (!part) return + const id = part.state.metadata?.sessionId + if (typeof id !== "string" || !id) return + const child = await sdk.session + .get({ sessionID: id }) + .then((x) => x.data) + .catch(() => undefined) + if (!child?.id) return + return { sessionID: id } + }, + }) + + if (!result) throw new Error("Timed out seeding task tool") + return result +} + +export async function seedSessionTodos( + sdk: ReturnType, + input: { + sessionID: string + todos: Array<{ content: string; status: string; priority: string }> + }, +) { + const text = [ + "Your only valid response is one todowrite tool call.", + `Use this JSON input: ${JSON.stringify({ todos: input.todos })}`, + "Do not output plain text.", + ].join("\n") + const target = JSON.stringify(input.todos) + + const result = await seed({ + sdk, + sessionID: input.sessionID, + prompt: text, + timeout: 30_000, + probe: async () => { + const todos = await sdk.session.todo({ sessionID: input.sessionID }).then((x) => x.data ?? []) + if (JSON.stringify(todos) !== target) return + return true + }, + }) + + if (!result) throw new Error("Timed out seeding todos") + return true +} + +export async function clearSessionDockSeed(sdk: ReturnType, sessionID: string) { + const [questions, permissions] = await Promise.all([ + sdk.question.list().then((x) => x.data ?? []), + sdk.permission.list().then((x) => x.data ?? []), + ]) + + await Promise.all([ + ...questions + .filter((item) => item.sessionID === sessionID) + .map((item) => sdk.question.reject({ requestID: item.id }).catch(() => undefined)), + ...permissions + .filter((item) => item.sessionID === sessionID) + .map((item) => sdk.permission.reply({ requestID: item.id, reply: "reject" }).catch(() => undefined)), + ]) + + return true +} + +export async function openStatusPopover(page: Page) { + await defocus(page) + + const rightSection = page.locator(titlebarRightSelector) + const trigger = rightSection.getByRole("button", { name: /status/i }).first() + + const popoverBody = page.locator(popoverBodySelector).filter({ has: page.locator('[data-component="tabs"]') }) + + const opened = await popoverBody + .isVisible() + .then((x) => x) + .catch(() => false) + + if (!opened) { + await expect(trigger).toBeVisible() + await trigger.click() + await expect(popoverBody).toBeVisible() + } + + return { rightSection, popoverBody } +} + +export async function openProjectMenu(page: Page, projectSlug: string) { + const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first() + await expect(trigger).toHaveCount(1) + + const menu = page + .locator(dropdownMenuContentSelector) + .filter({ has: page.locator(projectCloseMenuSelector(projectSlug)) }) + .first() + const close = menu.locator(projectCloseMenuSelector(projectSlug)).first() + + const clicked = await trigger + .click({ timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (clicked) { + const opened = await menu + .waitFor({ state: "visible", timeout: 1500 }) + .then(() => true) + .catch(() => false) + if (opened) { + await expect(close).toBeVisible() + return menu + } + } + + await trigger.focus() + await page.keyboard.press("Enter") + + const opened = await menu + .waitFor({ state: "visible", timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (opened) { + await expect(close).toBeVisible() + return menu + } + + throw new Error(`Failed to open project menu: ${projectSlug}`) +} + +export async function setWorkspacesEnabled(page: Page, projectSlug: string, enabled: boolean) { + const current = await page + .getByRole("button", { name: "New workspace" }) + .first() + .isVisible() + .then((x) => x) + .catch(() => false) + + if (current === enabled) return + + const flip = async (timeout?: number) => { + const menu = await openProjectMenu(page, projectSlug) + const toggle = menu.locator(projectWorkspacesToggleSelector(projectSlug)).first() + await expect(toggle).toBeVisible() + return toggle.click({ force: true, timeout }) + } + + const flipped = await flip(1500) + .then(() => true) + .catch(() => false) + + if (!flipped) await flip() + + const expected = enabled ? "New workspace" : "New session" + await expect(page.getByRole("button", { name: expected }).first()).toBeVisible() +} + +export async function openWorkspaceMenu(page: Page, workspaceSlug: string) { + const item = page.locator(workspaceItemSelector(workspaceSlug)).first() + await expect(item).toBeVisible() + await item.hover() + + const trigger = page.locator(workspaceMenuTriggerSelector(workspaceSlug)).first() + await expect(trigger).toBeVisible() + await trigger.click({ force: true }) + + const menu = page.locator(dropdownMenuContentSelector).first() + await expect(menu).toBeVisible() + return menu +} diff --git a/packages/app/e2e/app/home.spec.ts b/packages/app/e2e/app/home.spec.ts new file mode 100644 index 00000000000..a3cedf7cb6f --- /dev/null +++ b/packages/app/e2e/app/home.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "../fixtures" +import { serverNamePattern } from "../utils" + +test("home renders and shows core entrypoints", async ({ page }) => { + await page.goto("/") + + await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible() + await expect(page.getByRole("button", { name: serverNamePattern })).toBeVisible() +}) + +test("server picker dialog opens from home", async ({ page }) => { + await page.goto("/") + + const trigger = page.getByRole("button", { name: serverNamePattern }) + await expect(trigger).toBeVisible() + await trigger.click() + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + await expect(dialog.getByRole("textbox").first()).toBeVisible() +}) diff --git a/packages/app/e2e/app/navigation.spec.ts b/packages/app/e2e/app/navigation.spec.ts new file mode 100644 index 00000000000..328c950df36 --- /dev/null +++ b/packages/app/e2e/app/navigation.spec.ts @@ -0,0 +1,10 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { dirPath } from "../utils" + +test("project route redirects to /session", async ({ page, directory, slug }) => { + await page.goto(dirPath(directory)) + + await expect(page).toHaveURL(new RegExp(`/${slug}/session`)) + await expect(page.locator(promptSelector)).toBeVisible() +}) diff --git a/packages/app/e2e/app/palette.spec.ts b/packages/app/e2e/app/palette.spec.ts new file mode 100644 index 00000000000..3ccfd7a9250 --- /dev/null +++ b/packages/app/e2e/app/palette.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from "../fixtures" +import { openPalette } from "../actions" + +test("search palette opens and closes", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openPalette(page) + + await page.keyboard.press("Escape") + await expect(dialog).toHaveCount(0) +}) diff --git a/packages/app/e2e/app/server-default.spec.ts b/packages/app/e2e/app/server-default.spec.ts new file mode 100644 index 00000000000..2c63130f670 --- /dev/null +++ b/packages/app/e2e/app/server-default.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from "../fixtures" +import { serverNamePattern, serverUrls } from "../utils" +import { closeDialog, clickMenuItem } from "../actions" + +const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" + +test("can set a default server on web", async ({ page, gotoSession }) => { + await page.addInitScript((key: string) => { + try { + localStorage.removeItem(key) + } catch { + return + } + }, DEFAULT_SERVER_URL_KEY) + + await gotoSession() + + const status = page.getByRole("button", { name: "Status" }) + await expect(status).toBeVisible() + const popover = page.locator('[data-component="popover-content"]').filter({ hasText: "Manage servers" }) + + const ensurePopoverOpen = async () => { + if (await popover.isVisible()) return + await status.click() + await expect(popover).toBeVisible() + } + + await ensurePopoverOpen() + await popover.getByRole("button", { name: "Manage servers" }).click() + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + + await expect(dialog.getByText(serverNamePattern).first()).toBeVisible() + + const menuTrigger = dialog.locator('[data-slot="dropdown-menu-trigger"]').first() + await expect(menuTrigger).toBeVisible() + await menuTrigger.click({ force: true }) + + const menu = page.locator('[data-component="dropdown-menu-content"]').first() + await expect(menu).toBeVisible() + await clickMenuItem(menu, /set as default/i) + + await expect + .poll(async () => + serverUrls.includes((await page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)) ?? ""), + ) + .toBe(true) + await expect(dialog.getByText("Default", { exact: true })).toBeVisible() + + await closeDialog(page, dialog) + + await ensurePopoverOpen() + + const serverRow = popover.locator("button").filter({ hasText: serverNamePattern }).first() + await expect(serverRow).toBeVisible() + await expect(serverRow.getByText("Default", { exact: true })).toBeVisible() +}) diff --git a/packages/app/e2e/app/session.spec.ts b/packages/app/e2e/app/session.spec.ts new file mode 100644 index 00000000000..c7fdfdc542b --- /dev/null +++ b/packages/app/e2e/app/session.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { withSession } from "../actions" + +test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => { + const title = `e2e smoke ${Date.now()}` + + await withSession(sdk, title, async (session) => { + await gotoSession(session.id) + + const prompt = page.locator(promptSelector) + await prompt.click() + await page.keyboard.type("hello from e2e") + await expect(prompt).toContainText("hello from e2e") + }) +}) diff --git a/packages/app/e2e/app/titlebar-history.spec.ts b/packages/app/e2e/app/titlebar-history.spec.ts new file mode 100644 index 00000000000..a4592ff1dbc --- /dev/null +++ b/packages/app/e2e/app/titlebar-history.spec.ts @@ -0,0 +1,120 @@ +import { test, expect } from "../fixtures" +import { defocus, openSidebar, withSession } from "../actions" +import { promptSelector } from "../selectors" +import { modKey } from "../utils" + +test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const stamp = Date.now() + + await withSession(sdk, `e2e titlebar history 1 ${stamp}`, async (one) => { + await withSession(sdk, `e2e titlebar history 2 ${stamp}`, async (two) => { + await gotoSession(one.id) + + await openSidebar(page) + + const link = page.locator(`[data-session-id="${two.id}"] a`).first() + await expect(link).toBeVisible() + await link.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + const back = page.getByRole("button", { name: "Back" }) + const forward = page.getByRole("button", { name: "Forward" }) + + await expect(back).toBeVisible() + await expect(back).toBeEnabled() + await back.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await expect(forward).toBeVisible() + await expect(forward).toBeEnabled() + await forward.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + }) + }) +}) + +test("titlebar forward is cleared after branching history from sidebar", async ({ page, slug, sdk, gotoSession }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const stamp = Date.now() + + await withSession(sdk, `e2e titlebar history a ${stamp}`, async (a) => { + await withSession(sdk, `e2e titlebar history b ${stamp}`, async (b) => { + await withSession(sdk, `e2e titlebar history c ${stamp}`, async (c) => { + await gotoSession(a.id) + + await openSidebar(page) + + const second = page.locator(`[data-session-id="${b.id}"] a`).first() + await expect(second).toBeVisible() + await second.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${b.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + const back = page.getByRole("button", { name: "Back" }) + const forward = page.getByRole("button", { name: "Forward" }) + + await expect(back).toBeVisible() + await expect(back).toBeEnabled() + await back.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${a.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await openSidebar(page) + + const third = page.locator(`[data-session-id="${c.id}"] a`).first() + await expect(third).toBeVisible() + await third.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${c.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await expect(forward).toBeVisible() + await expect(forward).toBeDisabled() + }) + }) + }) +}) + +test("keyboard shortcuts navigate titlebar history", async ({ page, slug, sdk, gotoSession }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const stamp = Date.now() + + await withSession(sdk, `e2e titlebar shortcuts 1 ${stamp}`, async (one) => { + await withSession(sdk, `e2e titlebar shortcuts 2 ${stamp}`, async (two) => { + await gotoSession(one.id) + + await openSidebar(page) + + const link = page.locator(`[data-session-id="${two.id}"] a`).first() + await expect(link).toBeVisible() + await link.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await defocus(page) + await page.keyboard.press(`${modKey}+[`) + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await defocus(page) + await page.keyboard.press(`${modKey}+]`) + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + }) + }) +}) diff --git a/packages/app/e2e/commands/input-focus.spec.ts b/packages/app/e2e/commands/input-focus.spec.ts new file mode 100644 index 00000000000..4ba1aa3e690 --- /dev/null +++ b/packages/app/e2e/commands/input-focus.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" + +test("ctrl+l focuses the prompt", async ({ page, gotoSession }) => { + await gotoSession() + + const prompt = page.locator(promptSelector) + await expect(prompt).toBeVisible() + + await page.locator("main").click({ position: { x: 5, y: 5 } }) + await expect(prompt).not.toBeFocused() + + await page.keyboard.press("Control+L") + await expect(prompt).toBeFocused() +}) diff --git a/packages/app/e2e/commands/panels.spec.ts b/packages/app/e2e/commands/panels.spec.ts new file mode 100644 index 00000000000..7e5d7bd6e7a --- /dev/null +++ b/packages/app/e2e/commands/panels.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "../fixtures" +import { modKey } from "../utils" + +const expanded = async (el: { getAttribute: (name: string) => Promise }) => { + const value = await el.getAttribute("aria-expanded") + if (value !== "true" && value !== "false") throw new Error(`Expected aria-expanded to be true|false, got: ${value}`) + return value === "true" +} + +test("review panel can be toggled via keybind", async ({ page, gotoSession }) => { + await gotoSession() + + const reviewPanel = page.locator("#review-panel") + + const treeToggle = page.getByRole("button", { name: "Toggle file tree" }).first() + await expect(treeToggle).toBeVisible() + if (await expanded(treeToggle)) await treeToggle.click() + await expect(treeToggle).toHaveAttribute("aria-expanded", "false") + + const reviewToggle = page.getByRole("button", { name: "Toggle review" }).first() + await expect(reviewToggle).toBeVisible() + if (await expanded(reviewToggle)) await reviewToggle.click() + await expect(reviewToggle).toHaveAttribute("aria-expanded", "false") + await expect(reviewPanel).toHaveAttribute("aria-hidden", "true") + + await page.keyboard.press(`${modKey}+Shift+R`) + await expect(reviewToggle).toHaveAttribute("aria-expanded", "true") + await expect(reviewPanel).toHaveAttribute("aria-hidden", "false") + + await page.keyboard.press(`${modKey}+Shift+R`) + await expect(reviewToggle).toHaveAttribute("aria-expanded", "false") + await expect(reviewPanel).toHaveAttribute("aria-hidden", "true") +}) diff --git a/packages/app/e2e/commands/tab-close.spec.ts b/packages/app/e2e/commands/tab-close.spec.ts new file mode 100644 index 00000000000..981ee561e2b --- /dev/null +++ b/packages/app/e2e/commands/tab-close.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { modKey } from "../utils" + +test("mod+w closes the active file tab", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/open") + await expect(page.locator('[data-slash-id="file.open"]').first()).toBeVisible() + await page.keyboard.press("Enter") + + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() + + await dialog.getByRole("textbox").first().fill("package.json") + const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first() + await expect(item).toBeVisible({ timeout: 30_000 }) + await item.click() + await expect(dialog).toHaveCount(0) + + const tab = page.getByRole("tab", { name: "package.json" }).first() + await expect(tab).toBeVisible() + await tab.click() + await expect(tab).toHaveAttribute("aria-selected", "true") + + await page.keyboard.press(`${modKey}+W`) + await expect(page.getByRole("tab", { name: "package.json" })).toHaveCount(0) +}) diff --git a/packages/app/e2e/context.spec.ts b/packages/app/e2e/context.spec.ts deleted file mode 100644 index beabd2eb7dd..00000000000 --- a/packages/app/e2e/context.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => { - const title = `e2e smoke context ${Date.now()}` - const created = await sdk.session.create({ title }).then((r) => r.data) - - if (!created?.id) throw new Error("Session create did not return an id") - const sessionID = created.id - - try { - await sdk.session.promptAsync({ - sessionID, - noReply: true, - parts: [ - { - type: "text", - text: "seed context", - }, - ], - }) - - await expect - .poll(async () => { - const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? []) - return messages.length - }) - .toBeGreaterThan(0) - - await gotoSession(sessionID) - - const contextButton = page - .locator('[data-component="button"]') - .filter({ has: page.locator('[data-component="progress-circle"]').first() }) - .first() - - await expect(contextButton).toBeVisible() - await contextButton.click() - - const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') - await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible() - } finally { - await sdk.session.delete({ sessionID }).catch(() => undefined) - } -}) diff --git a/packages/app/e2e/file-open.spec.ts b/packages/app/e2e/file-open.spec.ts deleted file mode 100644 index fb7104b6b05..00000000000 --- a/packages/app/e2e/file-open.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("can open a file tab from the search palette", async ({ page, gotoSession }) => { - await gotoSession() - - await page.keyboard.press(`${modKey}+P`) - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - - const input = dialog.getByRole("textbox").first() - await input.fill("package.json") - - const fileItem = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first() - await expect(fileItem).toBeVisible() - await fileItem.click() - - await expect(dialog).toHaveCount(0) - - const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') - await expect(tabs.locator('[data-slot="tabs-trigger"]').first()).toBeVisible() -}) diff --git a/packages/app/e2e/file-viewer.spec.ts b/packages/app/e2e/file-viewer.spec.ts deleted file mode 100644 index 1e0f8a6f23a..00000000000 --- a/packages/app/e2e/file-viewer.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("smoke file viewer renders real file content", async ({ page, gotoSession }) => { - await gotoSession() - - const sep = process.platform === "win32" ? "\\" : "/" - const file = ["packages", "app", "package.json"].join(sep) - - await page.keyboard.press(`${modKey}+P`) - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - - const input = dialog.getByRole("textbox").first() - await input.fill(file) - - const fileItem = dialog - .locator( - '[data-slot="list-item"][data-key^="file:"][data-key*="packages"][data-key*="app"][data-key$="package.json"]', - ) - .first() - await expect(fileItem).toBeVisible() - await fileItem.click() - - await expect(dialog).toHaveCount(0) - - const tab = page.getByRole("tab", { name: "package.json" }) - await expect(tab).toBeVisible() - await tab.click() - - const code = page.locator('[data-component="code"]').first() - await expect(code).toBeVisible() - await expect(code.getByText("@opencode-ai/app")).toBeVisible() -}) diff --git a/packages/app/e2e/files/file-open.spec.ts b/packages/app/e2e/files/file-open.spec.ts new file mode 100644 index 00000000000..abb28242da5 --- /dev/null +++ b/packages/app/e2e/files/file-open.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" + +test("can open a file tab from the search palette", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/open") + + const command = page.locator('[data-slash-id="file.open"]').first() + await expect(command).toBeVisible() + await page.keyboard.press("Enter") + + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() + + const input = dialog.getByRole("textbox").first() + await input.fill("package.json") + + const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first() + await expect(item).toBeVisible({ timeout: 30_000 }) + await item.click() + + await expect(dialog).toHaveCount(0) + + const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') + await expect(tabs.locator('[data-slot="tabs-trigger"]').first()).toBeVisible() +}) diff --git a/packages/app/e2e/files/file-tree.spec.ts b/packages/app/e2e/files/file-tree.spec.ts new file mode 100644 index 00000000000..a5872bdf87c --- /dev/null +++ b/packages/app/e2e/files/file-tree.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from "../fixtures" + +test("file tree can expand folders and open a file", async ({ page, gotoSession }) => { + await gotoSession() + + const toggle = page.getByRole("button", { name: "Toggle file tree" }) + const panel = page.locator("#file-tree-panel") + const treeTabs = panel.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]') + + await expect(toggle).toBeVisible() + if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click() + await expect(toggle).toHaveAttribute("aria-expanded", "true") + await expect(panel).toBeVisible() + await expect(treeTabs).toBeVisible() + + const allTab = treeTabs.getByRole("tab", { name: /^all files$/i }) + await expect(allTab).toBeVisible() + await allTab.click() + await expect(allTab).toHaveAttribute("aria-selected", "true") + + const tree = treeTabs.locator('[data-slot="tabs-content"]:not([hidden])') + await expect(tree).toBeVisible() + + const expand = async (name: string) => { + const folder = tree.getByRole("button", { name, exact: true }).first() + await expect(folder).toBeVisible() + await expect(folder).toHaveAttribute("aria-expanded", /true|false/) + if ((await folder.getAttribute("aria-expanded")) === "false") await folder.click() + await expect(folder).toHaveAttribute("aria-expanded", "true") + } + + await expand("packages") + await expand("app") + await expand("src") + await expand("components") + + const file = tree.getByRole("button", { name: "file-tree.tsx", exact: true }).first() + await expect(file).toBeVisible() + await file.click() + + const tab = page.getByRole("tab", { name: "file-tree.tsx" }) + await expect(tab).toBeVisible() + await tab.click() + await expect(tab).toHaveAttribute("aria-selected", "true") + + await toggle.click() + await expect(toggle).toHaveAttribute("aria-expanded", "false") + + await toggle.click() + await expect(toggle).toHaveAttribute("aria-expanded", "true") + await expect(allTab).toHaveAttribute("aria-selected", "true") + + const viewer = page.locator('[data-component="file"][data-mode="text"]').first() + await expect(viewer).toBeVisible() + await expect(viewer).toContainText("export default function FileTree") +}) diff --git a/packages/app/e2e/files/file-viewer.spec.ts b/packages/app/e2e/files/file-viewer.spec.ts new file mode 100644 index 00000000000..49fe1baa138 --- /dev/null +++ b/packages/app/e2e/files/file-viewer.spec.ts @@ -0,0 +1,156 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { modKey } from "../utils" + +test("smoke file viewer renders real file content", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/open") + + const command = page.locator('[data-slash-id="file.open"]').first() + await expect(command).toBeVisible() + await page.keyboard.press("Enter") + + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() + + const input = dialog.getByRole("textbox").first() + await input.fill("package.json") + + const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]') + let index = -1 + await expect + .poll( + async () => { + const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? "")) + index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, ""))) + return index >= 0 + }, + { timeout: 30_000 }, + ) + .toBe(true) + + const item = items.nth(index) + await expect(item).toBeVisible() + await item.click() + + await expect(dialog).toHaveCount(0) + + const tab = page.getByRole("tab", { name: "package.json" }) + await expect(tab).toBeVisible() + await tab.click() + + const viewer = page.locator('[data-component="file"][data-mode="text"]').first() + await expect(viewer).toBeVisible() + await expect(viewer.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible() +}) + +test("cmd+f opens text viewer search while prompt is focused", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/open") + + const command = page.locator('[data-slash-id="file.open"]').first() + await expect(command).toBeVisible() + await page.keyboard.press("Enter") + + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() + + const input = dialog.getByRole("textbox").first() + await input.fill("package.json") + + const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]') + let index = -1 + await expect + .poll( + async () => { + const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? "")) + index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, ""))) + return index >= 0 + }, + { timeout: 30_000 }, + ) + .toBe(true) + + const item = items.nth(index) + await expect(item).toBeVisible() + await item.click() + + await expect(dialog).toHaveCount(0) + + const tab = page.getByRole("tab", { name: "package.json" }) + await expect(tab).toBeVisible() + await tab.click() + + const viewer = page.locator('[data-component="file"][data-mode="text"]').first() + await expect(viewer).toBeVisible() + + await page.locator(promptSelector).click() + await page.keyboard.press(`${modKey}+f`) + + const findInput = page.getByPlaceholder("Find") + await expect(findInput).toBeVisible() + await expect(findInput).toBeFocused() +}) + +test("cmd+f opens text viewer search while prompt is not focused", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/open") + + const command = page.locator('[data-slash-id="file.open"]').first() + await expect(command).toBeVisible() + await page.keyboard.press("Enter") + + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() + + const input = dialog.getByRole("textbox").first() + await input.fill("package.json") + + const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]') + let index = -1 + await expect + .poll( + async () => { + const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? "")) + index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, ""))) + return index >= 0 + }, + { timeout: 30_000 }, + ) + .toBe(true) + + const item = items.nth(index) + await expect(item).toBeVisible() + await item.click() + + await expect(dialog).toHaveCount(0) + + const tab = page.getByRole("tab", { name: "package.json" }) + await expect(tab).toBeVisible() + await tab.click() + + const viewer = page.locator('[data-component="file"][data-mode="text"]').first() + await expect(viewer).toBeVisible() + + await viewer.click() + await page.keyboard.press(`${modKey}+f`) + + const findInput = page.getByPlaceholder("Find") + await expect(findInput).toBeVisible() + await expect(findInput).toBeFocused() +}) diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/fixtures.ts index 721d60049ce..cf59eeb4761 100644 --- a/packages/app/e2e/fixtures.ts +++ b/packages/app/e2e/fixtures.ts @@ -1,9 +1,24 @@ -import { test as base, expect } from "@playwright/test" -import { createSdk, dirSlug, getWorktree, promptSelector, sessionPath } from "./utils" +import { test as base, expect, type Page } from "@playwright/test" +import type { E2EWindow } from "../src/testing/terminal" +import { cleanupSession, cleanupTestProject, createTestProject, seedProjects, sessionIDFromUrl } from "./actions" +import { promptSelector } from "./selectors" +import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils" + +export const settingsKey = "settings.v3" type TestFixtures = { sdk: ReturnType gotoSession: (sessionID?: string) => Promise + withProject: ( + callback: (project: { + directory: string + slug: string + gotoSession: (sessionID?: string) => Promise + trackSession: (sessionID: string, directory?: string) => void + trackDirectory: (directory: string) => void + }) => Promise, + options?: { extra?: string[] }, + ) => Promise } type WorkerFixtures = { @@ -29,12 +44,71 @@ export const test = base.extend({ await use(createSdk(directory)) }, gotoSession: async ({ page, directory }, use) => { + await seedStorage(page, { directory }) + const gotoSession = async (sessionID?: string) => { await page.goto(sessionPath(directory, sessionID)) await expect(page.locator(promptSelector)).toBeVisible() } await use(gotoSession) }, + withProject: async ({ page }, use) => { + await use(async (callback, options) => { + const root = await createTestProject() + const slug = dirSlug(root) + const sessions = new Map() + const dirs = new Set() + await seedStorage(page, { directory: root, extra: options?.extra }) + + const gotoSession = async (sessionID?: string) => { + await page.goto(sessionPath(root, sessionID)) + await expect(page.locator(promptSelector)).toBeVisible() + const current = sessionIDFromUrl(page.url()) + if (current) trackSession(current) + } + + const trackSession = (sessionID: string, directory?: string) => { + sessions.set(sessionID, directory ?? root) + } + + const trackDirectory = (directory: string) => { + if (directory !== root) dirs.add(directory) + } + + try { + await gotoSession() + return await callback({ directory: root, slug, gotoSession, trackSession, trackDirectory }) + } finally { + await Promise.allSettled( + Array.from(sessions, ([sessionID, directory]) => cleanupSession({ sessionID, directory })), + ) + await Promise.allSettled(Array.from(dirs, (directory) => cleanupTestProject(directory))) + await cleanupTestProject(root) + } + }) + }, }) +async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) { + await seedProjects(page, input) + await page.addInitScript(() => { + const win = window as E2EWindow + win.__opencode_e2e = { + ...win.__opencode_e2e, + terminal: { + enabled: true, + terminals: {}, + }, + } + localStorage.setItem( + "opencode.global.dat:model", + JSON.stringify({ + recent: [{ providerID: "opencode", modelID: "big-pickle" }], + user: [], + variant: {}, + }), + ) + }) +} + export { expect } diff --git a/packages/app/e2e/home.spec.ts b/packages/app/e2e/home.spec.ts deleted file mode 100644 index c6fb0e3b074..00000000000 --- a/packages/app/e2e/home.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test, expect } from "./fixtures" -import { serverName } from "./utils" - -test("home renders and shows core entrypoints", async ({ page }) => { - await page.goto("/") - - await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible() - await expect(page.getByRole("button", { name: serverName })).toBeVisible() -}) - -test("server picker dialog opens from home", async ({ page }) => { - await page.goto("/") - - const trigger = page.getByRole("button", { name: serverName }) - await expect(trigger).toBeVisible() - await trigger.click() - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - await expect(dialog.getByRole("textbox").first()).toBeVisible() -}) diff --git a/packages/app/e2e/model-picker.spec.ts b/packages/app/e2e/model-picker.spec.ts deleted file mode 100644 index 9e64b3dfb0a..00000000000 --- a/packages/app/e2e/model-picker.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => { - await gotoSession() - - await page.locator(promptSelector).click() - await page.keyboard.type("/model") - - const command = page.locator('[data-slash-id="model.choose"]') - await expect(command).toBeVisible() - await command.hover() - - await page.keyboard.press("Enter") - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - - const input = dialog.getByRole("textbox").first() - - const selected = dialog.locator('[data-slot="list-item"][data-selected="true"]').first() - await expect(selected).toBeVisible() - - const other = dialog.locator('[data-slot="list-item"]:not([data-selected="true"])').first() - const target = (await other.count()) > 0 ? other : selected - - const key = await target.getAttribute("data-key") - if (!key) throw new Error("Failed to resolve model key from list item") - - const name = (await target.locator("span").first().innerText()).trim() - const model = key.split(":").slice(1).join(":") - - await input.fill(model) - - const item = dialog.locator(`[data-slot="list-item"][data-key="${key}"]`) - await expect(item).toBeVisible() - await item.click() - - await expect(dialog).toHaveCount(0) - - const form = page.locator(promptSelector).locator("xpath=ancestor::form[1]") - await expect(form.locator('[data-component="button"]').filter({ hasText: name }).first()).toBeVisible() -}) diff --git a/packages/app/e2e/models/model-picker.spec.ts b/packages/app/e2e/models/model-picker.spec.ts new file mode 100644 index 00000000000..220a0baa1a8 --- /dev/null +++ b/packages/app/e2e/models/model-picker.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { clickListItem } from "../actions" + +test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + + const command = page.locator('[data-slash-id="model.choose"]') + await expect(command).toBeVisible() + await command.hover() + + await page.keyboard.press("Enter") + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + + const input = dialog.getByRole("textbox").first() + + const selected = dialog.locator('[data-slot="list-item"][data-selected="true"]').first() + await expect(selected).toBeVisible() + + const other = dialog.locator('[data-slot="list-item"]:not([data-selected="true"])').first() + const target = (await other.count()) > 0 ? other : selected + + const key = await target.getAttribute("data-key") + if (!key) throw new Error("Failed to resolve model key from list item") + + const model = key.split(":").slice(1).join(":") + + await input.fill(model) + + await clickListItem(dialog, { key }) + + await expect(dialog).toHaveCount(0) + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const dialogAgain = page.getByRole("dialog") + await expect(dialogAgain).toBeVisible() + await expect(dialogAgain.locator(`[data-slot="list-item"][data-key="${key}"][data-selected="true"]`)).toBeVisible() +}) diff --git a/packages/app/e2e/models/models-visibility.spec.ts b/packages/app/e2e/models/models-visibility.spec.ts new file mode 100644 index 00000000000..c6991117937 --- /dev/null +++ b/packages/app/e2e/models/models-visibility.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { closeDialog, openSettings, clickListItem } from "../actions" + +test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + + const command = page.locator('[data-slash-id="model.choose"]') + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const picker = page.getByRole("dialog") + await expect(picker).toBeVisible() + + const target = picker.locator('[data-slot="list-item"]').first() + await expect(target).toBeVisible() + + const key = await target.getAttribute("data-key") + if (!key) throw new Error("Failed to resolve model key from list item") + + const name = (await target.locator("span").first().innerText()).trim() + if (!name) throw new Error("Failed to resolve model name from list item") + + await page.keyboard.press("Escape") + await expect(picker).toHaveCount(0) + + const settings = await openSettings(page) + + await settings.getByRole("tab", { name: "Models" }).click() + const search = settings.getByPlaceholder("Search models") + await expect(search).toBeVisible() + await search.fill(name) + + const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first() + const input = toggle.locator('[data-slot="switch-input"]') + await expect(toggle).toBeVisible() + await expect(input).toHaveAttribute("aria-checked", "true") + await toggle.locator('[data-slot="switch-control"]').click() + await expect(input).toHaveAttribute("aria-checked", "false") + + await closeDialog(page, settings) + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const pickerAgain = page.getByRole("dialog") + await expect(pickerAgain).toBeVisible() + await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible() + + await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0) + + await page.keyboard.press("Escape") + await expect(pickerAgain).toHaveCount(0) +}) diff --git a/packages/app/e2e/navigation.spec.ts b/packages/app/e2e/navigation.spec.ts deleted file mode 100644 index 76923af6ede..00000000000 --- a/packages/app/e2e/navigation.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { test, expect } from "./fixtures" -import { dirPath, promptSelector } from "./utils" - -test("project route redirects to /session", async ({ page, directory, slug }) => { - await page.goto(dirPath(directory)) - - await expect(page).toHaveURL(new RegExp(`/${slug}/session`)) - await expect(page.locator(promptSelector)).toBeVisible() -}) diff --git a/packages/app/e2e/palette.spec.ts b/packages/app/e2e/palette.spec.ts deleted file mode 100644 index 617c55ac167..00000000000 --- a/packages/app/e2e/palette.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("search palette opens and closes", async ({ page, gotoSession }) => { - await gotoSession() - - await page.keyboard.press(`${modKey}+P`) - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - await expect(dialog.getByRole("textbox").first()).toBeVisible() - - await page.keyboard.press("Escape") - await expect(dialog).toHaveCount(0) -}) diff --git a/packages/app/e2e/projects/project-edit.spec.ts b/packages/app/e2e/projects/project-edit.spec.ts new file mode 100644 index 00000000000..7c20f29ec1d --- /dev/null +++ b/packages/app/e2e/projects/project-edit.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "../fixtures" +import { clickMenuItem, openProjectMenu, openSidebar } from "../actions" + +test("dialog edit project updates name and startup script", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + await withProject(async ({ slug }) => { + await openSidebar(page) + + const open = async () => { + const menu = await openProjectMenu(page, slug) + await clickMenuItem(menu, /^Edit$/i, { force: true }) + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project") + return dialog + } + + const name = `e2e project ${Date.now()}` + const startup = `echo e2e_${Date.now()}` + + const dialog = await open() + + const nameInput = dialog.getByLabel("Name") + await nameInput.fill(name) + + const startupInput = dialog.getByLabel("Workspace startup script") + await startupInput.fill(startup) + + await dialog.getByRole("button", { name: "Save" }).click() + await expect(dialog).toHaveCount(0) + + const header = page.locator(".group\\/project").first() + await expect(header).toContainText(name) + + const reopened = await open() + await expect(reopened.getByLabel("Name")).toHaveValue(name) + await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup) + await reopened.getByRole("button", { name: "Cancel" }).click() + await expect(reopened).toHaveCount(0) + }) +}) diff --git a/packages/app/e2e/projects/projects-close.spec.ts b/packages/app/e2e/projects/projects-close.spec.ts new file mode 100644 index 00000000000..9454d683f02 --- /dev/null +++ b/packages/app/e2e/projects/projects-close.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from "../fixtures" +import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem, openProjectMenu } from "../actions" +import { projectSwitchSelector } from "../selectors" +import { dirSlug } from "../utils" + +test("closing active project navigates to another open project", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const other = await createTestProject() + const otherSlug = dirSlug(other) + + try { + await withProject( + async ({ slug }) => { + await openSidebar(page) + + const otherButton = page.locator(projectSwitchSelector(otherSlug)).first() + await expect(otherButton).toBeVisible() + await otherButton.click() + + await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`)) + + const menu = await openProjectMenu(page, otherSlug) + + await clickMenuItem(menu, /^Close$/i, { force: true }) + + await expect + .poll( + () => { + const pathname = new URL(page.url()).pathname + if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project" + if (pathname === "/") return "home" + return "" + }, + { timeout: 15_000 }, + ) + .toMatch(/^(project|home)$/) + + await expect(page).not.toHaveURL(new RegExp(`/${otherSlug}/session(?:[/?#]|$)`)) + await expect + .poll( + async () => { + return await page.locator(projectSwitchSelector(otherSlug)).count() + }, + { timeout: 15_000 }, + ) + .toBe(0) + }, + { extra: [other] }, + ) + } finally { + await cleanupTestProject(other) + } +}) diff --git a/packages/app/e2e/projects/projects-switch.spec.ts b/packages/app/e2e/projects/projects-switch.spec.ts new file mode 100644 index 00000000000..6ad64f59278 --- /dev/null +++ b/packages/app/e2e/projects/projects-switch.spec.ts @@ -0,0 +1,143 @@ +import { base64Decode } from "@opencode-ai/util/encode" +import type { Page } from "@playwright/test" +import { test, expect } from "../fixtures" +import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitSlug } from "../actions" +import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors" +import { dirSlug, resolveDirectory } from "../utils" + +async function workspaces(page: Page, directory: string, enabled: boolean) { + await page.evaluate( + ({ directory, enabled }: { directory: string; enabled: boolean }) => { + const key = "opencode.global.dat:layout" + const raw = localStorage.getItem(key) + const data = raw ? JSON.parse(raw) : {} + const sidebar = data.sidebar && typeof data.sidebar === "object" ? data.sidebar : {} + const current = + sidebar.workspaces && typeof sidebar.workspaces === "object" && !Array.isArray(sidebar.workspaces) + ? sidebar.workspaces + : {} + const next = { ...current } + + if (enabled) next[directory] = true + if (!enabled) delete next[directory] + + localStorage.setItem( + key, + JSON.stringify({ + ...data, + sidebar: { + ...sidebar, + workspaces: next, + }, + }), + ) + }, + { directory, enabled }, + ) +} + +test("can switch between projects from sidebar", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const other = await createTestProject() + const otherSlug = dirSlug(other) + + try { + await withProject( + async ({ directory }) => { + await defocus(page) + + const currentSlug = dirSlug(directory) + const otherButton = page.locator(projectSwitchSelector(otherSlug)).first() + await expect(otherButton).toBeVisible() + await otherButton.click() + + await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`)) + + const currentButton = page.locator(projectSwitchSelector(currentSlug)).first() + await expect(currentButton).toBeVisible() + await currentButton.click() + + await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`)) + }, + { extra: [other] }, + ) + } finally { + await cleanupTestProject(other) + } +}) + +test("switching back to a project opens the latest workspace session", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const other = await createTestProject() + const otherSlug = dirSlug(other) + try { + await withProject( + async ({ directory, slug, trackSession, trackDirectory }) => { + await defocus(page) + await workspaces(page, directory, true) + await page.reload() + await expect(page.locator(promptSelector)).toBeVisible() + await openSidebar(page) + await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible() + + await page.getByRole("button", { name: "New workspace" }).first().click() + + const raw = await waitSlug(page, [slug]) + const dir = base64Decode(raw) + if (!dir) throw new Error(`Failed to decode workspace slug: ${raw}`) + const space = await resolveDirectory(dir) + const next = dirSlug(space) + trackDirectory(space) + await openSidebar(page) + + const item = page.locator(`${workspaceItemSelector(next)}, ${workspaceItemSelector(raw)}`).first() + await expect(item).toBeVisible() + await item.hover() + + const btn = page.locator(`${workspaceNewSessionSelector(next)}, ${workspaceNewSessionSelector(raw)}`).first() + await expect(btn).toBeVisible() + await btn.click({ force: true }) + + // A new workspace can be discovered via a transient slug before the route and sidebar + // settle to the canonical workspace path on Windows, so interact with either and assert + // against the resolved workspace slug. + await waitSlug(page) + await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`)) + + // Create a session by sending a prompt + const prompt = page.locator(promptSelector) + await expect(prompt).toBeVisible() + await prompt.fill("test") + await page.keyboard.press("Enter") + + // Wait for the URL to update with the new session ID + await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 15_000 }).not.toBe("") + + const created = sessionIDFromUrl(page.url()) + if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`) + trackSession(created, space) + + await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`)) + + await openSidebar(page) + + const otherButton = page.locator(projectSwitchSelector(otherSlug)).first() + await expect(otherButton).toBeVisible() + await otherButton.click() + await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`)) + + const rootButton = page.locator(projectSwitchSelector(slug)).first() + await expect(rootButton).toBeVisible() + await rootButton.click() + + await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created) + await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`)) + }, + { extra: [other] }, + ) + } finally { + await cleanupTestProject(other) + } +}) diff --git a/packages/app/e2e/projects/workspace-new-session.spec.ts b/packages/app/e2e/projects/workspace-new-session.spec.ts new file mode 100644 index 00000000000..18fa46d3299 --- /dev/null +++ b/packages/app/e2e/projects/workspace-new-session.spec.ts @@ -0,0 +1,109 @@ +import { base64Decode } from "@opencode-ai/util/encode" +import type { Page } from "@playwright/test" +import { test, expect } from "../fixtures" +import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, slugFromUrl, waitSlug } from "../actions" +import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors" +import { createSdk } from "../utils" + +async function waitWorkspaceReady(page: Page, slug: string) { + await openSidebar(page) + await expect + .poll( + async () => { + const item = page.locator(workspaceItemSelector(slug)).first() + try { + await item.hover({ timeout: 500 }) + return true + } catch { + return false + } + }, + { timeout: 60_000 }, + ) + .toBe(true) +} + +async function createWorkspace(page: Page, root: string, seen: string[]) { + await openSidebar(page) + await page.getByRole("button", { name: "New workspace" }).first().click() + + const slug = await waitSlug(page, [root, ...seen]) + const directory = base64Decode(slug) + if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`) + return { slug, directory } +} + +async function openWorkspaceNewSession(page: Page, slug: string) { + await waitWorkspaceReady(page, slug) + + const item = page.locator(workspaceItemSelector(slug)).first() + await item.hover() + + const button = page.locator(workspaceNewSessionSelector(slug)).first() + await expect(button).toBeVisible() + await button.click({ force: true }) + + const next = await waitSlug(page) + await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`)) + return next +} + +async function createSessionFromWorkspace(page: Page, slug: string, text: string) { + const next = await openWorkspaceNewSession(page, slug) + + const prompt = page.locator(promptSelector) + await expect(prompt).toBeVisible() + await expect(prompt).toBeEditable() + await prompt.click() + await expect(prompt).toBeFocused() + await prompt.fill(text) + await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text) + await prompt.press("Enter") + + await expect.poll(() => slugFromUrl(page.url())).toBe(next) + await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("") + + const sessionID = sessionIDFromUrl(page.url()) + if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`) + await expect(page).toHaveURL(new RegExp(`/${next}/session/${sessionID}(?:[/?#]|$)`)) + return { sessionID, slug: next } +} + +async function sessionDirectory(directory: string, sessionID: string) { + const info = await createSdk(directory) + .session.get({ sessionID }) + .then((x) => x.data) + .catch(() => undefined) + if (!info) return "" + return info.directory +} + +test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + await withProject(async ({ directory, slug: root, trackSession, trackDirectory }) => { + await openSidebar(page) + await setWorkspacesEnabled(page, root, true) + + const first = await createWorkspace(page, root, []) + trackDirectory(first.directory) + await waitWorkspaceReady(page, first.slug) + + const second = await createWorkspace(page, root, [first.slug]) + trackDirectory(second.directory) + await waitWorkspaceReady(page, second.slug) + + const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`) + trackSession(firstSession.sessionID, first.directory) + + const secondSession = await createSessionFromWorkspace(page, second.slug, `workspace two ${Date.now()}`) + trackSession(secondSession.sessionID, second.directory) + + const thirdSession = await createSessionFromWorkspace(page, first.slug, `workspace one again ${Date.now()}`) + trackSession(thirdSession.sessionID, first.directory) + + await expect.poll(() => sessionDirectory(first.directory, firstSession.sessionID)).toBe(first.directory) + await expect.poll(() => sessionDirectory(second.directory, secondSession.sessionID)).toBe(second.directory) + await expect.poll(() => sessionDirectory(first.directory, thirdSession.sessionID)).toBe(first.directory) + }) +}) diff --git a/packages/app/e2e/projects/workspaces.spec.ts b/packages/app/e2e/projects/workspaces.spec.ts new file mode 100644 index 00000000000..aeeccb9bba9 --- /dev/null +++ b/packages/app/e2e/projects/workspaces.spec.ts @@ -0,0 +1,373 @@ +import { base64Decode } from "@opencode-ai/util/encode" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import type { Page } from "@playwright/test" + +import { test, expect } from "../fixtures" + +test.describe.configure({ mode: "serial" }) +import { + cleanupTestProject, + clickMenuItem, + confirmDialog, + openSidebar, + openWorkspaceMenu, + setWorkspacesEnabled, + slugFromUrl, + waitSlug, +} from "../actions" +import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors" +import { createSdk, dirSlug } from "../utils" + +async function setupWorkspaceTest(page: Page, project: { slug: string }) { + const rootSlug = project.slug + await openSidebar(page) + + await setWorkspacesEnabled(page, rootSlug, true) + + await page.getByRole("button", { name: "New workspace" }).first().click() + const slug = await waitSlug(page, [rootSlug]) + const dir = base64Decode(slug) + + await openSidebar(page) + + await expect + .poll( + async () => { + const item = page.locator(workspaceItemSelector(slug)).first() + try { + await item.hover({ timeout: 500 }) + return true + } catch { + return false + } + }, + { timeout: 60_000 }, + ) + .toBe(true) + + return { rootSlug, slug, directory: dir } +} + +test("can enable and disable workspaces from project menu", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + await withProject(async ({ slug }) => { + await openSidebar(page) + + await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible() + await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0) + + await setWorkspacesEnabled(page, slug, true) + await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible() + await expect(page.locator(workspaceItemSelector(slug)).first()).toBeVisible() + + await setWorkspacesEnabled(page, slug, false) + await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible() + await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0) + }) +}) + +test("can create a workspace", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + await withProject(async ({ slug }) => { + await openSidebar(page) + await setWorkspacesEnabled(page, slug, true) + + await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible() + + await page.getByRole("button", { name: "New workspace" }).first().click() + const workspaceSlug = await waitSlug(page, [slug]) + const workspaceDir = base64Decode(workspaceSlug) + + await openSidebar(page) + + await expect + .poll( + async () => { + const item = page.locator(workspaceItemSelector(workspaceSlug)).first() + try { + await item.hover({ timeout: 500 }) + return true + } catch { + return false + } + }, + { timeout: 60_000 }, + ) + .toBe(true) + + await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible() + + await cleanupTestProject(workspaceDir) + }) +}) + +test("non-git projects keep workspace mode disabled", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const nonGit = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-nongit-")) + const nonGitSlug = dirSlug(nonGit) + + await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n") + + try { + await withProject(async () => { + await page.goto(`/${nonGitSlug}/session`) + + await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("") + + const activeDir = base64Decode(slugFromUrl(page.url())) + expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-") + + await openSidebar(page) + await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0) + + const trigger = page.locator('[data-action="project-menu"]').first() + const hasMenu = await trigger + .isVisible() + .then((x) => x) + .catch(() => false) + if (!hasMenu) return + + await trigger.click({ force: true }) + + const menu = page.locator(dropdownMenuContentSelector).first() + await expect(menu).toBeVisible() + + const toggle = menu.locator('[data-action="project-workspaces-toggle"]').first() + + await expect(toggle).toBeVisible() + await expect(toggle).toBeDisabled() + await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0) + }) + } finally { + await cleanupTestProject(nonGit) + } +}) + +test("can rename a workspace", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + await withProject(async (project) => { + const { slug } = await setupWorkspaceTest(page, project) + + const rename = `e2e workspace ${Date.now()}` + const menu = await openWorkspaceMenu(page, slug) + await clickMenuItem(menu, /^Rename$/i, { force: true }) + + await expect(menu).toHaveCount(0) + + const item = page.locator(workspaceItemSelector(slug)).first() + await expect(item).toBeVisible() + const input = item.locator(inlineInputSelector).first() + await expect(input).toBeVisible() + await input.fill(rename) + await input.press("Enter") + await expect(item).toContainText(rename) + }) +}) + +test("can reset a workspace", async ({ page, sdk, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + await withProject(async (project) => { + const { slug, directory: createdDir } = await setupWorkspaceTest(page, project) + + const readme = path.join(createdDir, "README.md") + const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`) + const original = await fs.readFile(readme, "utf8") + const dirty = `${original.trimEnd()}\n\nchange_${Date.now()}\n` + await fs.writeFile(readme, dirty, "utf8") + await fs.writeFile(extra, `created_${Date.now()}\n`, "utf8") + + await expect + .poll(async () => { + return await fs + .stat(extra) + .then(() => true) + .catch(() => false) + }) + .toBe(true) + + await expect + .poll(async () => { + const files = await sdk.file + .status({ directory: createdDir }) + .then((r) => r.data ?? []) + .catch(() => []) + return files.length + }) + .toBeGreaterThan(0) + + const menu = await openWorkspaceMenu(page, slug) + await clickMenuItem(menu, /^Reset$/i, { force: true }) + await confirmDialog(page, /^Reset workspace$/i) + + await expect + .poll( + async () => { + const files = await sdk.file + .status({ directory: createdDir }) + .then((r) => r.data ?? []) + .catch(() => []) + return files.length + }, + { timeout: 60_000 }, + ) + .toBe(0) + + await expect.poll(() => fs.readFile(readme, "utf8"), { timeout: 60_000 }).toBe(original) + + await expect + .poll(async () => { + return await fs + .stat(extra) + .then(() => true) + .catch(() => false) + }) + .toBe(false) + }) +}) + +test("can delete a workspace", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + await withProject(async (project) => { + const sdk = createSdk(project.directory) + const { rootSlug, slug, directory } = await setupWorkspaceTest(page, project) + + await expect + .poll( + async () => { + const worktrees = await sdk.worktree + .list() + .then((r) => r.data ?? []) + .catch(() => [] as string[]) + return worktrees.includes(directory) + }, + { timeout: 30_000 }, + ) + .toBe(true) + + const menu = await openWorkspaceMenu(page, slug) + await clickMenuItem(menu, /^Delete$/i, { force: true }) + await confirmDialog(page, /^Delete workspace$/i) + + await expect.poll(() => base64Decode(slugFromUrl(page.url()))).toBe(project.directory) + + await expect + .poll( + async () => { + const worktrees = await sdk.worktree + .list() + .then((r) => r.data ?? []) + .catch(() => [] as string[]) + return worktrees.includes(directory) + }, + { timeout: 60_000 }, + ) + .toBe(false) + + await project.gotoSession() + + await openSidebar(page) + await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 }) + await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible() + }) +}) + +test("can reorder workspaces by drag and drop", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + await withProject(async ({ slug: rootSlug }) => { + const workspaces = [] as { directory: string; slug: string }[] + + const listSlugs = async () => { + const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]') + const slugs = await nodes.evaluateAll((els) => { + return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0) + }) + return slugs + } + + const waitReady = async (slug: string) => { + await expect + .poll( + async () => { + const item = page.locator(workspaceItemSelector(slug)).first() + try { + await item.hover({ timeout: 500 }) + return true + } catch { + return false + } + }, + { timeout: 60_000 }, + ) + .toBe(true) + } + + const drag = async (from: string, to: string) => { + const src = page.locator(workspaceItemSelector(from)).first() + const dst = page.locator(workspaceItemSelector(to)).first() + + const a = await src.boundingBox() + const b = await dst.boundingBox() + if (!a || !b) throw new Error("Failed to resolve workspace drag bounds") + + await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2) + await page.mouse.down() + await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 }) + await page.mouse.up() + } + + try { + await openSidebar(page) + + await setWorkspacesEnabled(page, rootSlug, true) + + for (const _ of [0, 1]) { + const prev = slugFromUrl(page.url()) + await page.getByRole("button", { name: "New workspace" }).first().click() + const slug = await waitSlug(page, [rootSlug, prev]) + const dir = base64Decode(slug) + workspaces.push({ slug, directory: dir }) + + await openSidebar(page) + } + + if (workspaces.length !== 2) throw new Error("Expected two created workspaces") + + const a = workspaces[0].slug + const b = workspaces[1].slug + + await waitReady(a) + await waitReady(b) + + const list = async () => { + const slugs = await listSlugs() + return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2) + } + + await expect + .poll(async () => { + const slugs = await list() + return slugs.length === 2 + }) + .toBe(true) + + const before = await list() + const from = before[1] + const to = before[0] + if (!from || !to) throw new Error("Failed to resolve initial workspace order") + + await drag(from, to) + + await expect.poll(async () => await list()).toEqual([from, to]) + } finally { + await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory))) + } + }) +}) diff --git a/packages/app/e2e/prompt/context.spec.ts b/packages/app/e2e/prompt/context.spec.ts new file mode 100644 index 00000000000..366191fd70d --- /dev/null +++ b/packages/app/e2e/prompt/context.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from "../fixtures" +import type { Page } from "@playwright/test" +import { promptSelector } from "../selectors" +import { withSession } from "../actions" + +function contextButton(page: Page) { + return page + .locator('[data-component="button"]') + .filter({ has: page.locator('[data-component="progress-circle"]').first() }) + .first() +} + +async function seedContextSession(input: { sessionID: string; sdk: Parameters[0] }) { + await input.sdk.session.promptAsync({ + sessionID: input.sessionID, + noReply: true, + parts: [ + { + type: "text", + text: "seed context", + }, + ], + }) + + await expect + .poll(async () => { + const messages = await input.sdk.session + .messages({ sessionID: input.sessionID, limit: 1 }) + .then((r) => r.data ?? []) + return messages.length + }) + .toBeGreaterThan(0) +} + +test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => { + const title = `e2e smoke context ${Date.now()}` + + await withSession(sdk, title, async (session) => { + await seedContextSession({ sessionID: session.id, sdk }) + + await gotoSession(session.id) + + const trigger = contextButton(page) + await expect(trigger).toBeVisible() + await trigger.click() + + const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') + await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible() + }) +}) + +test("context panel can be closed from the context tab close action", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, `e2e context toggle ${Date.now()}`, async (session) => { + await seedContextSession({ sessionID: session.id, sdk }) + await gotoSession(session.id) + + await page.locator(promptSelector).click() + + const trigger = contextButton(page) + await expect(trigger).toBeVisible() + await trigger.click() + + const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') + const context = tabs.getByRole("tab", { name: "Context" }) + await expect(context).toBeVisible() + + await page.getByRole("button", { name: "Close tab" }).first().click() + await expect(context).toHaveCount(0) + }) +}) + +test("context panel can open file picker from context actions", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, `e2e context tabs ${Date.now()}`, async (session) => { + await seedContextSession({ sessionID: session.id, sdk }) + await gotoSession(session.id) + + await page.locator(promptSelector).click() + + const trigger = contextButton(page) + await expect(trigger).toBeVisible() + await trigger.click() + + await expect(page.getByRole("tab", { name: "Context" })).toBeVisible() + await page.getByRole("button", { name: "Open file" }).first().click() + + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() + + await page.keyboard.press("Escape") + await expect(dialog).toHaveCount(0) + }) +}) diff --git a/packages/app/e2e/prompt/prompt-async.spec.ts b/packages/app/e2e/prompt/prompt-async.spec.ts new file mode 100644 index 00000000000..51fbc3e4ae3 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-async.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { cleanupSession, sessionIDFromUrl, withSession } from "../actions" + +const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim() + +// Regression test for Issue #12453: the synchronous POST /message endpoint holds +// the connection open while the agent works, causing "Failed to fetch" over +// VPN/Tailscale. The fix switches to POST /prompt_async which returns immediately. +test("prompt succeeds when sync message endpoint is unreachable", async ({ page, sdk, gotoSession }) => { + test.setTimeout(120_000) + + // Simulate Tailscale/VPN killing the long-lived sync connection + await page.route("**/session/*/message", (route) => route.abort("connectionfailed")) + + await gotoSession() + + const token = `E2E_ASYNC_${Date.now()}` + await page.locator(promptSelector).click() + await page.keyboard.type(`Reply with exactly: ${token}`) + await page.keyboard.press("Enter") + + await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 }) + const sessionID = sessionIDFromUrl(page.url())! + + try { + // Agent response arrives via SSE despite sync endpoint being dead + await expect + .poll( + async () => { + const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? []) + return messages + .filter((m) => m.info.role === "assistant") + .flatMap((m) => m.parts) + .filter((p) => p.type === "text") + .map((p) => p.text) + .join("\n") + }, + { timeout: 90_000 }, + ) + .toContain(token) + } finally { + await cleanupSession({ sdk, sessionID }) + } +}) + +test("failed prompt send restores the composer input", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, `e2e prompt failure ${Date.now()}`, async (session) => { + const prompt = page.locator(promptSelector) + const value = `restore ${Date.now()}` + + await page.route(`**/session/${session.id}/prompt_async`, (route) => + route.fulfill({ + status: 500, + contentType: "application/json", + body: JSON.stringify({ message: "e2e prompt failure" }), + }), + ) + + await gotoSession(session.id) + await prompt.click() + await page.keyboard.type(value) + await page.keyboard.press("Enter") + + await expect.poll(async () => text(await prompt.textContent())).toBe(value) + await expect + .poll( + async () => { + const messages = await sdk.session.messages({ sessionID: session.id, limit: 50 }).then((r) => r.data ?? []) + return messages.length + }, + { timeout: 15_000 }, + ) + .toBe(0) + }) +}) diff --git a/packages/app/e2e/prompt/prompt-drop-file-uri.spec.ts b/packages/app/e2e/prompt/prompt-drop-file-uri.spec.ts new file mode 100644 index 00000000000..add2d8d8bc5 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-drop-file-uri.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" + +test("dropping text/plain file: uri inserts a file pill", async ({ page, gotoSession }) => { + await gotoSession() + + const prompt = page.locator(promptSelector) + await prompt.click() + + const path = process.platform === "win32" ? "C:\\opencode-e2e-drop.txt" : "/tmp/opencode-e2e-drop.txt" + const dt = await page.evaluateHandle((text) => { + const dt = new DataTransfer() + dt.setData("text/plain", text) + return dt + }, `file:${path}`) + + await page.dispatchEvent("body", "drop", { dataTransfer: dt }) + + const pill = page.locator(`${promptSelector} [data-type="file"]`).first() + await expect(pill).toBeVisible() + await expect(pill).toHaveAttribute("data-path", path) +}) diff --git a/packages/app/e2e/prompt/prompt-drop-file.spec.ts b/packages/app/e2e/prompt/prompt-drop-file.spec.ts new file mode 100644 index 00000000000..0a138de9977 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-drop-file.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" + +test("dropping an image file adds an attachment", async ({ page, gotoSession }) => { + await gotoSession() + + const prompt = page.locator(promptSelector) + await prompt.click() + + const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3+4uQAAAAASUVORK5CYII=" + const dt = await page.evaluateHandle((b64) => { + const dt = new DataTransfer() + const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)) + const file = new File([bytes], "drop.png", { type: "image/png" }) + dt.items.add(file) + return dt + }, png) + + await page.dispatchEvent("body", "drop", { dataTransfer: dt }) + + const img = page.locator('img[alt="drop.png"]').first() + await expect(img).toBeVisible() + + const remove = page.getByRole("button", { name: "Remove attachment" }).first() + await expect(remove).toBeVisible() + + await img.hover() + await remove.click() + await expect(page.locator('img[alt="drop.png"]')).toHaveCount(0) +}) diff --git a/packages/app/e2e/prompt/prompt-history.spec.ts b/packages/app/e2e/prompt/prompt-history.spec.ts new file mode 100644 index 00000000000..ec689981445 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-history.spec.ts @@ -0,0 +1,181 @@ +import type { ToolPart } from "@opencode-ai/sdk/v2/client" +import type { Page } from "@playwright/test" +import { test, expect } from "../fixtures" +import { withSession } from "../actions" +import { promptSelector } from "../selectors" + +const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim() + +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 edge(page: Page, pos: "start" | "end") { + await page.locator(promptSelector).evaluate((el: HTMLDivElement, pos: "start" | "end") => { + const selection = window.getSelection() + if (!selection) return + + const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT) + const nodes: Text[] = [] + for (let node = walk.nextNode(); node; node = walk.nextNode()) { + nodes.push(node as Text) + } + + if (nodes.length === 0) { + const node = document.createTextNode("") + el.appendChild(node) + nodes.push(node) + } + + const node = pos === "start" ? nodes[0]! : nodes[nodes.length - 1]! + const range = document.createRange() + range.setStart(node, pos === "start" ? 0 : (node.textContent ?? "").length) + range.collapse(true) + selection.removeAllRanges() + selection.addRange(range) + }, pos) +} + +async function wait(page: Page, value: string) { + await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value) +} + +async function reply(sdk: Parameters[0], sessionID: string, token: string) { + await expect + .poll( + async () => { + const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? []) + return messages + .filter((item) => item.info.role === "assistant") + .flatMap((item) => item.parts) + .filter((item) => item.type === "text") + .map((item) => item.text) + .join("\n") + }, + { timeout: 90_000 }, + ) + .toContain(token) +} + +async function shell(sdk: Parameters[0], 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, sdk, gotoSession }) => { + test.setTimeout(120_000) + + await withSession(sdk, `e2e prompt history ${Date.now()}`, async (session) => { + await gotoSession(session.id) + + const prompt = page.locator(promptSelector) + 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 prompt.click() + await page.keyboard.type(first) + await page.keyboard.press("Enter") + await wait(page, "") + await reply(sdk, session.id, firstToken) + + await prompt.click() + await page.keyboard.type(second) + await page.keyboard.press("Enter") + await wait(page, "") + await reply(sdk, session.id, secondToken) + + await prompt.click() + await page.keyboard.type(draft) + await wait(page, draft) + + await edge(page, "start") + 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, draft) + }) +}) + +test("shell history stays separate from normal prompt history", async ({ page, sdk, gotoSession }) => { + test.setTimeout(120_000) + + await withSession(sdk, `e2e shell history ${Date.now()}`, async (session) => { + await gotoSession(session.id) + + const prompt = page.locator(promptSelector) + 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 prompt.click() + await page.keyboard.type("!") + await page.keyboard.type(first) + await page.keyboard.press("Enter") + await wait(page, "") + await shell(sdk, session.id, 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, session.id, second, secondToken) + + 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, session.id, normalToken) + + await prompt.click() + await page.keyboard.press("ArrowUp") + await wait(page, normal) + }) +}) diff --git a/packages/app/e2e/prompt-mention.spec.ts b/packages/app/e2e/prompt/prompt-mention.spec.ts similarity index 90% rename from packages/app/e2e/prompt-mention.spec.ts rename to packages/app/e2e/prompt/prompt-mention.spec.ts index 113b8465f71..5cc9f6e6850 100644 --- a/packages/app/e2e/prompt-mention.spec.ts +++ b/packages/app/e2e/prompt/prompt-mention.spec.ts @@ -1,5 +1,5 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" test("smoke @mention inserts file pill token", async ({ page, gotoSession }) => { await gotoSession() diff --git a/packages/app/e2e/prompt/prompt-multiline.spec.ts b/packages/app/e2e/prompt/prompt-multiline.spec.ts new file mode 100644 index 00000000000..216aa3fdaec --- /dev/null +++ b/packages/app/e2e/prompt/prompt-multiline.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" + +test("shift+enter inserts a newline without submitting", async ({ page, gotoSession }) => { + await gotoSession() + + await expect(page).toHaveURL(/\/session\/?$/) + + const prompt = page.locator(promptSelector) + await prompt.click() + await page.keyboard.type("line one") + await page.keyboard.press("Shift+Enter") + await page.keyboard.type("line two") + + await expect(page).toHaveURL(/\/session\/?$/) + await expect(prompt).toContainText("line one") + await expect(prompt).toContainText("line two") +}) diff --git a/packages/app/e2e/prompt/prompt-shell.spec.ts b/packages/app/e2e/prompt/prompt-shell.spec.ts new file mode 100644 index 00000000000..4c92f4a2f28 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-shell.spec.ts @@ -0,0 +1,62 @@ +import type { ToolPart } from "@opencode-ai/sdk/v2/client" +import { test, expect } from "../fixtures" +import { sessionIDFromUrl } from "../actions" +import { promptSelector } from "../selectors" +import { createSdk } from "../utils" + +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, withProject }) => { + test.setTimeout(120_000) + + await withProject(async ({ directory, gotoSession, trackSession }) => { + const sdk = createSdk(directory) + const prompt = page.locator(promptSelector) + const cmd = process.platform === "win32" ? "dir" : "ls" + + await gotoSession() + await prompt.click() + await page.keyboard.type("!") + await expect(prompt).toHaveAttribute("aria-label", /enter shell command/i) + + await page.keyboard.type(cmd) + await page.keyboard.press("Enter") + + await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 }) + + const id = sessionIDFromUrl(page.url()) + if (!id) throw new Error(`Failed to parse session id from url: ${page.url()}`) + trackSession(id, directory) + + await expect + .poll( + async () => { + const list = await sdk.session.messages({ sessionID: id, limit: 50 }).then((x) => x.data ?? []) + const msg = list.findLast( + (item) => item.info.role === "assistant" && "path" in item.info && item.info.path.cwd === 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: directory, output } + }, + { timeout: 90_000 }, + ) + .toEqual(expect.objectContaining({ cwd: directory, output: expect.stringContaining("README.md") })) + + await expect(prompt).toHaveText("") + }) +}) diff --git a/packages/app/e2e/prompt-slash-open.spec.ts b/packages/app/e2e/prompt/prompt-slash-open.spec.ts similarity index 86% rename from packages/app/e2e/prompt-slash-open.spec.ts rename to packages/app/e2e/prompt/prompt-slash-open.spec.ts index 3c29d405c18..b4a93099d9d 100644 --- a/packages/app/e2e/prompt-slash-open.spec.ts +++ b/packages/app/e2e/prompt/prompt-slash-open.spec.ts @@ -1,5 +1,5 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" test("smoke /open opens file picker dialog", async ({ page, gotoSession }) => { await gotoSession() diff --git a/packages/app/e2e/prompt/prompt-slash-share.spec.ts b/packages/app/e2e/prompt/prompt-slash-share.spec.ts new file mode 100644 index 00000000000..817b353a7c1 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-slash-share.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { withSession } from "../actions" + +const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_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, sdk, gotoSession }) => { + test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).") + + await withSession(sdk, `e2e slash share ${Date.now()}`, async (session) => { + const prompt = page.locator(promptSelector) + + await seed(sdk, session.id) + await 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 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 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/prompt/prompt-slash-terminal.spec.ts b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts new file mode 100644 index 00000000000..100d1878ab4 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "../fixtures" +import { waitTerminalReady } from "../actions" +import { promptSelector, terminalSelector } from "../selectors" + +test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => { + await gotoSession() + + const prompt = page.locator(promptSelector) + const terminal = page.locator(terminalSelector) + const slash = page.locator('[data-slash-id="terminal.toggle"]').first() + + await expect(terminal).not.toBeVisible() + + await prompt.fill("/terminal") + await expect(slash).toBeVisible() + await page.keyboard.press("Enter") + await waitTerminalReady(page, { term: terminal }) + + // Terminal panel retries focus (immediate, RAF, 120ms, 240ms) after opening, + // which can steal focus from the prompt and prevent fill() from triggering + // the slash popover. Re-attempt click+fill until all retries are exhausted + // and the popover appears. + await expect + .poll( + async () => { + await prompt.click().catch(() => false) + await prompt.fill("/terminal").catch(() => false) + return slash.isVisible().catch(() => false) + }, + { timeout: 10_000 }, + ) + .toBe(true) + await page.keyboard.press("Enter") + await expect(terminal).not.toBeVisible() +}) diff --git a/packages/app/e2e/prompt.spec.ts b/packages/app/e2e/prompt/prompt.spec.ts similarity index 75% rename from packages/app/e2e/prompt.spec.ts rename to packages/app/e2e/prompt/prompt.spec.ts index 3e5892ce8d5..0466d0988c8 100644 --- a/packages/app/e2e/prompt.spec.ts +++ b/packages/app/e2e/prompt/prompt.spec.ts @@ -1,10 +1,6 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -function sessionIDFromUrl(url: string) { - const match = /\/session\/([^/?#]+)/.exec(url) - return match?.[1] -} +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { cleanupSession, sessionIDFromUrl, withSession } from "../actions" test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) => { test.setTimeout(120_000) @@ -48,12 +44,9 @@ test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) ) .toContain(token) - - const reply = page.locator('[data-slot="session-turn-summary-section"]').filter({ hasText: token }).first() - await expect(reply).toBeVisible({ timeout: 90_000 }) } finally { page.off("pageerror", onPageError) - await sdk.session.delete({ sessionID }).catch(() => undefined) + await cleanupSession({ sdk, sessionID }) } if (pageErrors.length > 0) { diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts new file mode 100644 index 00000000000..64b7bfe5456 --- /dev/null +++ b/packages/app/e2e/selectors.ts @@ -0,0 +1,72 @@ +export const promptSelector = '[data-component="prompt-input"]' +export const terminalPanelSelector = '#terminal-panel[aria-hidden="false"]' +export const terminalSelector = `${terminalPanelSelector} [data-component="terminal"]` +export const sessionComposerDockSelector = '[data-component="session-prompt-dock"]' +export const questionDockSelector = '[data-component="dock-prompt"][data-kind="question"]' +export const permissionDockSelector = '[data-component="dock-prompt"][data-kind="permission"]' +export const permissionRejectSelector = `${permissionDockSelector} [data-slot="permission-footer-actions"] [data-component="button"]:nth-child(1)` +export const permissionAllowAlwaysSelector = `${permissionDockSelector} [data-slot="permission-footer-actions"] [data-component="button"]:nth-child(2)` +export const permissionAllowOnceSelector = `${permissionDockSelector} [data-slot="permission-footer-actions"] [data-component="button"]:nth-child(3)` +export const sessionTodoDockSelector = '[data-component="session-todo-dock"]' +export const sessionTodoToggleSelector = '[data-action="session-todo-toggle"]' +export const sessionTodoToggleButtonSelector = '[data-action="session-todo-toggle-button"]' +export const sessionTodoListSelector = '[data-slot="session-todo-list"]' + +export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]' +export const settingsLanguageSelectSelector = '[data-action="settings-language"]' +export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]' +export const settingsThemeSelector = '[data-action="settings-theme"]' +export const settingsFontSelector = '[data-action="settings-font"]' +export const settingsNotificationsAgentSelector = '[data-action="settings-notifications-agent"]' +export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]' +export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]' +export const settingsSoundsAgentSelector = '[data-action="settings-sounds-agent"]' +export const settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]' +export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]' +export const settingsUpdatesStartupSelector = '[data-action="settings-updates-startup"]' +export const settingsReleaseNotesSelector = '[data-action="settings-release-notes"]' + +export const sidebarNavSelector = '[data-component="sidebar-nav-desktop"]' + +export const projectSwitchSelector = (slug: string) => + `${sidebarNavSelector} [data-action="project-switch"][data-project="${slug}"]` + +export const projectMenuTriggerSelector = (slug: string) => + `${sidebarNavSelector} [data-action="project-menu"][data-project="${slug}"]` + +export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]` + +export const projectClearNotificationsSelector = (slug: string) => + `[data-action="project-clear-notifications"][data-project="${slug}"]` + +export const projectWorkspacesToggleSelector = (slug: string) => + `[data-action="project-workspaces-toggle"][data-project="${slug}"]` + +export const titlebarRightSelector = "#opencode-titlebar-right" + +export const popoverBodySelector = '[data-slot="popover-body"]' + +export const dropdownMenuTriggerSelector = '[data-slot="dropdown-menu-trigger"]' + +export const dropdownMenuContentSelector = '[data-component="dropdown-menu-content"]' + +export const inlineInputSelector = '[data-component="inline-input"]' + +export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]` + +export const workspaceItemSelector = (slug: string) => + `${sidebarNavSelector} [data-component="workspace-item"][data-workspace="${slug}"]` + +export const workspaceMenuTriggerSelector = (slug: string) => + `${sidebarNavSelector} [data-action="workspace-menu"][data-workspace="${slug}"]` + +export const workspaceNewSessionSelector = (slug: string) => + `${sidebarNavSelector} [data-action="workspace-new-session"][data-workspace="${slug}"]` + +export const listItemSelector = '[data-slot="list-item"]' + +export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]` + +export const listItemKeySelector = (key: string) => `${listItemSelector}[data-key="${key}"]` + +export const keybindButtonSelector = (id: string) => `[data-keybind-id="${id}"]` diff --git a/packages/app/e2e/session.spec.ts b/packages/app/e2e/session.spec.ts deleted file mode 100644 index 19e25a42131..00000000000 --- a/packages/app/e2e/session.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => { - const title = `e2e smoke ${Date.now()}` - const created = await sdk.session.create({ title }).then((r) => r.data) - - if (!created?.id) throw new Error("Session create did not return an id") - const sessionID = created.id - - try { - await gotoSession(sessionID) - - const prompt = page.locator(promptSelector) - await prompt.click() - await page.keyboard.type("hello from e2e") - await expect(prompt).toContainText("hello from e2e") - } finally { - await sdk.session.delete({ sessionID }).catch(() => undefined) - } -}) diff --git a/packages/app/e2e/session/session-child-navigation.spec.ts b/packages/app/e2e/session/session-child-navigation.spec.ts new file mode 100644 index 00000000000..ac2dca33c80 --- /dev/null +++ b/packages/app/e2e/session/session-child-navigation.spec.ts @@ -0,0 +1,37 @@ +import { seedSessionTask, withSession } from "../actions" +import { test, expect } from "../fixtures" + +test("task tool child-session link does not trigger stale show errors", async ({ page, sdk, gotoSession }) => { + test.setTimeout(120_000) + + const errs: string[] = [] + const onError = (err: Error) => { + errs.push(err.message) + } + page.on("pageerror", onError) + + await withSession(sdk, `e2e child nav ${Date.now()}`, async (session) => { + const child = await seedSessionTask(sdk, { + sessionID: session.id, + description: "Open child session", + prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.", + }) + + try { + await gotoSession(session.id) + + const link = page + .locator("a.subagent-link") + .filter({ hasText: /open child session/i }) + .first() + await expect(link).toBeVisible({ timeout: 30_000 }) + await link.click() + + await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 }) + await page.waitForTimeout(1000) + expect(errs).toEqual([]) + } finally { + page.off("pageerror", onError) + } + }) +}) diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts new file mode 100644 index 00000000000..055e8eed292 --- /dev/null +++ b/packages/app/e2e/session/session-composer-dock.spec.ts @@ -0,0 +1,425 @@ +import { test, expect } from "../fixtures" +import { cleanupSession, clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions" +import { + permissionDockSelector, + promptSelector, + questionDockSelector, + sessionComposerDockSelector, + sessionTodoDockSelector, + sessionTodoListSelector, + sessionTodoToggleButtonSelector, +} from "../selectors" + +type Sdk = Parameters[0] +type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" } + +async function withDockSession( + sdk: Sdk, + title: string, + fn: (session: { id: string; title: string }) => Promise, + opts?: { permission?: PermissionRule[] }, +) { + const session = await sdk.session + .create(opts?.permission ? { title, permission: opts.permission } : { title }) + .then((r) => r.data) + if (!session?.id) throw new Error("Session create did not return an id") + try { + return await fn(session) + } finally { + await cleanupSession({ sdk, sessionID: session.id }) + } +} + +test.setTimeout(120_000) + +async function withDockSeed(sdk: Sdk, sessionID: string, fn: () => Promise) { + try { + return await fn() + } finally { + await clearSessionDockSeed(sdk, sessionID).catch(() => undefined) + } +} + +async function clearPermissionDock(page: any, label: RegExp) { + const dock = page.locator(permissionDockSelector) + for (let i = 0; i < 3; i++) { + const count = await dock.count() + if (count === 0) return + await dock.getByRole("button", { name: label }).click() + await page.waitForTimeout(150) + } +} + +async function setAutoAccept(page: any, enabled: boolean) { + const button = page.locator('[data-action="prompt-permissions"]').first() + await expect(button).toBeVisible() + const pressed = (await button.getAttribute("aria-pressed")) === "true" + if (pressed === enabled) return + await button.click() + await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false") +} + +async function withMockPermission( + page: any, + request: { + id: string + sessionID: string + permission: string + patterns: string[] + metadata?: Record + always?: string[] + }, + opts: { child?: any } | undefined, + fn: () => Promise, +) { + let pending = [ + { + ...request, + always: request.always ?? ["*"], + metadata: request.metadata ?? {}, + }, + ] + + const list = async (route: any) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(pending), + }) + } + + const reply = async (route: any) => { + const url = new URL(route.request().url()) + const id = url.pathname.split("/").pop() + pending = pending.filter((item) => item.id !== id) + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(true), + }) + } + + await page.route("**/permission", list) + await page.route("**/session/*/permissions/*", reply) + + const sessionList = opts?.child + ? async (route: any) => { + const res = await route.fetch() + const json = await res.json() + const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined + if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child) + await route.fulfill({ + status: res.status(), + headers: res.headers(), + contentType: "application/json", + body: JSON.stringify(json), + }) + } + : undefined + + if (sessionList) await page.route("**/session?*", sessionList) + + try { + return await fn() + } finally { + await page.unroute("**/permission", list) + await page.unroute("**/session/*/permissions/*", reply) + if (sessionList) await page.unroute("**/session?*", sessionList) + } +} + +test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock default", async (session) => { + await gotoSession(session.id) + + await expect(page.locator(sessionComposerDockSelector)).toBeVisible() + await expect(page.locator(promptSelector)).toBeVisible() + await expect(page.locator(questionDockSelector)).toHaveCount(0) + await expect(page.locator(permissionDockSelector)).toHaveCount(0) + + await page.locator(promptSelector).click() + await expect(page.locator(promptSelector)).toBeFocused() + }) +}) + +test("auto-accept toggle works before first submit", async ({ page, gotoSession }) => { + await gotoSession() + + const button = page.locator('[data-action="prompt-permissions"]').first() + await expect(button).toBeVisible() + await expect(button).toHaveAttribute("aria-pressed", "false") + + await setAutoAccept(page, true) + await setAutoAccept(page, false) +}) + +test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock question", async (session) => { + await withDockSeed(sdk, session.id, async () => { + await gotoSession(session.id) + + await seedSessionQuestion(sdk, { + sessionID: session.id, + questions: [ + { + header: "Need input", + question: "Pick one option", + options: [ + { label: "Continue", description: "Continue now" }, + { label: "Stop", description: "Stop here" }, + ], + }, + ], + }) + + const dock = page.locator(questionDockSelector) + await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await dock.locator('[data-slot="question-option"]').first().click() + await dock.getByRole("button", { name: /submit/i }).click() + + await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }) + }) +}) + +test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock permission once", async (session) => { + await gotoSession(session.id) + await setAutoAccept(page, false) + await withMockPermission( + page, + { + id: "per_e2e_once", + sessionID: session.id, + permission: "bash", + patterns: ["/tmp/opencode-e2e-perm-once"], + metadata: { description: "Need permission for command" }, + }, + undefined, + async () => { + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /allow once/i) + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) + }) +}) + +test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock permission reject", async (session) => { + await gotoSession(session.id) + await setAutoAccept(page, false) + await withMockPermission( + page, + { + id: "per_e2e_reject", + sessionID: session.id, + permission: "bash", + patterns: ["/tmp/opencode-e2e-perm-reject"], + }, + undefined, + async () => { + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /deny/i) + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) + }) +}) + +test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock permission always", async (session) => { + await gotoSession(session.id) + await setAutoAccept(page, false) + await withMockPermission( + page, + { + id: "per_e2e_always", + sessionID: session.id, + permission: "bash", + patterns: ["/tmp/opencode-e2e-perm-always"], + metadata: { description: "Need permission for command" }, + }, + undefined, + async () => { + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /allow always/i) + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) + }) +}) + +test("child session question request blocks parent dock and unblocks after submit", async ({ + page, + sdk, + gotoSession, +}) => { + await withDockSession(sdk, "e2e composer dock child question parent", async (session) => { + await gotoSession(session.id) + + const child = await sdk.session + .create({ + title: "e2e composer dock child question", + parentID: session.id, + }) + .then((r) => r.data) + if (!child?.id) throw new Error("Child session create did not return an id") + + try { + await withDockSeed(sdk, child.id, async () => { + await seedSessionQuestion(sdk, { + sessionID: child.id, + questions: [ + { + header: "Child input", + question: "Pick one child option", + options: [ + { label: "Continue", description: "Continue child" }, + { label: "Stop", description: "Stop child" }, + ], + }, + ], + }) + + const dock = page.locator(questionDockSelector) + await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await dock.locator('[data-slot="question-option"]').first().click() + await dock.getByRole("button", { name: /submit/i }).click() + + await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }) + } finally { + await cleanupSession({ sdk, sessionID: child.id }) + } + }) +}) + +test("child session permission request blocks parent dock and supports allow once", async ({ + page, + sdk, + gotoSession, +}) => { + await withDockSession(sdk, "e2e composer dock child permission parent", async (session) => { + await gotoSession(session.id) + await setAutoAccept(page, false) + + const child = await sdk.session + .create({ + title: "e2e composer dock child permission", + parentID: session.id, + }) + .then((r) => r.data) + if (!child?.id) throw new Error("Child session create did not return an id") + + try { + await withMockPermission( + page, + { + id: "per_e2e_child", + sessionID: child.id, + permission: "bash", + patterns: ["/tmp/opencode-e2e-perm-child"], + metadata: { description: "Need child permission" }, + }, + { child }, + async () => { + await page.goto(page.url()) + const dock = page.locator(permissionDockSelector) + await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /allow once/i) + await page.goto(page.url()) + + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) + } finally { + await cleanupSession({ sdk, sessionID: child.id }) + } + }) +}) + +test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock todo", async (session) => { + await withDockSeed(sdk, session.id, async () => { + await gotoSession(session.id) + + await seedSessionTodos(sdk, { + sessionID: session.id, + todos: [ + { content: "first task", status: "pending", priority: "high" }, + { content: "second task", status: "in_progress", priority: "medium" }, + ], + }) + + await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(sessionTodoListSelector)).toBeVisible() + + await page.locator(sessionTodoToggleButtonSelector).click() + await expect(page.locator(sessionTodoListSelector)).toBeHidden() + + await page.locator(sessionTodoToggleButtonSelector).click() + await expect(page.locator(sessionTodoListSelector)).toBeVisible() + + await seedSessionTodos(sdk, { + sessionID: session.id, + todos: [ + { content: "first task", status: "completed", priority: "high" }, + { content: "second task", status: "cancelled", priority: "medium" }, + ], + }) + + await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(0) + }) + }) +}) + +test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock keyboard", async (session) => { + await withDockSeed(sdk, session.id, async () => { + await gotoSession(session.id) + + await seedSessionQuestion(sdk, { + sessionID: session.id, + questions: [ + { + header: "Need input", + question: "Pick one option", + options: [{ label: "Continue", description: "Continue now" }], + }, + ], + }) + + await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await page.locator("main").click({ position: { x: 5, y: 5 } }) + await page.keyboard.type("abc") + await expect(page.locator(promptSelector)).toHaveCount(0) + }) + }) +}) diff --git a/packages/app/e2e/session/session-review.spec.ts b/packages/app/e2e/session/session-review.spec.ts new file mode 100644 index 00000000000..28c85edb0b1 --- /dev/null +++ b/packages/app/e2e/session/session-review.spec.ts @@ -0,0 +1,217 @@ +import { waitSessionIdle, withSession } from "../actions" +import { test, expect } from "../fixtures" +import { createSdk } from "../utils" + +const count = 14 + +function body(mark: string) { + return [ + `title ${mark}`, + `mark ${mark}`, + ...Array.from({ length: 32 }, (_, i) => `line ${String(i + 1).padStart(2, "0")} ${mark}`), + ] +} + +function files(tag: string) { + return Array.from({ length: count }, (_, i) => { + const id = String(i).padStart(2, "0") + return { + file: `review-scroll-${id}.txt`, + mark: `${tag}-${id}`, + } + }) +} + +function seed(list: ReturnType) { + const out = ["*** Begin Patch"] + + for (const item of list) { + out.push(`*** Add File: ${item.file}`) + for (const line of body(item.mark)) out.push(`+${line}`) + } + + out.push("*** End Patch") + return out.join("\n") +} + +function edit(file: string, prev: string, next: string) { + return ["*** Begin Patch", `*** Update File: ${file}`, "@@", `-mark ${prev}`, `+mark ${next}`, "*** End Patch"].join( + "\n", + ) +} + +async function patch(sdk: ReturnType, sessionID: string, patchText: string) { + await sdk.session.promptAsync({ + sessionID, + agent: "build", + system: [ + "You are seeding deterministic e2e UI state.", + "Your only valid response is one apply_patch tool call.", + `Use this JSON input: ${JSON.stringify({ patchText })}`, + "Do not call any other tools.", + "Do not output plain text.", + ].join("\n"), + parts: [{ type: "text", text: "Apply the provided patch exactly once." }], + }) + + await waitSessionIdle(sdk, sessionID, 120_000) +} + +async function show(page: Parameters[0]["page"]) { + const btn = page.getByRole("button", { name: "Toggle review" }).first() + await expect(btn).toBeVisible() + if ((await btn.getAttribute("aria-expanded")) !== "true") await btn.click() + await expect(btn).toHaveAttribute("aria-expanded", "true") +} + +async function expand(page: Parameters[0]["page"]) { + const close = page.getByRole("button", { name: /^Collapse all$/i }).first() + const open = await close + .isVisible() + .then((value) => value) + .catch(() => false) + + const btn = page.getByRole("button", { name: /^Expand all$/i }).first() + if (open) { + await close.click() + await expect(btn).toBeVisible() + } + + await expect(btn).toBeVisible() + await btn.click() + await expect(close).toBeVisible() +} + +async function waitMark(page: Parameters[0]["page"], file: string, mark: string) { + await page.waitForFunction( + ({ file, mark }) => { + const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport') + if (!(view instanceof HTMLElement)) return false + + const head = Array.from(view.querySelectorAll("h3")).find( + (node) => node instanceof HTMLElement && node.textContent?.includes(file), + ) + if (!(head instanceof HTMLElement)) return false + + return Array.from(head.parentElement?.querySelectorAll("diffs-container") ?? []).some((host) => { + if (!(host instanceof HTMLElement)) return false + const root = host.shadowRoot + return root?.textContent?.includes(`mark ${mark}`) ?? false + }) + }, + { file, mark }, + { timeout: 60_000 }, + ) +} + +async function spot(page: Parameters[0]["page"], file: string) { + return page.evaluate((file) => { + const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport') + if (!(view instanceof HTMLElement)) return null + + const row = Array.from(view.querySelectorAll("h3")).find( + (node) => node instanceof HTMLElement && node.textContent?.includes(file), + ) + if (!(row instanceof HTMLElement)) return null + + const a = row.getBoundingClientRect() + const b = view.getBoundingClientRect() + return { + top: a.top - b.top, + y: view.scrollTop, + } + }, file) +} + +test("review keeps scroll position after a live diff update", async ({ page, withProject }) => { + test.skip(Boolean(process.env.CI), "Flaky in CI for now.") + test.setTimeout(180_000) + + const tag = `review-${Date.now()}` + const list = files(tag) + const hit = list[list.length - 4]! + const next = `${tag}-live` + + await page.setViewportSize({ width: 1600, height: 1000 }) + + await withProject(async (project) => { + const sdk = createSdk(project.directory) + + await withSession(sdk, `e2e review ${tag}`, async (session) => { + await patch(sdk, session.id, seed(list)) + + await expect + .poll( + async () => { + const info = await sdk.session.get({ sessionID: session.id }).then((res) => res.data) + return info?.summary?.files ?? 0 + }, + { timeout: 60_000 }, + ) + .toBe(list.length) + + await expect + .poll( + async () => { + const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? []) + return diff.length + }, + { timeout: 60_000 }, + ) + .toBe(list.length) + + await project.gotoSession(session.id) + await show(page) + + const tab = page.getByRole("tab", { name: /Review/i }).first() + await expect(tab).toBeVisible() + await tab.click() + + const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first() + await expect(view).toBeVisible() + const heads = page.getByRole("heading", { level: 3 }).filter({ hasText: /^review-scroll-/ }) + await expect(heads).toHaveCount(list.length, { + timeout: 60_000, + }) + + await expand(page) + await waitMark(page, hit.file, hit.mark) + + const row = page + .getByRole("heading", { level: 3, name: new RegExp(hit.file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) }) + .first() + await expect(row).toBeVisible() + await row.evaluate((el) => el.scrollIntoView({ block: "center" })) + + await expect.poll(async () => (await spot(page, hit.file))?.y ?? 0).toBeGreaterThan(200) + const prev = await spot(page, hit.file) + if (!prev) throw new Error(`missing review row for ${hit.file}`) + + await patch(sdk, session.id, edit(hit.file, hit.mark, next)) + + await expect + .poll( + async () => { + const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? []) + const item = diff.find((item) => item.file === hit.file) + return typeof item?.after === "string" ? item.after : "" + }, + { timeout: 60_000 }, + ) + .toContain(`mark ${next}`) + + await waitMark(page, hit.file, next) + + await expect + .poll( + async () => { + const next = await spot(page, hit.file) + if (!next) return Number.POSITIVE_INFINITY + return Math.max(Math.abs(next.top - prev.top), Math.abs(next.y - prev.y)) + }, + { timeout: 60_000 }, + ) + .toBeLessThanOrEqual(32) + }) + }) +}) diff --git a/packages/app/e2e/session/session-undo-redo.spec.ts b/packages/app/e2e/session/session-undo-redo.spec.ts new file mode 100644 index 00000000000..eb0840f7ccf --- /dev/null +++ b/packages/app/e2e/session/session-undo-redo.spec.ts @@ -0,0 +1,233 @@ +import type { Page } from "@playwright/test" +import { test, expect } from "../fixtures" +import { withSession } from "../actions" +import { createSdk, modKey } from "../utils" +import { promptSelector } from "../selectors" + +async function seedConversation(input: { + page: Page + sdk: ReturnType + sessionID: string + token: string +}) { + const messages = async () => + await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? []) + const seeded = await messages() + const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id)) + + const prompt = input.page.locator(promptSelector) + await expect(prompt).toBeVisible() + await input.sdk.session.promptAsync({ + sessionID: input.sessionID, + noReply: true, + parts: [{ type: "text", text: input.token }], + }) + + let userMessageID: string | undefined + await expect + .poll( + async () => { + const users = (await messages()).filter( + (m) => + !userIDs.has(m.info.id) && + m.info.role === "user" && + m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)), + ) + if (users.length === 0) return false + + const user = users[users.length - 1] + if (!user) return false + userMessageID = user.info.id + return true + }, + { timeout: 90_000, intervals: [250, 500, 1_000] }, + ) + .toBe(true) + + if (!userMessageID) throw new Error("Expected a user message id") + await expect(input.page.locator(`[data-message-id="${userMessageID}"]`)).toHaveCount(1, { timeout: 30_000 }) + return { prompt, userMessageID } +} + +test("slash undo sets revert and restores prior prompt", async ({ page, withProject }) => { + test.setTimeout(120_000) + + const token = `undo_${Date.now()}` + + await withProject(async (project) => { + const sdk = createSdk(project.directory) + + await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => { + await project.gotoSession(session.id) + + const seeded = await seedConversation({ page, sdk, sessionID: session.id, token }) + + await seeded.prompt.click() + await page.keyboard.type("/undo") + + const undo = page.locator('[data-slash-id="session.undo"]').first() + await expect(undo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBe(seeded.userMessageID) + + await expect(seeded.prompt).toContainText(token) + await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0) + }) + }) +}) + +test("slash redo clears revert and restores latest state", async ({ page, withProject }) => { + test.setTimeout(120_000) + + const token = `redo_${Date.now()}` + + await withProject(async (project) => { + const sdk = createSdk(project.directory) + + await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => { + await project.gotoSession(session.id) + + const seeded = await seedConversation({ page, sdk, sessionID: session.id, token }) + + await seeded.prompt.click() + await page.keyboard.type("/undo") + + const undo = page.locator('[data-slash-id="session.undo"]').first() + await expect(undo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBe(seeded.userMessageID) + + await seeded.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/redo") + + const redo = page.locator('[data-slash-id="session.redo"]').first() + await expect(redo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBeUndefined() + + await expect(seeded.prompt).not.toContainText(token) + await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(1) + }) + }) +}) + +test("slash undo/redo traverses multi-step revert stack", async ({ page, withProject }) => { + test.setTimeout(120_000) + + const firstToken = `undo_redo_first_${Date.now()}` + const secondToken = `undo_redo_second_${Date.now()}` + + await withProject(async (project) => { + const sdk = createSdk(project.directory) + + await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => { + await project.gotoSession(session.id) + + const first = await seedConversation({ + page, + sdk, + sessionID: session.id, + token: firstToken, + }) + const second = await seedConversation({ + page, + sdk, + sessionID: session.id, + token: secondToken, + }) + + expect(first.userMessageID).not.toBe(second.userMessageID) + + const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`) + const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`) + + await expect(firstMessage).toHaveCount(1) + await expect(secondMessage).toHaveCount(1) + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/undo") + + const undo = page.locator('[data-slash-id="session.undo"]').first() + await expect(undo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBe(second.userMessageID) + + await expect(firstMessage).toHaveCount(1) + await expect(secondMessage).toHaveCount(0) + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/undo") + await expect(undo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBe(first.userMessageID) + + await expect(firstMessage).toHaveCount(0) + await expect(secondMessage).toHaveCount(0) + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/redo") + + const redo = page.locator('[data-slash-id="session.redo"]').first() + await expect(redo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBe(second.userMessageID) + + await expect(firstMessage).toHaveCount(1) + await expect(secondMessage).toHaveCount(0) + + await second.prompt.click() + await page.keyboard.press(`${modKey}+A`) + await page.keyboard.press("Backspace") + await page.keyboard.type("/redo") + await expect(redo).toBeVisible() + await page.keyboard.press("Enter") + + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, + }) + .toBeUndefined() + + await expect(firstMessage).toHaveCount(1) + await expect(secondMessage).toHaveCount(1) + }) + }) +}) diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts new file mode 100644 index 00000000000..68d99294996 --- /dev/null +++ b/packages/app/e2e/session/session.spec.ts @@ -0,0 +1,174 @@ +import { test, expect } from "../fixtures" +import { + openSidebar, + openSessionMoreMenu, + clickMenuItem, + confirmDialog, + openSharePopover, + withSession, +} from "../actions" +import { sessionItemSelector, inlineInputSelector } from "../selectors" + +const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_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, sdk, gotoSession }) => { + const stamp = Date.now() + const originalTitle = `e2e rename test ${stamp}` + const renamedTitle = `e2e renamed ${stamp}` + + await withSession(sdk, originalTitle, async (session) => { + await seedMessage(sdk, session.id) + await 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 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, sdk, gotoSession }) => { + const stamp = Date.now() + const title = `e2e archive test ${stamp}` + + await withSession(sdk, title, async (session) => { + await seedMessage(sdk, session.id) + await gotoSession(session.id) + const menu = await openSessionMoreMenu(page, session.id) + await clickMenuItem(menu, /archive/i) + + await expect + .poll( + async () => { + const data = await 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, sdk, gotoSession }) => { + const stamp = Date.now() + const title = `e2e delete test ${stamp}` + + await withSession(sdk, title, async (session) => { + await seedMessage(sdk, session.id) + await 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 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, sdk, gotoSession }) => { + test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).") + + const stamp = Date.now() + const title = `e2e share test ${stamp}` + + await withSession(sdk, title, async (session) => { + await seedMessage(sdk, session.id) + await gotoSession(session.id) + + 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 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 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/settings.spec.ts b/packages/app/e2e/settings.spec.ts deleted file mode 100644 index 09dc942cc92..00000000000 --- a/packages/app/e2e/settings.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => { - await gotoSession() - - const dialog = page.getByRole("dialog") - - await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined) - - const opened = await dialog - .waitFor({ state: "visible", timeout: 3000 }) - .then(() => true) - .catch(() => false) - - if (!opened) { - await page.getByRole("button", { name: "Settings" }).first().click() - await expect(dialog).toBeVisible() - } - - await dialog.getByRole("tab", { name: "Shortcuts" }).click() - await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible() - await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible() - - await page.keyboard.press("Escape") - - const closed = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - - if (closed) return - - await page.keyboard.press("Escape") - const closedSecond = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - - if (closedSecond) return - - await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) - await expect(dialog).toHaveCount(0) -}) diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts new file mode 100644 index 00000000000..9fc2a50ad37 --- /dev/null +++ b/packages/app/e2e/settings/settings-keybinds.spec.ts @@ -0,0 +1,389 @@ +import { test, expect } from "../fixtures" +import { openSettings, closeDialog, waitTerminalReady, withSession } from "../actions" +import { keybindButtonSelector, terminalSelector } from "../selectors" +import { modKey } from "../utils" + +test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")).first() + await expect(keybindButton).toBeVisible() + + const initialKeybind = await keybindButton.textContent() + expect(initialKeybind).toContain("B") + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyH`) + await page.waitForTimeout(100) + + const newKeybind = await keybindButton.textContent() + expect(newKeybind).toContain("H") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["sidebar.toggle"]).toBe("mod+shift+h") + + await closeDialog(page, dialog) + + const button = page.getByRole("button", { name: /toggle sidebar/i }).first() + const initiallyClosed = (await button.getAttribute("aria-expanded")) !== "true" + + await page.keyboard.press(`${modKey}+Shift+H`) + await expect(button).toHaveAttribute("aria-expanded", initiallyClosed ? "true" : "false") + + const afterToggleClosed = (await button.getAttribute("aria-expanded")) !== "true" + expect(afterToggleClosed).toBe(!initiallyClosed) + + await page.keyboard.press(`${modKey}+Shift+H`) + await expect(button).toHaveAttribute("aria-expanded", initiallyClosed ? "false" : "true") + + const finalClosed = (await button.getAttribute("aria-expanded")) !== "true" + expect(finalClosed).toBe(initiallyClosed) +}) + +test("sidebar toggle keybind guards against shortcut conflicts", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")) + await expect(keybindButton).toBeVisible() + + const initialKeybind = await keybindButton.textContent() + expect(initialKeybind).toContain("B") + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyP`) + await page.waitForTimeout(100) + + const toast = page.locator('[data-component="toast"]').last() + await expect(toast).toBeVisible() + await expect(toast).toContainText(/already/i) + + await keybindButton.click() + await expect(keybindButton).toContainText("B") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined() + + await closeDialog(page, dialog) +}) + +test("resetting all keybinds to defaults works", async ({ page, gotoSession }) => { + await page.addInitScript(() => { + localStorage.setItem("settings.v3", JSON.stringify({ keybinds: { "sidebar.toggle": "mod+shift+x" } })) + }) + + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")) + await expect(keybindButton).toBeVisible() + + const customKeybind = await keybindButton.textContent() + expect(customKeybind).toContain("X") + + const resetButton = dialog.getByRole("button", { name: "Reset to defaults" }) + await expect(resetButton).toBeVisible() + await expect(resetButton).toBeEnabled() + await resetButton.click() + await page.waitForTimeout(100) + + const restoredKeybind = await keybindButton.textContent() + expect(restoredKeybind).toContain("B") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined() + + await closeDialog(page, dialog) +}) + +test("clearing a keybind works", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")) + await expect(keybindButton).toBeVisible() + + const initialKeybind = await keybindButton.textContent() + expect(initialKeybind).toContain("B") + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press("Delete") + await page.waitForTimeout(100) + + const clearedKeybind = await keybindButton.textContent() + expect(clearedKeybind).toMatch(/unassigned|press/i) + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["sidebar.toggle"]).toBe("none") + + await closeDialog(page, dialog) + + await page.keyboard.press(`${modKey}+B`) + await page.waitForTimeout(100) + + const stillOnSession = page.url().includes("/session") + expect(stillOnSession).toBe(true) +}) + +test("changing settings open keybind works", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("settings.open")) + await expect(keybindButton).toBeVisible() + + const initialKeybind = await keybindButton.textContent() + expect(initialKeybind).toContain(",") + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Slash`) + await page.waitForTimeout(100) + + const newKeybind = await keybindButton.textContent() + expect(newKeybind).toContain("/") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["settings.open"]).toBe("mod+/") + + await closeDialog(page, dialog) + + const settingsDialog = page.getByRole("dialog") + await expect(settingsDialog).toHaveCount(0) + + await page.keyboard.press(`${modKey}+Slash`) + await page.waitForTimeout(100) + + await expect(settingsDialog).toBeVisible() + + await closeDialog(page, settingsDialog) +}) + +test("changing new session keybind works", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, "test session for keybind", async (session) => { + await gotoSession(session.id) + + const initialUrl = page.url() + expect(initialUrl).toContain(`/session/${session.id}`) + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("session.new")) + await expect(keybindButton).toBeVisible() + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyN`) + await page.waitForTimeout(100) + + const newKeybind = await keybindButton.textContent() + expect(newKeybind).toContain("N") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["session.new"]).toBe("mod+shift+n") + + await closeDialog(page, dialog) + + await page.keyboard.press(`${modKey}+Shift+N`) + await page.waitForTimeout(200) + + const newUrl = page.url() + expect(newUrl).toMatch(/\/session\/?$/) + expect(newUrl).not.toContain(session.id) + }) +}) + +test("changing file open keybind works", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("file.open")) + await expect(keybindButton).toBeVisible() + + const initialKeybind = await keybindButton.textContent() + expect(initialKeybind).toContain("P") + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyF`) + await page.waitForTimeout(100) + + const newKeybind = await keybindButton.textContent() + expect(newKeybind).toContain("F") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["file.open"]).toBe("mod+shift+f") + + await closeDialog(page, dialog) + + const filePickerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder(/search files/i) }) + await expect(filePickerDialog).toHaveCount(0) + + await page.keyboard.press(`${modKey}+Shift+F`) + await page.waitForTimeout(100) + + await expect(filePickerDialog).toBeVisible() + + await page.keyboard.press("Escape") + await expect(filePickerDialog).toHaveCount(0) +}) + +test("changing terminal toggle keybind works", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle")) + await expect(keybindButton).toBeVisible() + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+KeyY`) + await page.waitForTimeout(100) + + const newKeybind = await keybindButton.textContent() + expect(newKeybind).toContain("Y") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["terminal.toggle"]).toBe("mod+y") + + await closeDialog(page, dialog) + + const terminal = page.locator(terminalSelector) + await expect(terminal).not.toBeVisible() + + await page.keyboard.press(`${modKey}+Y`) + await waitTerminalReady(page, { term: terminal }) + + await page.keyboard.press(`${modKey}+Y`) + await expect(terminal).not.toBeVisible() +}) + +test("terminal toggle keybind persists after reload", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle")) + await expect(keybindButton).toBeVisible() + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyY`) + await page.waitForTimeout(100) + + await expect(keybindButton).toContainText("Y") + await closeDialog(page, dialog) + + await page.reload() + + await expect + .poll(async () => { + return await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + if (!raw) return + const parsed = JSON.parse(raw) + return parsed?.keybinds?.["terminal.toggle"] + }) + }) + .toBe("mod+shift+y") + + const reloaded = await openSettings(page) + await reloaded.getByRole("tab", { name: "Shortcuts" }).click() + const reloadedKeybind = reloaded.locator(keybindButtonSelector("terminal.toggle")).first() + await expect(reloadedKeybind).toContainText("Y") + await closeDialog(page, reloaded) +}) + +test("changing command palette keybind works", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + + const keybindButton = dialog.locator(keybindButtonSelector("command.palette")) + await expect(keybindButton).toBeVisible() + + const initialKeybind = await keybindButton.textContent() + expect(initialKeybind).toContain("P") + + await keybindButton.click() + await expect(keybindButton).toHaveText(/press/i) + + await page.keyboard.press(`${modKey}+Shift+KeyK`) + await page.waitForTimeout(100) + + const newKeybind = await keybindButton.textContent() + expect(newKeybind).toContain("K") + + const stored = await page.evaluate(() => { + const raw = localStorage.getItem("settings.v3") + return raw ? JSON.parse(raw) : null + }) + expect(stored?.keybinds?.["command.palette"]).toBe("mod+shift+k") + + await closeDialog(page, dialog) + + const palette = page.getByRole("dialog").filter({ has: page.getByRole("textbox").first() }) + await expect(palette).toHaveCount(0) + + await page.keyboard.press(`${modKey}+Shift+K`) + await page.waitForTimeout(100) + + await expect(palette).toBeVisible() + await expect(palette.getByRole("textbox").first()).toBeVisible() + + await page.keyboard.press("Escape") + await expect(palette).toHaveCount(0) +}) diff --git a/packages/app/e2e/settings/settings-models.spec.ts b/packages/app/e2e/settings/settings-models.spec.ts new file mode 100644 index 00000000000..f7397abe867 --- /dev/null +++ b/packages/app/e2e/settings/settings-models.spec.ts @@ -0,0 +1,122 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { closeDialog, openSettings } from "../actions" + +test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + + const command = page.locator('[data-slash-id="model.choose"]') + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const picker = page.getByRole("dialog") + await expect(picker).toBeVisible() + + const target = picker.locator('[data-slot="list-item"]').first() + await expect(target).toBeVisible() + + const key = await target.getAttribute("data-key") + if (!key) throw new Error("Failed to resolve model key from list item") + + const name = (await target.locator("span").first().innerText()).trim() + if (!name) throw new Error("Failed to resolve model name from list item") + + await page.keyboard.press("Escape") + await expect(picker).toHaveCount(0) + + const settings = await openSettings(page) + + await settings.getByRole("tab", { name: "Models" }).click() + const search = settings.getByPlaceholder("Search models") + await expect(search).toBeVisible() + await search.fill(name) + + const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first() + const input = toggle.locator('[data-slot="switch-input"]') + await expect(toggle).toBeVisible() + await expect(input).toHaveAttribute("aria-checked", "true") + await toggle.locator('[data-slot="switch-control"]').click() + await expect(input).toHaveAttribute("aria-checked", "false") + + await closeDialog(page, settings) + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const pickerAgain = page.getByRole("dialog") + await expect(pickerAgain).toBeVisible() + await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible() + + await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0) + + await page.keyboard.press("Escape") + await expect(pickerAgain).toHaveCount(0) +}) + +test("showing a hidden model restores it to the model picker", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + + const command = page.locator('[data-slash-id="model.choose"]') + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const picker = page.getByRole("dialog") + await expect(picker).toBeVisible() + + const target = picker.locator('[data-slot="list-item"]').first() + await expect(target).toBeVisible() + + const key = await target.getAttribute("data-key") + if (!key) throw new Error("Failed to resolve model key from list item") + + const name = (await target.locator("span").first().innerText()).trim() + if (!name) throw new Error("Failed to resolve model name from list item") + + await page.keyboard.press("Escape") + await expect(picker).toHaveCount(0) + + const settings = await openSettings(page) + + await settings.getByRole("tab", { name: "Models" }).click() + const search = settings.getByPlaceholder("Search models") + await expect(search).toBeVisible() + await search.fill(name) + + const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first() + const input = toggle.locator('[data-slot="switch-input"]') + await expect(toggle).toBeVisible() + await expect(input).toHaveAttribute("aria-checked", "true") + + await toggle.locator('[data-slot="switch-control"]').click() + await expect(input).toHaveAttribute("aria-checked", "false") + + await toggle.locator('[data-slot="switch-control"]').click() + await expect(input).toHaveAttribute("aria-checked", "true") + + await closeDialog(page, settings) + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const pickerAgain = page.getByRole("dialog") + await expect(pickerAgain).toBeVisible() + + await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toBeVisible() + + await page.keyboard.press("Escape") + await expect(pickerAgain).toHaveCount(0) +}) diff --git a/packages/app/e2e/settings/settings-providers.spec.ts b/packages/app/e2e/settings/settings-providers.spec.ts new file mode 100644 index 00000000000..a55eb34981e --- /dev/null +++ b/packages/app/e2e/settings/settings-providers.spec.ts @@ -0,0 +1,136 @@ +import { test, expect } from "../fixtures" +import { closeDialog, openSettings } from "../actions" + +test("custom provider form can be filled and validates input", async ({ page, gotoSession }) => { + await gotoSession() + + const settings = await openSettings(page) + await settings.getByRole("tab", { name: "Providers" }).click() + + const customProviderSection = settings.locator('[data-component="custom-provider-section"]') + await expect(customProviderSection).toBeVisible() + + const connectButton = customProviderSection.getByRole("button", { name: "Connect" }) + await connectButton.click() + + const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") }) + await expect(providerDialog).toBeVisible() + + await providerDialog.getByLabel("Provider ID").fill("test-provider") + await providerDialog.getByLabel("Display name").fill("Test Provider") + await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/fake") + await providerDialog.getByLabel("API key").fill("fake-key") + + await providerDialog.getByPlaceholder("model-id").first().fill("test-model") + await providerDialog.getByPlaceholder("Display Name").first().fill("Test Model") + + await expect(providerDialog.getByRole("textbox", { name: "Provider ID" })).toHaveValue("test-provider") + await expect(providerDialog.getByRole("textbox", { name: "Display name" })).toHaveValue("Test Provider") + await expect(providerDialog.getByRole("textbox", { name: "Base URL" })).toHaveValue("http://localhost:9999/fake") + await expect(providerDialog.getByRole("textbox", { name: "API key" })).toHaveValue("fake-key") + await expect(providerDialog.getByPlaceholder("model-id").first()).toHaveValue("test-model") + await expect(providerDialog.getByPlaceholder("Display Name").first()).toHaveValue("Test Model") + + await page.keyboard.press("Escape") + await expect(providerDialog).toHaveCount(0) + + await closeDialog(page, settings) +}) + +test("custom provider form shows validation errors", async ({ page, gotoSession }) => { + await gotoSession() + + const settings = await openSettings(page) + await settings.getByRole("tab", { name: "Providers" }).click() + + const customProviderSection = settings.locator('[data-component="custom-provider-section"]') + await customProviderSection.getByRole("button", { name: "Connect" }).click() + + const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") }) + await expect(providerDialog).toBeVisible() + + await providerDialog.getByLabel("Provider ID").fill("invalid provider id") + await providerDialog.getByLabel("Base URL").fill("not-a-url") + + await providerDialog.getByRole("button", { name: /submit|save/i }).click() + + await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /lowercase/i })).toBeVisible() + await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /http/i })).toBeVisible() + + await page.keyboard.press("Escape") + await expect(providerDialog).toHaveCount(0) + + await closeDialog(page, settings) +}) + +test("custom provider form can add and remove models", async ({ page, gotoSession }) => { + await gotoSession() + + const settings = await openSettings(page) + await settings.getByRole("tab", { name: "Providers" }).click() + + const customProviderSection = settings.locator('[data-component="custom-provider-section"]') + await customProviderSection.getByRole("button", { name: "Connect" }).click() + + const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") }) + await expect(providerDialog).toBeVisible() + + await providerDialog.getByLabel("Provider ID").fill("multi-model-test") + await providerDialog.getByLabel("Display name").fill("Multi Model Test") + await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/multi") + + await providerDialog.getByPlaceholder("model-id").first().fill("model-1") + await providerDialog.getByPlaceholder("Display Name").first().fill("Model 1") + + const idInputsBefore = await providerDialog.getByPlaceholder("model-id").count() + await providerDialog.getByRole("button", { name: "Add model" }).click() + const idInputsAfter = await providerDialog.getByPlaceholder("model-id").count() + expect(idInputsAfter).toBe(idInputsBefore + 1) + + await providerDialog.getByPlaceholder("model-id").nth(1).fill("model-2") + await providerDialog.getByPlaceholder("Display Name").nth(1).fill("Model 2") + + await expect(providerDialog.getByPlaceholder("model-id").nth(1)).toHaveValue("model-2") + await expect(providerDialog.getByPlaceholder("Display Name").nth(1)).toHaveValue("Model 2") + + await page.keyboard.press("Escape") + await expect(providerDialog).toHaveCount(0) + + await closeDialog(page, settings) +}) + +test("custom provider form can add and remove headers", async ({ page, gotoSession }) => { + await gotoSession() + + const settings = await openSettings(page) + await settings.getByRole("tab", { name: "Providers" }).click() + + const customProviderSection = settings.locator('[data-component="custom-provider-section"]') + await customProviderSection.getByRole("button", { name: "Connect" }).click() + + const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") }) + await expect(providerDialog).toBeVisible() + + await providerDialog.getByLabel("Provider ID").fill("header-test") + await providerDialog.getByLabel("Display name").fill("Header Test") + await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/headers") + + await providerDialog.getByPlaceholder("model-id").first().fill("model-x") + await providerDialog.getByPlaceholder("Display Name").first().fill("Model X") + + const headerInputsBefore = await providerDialog.getByPlaceholder("Header-Name").count() + await providerDialog.getByRole("button", { name: "Add header" }).click() + const headerInputsAfter = await providerDialog.getByPlaceholder("Header-Name").count() + expect(headerInputsAfter).toBe(headerInputsBefore + 1) + + await providerDialog.getByPlaceholder("Header-Name").first().fill("Authorization") + await providerDialog.getByPlaceholder("value").first().fill("Bearer token123") + + await expect(providerDialog.getByPlaceholder("Header-Name").first()).toHaveValue("Authorization") + await expect(providerDialog.getByPlaceholder("value").first()).toHaveValue("Bearer token123") + + await page.keyboard.press("Escape") + await expect(providerDialog).toHaveCount(0) + + await closeDialog(page, settings) +}) diff --git a/packages/app/e2e/settings/settings.spec.ts b/packages/app/e2e/settings/settings.spec.ts new file mode 100644 index 00000000000..f25e91a315e --- /dev/null +++ b/packages/app/e2e/settings/settings.spec.ts @@ -0,0 +1,519 @@ +import { test, expect, settingsKey } from "../fixtures" +import { closeDialog, openSettings } from "../actions" +import { + settingsColorSchemeSelector, + settingsFontSelector, + settingsLanguageSelectSelector, + settingsNotificationsAgentSelector, + settingsNotificationsErrorsSelector, + settingsNotificationsPermissionsSelector, + settingsReleaseNotesSelector, + settingsSoundsAgentSelector, + settingsSoundsErrorsSelector, + settingsSoundsPermissionsSelector, + settingsThemeSelector, + settingsUpdatesStartupSelector, +} from "../selectors" + +test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible() + await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible() + + await closeDialog(page, dialog) +}) + +test("changing language updates settings labels", async ({ page, gotoSession }) => { + await page.addInitScript(() => { + localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" })) + }) + + await gotoSession() + + const dialog = await openSettings(page) + + const heading = dialog.getByRole("heading", { level: 2 }) + await expect(heading).toHaveText("General") + + const select = dialog.locator(settingsLanguageSelectSelector) + await expect(select).toBeVisible() + await select.locator('[data-slot="select-select-trigger"]').click() + + await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click() + + await expect(heading).toHaveText("Allgemein") + + await select.locator('[data-slot="select-select-trigger"]').click() + await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click() + await expect(heading).toHaveText("General") +}) + +test("changing color scheme persists in localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const select = dialog.locator(settingsColorSchemeSelector) + await expect(select).toBeVisible() + + await select.locator('[data-slot="select-select-trigger"]').click() + await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click() + + const colorScheme = await page.evaluate(() => { + return document.documentElement.getAttribute("data-color-scheme") + }) + expect(colorScheme).toBe("dark") + + await select.locator('[data-slot="select-select-trigger"]').click() + await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Light" }).click() + + const lightColorScheme = await page.evaluate(() => { + return document.documentElement.getAttribute("data-color-scheme") + }) + expect(lightColorScheme).toBe("light") +}) + +test("changing theme persists in localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const select = dialog.locator(settingsThemeSelector) + await expect(select).toBeVisible() + + const currentThemeId = await page.evaluate(() => { + return document.documentElement.getAttribute("data-theme") + }) + const currentTheme = (await select.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + + await select.locator('[data-slot="select-select-trigger"]').click() + + const items = page.locator('[data-slot="select-select-item"]') + const count = await items.count() + expect(count).toBeGreaterThan(1) + + const nextTheme = (await items.locator('[data-slot="select-select-item-label"]').allTextContents()) + .map((x) => x.trim()) + .find((x) => x && x !== currentTheme) + expect(nextTheme).toBeTruthy() + + await items.filter({ hasText: nextTheme! }).first().click() + + await page.keyboard.press("Escape") + + const storedThemeId = await page.evaluate(() => { + return localStorage.getItem("opencode-theme-id") + }) + + expect(storedThemeId).not.toBeNull() + expect(storedThemeId).not.toBe(currentThemeId) + + const dataTheme = await page.evaluate(() => { + return document.documentElement.getAttribute("data-theme") + }) + expect(dataTheme).toBe(storedThemeId) +}) + +test("legacy oc-1 theme migrates to oc-2", async ({ page, gotoSession }) => { + await page.addInitScript(() => { + 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;") + }) + + await gotoSession() + + await expect(page.locator("html")).toHaveAttribute("data-theme", "oc-2") + + await expect + .poll(async () => { + return await page.evaluate(() => { + return localStorage.getItem("opencode-theme-id") + }) + }) + .toBe("oc-2") + + await expect + .poll(async () => { + return await page.evaluate(() => { + return localStorage.getItem("opencode-theme-css-light") + }) + }) + .toBeNull() + + await expect + .poll(async () => { + return await page.evaluate(() => { + return localStorage.getItem("opencode-theme-css-dark") + }) + }) + .toBeNull() +}) + +test("changing font persists in localStorage and updates CSS variable", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const select = dialog.locator(settingsFontSelector) + await expect(select).toBeVisible() + + const initialFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono") + }) + expect(initialFontFamily).toContain("IBM Plex Mono") + + await select.locator('[data-slot="select-select-trigger"]').click() + + const items = page.locator('[data-slot="select-select-item"]') + await items.nth(2).click() + + await page.waitForTimeout(100) + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.appearance?.font).not.toBe("ibm-plex-mono") + + const newFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono") + }) + expect(newFontFamily).not.toBe(initialFontFamily) +}) + +test("color scheme and font rehydrate after reload", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + + const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector) + await expect(colorSchemeSelect).toBeVisible() + await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click() + await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click() + await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark") + + const fontSelect = dialog.locator(settingsFontSelector) + await expect(fontSelect).toBeVisible() + + const initialFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + + const initialSettings = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + const currentFont = + (await fontSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + await fontSelect.locator('[data-slot="select-select-trigger"]').click() + + const fontItems = page.locator('[data-slot="select-select-item"]') + expect(await fontItems.count()).toBeGreaterThan(1) + + if (currentFont) { + await fontItems.filter({ hasNotText: currentFont }).first().click() + } + if (!currentFont) { + await fontItems.nth(1).click() + } + + await expect + .poll(async () => { + return await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + }) + .toMatchObject({ + appearance: { + font: expect.any(String), + }, + }) + + const updatedSettings = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + const updatedFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + expect(updatedFontFamily).not.toBe(initialFontFamily) + expect(updatedSettings?.appearance?.font).not.toBe(initialSettings?.appearance?.font) + + await closeDialog(page, dialog) + await page.reload() + + await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark") + + await expect + .poll(async () => { + return await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + }) + .toMatchObject({ + appearance: { + font: updatedSettings?.appearance?.font, + }, + }) + + const rehydratedSettings = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + await expect + .poll(async () => { + return await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + }) + .not.toBe(initialFontFamily) + + const rehydratedFontFamily = await page.evaluate(() => { + return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim() + }) + expect(rehydratedFontFamily).not.toBe(initialFontFamily) + expect(rehydratedSettings?.appearance?.font).toBe(updatedSettings?.appearance?.font) +}) + +test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const switchContainer = dialog.locator(settingsNotificationsAgentSelector) + await expect(switchContainer).toBeVisible() + + const toggleInput = switchContainer.locator('[data-slot="switch-input"]') + const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(initialState).toBe(true) + + await switchContainer.locator('[data-slot="switch-control"]').click() + await page.waitForTimeout(100) + + const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(newState).toBe(false) + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.notifications?.agent).toBe(false) +}) + +test("toggling notification permissions switch updates localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const switchContainer = dialog.locator(settingsNotificationsPermissionsSelector) + await expect(switchContainer).toBeVisible() + + const toggleInput = switchContainer.locator('[data-slot="switch-input"]') + const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(initialState).toBe(true) + + await switchContainer.locator('[data-slot="switch-control"]').click() + await page.waitForTimeout(100) + + const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(newState).toBe(false) + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.notifications?.permissions).toBe(false) +}) + +test("toggling notification errors switch updates localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const switchContainer = dialog.locator(settingsNotificationsErrorsSelector) + await expect(switchContainer).toBeVisible() + + const toggleInput = switchContainer.locator('[data-slot="switch-input"]') + const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(initialState).toBe(false) + + await switchContainer.locator('[data-slot="switch-control"]').click() + await page.waitForTimeout(100) + + const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(newState).toBe(true) + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.notifications?.errors).toBe(true) +}) + +test("changing sound agent selection persists in localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const select = dialog.locator(settingsSoundsAgentSelector) + await expect(select).toBeVisible() + + await select.locator('[data-slot="select-select-trigger"]').click() + + const items = page.locator('[data-slot="select-select-item"]') + await items.nth(2).click() + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.sounds?.agent).not.toBe("staplebops-01") +}) + +test("selecting none disables agent sound", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const select = dialog.locator(settingsSoundsAgentSelector) + const trigger = select.locator('[data-slot="select-select-trigger"]') + await expect(select).toBeVisible() + await expect(trigger).toBeEnabled() + + await trigger.click() + const items = page.locator('[data-slot="select-select-item"]') + await expect(items.first()).toBeVisible() + await items.first().click() + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.sounds?.agentEnabled).toBe(false) +}) + +test("changing permissions and errors sounds updates localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const permissionsSelect = dialog.locator(settingsSoundsPermissionsSelector) + const errorsSelect = dialog.locator(settingsSoundsErrorsSelector) + await expect(permissionsSelect).toBeVisible() + await expect(errorsSelect).toBeVisible() + + const initial = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + const permissionsCurrent = + (await permissionsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + await permissionsSelect.locator('[data-slot="select-select-trigger"]').click() + const permissionItems = page.locator('[data-slot="select-select-item"]') + expect(await permissionItems.count()).toBeGreaterThan(1) + if (permissionsCurrent) { + await permissionItems.filter({ hasNotText: permissionsCurrent }).first().click() + } + if (!permissionsCurrent) { + await permissionItems.nth(1).click() + } + + const errorsCurrent = + (await errorsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? "" + await errorsSelect.locator('[data-slot="select-select-trigger"]').click() + const errorItems = page.locator('[data-slot="select-select-item"]') + expect(await errorItems.count()).toBeGreaterThan(1) + if (errorsCurrent) { + await errorItems.filter({ hasNotText: errorsCurrent }).first().click() + } + if (!errorsCurrent) { + await errorItems.nth(1).click() + } + + await expect + .poll(async () => { + return await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + }) + .toMatchObject({ + sounds: { + permissions: expect.any(String), + errors: expect.any(String), + }, + }) + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.sounds?.permissions).not.toBe(initial?.sounds?.permissions) + expect(stored?.sounds?.errors).not.toBe(initial?.sounds?.errors) +}) + +test("toggling updates startup switch updates localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const switchContainer = dialog.locator(settingsUpdatesStartupSelector) + await expect(switchContainer).toBeVisible() + + const toggleInput = switchContainer.locator('[data-slot="switch-input"]') + + const isDisabled = await toggleInput.evaluate((el: HTMLInputElement) => el.disabled) + if (isDisabled) { + test.skip() + return + } + + const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(initialState).toBe(true) + + await switchContainer.locator('[data-slot="switch-control"]').click() + await page.waitForTimeout(100) + + const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(newState).toBe(false) + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.updates?.startup).toBe(false) +}) + +test("toggling release notes switch updates localStorage", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openSettings(page) + const switchContainer = dialog.locator(settingsReleaseNotesSelector) + await expect(switchContainer).toBeVisible() + + const toggleInput = switchContainer.locator('[data-slot="switch-input"]') + const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(initialState).toBe(true) + + await switchContainer.locator('[data-slot="switch-control"]').click() + await page.waitForTimeout(100) + + const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked) + expect(newState).toBe(false) + + const stored = await page.evaluate((key) => { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + }, settingsKey) + + expect(stored?.general?.releaseNotes).toBe(false) +}) diff --git a/packages/app/e2e/sidebar.spec.ts b/packages/app/e2e/sidebar.spec.ts deleted file mode 100644 index 925590f5106..00000000000 --- a/packages/app/e2e/sidebar.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { - await gotoSession() - - const main = page.locator("main") - const closedClass = /xl:border-l/ - const isClosed = await main.evaluate((node) => node.className.includes("xl:border-l")) - - if (isClosed) { - await page.keyboard.press(`${modKey}+B`) - await expect(main).not.toHaveClass(closedClass) - } - - await page.keyboard.press(`${modKey}+B`) - await expect(main).toHaveClass(closedClass) - - await page.keyboard.press(`${modKey}+B`) - await expect(main).not.toHaveClass(closedClass) -}) diff --git a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts new file mode 100644 index 00000000000..d10fca0e490 --- /dev/null +++ b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from "../fixtures" +import { cleanupSession, closeSidebar, hoverSessionItem } from "../actions" +import { projectSwitchSelector } from "../selectors" + +test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => { + const stamp = Date.now() + + const one = await sdk.session.create({ title: `e2e sidebar popover archive 1 ${stamp}` }).then((r) => r.data) + const two = await sdk.session.create({ title: `e2e sidebar popover archive 2 ${stamp}` }).then((r) => r.data) + + if (!one?.id) throw new Error("Session create did not return an id") + if (!two?.id) throw new Error("Session create did not return an id") + + try { + await gotoSession(one.id) + await closeSidebar(page) + + const oneItem = page.locator(`[data-session-id="${one.id}"]`).last() + const twoItem = page.locator(`[data-session-id="${two.id}"]`).last() + + const project = page.locator(projectSwitchSelector(slug)).first() + await expect(project).toBeVisible() + await project.hover() + + await expect(oneItem).toBeVisible() + await expect(twoItem).toBeVisible() + + const item = await hoverSessionItem(page, one.id) + await item + .getByRole("button", { name: /archive/i }) + .first() + .click() + + await expect(twoItem).toBeVisible() + } finally { + await cleanupSession({ sdk, sessionID: one.id }) + await cleanupSession({ sdk, sessionID: two.id }) + } +}) diff --git a/packages/app/e2e/sidebar/sidebar-session-links.spec.ts b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts new file mode 100644 index 00000000000..22f98e94caf --- /dev/null +++ b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from "../fixtures" +import { cleanupSession, openSidebar, withSession } from "../actions" +import { promptSelector } from "../selectors" + +test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }) => { + const stamp = Date.now() + + const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data) + const two = await sdk.session.create({ title: `e2e sidebar nav 2 ${stamp}` }).then((r) => r.data) + + if (!one?.id) throw new Error("Session create did not return an id") + if (!two?.id) throw new Error("Session create did not return an id") + + try { + await gotoSession(one.id) + + await openSidebar(page) + + const target = page.locator(`[data-session-id="${two.id}"] a`).first() + await expect(target).toBeVisible() + await target.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/) + } finally { + await cleanupSession({ sdk, sessionID: one.id }) + await cleanupSession({ sdk, sessionID: two.id }) + } +}) diff --git a/packages/app/e2e/sidebar/sidebar.spec.ts b/packages/app/e2e/sidebar/sidebar.spec.ts new file mode 100644 index 00000000000..c6bf3fa9ab0 --- /dev/null +++ b/packages/app/e2e/sidebar/sidebar.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from "../fixtures" +import { openSidebar, toggleSidebar, withSession } from "../actions" + +test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { + await gotoSession() + + await openSidebar(page) + const button = page.getByRole("button", { name: /toggle sidebar/i }).first() + await expect(button).toHaveAttribute("aria-expanded", "true") + + await toggleSidebar(page) + await expect(button).toHaveAttribute("aria-expanded", "false") + + await toggleSidebar(page) + await expect(button).toHaveAttribute("aria-expanded", "true") +}) + +test("sidebar collapsed state persists across navigation and reload", async ({ page, sdk, gotoSession }) => { + await withSession(sdk, "sidebar persist session 1", async (session1) => { + await withSession(sdk, "sidebar persist session 2", async (session2) => { + await gotoSession(session1.id) + + await openSidebar(page) + const button = page.getByRole("button", { name: /toggle sidebar/i }).first() + await toggleSidebar(page) + await expect(button).toHaveAttribute("aria-expanded", "false") + + await gotoSession(session2.id) + await expect(button).toHaveAttribute("aria-expanded", "false") + + await page.reload() + await expect(button).toHaveAttribute("aria-expanded", "false") + + const opened = await page.evaluate( + () => JSON.parse(localStorage.getItem("opencode.global.dat:layout") ?? "{}").sidebar?.opened, + ) + await expect(opened).toBe(false) + }) + }) +}) diff --git a/packages/app/e2e/status/status-popover.spec.ts b/packages/app/e2e/status/status-popover.spec.ts new file mode 100644 index 00000000000..d53578a4910 --- /dev/null +++ b/packages/app/e2e/status/status-popover.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from "../fixtures" +import { openStatusPopover } from "../actions" + +test("status popover opens and shows tabs", async ({ page, gotoSession }) => { + await gotoSession() + + const { popoverBody } = await openStatusPopover(page) + + await expect(popoverBody.getByRole("tab", { name: /servers/i })).toBeVisible() + await expect(popoverBody.getByRole("tab", { name: /mcp/i })).toBeVisible() + await expect(popoverBody.getByRole("tab", { name: /lsp/i })).toBeVisible() + await expect(popoverBody.getByRole("tab", { name: /plugins/i })).toBeVisible() + + await page.keyboard.press("Escape") + await expect(popoverBody).toHaveCount(0) +}) + +test("status popover servers tab shows current server", async ({ page, gotoSession }) => { + await gotoSession() + + const { popoverBody } = await openStatusPopover(page) + + const serversTab = popoverBody.getByRole("tab", { name: /servers/i }) + await expect(serversTab).toHaveAttribute("aria-selected", "true") + + const serverList = popoverBody.locator('[role="tabpanel"]').first() + await expect(serverList.locator("button").first()).toBeVisible() +}) + +test("status popover can switch to mcp tab", async ({ page, gotoSession }) => { + await gotoSession() + + const { popoverBody } = await openStatusPopover(page) + + const mcpTab = popoverBody.getByRole("tab", { name: /mcp/i }) + await mcpTab.click() + + const ariaSelected = await mcpTab.getAttribute("aria-selected") + expect(ariaSelected).toBe("true") + + const mcpContent = popoverBody.locator('[role="tabpanel"]:visible').first() + await expect(mcpContent).toBeVisible() +}) + +test("status popover can switch to lsp tab", async ({ page, gotoSession }) => { + await gotoSession() + + const { popoverBody } = await openStatusPopover(page) + + const lspTab = popoverBody.getByRole("tab", { name: /lsp/i }) + await lspTab.click() + + const ariaSelected = await lspTab.getAttribute("aria-selected") + expect(ariaSelected).toBe("true") + + const lspContent = popoverBody.locator('[role="tabpanel"]:visible').first() + await expect(lspContent).toBeVisible() +}) + +test("status popover can switch to plugins tab", async ({ page, gotoSession }) => { + await gotoSession() + + const { popoverBody } = await openStatusPopover(page) + + const pluginsTab = popoverBody.getByRole("tab", { name: /plugins/i }) + await pluginsTab.click() + + const ariaSelected = await pluginsTab.getAttribute("aria-selected") + expect(ariaSelected).toBe("true") + + const pluginsContent = popoverBody.locator('[role="tabpanel"]:visible').first() + await expect(pluginsContent).toBeVisible() +}) + +test("status popover closes on escape", async ({ page, gotoSession }) => { + await gotoSession() + + const { popoverBody } = await openStatusPopover(page) + await expect(popoverBody).toBeVisible() + + await page.keyboard.press("Escape") + await expect(popoverBody).toHaveCount(0) +}) + +test("status popover closes when clicking outside", async ({ page, gotoSession }) => { + await gotoSession() + + const { popoverBody } = await openStatusPopover(page) + await expect(popoverBody).toBeVisible() + + await page.getByRole("main").click({ position: { x: 5, y: 5 } }) + + await expect(popoverBody).toHaveCount(0) +}) diff --git a/packages/app/e2e/terminal-init.spec.ts b/packages/app/e2e/terminal-init.spec.ts deleted file mode 100644 index cfde2d01930..00000000000 --- a/packages/app/e2e/terminal-init.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector, terminalSelector, terminalToggleKey } from "./utils" - -test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => { - await gotoSession() - - const terminals = page.locator(terminalSelector) - const opened = await terminals.first().isVisible() - - if (!opened) { - await page.keyboard.press(terminalToggleKey) - } - - await expect(terminals.first()).toBeVisible() - await expect(terminals.first().locator("textarea")).toHaveCount(1) - await expect(terminals).toHaveCount(1) - - // Ghostty captures a lot of keybinds when focused; move focus back - // to the app shell before triggering `terminal.new`. - await page.locator(promptSelector).click() - await page.keyboard.press("Control+Alt+T") - - await expect(terminals).toHaveCount(2) - await expect(terminals.nth(1).locator("textarea")).toHaveCount(1) -}) diff --git a/packages/app/e2e/terminal.spec.ts b/packages/app/e2e/terminal.spec.ts deleted file mode 100644 index fc558b63259..00000000000 --- a/packages/app/e2e/terminal.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test, expect } from "./fixtures" -import { terminalSelector, terminalToggleKey } from "./utils" - -test("terminal panel can be toggled", async ({ page, gotoSession }) => { - await gotoSession() - - const terminal = page.locator(terminalSelector) - const initiallyOpen = await terminal.isVisible() - if (initiallyOpen) { - await page.keyboard.press(terminalToggleKey) - await expect(terminal).toHaveCount(0) - } - - await page.keyboard.press(terminalToggleKey) - await expect(terminal).toBeVisible() -}) diff --git a/packages/app/e2e/terminal/terminal-init.spec.ts b/packages/app/e2e/terminal/terminal-init.spec.ts new file mode 100644 index 00000000000..d9bbfa2bed9 --- /dev/null +++ b/packages/app/e2e/terminal/terminal-init.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from "../fixtures" +import { waitTerminalReady } from "../actions" +import { promptSelector, terminalSelector } from "../selectors" +import { terminalToggleKey } from "../utils" + +test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => { + await gotoSession() + + const terminals = page.locator(terminalSelector) + const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]') + const opened = await terminals.first().isVisible() + + if (!opened) { + await page.keyboard.press(terminalToggleKey) + } + + await waitTerminalReady(page, { term: terminals.first() }) + await expect(terminals).toHaveCount(1) + + // Ghostty captures a lot of keybinds when focused; move focus back + // to the app shell before triggering `terminal.new`. + await page.locator(promptSelector).click() + await page.keyboard.press("Control+Alt+T") + + await expect(tabs).toHaveCount(2) + await expect(terminals).toHaveCount(1) + await waitTerminalReady(page, { term: terminals.first() }) +}) diff --git a/packages/app/e2e/terminal/terminal-tabs.spec.ts b/packages/app/e2e/terminal/terminal-tabs.spec.ts new file mode 100644 index 00000000000..ca1f7eee8b7 --- /dev/null +++ b/packages/app/e2e/terminal/terminal-tabs.spec.ts @@ -0,0 +1,132 @@ +import type { Page } from "@playwright/test" +import { runTerminal, waitTerminalReady } from "../actions" +import { test, expect } from "../fixtures" +import { terminalSelector } from "../selectors" +import { terminalToggleKey, workspacePersistKey } from "../utils" + +type State = { + active?: string + all: Array<{ + id: string + title: string + titleNumber: number + buffer?: string + }> +} + +async function open(page: Page) { + const terminal = page.locator(terminalSelector) + const visible = await terminal.isVisible().catch(() => false) + if (!visible) await page.keyboard.press(terminalToggleKey) + await waitTerminalReady(page, { term: terminal }) +} + +async function store(page: Page, key: string) { + return page.evaluate((key) => { + const raw = localStorage.getItem(key) + if (raw) return JSON.parse(raw) as State + + for (let i = 0; i < localStorage.length; i++) { + const next = localStorage.key(i) + if (!next?.endsWith(":workspace:terminal")) continue + const value = localStorage.getItem(next) + if (!value) continue + return JSON.parse(value) as State + } + }, key) +} + +test("inactive terminal tab buffers persist across tab switches", async ({ page, withProject }) => { + await withProject(async ({ directory, gotoSession }) => { + const key = workspacePersistKey(directory, "terminal") + const one = `E2E_TERM_ONE_${Date.now()}` + const two = `E2E_TERM_TWO_${Date.now()}` + const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]') + const first = tabs.filter({ hasText: /Terminal 1/ }).first() + const second = tabs.filter({ hasText: /Terminal 2/ }).first() + + await gotoSession() + await open(page) + + await runTerminal(page, { cmd: `echo ${one}`, token: one }) + + await page.getByRole("button", { name: /new terminal/i }).click() + await expect(tabs).toHaveCount(2) + + await runTerminal(page, { cmd: `echo ${two}`, token: two }) + + await first.click() + await expect(first).toHaveAttribute("aria-selected", "true") + + await expect + .poll( + async () => { + const state = await store(page, key) + const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? "" + const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? "" + return { + first: first.includes(one), + second: second.includes(two), + } + }, + { timeout: 5_000 }, + ) + .toEqual({ first: false, second: true }) + + await second.click() + await expect(second).toHaveAttribute("aria-selected", "true") + await expect + .poll( + async () => { + const state = await store(page, key) + const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? "" + const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? "" + return { + first: first.includes(one), + second: second.includes(two), + } + }, + { timeout: 5_000 }, + ) + .toEqual({ first: true, second: false }) + }) +}) + +test("closing the active terminal tab falls back to the previous tab", async ({ page, withProject }) => { + await withProject(async ({ directory, gotoSession }) => { + const key = workspacePersistKey(directory, "terminal") + const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]') + + await gotoSession() + await open(page) + + await page.getByRole("button", { name: /new terminal/i }).click() + await expect(tabs).toHaveCount(2) + + const second = tabs.filter({ hasText: /Terminal 2/ }).first() + await second.click() + await expect(second).toHaveAttribute("aria-selected", "true") + + await second.hover() + await page + .getByRole("button", { name: /close terminal/i }) + .nth(1) + .click({ force: true }) + + const first = tabs.filter({ hasText: /Terminal 1/ }).first() + await expect(tabs).toHaveCount(1) + await expect(first).toHaveAttribute("aria-selected", "true") + await expect + .poll( + async () => { + const state = await store(page, key) + return { + count: state?.all.length ?? 0, + first: state?.all.some((item) => item.titleNumber === 1) ?? false, + } + }, + { timeout: 15_000 }, + ) + .toEqual({ count: 1, first: true }) + }) +}) diff --git a/packages/app/e2e/terminal/terminal.spec.ts b/packages/app/e2e/terminal/terminal.spec.ts new file mode 100644 index 00000000000..768f7c18213 --- /dev/null +++ b/packages/app/e2e/terminal/terminal.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "../fixtures" +import { waitTerminalReady } from "../actions" +import { terminalSelector } from "../selectors" +import { terminalToggleKey } from "../utils" + +test("terminal panel can be toggled", async ({ page, gotoSession }) => { + await gotoSession() + + const terminal = page.locator(terminalSelector) + const initiallyOpen = await terminal.isVisible() + if (initiallyOpen) { + await page.keyboard.press(terminalToggleKey) + await expect(terminal).toHaveCount(0) + } + + await page.keyboard.press(terminalToggleKey) + await waitTerminalReady(page, { term: terminal }) +}) diff --git a/packages/app/e2e/thinking-level.spec.ts b/packages/app/e2e/thinking-level.spec.ts new file mode 100644 index 00000000000..92200933e5d --- /dev/null +++ b/packages/app/e2e/thinking-level.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from "./fixtures" +import { modelVariantCycleSelector } from "./selectors" + +test("smoke model variant cycle updates label", async ({ page, gotoSession }) => { + await gotoSession() + + await page.addStyleTag({ + content: `${modelVariantCycleSelector} { display: inline-block !important; }`, + }) + + const button = page.locator(modelVariantCycleSelector) + const exists = (await button.count()) > 0 + test.skip(!exists, "current model has no variants") + if (!exists) return + + await expect(button).toBeVisible() + + const before = (await button.innerText()).trim() + await button.click() + await expect(button).not.toHaveText(before) + + const after = (await button.innerText()).trim() + await button.click() + await expect(button).not.toHaveText(after) +}) diff --git a/packages/app/e2e/tsconfig.json b/packages/app/e2e/tsconfig.json index 76438a03cc8..18e88ddc9c7 100644 --- a/packages/app/e2e/tsconfig.json +++ b/packages/app/e2e/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "noEmit": true, - "types": ["node"] + "types": ["node", "bun"] }, "include": ["./**/*.ts"] } diff --git a/packages/app/e2e/utils.ts b/packages/app/e2e/utils.ts index eb0395950ae..f07a8d3f111 100644 --- a/packages/app/e2e/utils.ts +++ b/packages/app/e2e/utils.ts @@ -1,22 +1,41 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Encode, checksum } from "@opencode-ai/util/encode" -export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" +export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1" export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" export const serverUrl = `http://${serverHost}:${serverPort}` export const serverName = `${serverHost}:${serverPort}` +const localHosts = ["127.0.0.1", "localhost"] + +const serverLabels = (() => { + const url = new URL(serverUrl) + if (!localHosts.includes(url.hostname)) return [serverName] + return localHosts.map((host) => `${host}:${url.port}`) +})() + +export const serverNames = [...new Set(serverLabels)] + +export const serverUrls = serverNames.map((name) => `http://${name}`) + +const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + +export const serverNamePattern = new RegExp(`(?:${serverNames.map(escape).join("|")})`) + export const modKey = process.platform === "darwin" ? "Meta" : "Control" export const terminalToggleKey = "Control+Backquote" -export const promptSelector = '[data-component="prompt-input"]' -export const terminalSelector = '[data-component="terminal"]' - export function createSdk(directory?: string) { return createOpencodeClient({ baseUrl: serverUrl, directory, throwOnError: true }) } +export async function resolveDirectory(directory: string) { + return createSdk(directory) + .path.get() + .then((x) => x.data?.directory ?? directory) +} + export async function getWorktree() { const sdk = createSdk() const result = await sdk.path.get() @@ -36,3 +55,9 @@ export function dirPath(directory: string) { export function sessionPath(directory: string, sessionID?: string) { return `${dirPath(directory)}/session${sessionID ? `/${sessionID}` : ""}` } + +export function workspacePersistKey(directory: string, key: string) { + const head = (directory.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-") + const sum = checksum(directory) ?? "0" + return `opencode.workspace.${head}.${sum}.dat:workspace:${key}` +} diff --git a/packages/app/package.json b/packages/app/package.json index de0e50b5fc6..1e69a64f78c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,11 +1,12 @@ { "name": "@opencode-ai/app", - "version": "1.1.33", + "version": "1.2.24", "description": "", "type": "module", "exports": { ".": "./src/index.ts", - "./vite": "./vite.js" + "./vite": "./vite.js", + "./index.css": "./src/index.css" }, "scripts": { "typecheck": "tsgo -b", @@ -13,7 +14,9 @@ "dev": "vite", "build": "vite build", "serve": "vite preview", - "test": "playwright test", + "test": "bun run test:unit", + "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": "bun script/e2e-local.ts", "test:e2e:ui": "playwright test --ui", @@ -42,8 +45,8 @@ "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", - "@solid-primitives/i18n": "2.2.1", "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/i18n": "2.2.1", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", @@ -53,8 +56,9 @@ "@solidjs/router": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", "diff": "catalog:", + "effect": "4.0.0-beta.31", "fuzzysort": "catalog:", - "ghostty-web": "0.3.0", + "ghostty-web": "github:anomalyco/ghostty-web#main", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts index 10819e69ffe..a97c8265144 100644 --- a/packages/app/playwright.config.ts +++ b/packages/app/playwright.config.ts @@ -1,8 +1,8 @@ import { defineConfig, devices } from "@playwright/test" const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000) -const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${port}` -const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost" +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 @@ -14,7 +14,7 @@ export default defineConfig({ expect: { timeout: 10_000, }, - fullyParallel: true, + fullyParallel: process.env.PLAYWRIGHT_FULLY_PARALLEL === "1", forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]], diff --git a/packages/app/public/oc-theme-preload.js b/packages/app/public/oc-theme-preload.js index f8c71049619..36fa5d726af 100644 --- a/packages/app/public/oc-theme-preload.js +++ b/packages/app/public/oc-theme-preload.js @@ -1,6 +1,13 @@ ;(function () { - var themeId = localStorage.getItem("opencode-theme-id") - if (!themeId) return + 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) @@ -9,9 +16,9 @@ document.documentElement.dataset.theme = themeId document.documentElement.dataset.colorScheme = mode - if (themeId === "oc-1") return + if (themeId === "oc-2") return - var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode) + var css = localStorage.getItem("opencode-theme-css-" + mode) if (css) { var style = document.createElement("style") style.id = "oc-theme-preload" diff --git a/packages/app/script/e2e-local.ts b/packages/app/script/e2e-local.ts index aac2c15bb76..9a83411b1d2 100644 --- a/packages/app/script/e2e-local.ts +++ b/packages/app/script/e2e-local.ts @@ -45,7 +45,6 @@ async function waitForHealth(url: string) { const appDir = process.cwd() const repoDir = path.resolve(appDir, "../..") const opencodeDir = path.join(repoDir, "packages", "opencode") -const modelsJson = path.join(opencodeDir, "test", "tool", "fixtures", "models-api.json") const extraArgs = (() => { const args = process.argv.slice(2) @@ -56,12 +55,11 @@ const extraArgs = (() => { const [serverPort, webPort] = await Promise.all([freePort(), freePort()]) const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-")) +const keepSandbox = process.env.OPENCODE_E2E_KEEP_SANDBOX === "1" const serverEnv = { ...process.env, - MODELS_DEV_API_JSON: modelsJson, - OPENCODE_DISABLE_MODELS_FETCH: "true", - OPENCODE_DISABLE_SHARE: "true", + OPENCODE_DISABLE_SHARE: process.env.OPENCODE_DISABLE_SHARE ?? "true", OPENCODE_DISABLE_LSP_DOWNLOAD: "true", OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true", @@ -86,58 +84,96 @@ const runnerEnv = { PLAYWRIGHT_PORT: String(webPort), } satisfies Record -const seed = Bun.spawn(["bun", "script/seed-e2e.ts"], { - cwd: opencodeDir, - env: serverEnv, - stdout: "inherit", - stderr: "inherit", -}) +let seed: ReturnType | undefined +let runner: ReturnType | undefined +let server: { stop: () => Promise | void } | undefined +let inst: { Instance: { disposeAll: () => Promise | void } } | undefined +let cleaned = false + +const cleanup = async () => { + if (cleaned) return + cleaned = true + + if (seed && seed.exitCode === null) seed.kill("SIGTERM") + if (runner && runner.exitCode === null) runner.kill("SIGTERM") + + const jobs = [ + inst?.Instance.disposeAll(), + server?.stop(), + keepSandbox ? undefined : fs.rm(sandbox, { recursive: true, force: true }), + ].filter(Boolean) + await Promise.allSettled(jobs) +} -const seedExit = await seed.exited -if (seedExit !== 0) { - process.exit(seedExit) +const shutdown = (code: number, reason: string) => { + process.exitCode = code + void cleanup().finally(() => { + console.error(`e2e-local shutdown: ${reason}`) + process.exit(code) + }) } -Object.assign(process.env, serverEnv) -process.env.AGENT = "1" -process.env.OPENCODE = "1" +const reportInternalError = (reason: string, error: unknown) => { + console.warn(`e2e-local ignored server error: ${reason}`) + console.warn(error) +} -const log = await import("../../opencode/src/util/log") -const install = await import("../../opencode/src/installation") -await log.Log.init({ - print: true, - dev: install.Installation.isLocal(), - level: "WARN", +process.once("SIGINT", () => shutdown(130, "SIGINT")) +process.once("SIGTERM", () => shutdown(143, "SIGTERM")) +process.once("SIGHUP", () => shutdown(129, "SIGHUP")) +process.once("uncaughtException", (error) => { + reportInternalError("uncaughtException", error) +}) +process.once("unhandledRejection", (error) => { + reportInternalError("unhandledRejection", error) }) -const servermod = await import("../../opencode/src/server/server") -const inst = await import("../../opencode/src/project/instance") -const server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" }) -console.log(`opencode server listening on http://127.0.0.1:${serverPort}`) +let code = 1 -const result = await (async () => { - try { - await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`) +try { + seed = Bun.spawn(["bun", "script/seed-e2e.ts"], { + cwd: opencodeDir, + env: serverEnv, + stdout: "inherit", + stderr: "inherit", + }) - const runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], { + const seedExit = await seed.exited + if (seedExit !== 0) { + code = seedExit + } else { + Object.assign(process.env, serverEnv) + process.env.AGENT = "1" + process.env.OPENCODE = "1" + process.env.OPENCODE_PID = String(process.pid) + + const log = await import("../../opencode/src/util/log") + const install = await import("../../opencode/src/installation") + await log.Log.init({ + print: true, + dev: install.Installation.isLocal(), + level: "WARN", + }) + + const servermod = await import("../../opencode/src/server/server") + inst = await import("../../opencode/src/project/instance") + server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" }) + console.log(`opencode server listening on http://127.0.0.1:${serverPort}`) + + await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`) + runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], { cwd: appDir, env: runnerEnv, stdout: "inherit", stderr: "inherit", }) - - return { code: await runner.exited } - } catch (error) { - return { error } - } finally { - await inst.Instance.disposeAll() - await server.stop() + code = await runner.exited } -})() - -if ("error" in result) { - console.error(result.error) - process.exit(1) +} catch (error) { + console.error(error) + code = 1 +} finally { + await cleanup() } -process.exit(result.code) +process.exit(code) diff --git a/packages/app/src/addons/serialize.ts b/packages/app/src/addons/serialize.ts index 4309a725e51..4cab55b3f2f 100644 --- a/packages/app/src/addons/serialize.ts +++ b/packages/app/src/addons/serialize.ts @@ -56,6 +56,39 @@ interface IBufferCell { 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 // ============================================================================ @@ -241,19 +274,19 @@ class StringSerializeHandler extends BaseSerializeHandler { protected _rowEnd(row: number, isLastRow: boolean): void { let rowSeparator = "" - if (this._nullCellCount > 0) { + 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) { - const nextLine = this._buffer.getLine(row + 1) + this._nullCellCount = 0 - if (!nextLine?.isWrapped) { - rowSeparator = "\r\n" - this._lastCursorRow = row + 1 - this._lastCursorCol = 0 - } + if (!isLastRow && !wrapped) { + rowSeparator = "\r\n" + this._lastCursorRow = row + 1 + this._lastCursorCol = 0 } this._allRows[this._rowIndex] = this._currentRow @@ -389,7 +422,7 @@ class StringSerializeHandler extends BaseSerializeHandler { const sgrSeq = this._diffStyle(cell, this._cursorStyle) - const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0 + const styleChanged = sgrSeq.length > 0 if (styleChanged) { if (this._nullCellCount > 0) { @@ -442,12 +475,24 @@ class StringSerializeHandler extends BaseSerializeHandler { } } - if (!excludeFinalCursorPosition) { - 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` - } + 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 } @@ -486,14 +531,13 @@ export class SerializeAddon implements ITerminalAddon { throw new Error("Cannot use addon until it has been loaded") } - const terminal = this._terminal as any - const buffer = terminal.buffer + const buffer = getTerminalBuffers(this._terminal) if (!buffer) { return "" } - const normalBuffer = buffer.normal || buffer.active + const normalBuffer = buffer.normal ?? buffer.active const altBuffer = buffer.alternate if (!normalBuffer) { @@ -521,14 +565,13 @@ export class SerializeAddon implements ITerminalAddon { throw new Error("Cannot use addon until it has been loaded") } - const terminal = this._terminal as any - const buffer = terminal.buffer + const buffer = getTerminalBuffers(this._terminal) if (!buffer) { return "" } - const activeBuffer = buffer.active || buffer.normal + const activeBuffer = buffer.active ?? buffer.normal if (!activeBuffer) { return "" } diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index d5009c8d1d1..1b7ffde46a1 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,41 +1,70 @@ import "@/index.css" -import { ErrorBoundary, Show, lazy, type ParentProps } from "solid-js" -import { Router, Route, Navigate } from "@solidjs/router" -import { MetaProvider } from "@solidjs/meta" -import { Font } from "@opencode-ai/ui/font" -import { MarkedProvider } from "@opencode-ai/ui/context/marked" -import { DiffComponentProvider } from "@opencode-ai/ui/context/diff" -import { CodeComponentProvider } from "@opencode-ai/ui/context/code" import { I18nProvider } from "@opencode-ai/ui/context" -import { Diff } from "@opencode-ai/ui/diff" -import { Code } from "@opencode-ai/ui/code" +import { 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" +import { MetaProvider } from "@solidjs/meta" +import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" +import { type Duration, Effect } from "effect" +import { + type Component, + 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 { PermissionProvider } from "@/context/permission" +import { HighlightsProvider } from "@/context/highlights" +import { LanguageProvider, useLanguage } from "@/context/language" import { LayoutProvider } from "@/context/layout" -import { GlobalSDKProvider } from "@/context/global-sdk" -import { ServerProvider, useServer } from "@/context/server" -import { SettingsProvider } from "@/context/settings" -import { TerminalProvider } from "@/context/terminal" -import { PromptProvider } from "@/context/prompt" -import { FileProvider } from "@/context/file" -import { CommentsProvider } from "@/context/comments" +import { ModelsProvider } from "@/context/models" import { NotificationProvider } from "@/context/notification" -import { DialogProvider } from "@opencode-ai/ui/context/dialog" -import { CommandProvider } from "@/context/command" -import { LanguageProvider, useLanguage } from "@/context/language" +import { PermissionProvider } from "@/context/permission" import { usePlatform } from "@/context/platform" -import { Logo } from "@opencode-ai/ui/logo" -import Layout from "@/pages/layout" +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 { iife } from "@opencode-ai/util/iife" -import { Suspense } from "solid-js" +import { useCheckServerHealth } from "./utils/server-health" const Home = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) const Loading = () =>
+const HomeRoute = () => ( + }> + + +) + +const SessionRoute = () => ( + + }> + + + +) + +const SessionIndexRoute = () => + function UiI18nBridge(props: ParentProps) { const language = useLanguage() return {props.children} @@ -43,7 +72,14 @@ function UiI18nBridge(props: ParentProps) { declare global { interface Window { - __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string } + __OPENCODE__?: { + updaterEnabled?: boolean + deepLinks?: string[] + wsl?: boolean + } + api?: { + setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise + } } } @@ -52,19 +88,62 @@ function MarkedProviderWithNativeParser(props: ParentProps) { 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) { return ( - + { + void window.api?.setTitlebar?.({ mode }) + }} + > }> - - {props.children} - + {props.children} @@ -75,78 +154,132 @@ export function AppBaseProviders(props: ParentProps) { ) } -function ServerKey(props: ParentProps) { +const effectMinDuration = + (duration: Duration.Input) => + (e: Effect.Effect) => + Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0])) + +function ConnectionGate(props: ParentProps) { 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(() => + 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( + effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0), + Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }), + Effect.ensuring(Effect.sync(() => setCheckMode("background"))), + Effect.runPromise, + ), + ) + return ( - - {props.children} + + +
+ } + > + { + if (checkMode() === "background") healthCheckActions.refetch() + }} + onServerSelected={(key) => { + setCheckMode("blocking") + server.setActive(key) + healthCheckActions.refetch() + }} + /> + } + > + {props.children} + ) } -export function AppInterface(props: { defaultUrl?: string }) { - const defaultServerUrl = () => { - if (props.defaultUrl) return props.defaultUrl - if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" - if (import.meta.env.DEV) - return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` +function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) { + const server = useServer() + const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key) + + const timer = setInterval(() => props.onRetry?.(), 1000) + onCleanup(() => clearInterval(timer)) - return window.location.origin - } + return ( +
+
+ +

+ Could not reach {server.name || server.key} +

+

Retrying automatically...

+
+ 0}> +
+ Other servers +
+ + {(conn) => { + const key = ServerConnection.key(conn) + return ( + + ) + }} + +
+
+
+
+ ) +} +export function AppInterface(props: { + children?: JSX.Element + defaultServer: ServerConnection.Key + servers?: Array + router?: Component +}) { return ( - - + + - ( - - - - - - {props.children} - - - - - - )} + {routerProps.children}} > - ( - }> - - - )} - /> + - } /> - ( - - - - - - }> - - - - - - - - )} - /> + + - + - + ) } diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx new file mode 100644 index 00000000000..6fde71f3b2e --- /dev/null +++ b/packages/app/src/components/debug-bar.tsx @@ -0,0 +1,441 @@ +import { useIsRouting, useLocation } from "@solidjs/router" +import { batch, createEffect, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" +import { Tooltip } from "@opencode-ai/ui/tooltip" + +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 "n/a" + return `${n.toFixed(d)}ms` +} + +const time = (n?: number) => { + if (n === undefined || Number.isNaN(n)) return "n/a" + return `${Math.round(n)}` +} + +const mb = (n?: number) => { + if (n === undefined || Number.isNaN(n)) return "n/a" + 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 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 heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined) + const heapv = () => { + const value = heap() + if (value === undefined) return "n/a" + return `${Math.round(value * 100)}%` + } + const longv = () => (state.long.count === undefined ? "n/a" : `${time(state.long.block)}/${state.long.count}`) + const navv = () => (state.nav.pending ? "..." : time(state.nav.dur)) + + 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() + document.addEventListener("visibilitychange", vis) + + onCleanup(() => { + if (one !== 0) cancelAnimationFrame(one) + if (two !== 0) cancelAnimationFrame(two) + stop() + document.removeEventListener("visibilitychange", vis) + 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 index be33cba75fc..b042205cf4d 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -4,13 +4,11 @@ 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 type { IconName } from "@opencode-ai/ui/icons/provider" 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 { iife } from "@opencode-ai/util/iife" import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" @@ -27,6 +25,17 @@ export function DialogConnectProvider(props: { provider: string }) { const globalSDK = useGlobalSDK() const platform = usePlatform() const language = useLanguage() + + 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(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!) const methods = createMemo( () => @@ -44,6 +53,47 @@ export function DialogConnectProvider(props: { provider: string }) { error: undefined as string | undefined, }) + type Action = + | { type: "method.select"; index: number } + | { type: "method.reset" } + | { 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.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 }) => { @@ -52,19 +102,35 @@ export function DialogConnectProvider(props: { provider: string }) { 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) { + if (timer.current !== undefined) { + clearTimeout(timer.current) + timer.current = undefined + } + const method = methods()[index] - setStore( - produce((draft) => { - draft.methodIndex = index - draft.authorization = undefined - draft.state = undefined - draft.error = undefined - }), - ) + dispatch({ type: "method.select", index }) if (method.type === "oauth") { - setStore("state", "pending") + dispatch({ type: "auth.pending" }) const start = Date.now() await globalSDK.client.provider.oauth .authorize( @@ -75,22 +141,24 @@ export function DialogConnectProvider(props: { provider: string }) { { throwOnError: true }, ) .then((x) => { + if (!alive.value) return const elapsed = Date.now() - start const delay = 1000 - elapsed if (delay > 0) { - setTimeout(() => { - setStore("state", "complete") - setStore("authorization", x.data!) + 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 } - setStore("state", "complete") - setStore("authorization", x.data!) + dispatch({ type: "auth.complete", authorization: x.data! }) }) .catch((e) => { - setStore("state", "error") - setStore("error", String(e)) + if (!alive.value) return + dispatch({ type: "auth.error", error: formatError(e, language.t("common.requestFailed")) }) }) } } @@ -108,10 +176,6 @@ export function DialogConnectProvider(props: { provider: string }) { if (methods().length === 1) { selectMethod(0) } - document.addEventListener("keydown", handleKey) - onCleanup(() => { - document.removeEventListener("keydown", handleKey) - }) }) async function complete() { @@ -131,17 +195,243 @@ export function DialogConnectProvider(props: { provider: string }) { return } if (store.authorization) { - setStore("authorization", undefined) - setStore("methodIndex", undefined) + dispatch({ type: "method.reset" }) return } - if (store.methodIndex) { - setStore("methodIndex", undefined) + if (store.methodIndex !== undefined) { + dispatch({ type: "method.reset" }) return } dialog.show(() => ) } + 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 + 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 ( +
+ + +
+
{language.t("provider.connect.opencodeZen.line1")}
+
{language.t("provider.connect.opencodeZen.line2")}
+
+ {language.t("provider.connect.opencodeZen.visit.prefix")} + + {language.t("provider.connect.opencodeZen.visit.link")} + + {language.t("provider.connect.opencodeZen.visit.suffix")} +
+
+
+ +
+ {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, + }) + + onMount(() => { + if (store.authorization?.method === "code" && store.authorization?.url) { + platform.openLink(store.authorization.url) + } + }) + + 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 () => { + if (store.authorization?.url) { + platform.openLink(store.authorization.url) + } + + 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 (
- +
@@ -167,257 +457,42 @@ export function DialogConnectProvider(props: { provider: string }) {
- - -
- {language.t("provider.connect.selectMethod", { provider: provider().name })} -
-
- { - listRef = ref - }} - items={methods} - key={(m) => m?.label} - onSelect={async (method, index) => { - if (!method) return - selectMethod(index) - }} - > - {(i) => ( -
-
- - {methodLabel(i)} -
- )} - -
- - -
-
- - {language.t("provider.connect.status.inProgress")} -
-
-
- -
-
- - {language.t("provider.connect.status.failed", { error: store.error ?? "" })} +
+ + + + + +
+
+ + {language.t("provider.connect.status.inProgress")} +
-
- - - {iife(() => { - 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 ( -
- - -
-
- {language.t("provider.connect.opencodeZen.line1")} -
-
- {language.t("provider.connect.opencodeZen.line2")} -
-
- {language.t("provider.connect.opencodeZen.visit.prefix")} - - {language.t("provider.connect.opencodeZen.visit.link")} - - {language.t("provider.connect.opencodeZen.visit.suffix")} -
-
-
- -
- {language.t("provider.connect.apiKey.description", { provider: provider().name })} -
-
-
-
- - - + + +
+
+ + {language.t("provider.connect.status.failed", { error: store.error ?? "" })}
- ) - })} - - - - - {iife(() => { - const [formStore, setFormStore] = createStore({ - value: "", - error: undefined as string | undefined, - }) - - onMount(() => { - if (store.authorization?.method === "code" && store.authorization?.url) { - platform.openLink(store.authorization.url) - } - }) - - 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 - } - const message = result.error instanceof Error ? result.error.message : String(result.error) - setFormStore("error", message || 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 })} -
-
- - - -
- ) - })} -
- - {iife(() => { - const code = createMemo(() => { - const instructions = store.authorization?.instructions - if (instructions?.includes(":")) { - return instructions?.split(":")[1]?.trim() - } - return instructions - }) - - onMount(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 (!result.ok) { - const message = result.error instanceof Error ? result.error.message : String(result.error) - setStore("state", "error") - setStore("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")} -
-
- ) - })} -
-
-
- +
+
+ + + + + + + + + + + + + + +
diff --git a/packages/app/src/components/dialog-custom-provider-form.ts b/packages/app/src/components/dialog-custom-provider-form.ts new file mode 100644 index 00000000000..92d235c3bcc --- /dev/null +++ b/packages/app/src/components/dialog-custom-provider-form.ts @@ -0,0 +1,159 @@ +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[] + saving: boolean + 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 new file mode 100644 index 00000000000..8cfd78ebeb3 --- /dev/null +++ b/packages/app/src/components/dialog-custom-provider.test.ts @@ -0,0 +1,82 @@ +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: {} }, + ], + saving: false, + 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: {} }, + ], + saving: false, + 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 new file mode 100644 index 00000000000..4d220a0b191 --- /dev/null +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -0,0 +1,322 @@ +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 { 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()], + saving: false, + 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 save = async (e: SubmitEvent) => { + e.preventDefault() + if (form.saving) return + + const result = validate() + if (!result) return + + setForm("saving", true) + + const disabledProviders = globalSync.data.config.disabled_providers ?? [] + const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) + + const auth = result.key + ? globalSDK.client.auth.set({ + providerID: result.providerID, + auth: { + type: "api", + key: result.key, + }, + }) + : Promise.resolve() + + auth + .then(() => + globalSync.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled }), + ) + .then(() => { + 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 }), + }) + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + .finally(() => { + setForm("saving", false) + }) + } + + 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 index 9e2bddc6be4..ec0793c540e 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { TextField } from "@opencode-ai/ui/text-field" import { Icon } from "@opencode-ai/ui/icon" -import { createMemo, createSignal, For, Show } from "solid-js" +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" @@ -29,35 +29,36 @@ export function DialogEditProject(props: { project: LocalProject }) { iconUrl: props.project.icon?.override || "", startup: props.project.commands?.start ?? "", saving: false, + dragOver: false, + iconHover: false, }) - const [dragOver, setDragOver] = createSignal(false) - const [iconHover, setIconHover] = createSignal(false) + let iconInput: HTMLInputElement | undefined function handleFileSelect(file: File) { if (!file.type.startsWith("image/")) return const reader = new FileReader() reader.onload = (e) => { setStore("iconUrl", e.target?.result as string) - setIconHover(false) + setStore("iconHover", false) } reader.readAsDataURL(file) } function handleDrop(e: DragEvent) { e.preventDefault() - setDragOver(false) + setStore("dragOver", false) const file = e.dataTransfer?.files[0] if (file) handleFileSelect(file) } function handleDragOver(e: DragEvent) { e.preventDefault() - setDragOver(true) + setStore("dragOver", true) } function handleDragLeave() { - setDragOver(false) + setStore("dragOver", false) } function handleInputChange(e: Event) { @@ -73,31 +74,35 @@ export function DialogEditProject(props: { project: LocalProject }) { async function handleSubmit(e: SubmitEvent) { e.preventDefault() - setStore("saving", true) - const name = store.name.trim() === folderName() ? "" : store.name.trim() - const start = store.startup.trim() + await Promise.resolve() + .then(async () => { + setStore("saving", true) + 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.iconUrl }, - commands: { start }, - }) - globalSync.project.icon(props.project.worktree, store.iconUrl || undefined) - setStore("saving", false) - dialog.close() - return - } + 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.iconUrl }, + commands: { start }, + }) + globalSync.project.icon(props.project.worktree, store.iconUrl || undefined) + dialog.close() + return + } - globalSync.project.meta(props.project.worktree, { - name, - icon: { color: store.color, override: store.iconUrl || undefined }, - commands: { start: start || undefined }, - }) - setStore("saving", false) - dialog.close() + globalSync.project.meta(props.project.worktree, { + name, + icon: { color: store.color, override: store.iconUrl || undefined }, + commands: { start: start || undefined }, + }) + dialog.close() + }) + .finally(() => { + setStore("saving", false) + }) } return ( @@ -116,22 +121,26 @@ export function DialogEditProject(props: { project: LocalProject }) {
-
setIconHover(true)} onMouseLeave={() => setIconHover(false)}> +
setStore("iconHover", true)} + onMouseLeave={() => setStore("iconHover", false)} + >
{ - if (store.iconUrl && iconHover()) { + if (store.iconUrl && store.iconHover) { clearIcon() } else { - document.getElementById("icon-upload")?.click() + iconInput?.click() } }} > @@ -142,8 +151,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
} @@ -156,45 +164,34 @@ export function DialogEditProject(props: { project: LocalProject }) {
- +
- +
- + { + iconInput = el + }} + type="file" + accept="image/*" + class="hidden" + onChange={handleInputChange} + />
{language.t("dialog.project.edit.icon.hint")} {language.t("dialog.project.edit.icon.recommended")} @@ -241,7 +238,7 @@ export function DialogEditProject(props: { project: LocalProject }) { value={store.startup} onChange={(v) => setStore("startup", v)} spellcheck={false} - class="max-h-40 w-full font-mono text-xs no-scrollbar" + 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 index 17782f5ab8d..8810955cc65 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -6,6 +6,7 @@ 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 "@opencode-ai/sdk/v2/client" import { base64Encode } from "@opencode-ai/util/encode" @@ -66,15 +67,23 @@ export const DialogFork: Component = () => { attachmentName: language.t("common.attachment"), }) - dialog.close() - - sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => { - if (!forked.data) return - navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`) - requestAnimationFrame(() => { - prompt.set(restored) + sdk.client.session + .fork({ sessionID, messageID: item.id }) + .then((forked) => { + if (!forked.data) { + showToast({ title: language.t("common.requestFailed") }) + return + } + dialog.close() + navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`) + requestAnimationFrame(() => { + prompt.set(restored) + }) + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) }) - }) } return ( @@ -90,12 +99,8 @@ export const DialogFork: Component = () => { > {(item) => (
- - {item.text} - - - {item.time} - + {item.text} + {item.time}
)} diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index 1ecefa2cbbf..ace79e38a7c 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/packages/app/src/components/dialog-manage-models.tsx @@ -1,16 +1,43 @@ 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")} + + } + > { items={local.model.list()} filterKeys={["provider.name", "name", "id"]} sortBy={(a, b) => a.name.localeCompare(b.name)} - groupBy={(x) => x.provider.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 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) + 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 visible = local.model.visible({ - modelID: x.id, - providerID: x.provider.id, - }) - local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible) + const key = { modelID: x.id, providerID: x.provider.id } + local.model.setVisibility(key, !local.model.visible(key)) }} > {(i) => ( @@ -40,12 +87,7 @@ export const DialogManageModels: Component = () => { {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 new file mode 100644 index 00000000000..d0a35b71beb --- /dev/null +++ b/packages/app/src/components/dialog-release-notes.tsx @@ -0,0 +1,144 @@ +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 index b9a7d6ed9b1..91e23f8ffa5 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -2,11 +2,13 @@ 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/util/path" import fuzzysort from "fuzzysort" -import { createMemo } from "solid-js" +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 { @@ -15,90 +17,146 @@ interface DialogSelectDirectoryProps { onSelect: (result: string | string[] | null) => void } -export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { - const sync = useGlobalSync() - const sdk = useGlobalSDK() - const dialog = useDialog() - const language = useLanguage() +type Row = { + absolute: string + search: string + group: "recent" | "folders" +} - const home = createMemo(() => sync.data.path.home) +function cleanInput(value: string) { + const first = (value ?? "").split(/\r?\n/)[0] ?? "" + return first.replace(/[\u0000-\u001F\u007F]/g, "").trim() +} - const start = createMemo(() => sync.data.path.home || sync.data.path.directory) +function normalizePath(input: string) { + const v = input.replaceAll("\\", "/") + if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/") + return v.replace(/\/+/g, "/") +} - const cache = new Map>>() +function normalizeDriveRoot(input: string) { + const v = normalizePath(input) + if (/^[A-Za-z]:$/.test(v)) return v + "/" + return v +} - function normalize(input: string) { - const v = input.replaceAll("\\", "/") - if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/") - return v.replace(/\/+/g, "/") - } +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 normalizeDriveRoot(input: string) { - const v = normalize(input) - if (/^[A-Za-z]:$/.test(v)) return v + "/" - return v - } +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 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 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 join(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 parentOf(input: string) { + const v = trimTrailing(input) + if (v === "/") return v + if (v === "//") return v + if (/^[A-Za-z]:\/$/.test(v)) return v - 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 "" - } + 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 display(path: string) { - const full = trimTrailing(path) - const h = home() - if (!h) return full - - const hn = trimTrailing(h) - const lc = full.toLowerCase() - const hc = hn.toLowerCase() - if (lc === hc) return "~" - if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length) - return 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 + "/" } - function scoped(filter: string) { - const base = start() + 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(filter.trim()) + const raw = normalizeDriveRoot(value) if (!raw) return { directory: trimTrailing(base), path: "" } - const h = home() - if (raw === "~") return { directory: trimTrailing(h ?? base), path: "" } - if (raw.startsWith("~/")) return { directory: trimTrailing(h ?? base), path: raw.slice(2) } + 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 } } - async function dirs(dir: string) { + const dirs = async (dir: string) => { const key = trimTrailing(dir) const existing = cache.get(key) if (existing) return existing - const request = sdk.client.file + const request = args.sdk.client.file .list({ directory: key, path: "" }) .then((x) => x.data ?? []) .catch(() => []) @@ -115,28 +173,34 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { return request } - async function match(dir: string, query: string, limit: number) { + 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) } - const directories = async (filter: string) => { - const input = scoped(filter) - if (!input) return [] as string[] + return async (filter: string) => { + const token = ++current + const active = () => token === current - const raw = normalizeDriveRoot(filter.trim()) - const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/") + const value = cleanInput(filter) + const scopedInput = scoped(value) + if (!scopedInput) return [] as string[] - const query = normalizeDriveRoot(input.path) + const raw = normalizeDriveRoot(value) + const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/") + const query = normalizeDriveRoot(scopedInput.path) - if (!isPath) { - const results = await sdk.client.find - .files({ directory: input.directory, query, type: "directory", limit: 50 }) + const find = () => + args.sdk.client.find + .files({ directory: scopedInput.directory, query, type: "directory", limit: 50 }) .then((x) => x.data ?? []) .catch(() => []) - return results.map((rel) => join(input.directory, rel)).slice(0, 50) + 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("/") @@ -145,27 +209,111 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const cap = 12 const branch = 4 - let paths = [input.directory] + let paths = [scopedInput.directory] for (const part of head) { + if (!active()) return [] if (part === "..") { - paths = paths.map((p) => { - const v = trimTrailing(p) - if (v === "/") return v - if (/^[A-Za-z]:\/$/.test(v)) return v - const i = v.lastIndexOf("/") - if (i <= 0) return "/" - return v.slice(0, i) - }) + 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() - return Array.from(new Set(out)).slice(0, 50) + 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) { @@ -179,24 +327,60 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }} emptyMessage={language.t("dialog.directory.empty")} loadingMessage={language.t("common.loading")} - items={directories} - key={(x) => x} + items={items} + key={(x) => 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) + resolve(path.absolute) }} > - {(absolute) => { - const path = display(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 index 7c3113a544e..3d4dbecbdcf 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -1,17 +1,25 @@ 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/util/encode" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { useParams } from "@solidjs/router" -import { createMemo, createSignal, onCleanup, Show } from "solid-js" +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" +type EntryType = "command" | "file" | "session" type Entry = { id: string @@ -22,90 +30,313 @@ type Entry = { category: string option?: CommandOption path?: string + directory?: string + sessionID?: string + archived?: number + updated?: number } -export function DialogSelectFile() { - const command = useCommand() - const language = useLanguage() - const layout = useLayout() - const file = useFile() - const dialog = useDialog() - const params = useParams() - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - const tabs = createMemo(() => layout.tabs(sessionKey())) - const view = createMemo(() => layout.view(sessionKey())) - const state = { cleanup: undefined as (() => void) | void, committed: false } - const [grouped, setGrouped] = createSignal(false) - const common = [ - "session.new", - "workspace.new", - "session.previous", - "session.next", - "terminal.toggle", - "review.toggle", - ] - const limit = 5 - - const allowed = createMemo(() => - command.options.filter( - (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open", - ), - ) +type DialogSelectFileMode = "all" | "files" - const commandItem = (option: CommandOption): Entry => ({ - id: "command:" + option.id, - type: "command", - title: option.title, - description: option.description, - keybind: option.keybind, - category: language.t("palette.group.commands"), - option, - }) +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 fileItem = (path: string): Entry => ({ - id: "file:" + path, - type: "file", - title: path, - category: language.t("palette.group.files"), - path, +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(() => allowed().map(commandItem)) + 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.map((id, index) => [id, index])) + 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, limit) + 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 - return sorted.map(commandItem) + 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 = tabs().all() - const active = tabs().active() + 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 = file.pathFromTab(item) + const path = props.file.pathFromTab(item) if (!path) continue if (seen.has(path)) continue seen.add(path) - items.push(fileItem(path)) + items.push(createFileEntry(path, category)) } - return items.slice(0, limit) + 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)) }) - const items = async (filter: string) => { - const query = filter.trim() + 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) return [...picks(), ...recent()] - const files = await file.searchFiles(query) - const entries = files.map(fileItem) - return [...list(), ...entries] + + 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) => { @@ -119,7 +350,10 @@ export function DialogSelectFile() { const value = file.tab(path) tabs().open(value) file.load(path) - view().reviewPanel.open() + if (!view().reviewPanel.opened()) view().reviewPanel.open() + layout.fileTree.setTab("all") + props.onOpenFile?.(path) + tabs().setActive(value) } const handleSelect = (item: Entry | undefined) => { @@ -133,6 +367,12 @@ export function DialogSelectFile() { 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) } @@ -143,26 +383,26 @@ export function DialogSelectFile() { }) return ( - + item.id} filterKeys={["title", "description", "category"]} - groupBy={(item) => item.category} + groupBy={grouped() ? (item) => item.category : () => ""} onMove={handleMove} onSelect={handleSelect} > {(item) => ( -
@@ -177,18 +417,48 @@ export function DialogSelectFile() {
} > -
-
- {item.title} - - {item.description} + +
+
+ {item.title} + + {item.description} + +
+ + {formatKeybind(item.keybind ?? "")} + +
+
+ +
+
+ +
+ + {item.title} + + + + {item.description} + + +
+
+ + + {getRelativeTime(new Date(item.updated!).toISOString(), language.t)} +
- - {formatKeybind(item.keybind ?? "")} - -
- + + )}
diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index 8eb08878912..f8913eee4fb 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -6,6 +6,13 @@ import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" 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() @@ -21,15 +28,19 @@ export const DialogSelectMcp: Component = () => { const toggle = async (name: string) => { if (loading()) return setLoading(name) - const status = sync.data.mcp[name] - if (status?.status === "connected") { - await sdk.client.mcp.disconnect({ name }) - } else { - await sdk.client.mcp.connect({ name }) + try { + 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) + } finally { + setLoading(null) } - const result = await sdk.client.mcp.status() - if (result.data) sync.set("mcp", result.data) - setLoading(null) } const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length) @@ -54,6 +65,11 @@ export const DialogSelectMcp: Component = () => { {(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 @@ -64,17 +80,8 @@ export const DialogSelectMcp: Component = () => {
{i.name} - - {language.t("mcp.status.connected")} - - - {language.t("mcp.status.failed")} - - - {language.t("mcp.status.needs_auth")} - - - {language.t("mcp.status.disabled")} + + {statusLabel()} {language.t("common.loading.ellipsis")} diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 1e7c9f52a9a..bcee3f501f5 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -1,12 +1,11 @@ import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" -import type { IconName } from "@opencode-ai/ui/icons/provider" 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, onCleanup, onMount, Show } from "solid-js" +import { type Component, Show } from "solid-js" import { useLocal } from "@/context/local" import { popularProviders, useProviders } from "@/hooks/use-providers" import { DialogConnectProvider } from "./dialog-connect-provider" @@ -21,23 +20,20 @@ export const DialogSelectModelUnpaid: Component = () => { const language = useLanguage() let listRef: ListRef | undefined - const handleKey = (e: KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") return listRef?.onKeyDown(e) } - onMount(() => { - document.addEventListener("keydown", handleKey) - onCleanup(() => { - document.removeEventListener("keydown", handleKey) - }) - }) - return ( - -
+ +
{language.t("dialog.model.unpaid.freeModels.title")}
(listRef = ref)} items={local.model.list} current={local.model.current()} @@ -75,8 +71,6 @@ export const DialogSelectModelUnpaid: Component = () => {
)} -
-
@@ -100,11 +94,22 @@ export const DialogSelectModelUnpaid: Component = () => { > {(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 index 5569d7780fe..9f7afb8cd27 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -1,5 +1,6 @@ import { Popover as Kobalte } from "@kobalte/core/popover" -import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js" +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" @@ -14,6 +15,9 @@ import { DialogManageModels } from "./dialog-manage-models" import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" +const isFree = (provider: string, cost: { input: number } | undefined) => + provider === "opencode" && (!cost || cost.input === 0) + const ModelList: Component<{ provider?: string class?: string @@ -53,13 +57,7 @@ const ModelList: Component<{ class="w-full" placement="right-start" gutter={12} - value={ - - } + value={} > {node} @@ -74,7 +72,7 @@ const ModelList: Component<{ {(i) => (
{i.name} - + {language.t("model.tag.free")} @@ -86,43 +84,98 @@ const ModelList: Component<{ ) } -export function ModelSelectorPopover(props: { +type ModelSelectorTriggerProps = Omit, "as" | "ref"> + +export function ModelSelectorPopover(props: { provider?: string children?: JSX.Element - triggerAs?: T - triggerProps?: ComponentProps + triggerAs?: ValidComponent + triggerProps?: ModelSelectorTriggerProps }) { - const [open, setOpen] = createSignal(false) + const [store, setStore] = createStore<{ + open: boolean + dismiss: "escape" | "outside" | null + }>({ + open: false, + dismiss: null, + }) const dialog = useDialog() const handleManage = () => { - setOpen(false) + setStore("open", false) dialog.show(() => ) } + + const handleConnectProvider = () => { + setStore("open", false) + dialog.show(() => ) + } const language = useLanguage() return ( - - + { + if (next) setStore("dismiss", null) + setStore("open", next) + }} + modal={false} + placement="top-start" + gutter={4} + > + {props.children} - + { + setStore("dismiss", "escape") + setStore("open", false) + event.preventDefault() + event.stopPropagation() + }} + onPointerDownOutside={() => { + setStore("dismiss", "outside") + setStore("open", false) + }} + onFocusOutside={() => { + setStore("dismiss", "outside") + setStore("open", false) + }} + onCloseAutoFocus={(event) => { + if (store.dismiss === "outside") event.preventDefault() + setStore("dismiss", null) + }} + > {language.t("dialog.model.select.title")} setOpen(false)} + onSelect={() => setStore("open", false)} class="p-1" action={ - +
+ + + + + + +
} />
diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index 1e059c21983..e53738399ab 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -5,9 +5,11 @@ 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 { IconName } from "@opencode-ai/ui/icons/provider" 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() @@ -16,9 +18,16 @@ export const DialogSelectProvider: Component = () => { const popularGroup = () => language.t("dialog.provider.group.popular") 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 ( - + { key={(x) => x?.id} items={() => { language.locale() - return providers.all() + 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) @@ -43,18 +54,29 @@ export const DialogSelectProvider: Component = () => { }} onSelect={(x) => { if (!x) return + if (x.id === CUSTOM_ID) { + dialog.show(() => ) + return + } dialog.show(() => ) }} > {(i) => (
- + {i.name} + +
{language.t("dialog.provider.opencode.tagline")}
+
+ + {language.t("settings.providers.tag.custom")} + {language.t("dialog.provider.tag.recommended")} - -
{language.t("dialog.provider.anthropic.note")}
+ {(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 index 4466fdde16a..cba401a467e 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -1,29 +1,173 @@ -import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js" -import { createStore, reconcile } from "solid-js/store" +import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" +import { 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 { Button } from "@opencode-ai/ui/button" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server" -import { usePlatform } from "@/context/platform" -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +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" -type ServerStatus = { healthy: boolean; version?: string } +const DEFAULT_USERNAME = "opencode" -async function checkHealth(url: string, fetch?: typeof globalThis.fetch): Promise { - const sdk = createOpencodeClient({ - baseUrl: url, - fetch, - signal: AbortSignal.timeout(3000), +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), }) - return sdk.global - .health() - .then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version })) - .catch(() => ({ healthy: false })) +} + +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() { @@ -32,31 +176,83 @@ export function DialogSelectServer() { const server = useServer() const platform = usePlatform() const language = useLanguage() + const { defaultKey, canDefault, setDefault } = useDefaultServer() + const { previewStatus } = useServerPreview() + const checkServerHealth = useCheckServerHealth() const [store, setStore] = createStore({ - url: "", - adding: false, - error: "", - status: {} as Record, + status: {} as Record, + addServer: { + url: "", + name: "", + username: DEFAULT_USERNAME, + password: "", + adding: false, + error: "", + showForm: false, + status: undefined as boolean | undefined, + }, + editServer: { + id: undefined as string | undefined, + value: "", + name: "", + username: "", + password: "", + error: "", + busy: false, + status: undefined as boolean | undefined, + }, }) - const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.()) - const isDesktop = platform.platform === "desktop" + + const resetAdd = () => { + setStore("addServer", { + url: "", + name: "", + username: DEFAULT_USERNAME, + password: "", + adding: false, + error: "", + showForm: false, + status: undefined, + }) + } + const resetEdit = () => { + setStore("editServer", { + id: undefined, + value: "", + name: "", + username: "", + password: "", + error: "", + status: undefined, + busy: false, + }) + } + + 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.url + const current = server.current const list = server.list if (!current) return list if (!list.includes(current)) return [current, ...list] return [current, ...list.filter((x) => x !== current)] }) - const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0]) + const current = createMemo(() => items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0]) const sortedItems = createMemo(() => { const list = items() if (!list.length) return list const active = current() const order = new Map(list.map((url, index) => [url, index] as const)) - const rank = (value?: ServerStatus) => { + const rank = (value?: ServerHealth) => { if (value?.healthy === true) return 0 if (value?.healthy === false) return 2 return 1 @@ -64,17 +260,17 @@ export function DialogSelectServer() { return list.slice().sort((a, b) => { if (a === active) return -1 if (b === active) return 1 - const diff = rank(store.status[a]) - rank(store.status[b]) + const diff = rank(store.status[ServerConnection.key(a)]) - rank(store.status[ServerConnection.key(b)]) if (diff !== 0) return diff return (order.get(a) ?? 0) - (order.get(b) ?? 0) }) }) async function refreshHealth() { - const results: Record = {} + const results: Record = {} await Promise.all( - items().map(async (url) => { - results[url] = await checkHealth(url, platform.fetch) + items().map(async (conn) => { + results[ServerConnection.key(conn)] = await checkServerHealth(conn.http) }), ) setStore("status", reconcile(results)) @@ -87,162 +283,369 @@ export function DialogSelectServer() { onCleanup(() => clearInterval(interval)) }) - function select(value: string, persist?: boolean) { - if (!persist && store.status[value]?.healthy === false) return + async function select(conn: ServerConnection.Any, persist?: boolean) { + if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return dialog.close() - if (persist) { - server.add(value) + if (persist && conn.type === "http") { + server.add(conn) navigate("/") return } - server.setActive(value) + server.setActive(ServerConnection.key(conn)) navigate("/") } - async function handleSubmit(e: SubmitEvent) { - e.preventDefault() - const value = normalizeServerUrl(store.url) - if (!value) return + const handleAddChange = (value: string) => { + if (store.addServer.adding) 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 (store.addServer.adding) return + setStore("addServer", { name: value, error: "" }) + } - setStore("adding", true) - setStore("error", "") + const handleAddUsernameChange = (value: string) => { + if (store.addServer.adding) return + setStore("addServer", { username: value, error: "" }) + void previewStatus(store.addServer.url, value, store.addServer.password, (next) => + setStore("addServer", { status: next }), + ) + } - const result = await checkHealth(value, platform.fetch) - setStore("adding", false) + const handleAddPasswordChange = (value: string) => { + if (store.addServer.adding) 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 (store.editServer.busy) return + setStore("editServer", { value, error: "" }) + void previewStatus(value, store.editServer.username, store.editServer.password, (next) => + setStore("editServer", { status: next }), + ) + } + + const handleEditNameChange = (value: string) => { + if (store.editServer.busy) return + setStore("editServer", { name: value, error: "" }) + } + + const handleEditUsernameChange = (value: string) => { + if (store.editServer.busy) 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 (store.editServer.busy) return + setStore("editServer", { password: value, error: "" }) + void previewStatus(store.editServer.value, store.editServer.username, value, (next) => + setStore("editServer", { status: next }), + ) + } + + async function handleAdd(value: string) { + if (store.addServer.adding) return + const normalized = normalizeServerUrl(value) + if (!normalized) { + resetAdd() + return + } + + setStore("addServer", { adding: true, error: "" }) + + 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) + setStore("addServer", { adding: false }) + if (!result.healthy) { + setStore("addServer", { error: language.t("dialog.server.add.error") }) + return + } + + resetAdd() + await select(conn, true) + } + + async function handleEdit(original: ServerConnection.Any, value: string) { + if (store.editServer.busy || original.type !== "http") return + const normalized = normalizeServerUrl(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 = original.displayName + if ( + normalized === original.http.url && + name === existingName && + username === original.http.username && + password === original.http.password + ) { + resetEdit() + return + } + + setStore("editServer", { busy: true, error: "" }) + + const conn: ServerConnection.Http = { + type: "http", + displayName: name, + http: { url: normalized, username, password }, + } + const result = await checkServerHealth(conn.http) + setStore("editServer", { busy: false }) if (!result.healthy) { - setStore("error", language.t("dialog.server.add.error")) + setStore("editServer", { error: language.t("dialog.server.add.error") }) return } + if (normalized === original.http.url) { + server.add(conn) + } else { + replaceServer(original, conn) + } + + resetEdit() + } + + 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) + }) - setStore("url", "") - select(value, true) + const resetForm = () => { + resetAdd() + resetEdit() } - async function handleRemove(url: string) { + 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, + busy: false, + }) + } + + const submitForm = () => { + if (mode() === "add") { + void handleAdd(store.addServer.url) + return + } + const original = editing() + if (!original) return + void handleEdit(original, store.editServer.value) + } + + const isFormMode = createMemo(() => mode() !== "list") + const isAddMode = createMemo(() => mode() === "add") + const formBusy = createMemo(() => (isAddMode() ? store.addServer.adding : store.editServer.busy)) + + 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) { + platform.setDefaultServer?.(null) + } } return ( - -
- x} - current={current()} - onSelect={(x) => { - if (x) select(x) - }} + +
+ + } > - {(i) => ( -
-
-
- {serverDisplayName(i)} - {store.status[i]?.version} -
- - { - e.stopPropagation() - handleRemove(i) - }} - /> - -
- )} - - -
-
-

{language.t("dialog.server.add.title")}

-
-
-
-
- { - setStore("url", v) - setStore("error", "") - }} - validationState={store.error ? "invalid" : "valid"} - error={store.error} - /> -
- -
-
-
- - -
-
-

{language.t("dialog.server.default.title")}

-

{language.t("dialog.server.default.description")}

-
-
- {language.t("dialog.server.default.none")} + x.http.url} + onSelect={(x) => { + if (x) select(x) + }} + divider={true} + class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-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")} + + } - > - - - } - > -
- {serverDisplayName(defaultUrl()!)} + 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 index dbbc8fa7adc..83cea131f5d 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -6,81 +6,54 @@ import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { SettingsGeneral } from "./settings-general" import { SettingsKeybinds } from "./settings-keybinds" -import { SettingsPermissions } from "./settings-permissions" import { SettingsProviders } from "./settings-providers" import { SettingsModels } from "./settings-models" -import { SettingsAgents } from "./settings-agents" -import { SettingsCommands } from "./settings-commands" -import { SettingsMcp } from "./settings-mcp" 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.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")} + +
+
- OpenCode Desktop + {language.t("app.name.desktop")} v{platform.version}
- {/* Server */} - {/* */} - {/* */} - {/* Permissions */} - {/* */} - {/* */} - {/* */} - {/* Providers */} - {/* */} - {/* */} - {/* */} - {/* Models */} - {/* */} - {/* */} - {/* */} - {/* Agents */} - {/* */} - {/* */} - {/* */} - {/* Commands */} - {/* */} - {/* */} - {/* */} - {/* MCP */} - {/* */} @@ -88,24 +61,12 @@ export const DialogSettings: Component = () => { - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} + + + + + +
) diff --git a/packages/app/src/components/file-tree.test.ts b/packages/app/src/components/file-tree.test.ts new file mode 100644 index 00000000000..29e20b4807c --- /dev/null +++ b/packages/app/src/components/file-tree.test.ts @@ -0,0 +1,78 @@ +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 index 3439d366cee..930832fb655 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -1,111 +1,506 @@ -import { useLocal, type LocalFile } from "@/context/local" +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 { Tooltip } from "@opencode-ai/ui/tooltip" -import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js" +import { Icon } from "@opencode-ai/ui/icon" +import { + createEffect, + createMemo, + For, + Match, + on, + Show, + splitProps, + Switch, + untrack, + type ComponentProps, + type JSXElement, + type ParentProps, +} from "solid-js" import { Dynamic } from "solid-js/web" +import type { FileNode } from "@opencode-ai/sdk/v2" -export default function FileTree(props: { - path: string - class?: string - nodeClass?: string - level?: number - onFileClick?: (file: LocalFile) => void +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 } }) { - const local = useLocal() - const level = props.level ?? 0 + 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 Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => ( +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 ( { - const evt = e as globalThis.DragEvent - evt.dataTransfer!.effectAllowed = "copy" - evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`) - - // Create custom drag image without margins - const dragImage = document.createElement("div") - dragImage.className = - "flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1" - dragImage.style.position = "absolute" - dragImage.style.top = "-1000px" - - // Copy only the icon and text content without padding - const icon = e.currentTarget.querySelector("svg") - const text = e.currentTarget.querySelector("span") - if (icon && text) { - dragImage.innerHTML = icon.outerHTML + text.outerHTML - } - - document.body.appendChild(dragImage) - evt.dataTransfer!.setDragImage(dragImage, 0, 12) - setTimeout(() => document.body.removeChild(dragImage), 0) + style={`padding-left: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`} + draggable={local.draggable} + onDragStart={(event: DragEvent) => { + 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) }} - {...p} + {...rest} > - {p.children} + {local.children} - {p.node.name} + {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) => ( - +
+ + {(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 ? local.file.expand(node.path) : local.file.collapse(node.path))} + open={expanded()} + onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))} > - - - - + +
+ +
+
- - + +
+ ...
} + > + +
- props.onFileClick?.(node)}> + props.onFileClick?.(node)} + >
- - + + + + + + + + + + + + + + + - - )} + ) + }}
) diff --git a/packages/app/src/components/link.tsx b/packages/app/src/components/link.tsx index e13c3133048..85f7efc539e 100644 --- a/packages/app/src/components/link.tsx +++ b/packages/app/src/components/link.tsx @@ -1,17 +1,26 @@ import { ComponentProps, splitProps } from "solid-js" import { usePlatform } from "@/context/platform" -export interface LinkProps extends ComponentProps<"button"> { +export interface LinkProps extends Omit, "href"> { href: string } export function Link(props: LinkProps) { const platform = usePlatform() - const [local, rest] = splitProps(props, ["href", "children"]) + const [local, rest] = splitProps(props, ["href", "children", "class"]) return ( - + ) } diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 72bc927fabc..e129b499ae1 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1,21 +1,10 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" -import { - createEffect, - on, - Component, - Show, - For, - onMount, - onCleanup, - Switch, - Match, - createMemo, - createSignal, -} from "solid-js" -import { createStore, produce } from "solid-js/store" +import { useSpring } from "@opencode-ai/ui/motion-spring" +import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js" +import { createStore } from "solid-js/store" import { createFocusSignal } from "@solid-primitives/active-element" import { useLocal } from "@/context/local" -import { useFile, type FileSelection } from "@/context/file" +import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" import { ContentPart, DEFAULT_PROMPT, @@ -28,38 +17,45 @@ import { } from "@/context/prompt" import { useLayout } from "@/context/layout" import { useSDK } from "@/context/sdk" -import { useNavigate, useParams } from "@solidjs/router" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" -import { FileIcon } from "@opencode-ai/ui/file-icon" 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 type { IconName } from "@opencode-ai/ui/icons/provider" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" -import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { RadioGroup } from "@opencode-ai/ui/radio-group" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { ImagePreview } from "@opencode-ai/ui/image-preview" import { ModelSelectorPopover } from "@/components/dialog-select-model" import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" import { useProviders } from "@/hooks/use-providers" import { useCommand } from "@/context/command" import { Persist, persisted } from "@/utils/persist" -import { Identifier } from "@/utils/id" -import { SessionContextUsage } from "@/components/session-context-usage" import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" -import { useGlobalSync } from "@/context/global-sync" import { usePlatform } from "@/context/platform" -import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client" -import { Binary } from "@opencode-ai/util/binary" -import { showToast } from "@opencode-ai/ui/toast" -import { base64Encode } from "@opencode-ai/util/encode" - -const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] -const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"] +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, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments" +import { + canNavigateHistoryAtCursor, + navigatePromptHistory, + prependHistoryEntry, + type PromptHistoryComment, + type PromptHistoryEntry, + type PromptHistoryStoredEntry, + promptLength, +} from "./prompt-input/history" +import { createPromptSubmit } 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" interface PromptInputProps { class?: string @@ -97,37 +93,31 @@ const EXAMPLES = [ "prompt.example.25", ] as const -interface SlashCommand { - id: string - trigger: string - title: string - description?: string - keybind?: string - type: "builtin" | "custom" -} +const NON_EMPTY_TEXT = /[^\s\u200B]/ export const PromptInput: Component = (props) => { - const navigate = useNavigate() const sdk = useSDK() const sync = useSync() - const globalSync = useGlobalSync() - const platform = usePlatform() const local = useLocal() const files = useFile() const prompt = usePrompt() const layout = useLayout() const comments = useComments() - const params = useParams() 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 + let fileInputRef: HTMLInputElement | undefined let scrollRef!: HTMLDivElement let slashPopoverRef!: HTMLDivElement + const mirror = { input: false } + const inset = 44 + const scrollCursorIntoView = () => { const container = scrollRef const selection = window.getSelection() @@ -136,7 +126,14 @@ export const PromptInput: Component = (props) => { const range = selection.getRangeAt(0) if (!editorRef.contains(range.startContainer)) return - const rect = range.getBoundingClientRect() + const cursor = getCursorPosition(editorRef) + const length = promptLength(prompt.current().filter((part) => part.type !== "image")) + if (cursor >= length) { + container.scrollTop = container.scrollHeight + return + } + + const rect = range.getClientRects().item(0) ?? range.getBoundingClientRect() if (!rect.height) return const containerRect = container.getBoundingClientRect() @@ -149,8 +146,8 @@ export const PromptInput: Component = (props) => { return } - if (bottom > container.scrollTop + container.clientHeight - padding) { - container.scrollTop = bottom - container.clientHeight + padding + if (bottom > container.scrollTop + container.clientHeight - inset) { + container.scrollTop = bottom - container.clientHeight + inset } } @@ -158,13 +155,64 @@ export const PromptInput: Component = (props) => { requestAnimationFrame(scrollCursorIntoView) } - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - const tabs = createMemo(() => layout.tabs(sessionKey())) - const view = createMemo(() => layout.view(sessionKey())) + 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) + tabs().open(tab) + tabs().setActive(tab) + Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus()) + } const recent = createMemo(() => { const all = tabs().all() - const active = tabs().active() + const active = activeFileTab() const order = active ? [active, ...all.filter((x) => x !== active)] : all const seen = new Set() const paths: string[] = [] @@ -187,33 +235,62 @@ export const PromptInput: Component = (props) => { }, ) const working = createMemo(() => status()?.type !== "idle") - const imageAttachments = createMemo( - () => prompt.current().filter((part) => part.type === "image") as ImageAttachmentPart[], + const imageAttachments = createMemo(() => + prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), ) const [store, setStore] = createStore<{ popover: "at" | "slash" | null historyIndex: number - savedPrompt: Prompt | null + savedPrompt: PromptHistoryEntry | null placeholder: number - dragging: boolean + draggingType: "image" | "@mention" | null mode: "normal" | "shell" applyingHistory: boolean }>({ popover: null, historyIndex: -1, - savedPrompt: null, + savedPrompt: null as PromptHistoryEntry | null, placeholder: Math.floor(Math.random() * EXAMPLES.length), - dragging: false, + draggingType: null, mode: "normal", applyingHistory: false, }) - const MAX_HISTORY = 100 + const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) + const motion = (value: number) => ({ + opacity: value, + transform: `scale(${0.95 + value * 0.05})`, + 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 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: Prompt[] + entries: PromptHistoryStoredEntry[] }>({ entries: [], }), @@ -221,29 +298,84 @@ export const PromptInput: Component = (props) => { const [shellHistory, setShellHistory] = persisted( Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]), createStore<{ - entries: Prompt[] + entries: PromptHistoryStoredEntry[] }>({ entries: [], }), ) - const clonePromptParts = (prompt: Prompt): Prompt => - 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, - } + const suggest = createMemo(() => !hasUserPrompt()) + + const placeholder = createMemo(() => + promptPlaceholder({ + mode: store.mode, + commentCount: commentCount(), + example: suggest() ? language.t(EXAMPLES[store.placeholder]) : "", + suggest: suggest(), + t: (key, params) => language.t(key as Parameters[0], params as never), + }), + ) + + const 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 promptLength = (prompt: Prompt) => - prompt.reduce((len, part) => len + ("content" in part ? part.content.length : 0), 0) + 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 = (p: Prompt, position: "start" | "end") => { + 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() @@ -271,142 +403,123 @@ export const PromptInput: Component = (props) => { } const isFocused = createFocusSignal(() => editorRef) + const escBlur = () => platform.platform === "desktop" && platform.os === "macos" - createEffect(() => { - params.id - if (params.id) 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 pick = () => fileInputRef?.click() - const addImageAttachment = async (file: File) => { - if (!ACCEPTED_FILE_TYPES.includes(file.type)) return - - const reader = new FileReader() - reader.onload = () => { - const dataUrl = reader.result as string - const attachment: ImageAttachmentPart = { - type: "image", - id: crypto.randomUUID(), - filename: file.name, - mime: file.type, - dataUrl, - } - const cursorPosition = prompt.cursor() ?? getCursorPosition(editorRef) - prompt.set([...prompt.current(), attachment], cursorPosition) - } - reader.readAsDataURL(file) - } - - const removeImageAttachment = (id: string) => { - const current = prompt.current() - const next = current.filter((part) => part.type !== "image" || part.id !== id) - prompt.set(next, prompt.cursor()) + const setMode = (mode: "normal" | "shell") => { + setStore("mode", mode) + setStore("popover", null) + requestAnimationFrame(() => editorRef?.focus()) } - const handlePaste = async (event: ClipboardEvent) => { - if (!isFocused()) return - const clipboardData = event.clipboardData - if (!clipboardData) return - - event.preventDefault() - event.stopPropagation() - - const items = Array.from(clipboardData.items) - const fileItems = items.filter((item) => item.kind === "file") - const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type)) + 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"), + }, + ]) - if (imageItems.length > 0) { - for (const item of imageItems) { - const file = item.getAsFile() - if (file) await addImageAttachment(file) - } - return - } + const closePopover = () => setStore("popover", null) - if (fileItems.length > 0) { - showToast({ - title: language.t("prompt.toast.pasteUnsupported.title"), - description: language.t("prompt.toast.pasteUnsupported.description"), - }) - return - } - - const plainText = clipboardData.getData("text/plain") ?? "" - if (!plainText) return - addPart({ type: "text", content: plainText, start: 0, end: 0 }) + const resetHistoryNavigation = (force = false) => { + if (!force && (store.historyIndex < 0 || store.applyingHistory)) return + setStore("historyIndex", -1) + setStore("savedPrompt", null) } - const handleGlobalDragOver = (event: DragEvent) => { - if (dialog.active) return - - event.preventDefault() - const hasFiles = event.dataTransfer?.types.includes("Files") - if (hasFiles) { - setStore("dragging", true) - } + const clearEditor = () => { + editorRef.innerHTML = "" } - const handleGlobalDragLeave = (event: DragEvent) => { - if (dialog.active) return - - // relatedTarget is null when leaving the document window - if (!event.relatedTarget) { - setStore("dragging", false) - } + const setEditorText = (text: string) => { + clearEditor() + editorRef.textContent = text } - const handleGlobalDrop = async (event: DragEvent) => { - if (dialog.active) return - - event.preventDefault() - setStore("dragging", false) - - const dropped = event.dataTransfer?.files - if (!dropped) return + const focusEditorEnd = () => { + requestAnimationFrame(() => { + editorRef.focus() + const range = document.createRange() + const selection = window.getSelection() + range.selectNodeContents(editorRef) + range.collapse(false) + selection?.removeAllRanges() + selection?.addRange(range) + }) + } - for (const file of Array.from(dropped)) { - if (ACCEPTED_FILE_TYPES.includes(file.type)) { - await addImageAttachment(file) - } - } + const currentCursor = () => { + const selection = window.getSelection() + if (!selection || selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) return null + return getCursorPosition(editorRef) } - onMount(() => { - document.addEventListener("dragover", handleGlobalDragOver) - document.addEventListener("dragleave", handleGlobalDragLeave) - document.addEventListener("drop", handleGlobalDrop) - }) - onCleanup(() => { - document.removeEventListener("dragover", handleGlobalDragOver) - document.removeEventListener("dragleave", handleGlobalDragLeave) - document.removeEventListener("drop", handleGlobalDrop) - }) + const renderEditorWithCursor = (parts: Prompt) => { + const cursor = currentCursor() + renderEditor(parts) + if (cursor !== null) setCursorPosition(editorRef, cursor) + } createEffect(() => { - if (!isFocused()) setStore("popover", null) + params.id + if (params.id) return + if (!suggest()) return + const interval = setInterval(() => { + setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length) + }, 6500) + onCleanup(() => clearInterval(interval)) }) - // Safety: reset composing state on focus change to prevent stuck state - // This handles edge cases where compositionend event may not fire - createEffect(() => { - if (!isFocused()) setComposing(false) - }) + const [composing, setComposing] = createSignal(false) + const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 - type AtOption = - | { type: "agent"; name: string; display: string } - | { type: "file"; path: string; display: string; recent?: boolean } + 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 @@ -476,6 +589,7 @@ export const PromptInput: Component = (props) => { title: cmd.name, description: cmd.description, type: "custom" as const, + source: cmd.source, })) return [...custom, ...builtin] @@ -483,26 +597,17 @@ export const PromptInput: Component = (props) => { const handleSlashSelect = (cmd: SlashCommand | undefined) => { if (!cmd) return - setStore("popover", null) + closePopover() if (cmd.type === "custom") { const text = `/${cmd.trigger} ` - editorRef.innerHTML = "" - editorRef.textContent = text + setEditorText(text) prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) - requestAnimationFrame(() => { - editorRef.focus() - const range = document.createRange() - const sel = window.getSelection() - range.selectNodeContents(editorRef) - range.collapse(false) - sel?.removeAllRanges() - sel?.addRange(range) - }) + focusEditorEnd() return } - editorRef.innerHTML = "" + clearEditor() prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0) command.trigger(cmd.id, "slash") } @@ -513,11 +618,10 @@ export const PromptInput: Component = (props) => { setActive: setSlashActive, onInput: slashOnInput, onKeyDown: slashOnKeyDown, - refetch: slashRefetch, } = useFilteredList({ items: slashCommands, key: (x) => x?.id, - filterKeys: ["trigger", "title", "description"], + filterKeys: ["trigger", "title"], onSelect: handleSlashSelect, }) @@ -543,10 +647,7 @@ export const PromptInput: Component = (props) => { const prev = node.previousSibling const next = node.nextSibling const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR" - const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR" - if (!prevIsBr && !nextIsBr) return false - if (nextIsBr && !prevIsBr && prev) return false - return true + return !!prevIsBr && !next } if (node.nodeType !== Node.ELEMENT_NODE) return false const el = node as HTMLElement @@ -556,7 +657,7 @@ export const PromptInput: Component = (props) => { }) const renderEditor = (parts: Prompt) => { - editorRef.innerHTML = "" + clearEditor() for (const part of parts) { if (part.type === "text") { editorRef.appendChild(createTextFragment(part.content)) @@ -566,15 +667,12 @@ export const PromptInput: Component = (props) => { editorRef.appendChild(createPill(part)) } } - } - createEffect( - on( - () => sync.data.command, - () => slashRefetch(), - { defer: true }, - ), - ) + 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(() => { @@ -606,25 +704,27 @@ export const PromptInput: Component = (props) => { } } + 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(), - (currentParts) => { - const inputParts = currentParts.filter((part) => part.type !== "image") as Prompt - const domParts = parseFromDOM() - if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return - - const selection = window.getSelection() - let cursorPosition: number | null = null - if (selection && selection.rangeCount > 0 && editorRef.contains(selection.anchorNode)) { - cursorPosition = getCursorPosition(editorRef) - } - - renderEditor(inputParts) - - if (cursorPosition !== null) { - setCursorPosition(editorRef, cursorPosition) - } + (parts) => { + if (composing()) return + reconcile(parts.filter((part) => part.type !== "image")) }, ), ) @@ -635,7 +735,9 @@ export const PromptInput: Component = (props) => { let buffer = "" const flushText = () => { - const content = buffer.replace(/\r\n?/g, "\n").replace(/\u200B/g, "") + 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 }) @@ -713,18 +815,18 @@ export const PromptInput: Component = (props) => { const rawParts = parseFromDOM() const images = imageAttachments() const cursorPosition = getCursorPosition(editorRef) - const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("") - const trimmed = rawText.replace(/\u200B/g, "").trim() + 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 = trimmed.length === 0 && !hasNonText && images.length === 0 + const shouldReset = !NON_EMPTY_TEXT.test(rawText) && !hasNonText && images.length === 0 if (shouldReset) { - setStore("popover", null) - if (store.historyIndex >= 0 && !store.applyingHistory) { - setStore("historyIndex", -1) - setStore("savedPrompt", null) - } + closePopover() + resetHistoryNavigation() if (prompt.dirty()) { + mirror.input = true prompt.set(DEFAULT_PROMPT, 0) } queueScroll() @@ -744,70 +846,50 @@ export const PromptInput: Component = (props) => { slashOnInput(slashMatch[1]) setStore("popover", "slash") } else { - setStore("popover", null) + closePopover() } } else { - setStore("popover", null) + closePopover() } - if (store.historyIndex >= 0 && !store.applyingHistory) { - setStore("historyIndex", -1) - setStore("savedPrompt", null) - } + resetHistoryNavigation() + mirror.input = true prompt.set([...rawParts, ...images], cursorPosition) queueScroll() } - const setRangeEdge = (range: Range, edge: "start" | "end", offset: number) => { - let remaining = offset - const nodes = Array.from(editorRef.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 - } + const addPart = (part: ContentPart) => { + if (part.type === "image") return false - 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 - } + const selection = window.getSelection() + if (!selection) return false - remaining -= length + if (selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) { + editorRef.focus() + const cursor = prompt.cursor() ?? promptLength(prompt.current()) + setCursorPosition(editorRef, cursor) } - } - const addPart = (part: ContentPart) => { - const selection = window.getSelection() - if (!selection || selection.rangeCount === 0) return - - const cursorPosition = getCursorPosition(editorRef) - const currentPrompt = prompt.current() - const rawText = currentPrompt.map((p) => ("content" in p ? p.content : "")).join("") - const textBeforeCursor = rawText.substring(0, cursorPosition) - const atMatch = textBeforeCursor.match(/@(\S*)$/) + 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(" ") - const range = selection.getRangeAt(0) if (atMatch) { const start = atMatch.index ?? cursorPosition - atMatch[0].length - setRangeEdge(range, "start", start) - setRangeEdge(range, "end", cursorPosition) + setRangeEdge(editorRef, range, "start", start) + setRangeEdge(editorRef, range, "end", cursorPosition) } range.deleteContents() @@ -817,8 +899,9 @@ export const PromptInput: Component = (props) => { range.collapse(true) selection.removeAllRanges() selection.addRange(range) - } else if (part.type === "text") { - const range = selection.getRangeAt(0) + } + + if (part.type === "text") { const fragment = createTextFragment(part.content) const last = fragment.lastChild range.deleteContents() @@ -834,7 +917,17 @@ export const PromptInput: Component = (props) => { } } if (last.nodeType !== Node.TEXT_NODE) { - range.setStartAfter(last) + 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) @@ -843,76 +936,83 @@ export const PromptInput: Component = (props) => { } handleInput() - setStore("popover", null) + closePopover() + return true } - const abort = () => - sdk.client.session - .abort({ - sessionID: params.id!, - }) - .catch(() => {}) - const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => { - const text = prompt - .map((p) => ("content" in p ? p.content : "")) - .join("") - .trim() - const hasImages = prompt.some((part) => part.type === "image") - if (!text && !hasImages) return - - const entry = clonePromptParts(prompt) const currentHistory = mode === "shell" ? shellHistory : history const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory - const lastEntry = currentHistory.entries[0] - if (lastEntry && isPromptEqual(lastEntry, entry)) return - - setCurrentHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY)) + const next = prependHistoryEntry(currentHistory.entries, prompt, mode === "shell" ? [] : historyComments()) + if (next === currentHistory.entries) return + setCurrentHistory("entries", next) } const navigateHistory = (direction: "up" | "down") => { - const entries = store.mode === "shell" ? shellHistory.entries : history.entries - const current = store.historyIndex - - if (direction === "up") { - if (entries.length === 0) return false - if (current === -1) { - setStore("savedPrompt", clonePromptParts(prompt.current())) - setStore("historyIndex", 0) - applyHistoryPrompt(entries[0], "start") - return true - } - if (current < entries.length - 1) { - const next = current + 1 - setStore("historyIndex", next) - applyHistoryPrompt(entries[next], "start") - return true - } - return false - } + 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 + } - if (current > 0) { - const next = current - 1 - setStore("historyIndex", next) - applyHistoryPrompt(entries[next], "end") - return true - } - if (current === 0) { - setStore("historyIndex", -1) - const saved = store.savedPrompt - if (saved) { - applyHistoryPrompt(saved, "end") - setStore("savedPrompt", null) - return true - } - applyHistoryPrompt(DEFAULT_PROMPT, "end") - return true - } + const { addImageAttachment, removeImageAttachment, handlePaste } = createPromptAttachments({ + editor: () => editorRef, + isFocused, + isDialogActive: () => !!dialog.active, + setDraggingType: (type) => setStore("draggingType", type), + focusEditor: () => { + editorRef.focus() + setCursorPosition(editorRef, promptLength(prompt.current())) + }, + addPart, + readClipboardImage: platform.readClipboardImage, + }) - return false - } + 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, + 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) { @@ -940,13 +1040,39 @@ export const PromptInput: Component = (props) => { return } } - if (store.mode === "shell") { - const { collapsed, cursorPosition, textLength } = getCaretState() - if (event.key === "Escape") { + + if (event.key === "Escape") { + if (store.popover) { + closePopover() + event.preventDefault() + event.stopPropagation() + return + } + + if (store.mode === "shell") { setStore("mode", "normal") event.preventDefault() + event.stopPropagation() + return + } + + if (working()) { + abort() + event.preventDefault() + event.stopPropagation() + return + } + + if (escBlur()) { + editorRef.blur() + event.preventDefault() + event.stopPropagation() return } + } + + if (store.mode === "shell") { + const { collapsed, cursorPosition, textLength } = getCaretState() if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) { setStore("mode", "normal") event.preventDefault() @@ -966,13 +1092,17 @@ export const PromptInput: Component = (props) => { return } + const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey + if (store.popover) { if (event.key === "Tab") { selectPopoverActive() event.preventDefault() return } - if (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter") { + 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() @@ -986,11 +1116,9 @@ export const PromptInput: Component = (props) => { } } - const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey - if (ctrl && event.code === "KeyG") { if (store.popover) { - setStore("popover", null) + closePopover() event.preventDefault() return } @@ -1007,29 +1135,13 @@ export const PromptInput: Component = (props) => { if (!collapsed) return const cursorPosition = getCursorPosition(editorRef) - const textLength = promptLength(prompt.current()) const textContent = prompt .current() .map((part) => ("content" in part ? part.content : "")) .join("") - const isEmpty = textContent.trim() === "" || textLength <= 1 - const hasNewlines = textContent.includes("\n") - const inHistory = store.historyIndex >= 0 - const atStart = cursorPosition <= (isEmpty ? 1 : 0) - const atEnd = cursorPosition >= (isEmpty ? textLength - 1 : textLength) - const allowUp = isEmpty || atStart || (!hasNewlines && !inHistory) || (inHistory && atEnd) - const allowDown = isEmpty || atEnd || (!hasNewlines && !inHistory) || (inHistory && atStart) - - if (event.key === "ArrowUp") { - if (!allowUp) return - if (navigateHistory("up")) { - event.preventDefault() - } - return - } - - if (!allowDown) return - if (navigateHistory("down")) { + const direction = event.key === "ArrowUp" ? "up" : "down" + if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition, store.historyIndex >= 0)) return + if (navigateHistory(direction)) { event.preventDefault() } return @@ -1039,674 +1151,259 @@ export const PromptInput: Component = (props) => { if (event.key === "Enter" && !event.shiftKey) { handleSubmit(event) } - if (event.key === "Escape") { - if (store.popover) { - setStore("popover", null) - } else if (working()) { - abort() - } - } - } - - const handleSubmit = async (event: Event) => { - event.preventDefault() - - const currentPrompt = prompt.current() - const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("") - const images = imageAttachments().slice() - const mode = store.mode - - if (text.trim().length === 0 && images.length === 0) { - if (working()) abort() - return - } - - const currentModel = local.model.current() - const currentAgent = local.agent.current() - if (!currentModel || !currentAgent) { - showToast({ - title: language.t("prompt.toast.modelAgentRequired.title"), - description: language.t("prompt.toast.modelAgentRequired.description"), - }) - return - } - - 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") - } - - addToHistory(currentPrompt, mode) - setStore("historyIndex", -1) - setStore("savedPrompt", null) - - const projectDirectory = sdk.directory - const isNewSession = !params.id - const worktreeSelection = props.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 - } - sessionDirectory = createdWorktree.directory - } - - if (worktreeSelection !== "main" && worktreeSelection !== "create") { - sessionDirectory = worktreeSelection - } - - if (sessionDirectory !== projectDirectory) { - client = createOpencodeClient({ - baseUrl: sdk.url, - fetch: platform.fetch, - directory: sessionDirectory, - throwOnError: true, - }) - globalSync.child(sessionDirectory) - } - - props.onNewSessionWorktreeReset?.() - } - - let session = info() - if (!session && isNewSession) { - session = 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 (session) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) - } - if (!session) return - - props.onSubmit?.() - - const model = { - modelID: currentModel.id, - providerID: currentModel.provider.id, - } - const agent = currentAgent.name - const variant = local.model.variant.current() - - const clearInput = () => { - prompt.reset() - setStore("mode", "normal") - setStore("popover", null) - } - - const restoreInput = () => { - prompt.set(currentPrompt, promptLength(currentPrompt)) - setStore("mode", mode) - setStore("popover", null) - requestAnimationFrame(() => { - editorRef.focus() - setCursorPosition(editorRef, promptLength(currentPrompt)) - queueScroll() - }) - } - - 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: errorMessage(err), - }) - restoreInput() - }) - return - } - } - - const toAbsolutePath = (path: string) => - path.startsWith("/") ? path : (sessionDirectory + "/" + path).replace("//", "/") - - const fileAttachments = currentPrompt.filter((part) => part.type === "file") as FileAttachmentPart[] - const agentAttachments = currentPrompt.filter((part) => part.type === "agent") as AgentPart[] - - const fileAttachmentParts = fileAttachments.map((attachment) => { - const absolute = toAbsolutePath(attachment.path) - const query = attachment.selection - ? `?start=${attachment.selection.startLine}&end=${attachment.selection.endLine}` - : "" - return { - id: Identifier.ascending("part"), - type: "file" as const, - mime: "text/plain", - url: `file://${absolute}${query}`, - filename: getFilename(attachment.path), - source: { - type: "file" as const, - text: { - value: attachment.content, - start: attachment.start, - end: attachment.end, - }, - path: absolute, - }, - } - }) - - const agentAttachmentParts = agentAttachments.map((attachment) => ({ - id: Identifier.ascending("part"), - type: "agent" as const, - name: attachment.name, - source: { - value: attachment.content, - start: attachment.start, - end: attachment.end, - }, - })) - - const usedUrls = new Set(fileAttachmentParts.map((part) => part.url)) - - const context = prompt.context.items().slice() - - const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim()) - - const contextParts: Array< - | { - id: string - type: "text" - text: string - synthetic?: boolean - } - | { - id: string - type: "file" - mime: string - url: string - filename?: string - } - > = [] - - const commentNote = (path: string, selection: FileSelection | undefined, comment: string) => { - const start = selection ? Math.min(selection.startLine, selection.endLine) : undefined - const end = selection ? Math.max(selection.startLine, 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 ${path}: ${comment}` - } - - const addContextFile = (input: { path: string; selection?: FileSelection; comment?: string }) => { - const absolute = toAbsolutePath(input.path) - const query = input.selection ? `?start=${input.selection.startLine}&end=${input.selection.endLine}` : "" - const url = `file://${absolute}${query}` - - const comment = input.comment?.trim() - if (!comment && usedUrls.has(url)) return - usedUrls.add(url) - - if (comment) { - contextParts.push({ - id: Identifier.ascending("part"), - type: "text", - text: commentNote(input.path, input.selection, comment), - synthetic: true, - }) - } - - contextParts.push({ - id: Identifier.ascending("part"), - type: "file", - mime: "text/plain", - url, - filename: getFilename(input.path), - }) - } - - for (const item of context) { - if (item.type !== "file") continue - addContextFile({ path: item.path, selection: item.selection, comment: item.comment }) - } - - const imageAttachmentParts = images.map((attachment) => ({ - id: Identifier.ascending("part"), - type: "file" as const, - mime: attachment.mime, - url: attachment.dataUrl, - filename: attachment.filename, - })) - - const messageID = Identifier.ascending("message") - const textPart = { - id: Identifier.ascending("part"), - type: "text" as const, - text, - } - const requestParts = [ - textPart, - ...fileAttachmentParts, - ...contextParts, - ...agentAttachmentParts, - ...imageAttachmentParts, - ] - - const optimisticParts = requestParts.map((part) => ({ - ...part, - sessionID: session.id, - messageID, - })) as unknown as Part[] - - const optimisticMessage: Message = { - id: messageID, - sessionID: session.id, - role: "user", - time: { created: Date.now() }, - agent, - model, - } - - const setSyncStore = sessionDirectory === projectDirectory ? sync.set : globalSync.child(sessionDirectory)[1] - - const addOptimisticMessage = () => { - setSyncStore( - produce((draft) => { - const messages = draft.message[session.id] - if (!messages) { - draft.message[session.id] = [optimisticMessage] - } else { - const result = Binary.search(messages, messageID, (m) => m.id) - messages.splice(result.index, 0, optimisticMessage) - } - draft.part[messageID] = optimisticParts - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) - }), - ) - } - - const removeOptimisticMessage = () => { - setSyncStore( - produce((draft) => { - const messages = draft.message[session.id] - if (messages) { - const result = Binary.search(messages, messageID, (m) => m.id) - if (result.found) messages.splice(result.index, 1) - } - delete draft.part[messageID] - }), - ) - } - - for (const item of commentItems) { - prompt.context.remove(item.key) - } - - clearInput() - addOptimisticMessage() - - client.session - .prompt({ - sessionID: session.id, - agent, - model, - messageID, - parts: requestParts, - variant, - }) - .catch((err) => { - showToast({ - title: language.t("prompt.toast.promptSendFailed.title"), - description: errorMessage(err), - }) - removeOptimisticMessage() - for (const item of commentItems) { - prompt.context.add({ - type: "file", - path: item.path, - selection: item.selection, - comment: item.comment, - commentID: item.commentID, - preview: item.preview, - }) - } - restoreInput() - }) } return ( -
- -
{ - if (store.popover === "slash") slashPopoverRef = el - }} - class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10 - overflow-auto no-scrollbar flex flex-col p-2 rounded-md - border border-border-base bg-surface-raised-stronger-non-alpha shadow-md" - onMouseDown={(e) => e.preventDefault()} - > - - - 0} - fallback={
{language.t("prompt.popover.emptyResults")}
} - > - - {(item) => ( - - )} - -
-
- - 0} - fallback={
{language.t("prompt.popover.emptyCommands")}
} - > - - {(cmd) => ( - - )} - -
-
-
-
-
-
+ (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])} + /> + - -
-
- - {language.t("prompt.dropzone.label")} -
-
-
- 0}> -
- - {(item) => { - return ( -
{ - if (!item.commentID) return - comments.setFocus({ file: item.path, id: item.commentID }) - view().reviewPanel.open() - tabs().open("review") - }} - > -
- -
- {getDirectory(item.path)} - {getFilename(item.path)} - - {(sel) => ( - - {sel().startLine === sel().endLine - ? `:${sel().startLine}` - : `:${sel().startLine}-${sel().endLine}`} - - )} - -
- { - e.stopPropagation() - if (item.commentID) comments.remove(item.path, item.commentID) - prompt.context.remove(item.key) - }} - aria-label={language.t("prompt.context.removeFile")} - /> -
- - {(comment) =>
{comment()}
} -
-
- ) + + { + 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={removeImageAttachment} + 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"], [data-action="prompt-permissions"]', + ) + ) { + return + } + editorRef?.focus() + }} + > +
(scrollRef = el)}> +
{ + 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"} + onInput={handleInput} + onPaste={handlePaste} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + classList={{ + "select-text": true, + "w-full pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, + "[&_[data-type=file]]:text-syntax-property": true, + "[&_[data-type=agent]]:text-syntax-type": true, + "font-mono!": store.mode === "shell", }} - + /> + +
+ {placeholder()} +
+
- - 0}> -
- - {(attachment) => ( -
- - + +
+ { + const file = e.currentTarget.files?.[0] + if (file) addImageAttachment(file) + e.currentTarget.value = "" + }} + /> + +
0.5 ? "auto" : "none", + }} + > + + + + + + +
+ {language.t("prompt.action.stop")} + {language.t("common.key.esc")}
- } - > - {attachment.filename} - dialog.show(() => ) - } - /> - - -
- {attachment.filename} -
-
- )} - + + +
+ {language.t("prompt.action.send")} + +
+
+ + } + > + + +
- -
(scrollRef = el)}> -
{ - editorRef = el - props.ref?.(el) - }} - role="textbox" - aria-multiline="true" - aria-label={ - store.mode === "shell" - ? language.t("prompt.placeholder.shell") - : language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) }) - } - contenteditable="true" - onInput={handleInput} - onPaste={handlePaste} - onCompositionStart={() => setComposing(true)} - onCompositionEnd={() => setComposing(false)} - onKeyDown={handleKeyDown} - classList={{ - "select-text": true, - "w-full px-5 py-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, - "[&_[data-type=file]]:text-syntax-property": true, - "[&_[data-type=agent]]:text-syntax-type": true, - "font-mono!": store.mode === "shell", - }} - /> - -
- {store.mode === "shell" - ? language.t("prompt.placeholder.shell") - : language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })} + +
+
+ + +
- +
-
-
- - -
- - {language.t("prompt.mode.shell")} - {language.t("prompt.mode.shell.exit")} -
-
- + + + +
+
+
+ {language.t("prompt.mode.shell")} +
+
+
(x === "default" ? language.t("common.default") : x)} + onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)} + class="capitalize max-w-[160px]" + valueClass="truncate text-13-regular" + triggerStyle={control()} + variant="ghost" + /> + +
+
+
+ mode} + label={(mode) => ( - + /> - - - -
-
- { - const file = e.currentTarget.files?.[0] - if (file) addImageAttachment(file) - e.currentTarget.value = "" - }} - /> -
- - - - - - -
- - -
- {language.t("prompt.action.stop")} - {language.t("common.key.esc")} -
-
- -
- {language.t("prompt.action.send")} - -
-
- - } - > - mode && setMode(mode)} + fill + pad="none" + class="w-[68px]" /> -
+
-
- +
+
) } - -function createTextFragment(content: string): DocumentFragment { - const fragment = document.createDocumentFragment() - const segments = content.split("\n") - segments.forEach((segment, index) => { - if (segment) { - fragment.appendChild(document.createTextNode(segment)) - } else if (segments.length > 1) { - fragment.appendChild(document.createTextNode("\u200B")) - } - if (index < segments.length - 1) { - fragment.appendChild(document.createElement("br")) - } - }) - return fragment -} - -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 -} - -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 -} - -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()) -} - -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) -} diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts new file mode 100644 index 00000000000..a9e4e496512 --- /dev/null +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -0,0 +1,180 @@ +import { onCleanup, onMount } from "solid-js" +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" + +export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] +export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"] +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 +} + +type PromptAttachmentsInput = { + editor: () => HTMLDivElement | undefined + isFocused: () => boolean + 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 addImageAttachment = async (file: File) => { + if (!ACCEPTED_FILE_TYPES.includes(file.type)) return + + const reader = new FileReader() + reader.onload = () => { + const editor = input.editor() + if (!editor) return + const dataUrl = reader.result as string + const attachment: ImageAttachmentPart = { + type: "image", + id: uuid(), + filename: file.name, + mime: file.type, + dataUrl, + } + const cursorPosition = prompt.cursor() ?? getCursorPosition(editor) + prompt.set([...prompt.current(), attachment], cursorPosition) + } + reader.readAsDataURL(file) + } + + const removeImageAttachment = (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) => { + if (!input.isFocused()) return + const clipboardData = event.clipboardData + if (!clipboardData) return + + event.preventDefault() + event.stopPropagation() + + const items = Array.from(clipboardData.items) + const fileItems = items.filter((item) => item.kind === "file") + const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type)) + + if (imageItems.length > 0) { + for (const item of imageItems) { + const file = item.getAsFile() + if (file) await addImageAttachment(file) + } + return + } + + if (fileItems.length > 0) { + showToast({ + title: language.t("prompt.toast.pasteUnsupported.title"), + description: language.t("prompt.toast.pasteUnsupported.description"), + }) + 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 addImageAttachment(file) + return + } + } + + if (!plainText) return + + if (largePaste(plainText)) { + if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return + input.focusEditor() + if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return + } + + const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText) + if (inserted) return + + input.addPart({ type: "text", content: plainText, start: 0, end: 0 }) + } + + 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 + + for (const file of Array.from(dropped)) { + if (ACCEPTED_FILE_TYPES.includes(file.type)) { + await addImageAttachment(file) + } + } + } + + onMount(() => { + document.addEventListener("dragover", handleGlobalDragOver) + document.addEventListener("dragleave", handleGlobalDragLeave) + document.addEventListener("drop", handleGlobalDrop) + }) + + onCleanup(() => { + document.removeEventListener("dragover", handleGlobalDragOver) + document.removeEventListener("dragleave", handleGlobalDragLeave) + document.removeEventListener("drop", handleGlobalDrop) + }) + + return { + addImageAttachment, + removeImageAttachment, + 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 new file mode 100644 index 00000000000..4c2e2d8bec9 --- /dev/null +++ b/packages/app/src/components/prompt-input/build-request-parts.test.ts @@ -0,0 +1,286 @@ +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("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("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 new file mode 100644 index 00000000000..4146fb4847f --- /dev/null +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -0,0 +1,175 @@ +import { getFilename } from "@opencode-ai/util/path" +import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/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 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] + + 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, + ] + }) + + 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 new file mode 100644 index 00000000000..b138fe3ef69 --- /dev/null +++ b/packages/app/src/components/prompt-input/context-items.tsx @@ -0,0 +1,88 @@ +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/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 new file mode 100644 index 00000000000..41962ce536e --- /dev/null +++ b/packages/app/src/components/prompt-input/drag-overlay.tsx @@ -0,0 +1,25 @@ +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 new file mode 100644 index 00000000000..3088522a59f --- /dev/null +++ b/packages/app/src/components/prompt-input/editor-dom.test.ts @@ -0,0 +1,99 @@ +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 new file mode 100644 index 00000000000..8575140d7d5 --- /dev/null +++ b/packages/app/src/components/prompt-input/editor-dom.ts @@ -0,0 +1,148 @@ +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/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts new file mode 100644 index 00000000000..37b5ce19627 --- /dev/null +++ b/packages/app/src/components/prompt-input/history.test.ts @@ -0,0 +1,150 @@ +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(true) + 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(true) + expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false) + expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false) + + 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 new file mode 100644 index 00000000000..de62653211d --- /dev/null +++ b/packages/app/src/components/prompt-input/history.ts @@ -0,0 +1,256 @@ +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 + 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 new file mode 100644 index 00000000000..835fddc3071 --- /dev/null +++ b/packages/app/src/components/prompt-input/image-attachments.tsx @@ -0,0 +1,58 @@ +import { Component, For, Show } from "solid-js" +import { Icon } from "@opencode-ai/ui/icon" +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/placeholder.test.ts b/packages/app/src/components/prompt-input/placeholder.test.ts new file mode 100644 index 00000000000..5f6aa59e9a4 --- /dev/null +++ b/packages/app/src/components/prompt-input/placeholder.test.ts @@ -0,0 +1,48 @@ +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") + }) + + 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 new file mode 100644 index 00000000000..395fee51b1c --- /dev/null +++ b/packages/app/src/components/prompt-input/placeholder.ts @@ -0,0 +1,15 @@ +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") + 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 new file mode 100644 index 00000000000..65eb01c797b --- /dev/null +++ b/packages/app/src/components/prompt-input/slash-popover.tsx @@ -0,0 +1,141 @@ +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/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 new file mode 100644 index 00000000000..4109417d2b1 --- /dev/null +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -0,0 +1,274 @@ +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<{ + message: { + agent: string + model: { providerID: string; modelID: string } + variant?: 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}` } } + }, + 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("@opencode-ai/sdk/v2/client", () => ({ + createOpencodeClient: (input: { directory: string }) => { + createdClients.push(input.directory) + return clientFor(input.directory) + }, + })) + + mock.module("@opencode-ai/ui/toast", () => ({ + showToast: () => 0, + })) + + mock.module("@opencode-ai/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" }), + }, + }), + })) + + 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: { + message: { agent: string; model: { providerID: string; modelID: string }; variant?: string } + }) => { + optimistic.push(value) + }, + remove: () => undefined, + }, + }, + set: () => undefined, + }), + })) + + mock.module("@/context/global-sync", () => ({ + useGlobalSync: () => ({ + child: (directory: string) => { + syncedDirectories.push(directory) + return [{}, () => undefined] + }, + }), + })) + + 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 + params = {} + sentShell.length = 0 + syncedDirectories.length = 0 + selected = "/repo/worktree-a" + variant = undefined +}) + +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-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", + }, + }) + }) +}) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts new file mode 100644 index 00000000000..fee6b070d94 --- /dev/null +++ b/packages/app/src/components/prompt-input/submit.ts @@ -0,0 +1,428 @@ +import type { Message } from "@opencode-ai/sdk/v2/client" +import { showToast } from "@opencode-ai/ui/toast" +import { base64Encode } from "@opencode-ai/util/encode" +import { useNavigate, useParams } from "@solidjs/router" +import type { Accessor } from "solid-js" +import type { FileSelection } from "@/context/file" +import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { useLocal } from "@/context/local" +import { usePermission } from "@/context/permission" +import { 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() + +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 + 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, []) + + 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 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()) abort() + return + } + + const currentModel = local.model.current() + const currentAgent = local.agent.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) { + session = 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 (session) { + if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory) + 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 + } + + input.onSubmit?.() + + const model = { + modelID: currentModel.id, + providerID: currentModel.provider.id, + } + const agent = currentAgent.name + const variant = local.model.variant.current() + + 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 (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 context = prompt.context.items().slice() + const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim()) + + const messageID = Identifier.ascending("message") + const { requestParts, optimisticParts } = buildRequestParts({ + prompt: currentPrompt, + context, + images, + text, + sessionID: session.id, + messageID, + sessionDirectory, + }) + + const optimisticMessage: Message = { + id: messageID, + sessionID: session.id, + role: "user", + time: { created: Date.now() }, + agent, + model, + variant, + } + + const addOptimisticMessage = () => + sync.session.optimistic.add({ + directory: sessionDirectory, + sessionID: session.id, + message: optimisticMessage, + parts: optimisticParts, + }) + + const removeOptimisticMessage = () => + sync.session.optimistic.remove({ + directory: sessionDirectory, + sessionID: session.id, + messageID, + }) + + removeCommentItems(commentItems) + clearInput() + addOptimisticMessage() + + 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 + } + + const send = async () => { + const ok = await waitForWorktree() + if (!ok) return + await client.session.promptAsync({ + sessionID: session.id, + agent, + model, + messageID, + parts: requestParts, + variant, + }) + } + + void send().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 new file mode 100644 index 00000000000..5bb290ec303 --- /dev/null +++ b/packages/app/src/components/server/server-row.tsx @@ -0,0 +1,124 @@ +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { + children, + createEffect, + createMemo, + createSignal, + type JSXElement, + onCleanup, + onMount, + type ParentProps, + Show, +} from "solid-js" +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 [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(() => { + check() + if (typeof ResizeObserver !== "function") return + const observer = new ResizeObserver(check) + if (nameRef) observer.observe(nameRef) + if (versionRef) observer.observe(versionRef) + onCleanup(() => observer.disconnect()) + }) + + 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} + ) : ( + no username + )} + + {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 index ee93b3f03c0..7379833f8b8 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -1,66 +1,78 @@ import { Match, Show, Switch, createMemo } from "solid-js" -import { Tooltip } from "@opencode-ai/ui/tooltip" +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 { useParams } from "@solidjs/router" -import { AssistantMessage } from "@opencode-ai/sdk/v2/client" +import { useFile } from "@/context/file" import { useLayout } from "@/context/layout" import { useSync } from "@/context/sync" import { useLanguage } from "@/context/language" +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") + args.tabs.open("context") + args.tabs.setActive("context") } export function SessionContextUsage(props: SessionContextUsageProps) { const sync = useSync() - const params = useParams() + const file = useFile() const layout = useLayout() const language = useLanguage() + const { params, tabs, view } = useSessionLayout() const variant = createMemo(() => props.variant ?? "button") - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - const tabs = createMemo(() => layout.tabs(sessionKey())) - const view = createMemo(() => layout.view(sessionKey())) + 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 cost = createMemo(() => { - const locale = language.locale() - const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) - return new Intl.NumberFormat(locale, { - style: "currency", - currency: "USD", - }).format(total) - }) + const usd = createMemo( + () => + new Intl.NumberFormat(language.intl(), { + style: "currency", + currency: "USD", + }), + ) - const context = createMemo(() => { - const locale = language.locale() - const last = messages().findLast((x) => { - if (x.role !== "assistant") return false - const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write - return total > 0 - }) as AssistantMessage - if (!last) return - const total = - last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write - const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID] - return { - tokens: total.toLocaleString(locale), - percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null, - } + const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all)) + const context = createMemo(() => metrics().context) + const cost = createMemo(() => { + return usd().format(metrics().totalCost) }) const openContext = () => { if (!params.id) return - view().reviewPanel.open() - tabs().open("context") - tabs().setActive("context") + + if (tabState.activeTab() === "context") { + tabs().close("context") + return + } + openSessionContext({ + view: view(), + layout, + tabs: tabs(), + }) } const circle = () => ( -
- +
+
) @@ -70,11 +82,11 @@ export function SessionContextUsage(props: SessionContextUsageProps) { {(ctx) => ( <>
- {ctx().tokens} + {ctx().total.toLocaleString(language.intl())} {language.t("context.usage.tokens")}
- {ctx().percentage ?? 0}% + {ctx().usage ?? 0}% {language.t("context.usage.usage")}
@@ -84,15 +96,12 @@ export function SessionContextUsage(props: SessionContextUsageProps) { {cost()} {language.t("context.usage.cost")}
- -
{language.t("context.usage.clickToView")}
-
) return ( - + {circle()} diff --git a/packages/app/src/components/session-lsp-indicator.tsx b/packages/app/src/components/session-lsp-indicator.tsx deleted file mode 100644 index dab92920ec4..00000000000 --- a/packages/app/src/components/session-lsp-indicator.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { createMemo, Show } from "solid-js" -import { useSync } from "@/context/sync" -import { useLanguage } from "@/context/language" -import { Tooltip } from "@opencode-ai/ui/tooltip" - -export function SessionLspIndicator() { - const sync = useSync() - const language = useLanguage() - - const lspStats = createMemo(() => { - const lsp = sync.data.lsp ?? [] - const connected = lsp.filter((s) => s.status === "connected").length - const hasError = lsp.some((s) => s.status === "error") - const total = lsp.length - return { connected, hasError, total } - }) - - const tooltipContent = createMemo(() => { - const lsp = sync.data.lsp ?? [] - if (lsp.length === 0) return language.t("lsp.tooltip.none") - return lsp.map((s) => s.name).join(", ") - }) - - return ( - 0}> - -
-
0, - }} - /> - - {language.t("lsp.label.connected", { count: lspStats().connected })} - -
- - - ) -} diff --git a/packages/app/src/components/session-mcp-indicator.tsx b/packages/app/src/components/session-mcp-indicator.tsx deleted file mode 100644 index 489223b9bf5..00000000000 --- a/packages/app/src/components/session-mcp-indicator.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { createMemo, Show } from "solid-js" -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useSync } from "@/context/sync" -import { DialogSelectMcp } from "@/components/dialog-select-mcp" - -export function SessionMcpIndicator() { - const sync = useSync() - const dialog = useDialog() - - const mcpStats = createMemo(() => { - const mcp = sync.data.mcp ?? {} - const entries = Object.entries(mcp) - const enabled = entries.filter(([, status]) => status.status === "connected").length - const failed = entries.some(([, status]) => status.status === "failed") - const total = entries.length - return { enabled, failed, total } - }) - - return ( - 0}> - - - ) -} diff --git a/packages/app/src/components/session/session-context-breakdown.test.ts b/packages/app/src/components/session/session-context-breakdown.test.ts new file mode 100644 index 00000000000..f38aecb55da --- /dev/null +++ b/packages/app/src/components/session/session-context-breakdown.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from "bun:test" +import type { Message, Part } from "@opencode-ai/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 new file mode 100644 index 00000000000..e263b2957b3 --- /dev/null +++ b/packages/app/src/components/session/session-context-breakdown.ts @@ -0,0 +1,132 @@ +import type { Message, Part } from "@opencode-ai/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 new file mode 100644 index 00000000000..e7c536d5841 --- /dev/null +++ b/packages/app/src/components/session/session-context-format.ts @@ -0,0 +1,20 @@ +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 new file mode 100644 index 00000000000..0e109a71bda --- /dev/null +++ b/packages/app/src/components/session/session-context-metrics.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, test } from "bun:test" +import type { Message } from "@opencode-ai/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 new file mode 100644 index 00000000000..0789b05f173 --- /dev/null +++ b/packages/app/src/components/session/session-context-metrics.ts @@ -0,0 +1,82 @@ +import type { AssistantMessage, Message } from "@opencode-ai/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 index 57648c380d7..9aa101bdb9a 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -1,76 +1,145 @@ import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js" import type { JSX } from "solid-js" -import { useParams } from "@solidjs/router" -import { DateTime } from "luxon" import { useSync } from "@/context/sync" -import { useLayout } from "@/context/layout" import { checksum } from "@opencode-ai/util/encode" +import { findLast } from "@opencode-ai/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 { Code } from "@opencode-ai/ui/code" +import { File } from "@opencode-ai/ui/file" import { Markdown } from "@opencode-ai/ui/markdown" -import type { AssistantMessage, Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" import { useLanguage } from "@/context/language" +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)} + /> + ) +} -interface SessionContextTabProps { - messages: () => Message[] - visibleUserMessages: () => UserMessage[] - view: () => ReturnType["view"]> - info: () => ReturnType["session"]["get"]> +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)}
+ +
+
+
+
+ +
+ +
+
+
+ ) } -export function SessionContextTab(props: SessionContextTabProps) { - const params = useParams() +const emptyMessages: Message[] = [] +const emptyUserMessages: UserMessage[] = [] + +export function SessionContextTab() { const sync = useSync() const language = useLanguage() + 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 ctx = createMemo(() => { - const last = props.messages().findLast((x) => { - if (x.role !== "assistant") return false - const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write - return total > 0 - }) as AssistantMessage - if (!last) return - - const provider = sync.data.provider.all.find((x) => x.id === last.providerID) - const model = provider?.models[last.modelID] - const limit = model?.limit.context - - const input = last.tokens.input - const output = last.tokens.output - const reasoning = last.tokens.reasoning - const cacheRead = last.tokens.cache.read - const cacheWrite = last.tokens.cache.write - const total = input + output + reasoning + cacheRead + cacheWrite - const usage = limit ? Math.round((total / limit) * 100) : null + const userMessages = createMemo( + () => messages().filter((m) => m.role === "user") as UserMessage[], + emptyUserMessages, + { equals: same }, + ) - return { - message: last, - provider, - model, - limit, - input, - output, - reasoning, - cacheRead, - cacheWrite, - total, - usage, - } - }) + 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(), sync.data.provider.all)) + const ctx = createMemo(() => metrics().context) + const formatter = createMemo(() => createSessionContextFormatter(language.intl())) const cost = createMemo(() => { - const locale = language.locale() - const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) - return new Intl.NumberFormat(locale, { - style: "currency", - currency: "USD", - }).format(total) + return usd().format(metrics().totalCost) }) const counts = createMemo(() => { - const all = props.messages() + 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 { @@ -81,7 +150,7 @@ export function SessionContextTab(props: SessionContextTabProps) { }) const systemPrompt = createMemo(() => { - const msg = props.visibleUserMessages().findLast((m) => !!m.system) + const msg = findLast(visibleUserMessages(), (m) => !!m.system) const system = msg?.system if (!system) return const trimmed = system.trim() @@ -89,238 +158,74 @@ export function SessionContextTab(props: SessionContextTabProps) { return trimmed }) - const number = (value: number | null | undefined) => { - if (value === undefined) return "—" - if (value === null) return "—" - return value.toLocaleString(language.locale()) - } - - const percent = (value: number | null | undefined) => { - if (value === undefined) return "—" - if (value === null) return "—" - return value.toLocaleString(language.locale()) + "%" - } - - const time = (value: number | undefined) => { - if (!value) return "—" - return DateTime.fromMillis(value).setLocale(language.locale()).toLocaleString(DateTime.DATETIME_MED) - } - const providerLabel = createMemo(() => { const c = ctx() if (!c) return "—" - return c.provider?.name ?? c.message.providerID + return c.providerLabel }) const modelLabel = createMemo(() => { const c = ctx() if (!c) return "—" - if (c.model?.name) return c.model.name - return c.message.modelID + return c.modelLabel }) const breakdown = createMemo( on( - () => [ctx()?.message.id, ctx()?.input, props.messages().length, systemPrompt()], + () => [ctx()?.message.id, ctx()?.input, messages().length, systemPrompt()], () => { const c = ctx() - if (!c) return [] - const input = c.input - if (!input) return [] - - const out = { - system: systemPrompt()?.length ?? 0, - user: 0, - assistant: 0, - tool: 0, - } - - for (const msg of props.messages()) { - const parts = (sync.data.part[msg.id] ?? []) as Part[] - - if (msg.role === "user") { - for (const part of parts) { - if (part.type === "text") out.user += part.text.length - if (part.type === "file") out.user += part.source?.text.value.length ?? 0 - if (part.type === "agent") out.user += part.source?.value.length ?? 0 - } - continue - } - - if (msg.role === "assistant") { - for (const part of parts) { - if (part.type === "text") out.assistant += part.text.length - if (part.type === "reasoning") out.assistant += part.text.length - if (part.type === "tool") { - out.tool += Object.keys(part.state.input).length * 16 - if (part.state.status === "pending") out.tool += part.state.raw.length - if (part.state.status === "completed") out.tool += part.state.output.length - if (part.state.status === "error") out.tool += part.state.error.length - } - } - } - } - - const estimateTokens = (chars: number) => Math.ceil(chars / 4) - const system = estimateTokens(out.system) - const user = estimateTokens(out.user) - const assistant = estimateTokens(out.assistant) - const tool = estimateTokens(out.tool) - const estimated = system + user + assistant + tool - - const pct = (tokens: number) => (tokens / input) * 100 - const pctLabel = (tokens: number) => (Math.round(pct(tokens) * 10) / 10).toString() + "%" - - const build = (tokens: { system: number; user: number; assistant: number; tool: number; other: number }) => { - return [ - { - key: "system", - label: language.t("context.breakdown.system"), - tokens: tokens.system, - width: pct(tokens.system), - percent: pctLabel(tokens.system), - color: "var(--syntax-info)", - }, - { - key: "user", - label: language.t("context.breakdown.user"), - tokens: tokens.user, - width: pct(tokens.user), - percent: pctLabel(tokens.user), - color: "var(--syntax-success)", - }, - { - key: "assistant", - label: language.t("context.breakdown.assistant"), - tokens: tokens.assistant, - width: pct(tokens.assistant), - percent: pctLabel(tokens.assistant), - color: "var(--syntax-property)", - }, - { - key: "tool", - label: language.t("context.breakdown.tool"), - tokens: tokens.tool, - width: pct(tokens.tool), - percent: pctLabel(tokens.tool), - color: "var(--syntax-warning)", - }, - { - key: "other", - label: language.t("context.breakdown.other"), - tokens: tokens.other, - width: pct(tokens.other), - percent: pctLabel(tokens.other), - color: "var(--syntax-comment)", - }, - ].filter((x) => x.tokens > 0) - } - - if (estimated <= input) { - return build({ system, user, assistant, tool, other: input - estimated }) - } - - const scale = input / estimated - const scaled = { - system: Math.floor(system * scale), - user: Math.floor(user * scale), - assistant: Math.floor(assistant * scale), - tool: Math.floor(tool * scale), - } - const scaledTotal = scaled.system + scaled.user + scaled.assistant + scaled.tool - return build({ ...scaled, other: Math.max(0, input - scaledTotal) }) + if (!c?.input) return [] + return estimateSessionContextBreakdown({ + messages: messages(), + parts: sync.data.part as Record, + input: c.input, + systemPrompt: systemPrompt(), + }) }, ), ) - function Stat(statProps: { label: string; value: JSX.Element }) { - return ( -
-
{statProps.label}
-
{statProps.value}
-
- ) - } - - const stats = createMemo(() => { - const c = ctx() - const count = counts() - return [ - { label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" }, - { label: language.t("context.stats.messages"), value: count.all.toLocaleString(language.locale()) }, - { label: language.t("context.stats.provider"), value: providerLabel() }, - { label: language.t("context.stats.model"), value: modelLabel() }, - { label: language.t("context.stats.limit"), value: number(c?.limit) }, - { label: language.t("context.stats.totalTokens"), value: number(c?.total) }, - { label: language.t("context.stats.usage"), value: percent(c?.usage) }, - { label: language.t("context.stats.inputTokens"), value: number(c?.input) }, - { label: language.t("context.stats.outputTokens"), value: number(c?.output) }, - { label: language.t("context.stats.reasoningTokens"), value: number(c?.reasoning) }, - { - label: language.t("context.stats.cacheTokens"), - value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`, - }, - { label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) }, - { - label: language.t("context.stats.assistantMessages"), - value: count.assistant.toLocaleString(language.locale()), - }, - { label: language.t("context.stats.totalCost"), value: cost() }, - { label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) }, - { label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) }, - ] satisfies { label: string; value: JSX.Element }[] - }) - - function RawMessageContent(msgProps: { message: Message }) { - const file = createMemo(() => { - const parts = (sync.data.part[msgProps.message.id] ?? []) as Part[] - const contents = JSON.stringify({ message: msgProps.message, parts }, null, 2) - return { - name: `${msgProps.message.role}-${msgProps.message.id}.json`, - contents, - cacheKey: checksum(contents), - } - }) - - return ( - requestAnimationFrame(restoreScroll)} /> - ) + 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") } - function RawMessage(msgProps: { message: Message }) { - return ( - - - -
-
- {msgProps.message.role} • {msgProps.message.id} -
-
-
{time(msgProps.message.time.created)}
- -
-
-
-
- -
- -
-
-
- ) - } + 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 = props.view()?.scroll("context") + const s = view().scroll("context") if (!s) return if (el.scrollTop !== s.y) el.scrollTop = s.y @@ -341,13 +246,13 @@ export function SessionContextTab(props: SessionContextTabProps) { pending = undefined if (!next) return - props.view().setScroll("context", next) + view().setScroll("context", next) }) } createEffect( on( - () => props.messages().length, + () => messages().length, () => { requestAnimationFrame(restoreScroll) }, @@ -361,9 +266,9 @@ export function SessionContextTab(props: SessionContextTabProps) { }) return ( -
{ + { scroll = el restoreScroll() }} @@ -371,7 +276,9 @@ export function SessionContextTab(props: SessionContextTabProps) { >
- {(stat) => } + + {(stat) => [0])} value={stat.value()} />} +
0}> @@ -384,7 +291,7 @@ export function SessionContextTab(props: SessionContextTabProps) { class="h-full" style={{ width: `${segment.width}%`, - "background-color": segment.color, + "background-color": BREAKDOWN_COLOR[segment.key], }} /> )} @@ -394,9 +301,9 @@ export function SessionContextTab(props: SessionContextTabProps) { {(segment) => (
-
-
{segment.label}
-
{segment.percent}
+
+
{breakdownLabel(segment.key)}
+
{segment.percent.toLocaleString(language.intl())}%
)} @@ -419,10 +326,14 @@ export function SessionContextTab(props: SessionContextTabProps) {
{language.t("context.rawMessages.title")}
- {(message) => } + + {(message) => ( + + )} +
-
+ ) } diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index a76734a691d..9476f8b9ba0 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -1,64 +1,161 @@ -import { createEffect, createMemo, onCleanup, Show } from "solid-js" +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 { Popover } from "@opencode-ai/ui/popover" +import { Spinner } from "@opencode-ai/ui/spinner" +import { TextField } from "@opencode-ai/ui/text-field" +import { showToast } from "@opencode-ai/ui/toast" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { getFilename } from "@opencode-ai/util/path" +import { createEffect, createMemo, For, onCleanup, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" -import { useParams } from "@solidjs/router" -import { useLayout } from "@/context/layout" import { useCommand } from "@/context/command" +import { useGlobalSDK } from "@/context/global-sdk" import { useLanguage } from "@/context/language" -// import { useServer } from "@/context/server" -// import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" +import { useServer } from "@/context/server" import { useSync } from "@/context/sync" -import { useGlobalSDK } from "@/context/global-sdk" -import { getFilename } from "@opencode-ai/util/path" -import { base64Decode } from "@opencode-ai/util/encode" +import { useTerminal } from "@/context/terminal" +import { focusTerminalById } from "@/pages/session/helpers" +import { useSessionLayout } from "@/pages/session/session-layout" +import { decode64 } from "@/utils/base64" +import { Persist, persisted } from "@/utils/persist" +import { StatusPopover } from "../status-popover" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Button } from "@opencode-ai/ui/button" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { Popover } from "@opencode-ai/ui/popover" -import { TextField } from "@opencode-ai/ui/text-field" -import { Keybind } from "@opencode-ai/ui/keybind" +const OPEN_APPS = [ + "vscode", + "cursor", + "zed", + "textmate", + "antigravity", + "finder", + "terminal", + "iterm2", + "ghostty", + "warp", + "xcode", + "android-studio", + "powershell", + "sublime-text", +] as const -export function SessionHeader() { - const globalSDK = useGlobalSDK() - const layout = useLayout() - const params = useParams() - const command = useCommand() - // const server = useServer() - // const dialog = useDialog() - const sync = useSync() - const platform = usePlatform() - const language = useLanguage() +type OpenApp = (typeof OPEN_APPS)[number] +type OS = "macos" | "windows" | "linux" | "unknown" - const projectDirectory = createMemo(() => base64Decode(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 MAC_APPS = [ + { + id: "vscode", + label: "VS Code", + icon: "vscode", + openWith: "Visual Studio Code", + }, + { id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" }, + { id: "zed", label: "Zed", icon: "zed", openWith: "Zed" }, + { id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" }, + { + id: "antigravity", + label: "Antigravity", + icon: "antigravity", + openWith: "Antigravity", + }, + { id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" }, + { id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" }, + { id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" }, + { id: "warp", label: "Warp", icon: "warp", openWith: "Warp" }, + { id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" }, + { + id: "android-studio", + label: "Android Studio", + icon: "android-studio", + openWith: "Android Studio", + }, + { + id: "sublime-text", + label: "Sublime Text", + icon: "sublime-text", + openWith: "Sublime Text", + }, +] as const - const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id)) - const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") - const showShare = createMemo(() => shareEnabled() && !!currentSession()) - const showReview = createMemo(() => !!currentSession()) - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - const view = createMemo(() => layout.view(sessionKey())) +const WINDOWS_APPS = [ + { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" }, + { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" }, + { id: "zed", label: "Zed", icon: "zed", openWith: "zed" }, + { + id: "powershell", + label: "PowerShell", + icon: "powershell", + openWith: "powershell", + }, + { + id: "sublime-text", + label: "Sublime Text", + icon: "sublime-text", + openWith: "Sublime Text", + }, +] as const + +const LINUX_APPS = [ + { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" }, + { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" }, + { id: "zed", label: "Zed", icon: "zed", openWith: "zed" }, + { + id: "sublime-text", + label: "Sublime Text", + icon: "sublime-text", + openWith: "Sublime Text", + }, +] as const + +type OpenOption = (typeof MAC_APPS)[number] | (typeof WINDOWS_APPS)[number] | (typeof LINUX_APPS)[number] +type OpenIcon = OpenApp | "file-explorer" +const OPEN_ICON_BASE = new Set(["finder", "vscode", "cursor", "zed"]) + +const openIconSize = (id: OpenIcon) => (OPEN_ICON_BASE.has(id) ? "size-4" : "size-[19px]") + +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), + }) +} +function useSessionShare(args: { + globalSDK: ReturnType + currentSession: () => + | { + share?: { + url?: string + } + } + | undefined + sessionID: () => string | undefined + projectDirectory: () => string + platform: ReturnType +}) { const [state, setState] = createStore({ share: false, unshare: false, copied: false, timer: undefined as number | undefined, }) - const shareUrl = createMemo(() => currentSession()?.share?.url) + const shareUrl = createMemo(() => args.currentSession()?.share?.url) createEffect(() => { const url = shareUrl() @@ -71,12 +168,12 @@ export function SessionHeader() { if (state.timer) window.clearTimeout(state.timer) }) - function shareSession() { - const session = currentSession() - if (!session || state.share) return + const shareSession = () => { + const sessionID = args.sessionID() + if (!sessionID || state.share) return setState("share", true) - globalSDK.client.session - .share({ sessionID: session.id, directory: projectDirectory() }) + args.globalSDK.client.session + .share({ sessionID, directory: args.projectDirectory() }) .catch((error) => { console.error("Failed to share session", error) }) @@ -85,12 +182,12 @@ export function SessionHeader() { }) } - function unshareSession() { - const session = currentSession() - if (!session || state.unshare) return + const unshareSession = () => { + const sessionID = args.sessionID() + if (!sessionID || state.unshare) return setState("unshare", true) - globalSDK.client.session - .unshare({ sessionID: session.id, directory: projectDirectory() }) + args.globalSDK.client.session + .unshare({ sessionID, directory: args.projectDirectory() }) .catch((error) => { console.error("Failed to unshare session", error) }) @@ -99,7 +196,7 @@ export function SessionHeader() { }) } - function copyLink() { + const copyLink = (onError: (error: unknown) => void) => { const url = shareUrl() if (!url) return navigator.clipboard @@ -113,17 +210,160 @@ export function SessionHeader() { }, 3000) setState("timer", timer) }) - .catch((error) => { - console.error("Failed to copy share link", error) - }) + .catch(onError) } - function viewShare() { + const viewShare = () => { const url = shareUrl() if (!url) return - platform.openLink(url) + args.platform.openLink(url) } + return { state, shareUrl, shareSession, unshareSession, copyLink, viewShare } +} + +export function SessionHeader() { + const globalSDK = useGlobalSDK() + const layout = useLayout() + const command = useCommand() + const server = useServer() + const sync = useSync() + const platform = usePlatform() + const language = useLanguage() + 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 currentSession = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) + const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") + const showShare = createMemo(() => shareEnabled() && !!params.id) + const os = createMemo(() => detectOS(platform)) + + 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: "Finder", icon: "finder" as const } + if (os() === "windows") return { label: "File Explorer", icon: "file-explorer" as const } + return { label: "File Manager", 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: fileManager().label, icon: fileManager().icon }, + ...apps().filter((app) => exists[app.id]), + ] 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 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 share = useSessionShare({ + globalSDK, + currentSession, + sessionID: () => params.id, + projectDirectory, + platform, + }) + const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center")) const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right")) @@ -132,165 +372,215 @@ export function SessionHeader() { {(mount) => ( - + + {(keybind) => ( + {keybind()} + )} + + )} {(mount) => ( -
- {/* */} -
- - - -
-
- - -
+ +
+
{language.t("session.share.action.share")}} >
} > -
- +
+
@@ -298,8 +588,8 @@ export function SessionHeader() { size="large" variant="primary" class="w-full" - onClick={viewShare} - disabled={state.unshare} + onClick={share.viewShare} + disabled={share.state.unshare} > {language.t("session.share.action.view")} @@ -308,10 +598,10 @@ export function SessionHeader() {
-
+
+ + + + + +
)} diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx index f7f422fc5ea..e4ef3639362 100644 --- a/packages/app/src/components/session/session-new-view.tsx +++ b/packages/app/src/components/session/session-new-view.tsx @@ -1,21 +1,23 @@ 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/util/path" -import { Select } from "@opencode-ai/ui/select" const MAIN_WORKTREE = "main" const CREATE_WORKTREE = "create" +const ROOT_CLASS = "size-full flex flex-col" interface NewSessionViewProps { worktree: string - onWorktreeChange: (value: string) => void } export function NewSessionView(props: NewSessionViewProps) { const sync = useSync() + const sdk = useSDK() const language = useLanguage() const sandboxes = createMemo(() => sync.project?.sandboxes ?? []) @@ -25,11 +27,11 @@ export function NewSessionView(props: NewSessionViewProps) { if (options().includes(selection)) return selection return MAIN_WORKTREE }) - const projectRoot = createMemo(() => sync.project?.worktree ?? sync.data.path.directory) + const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory) const isWorktree = createMemo(() => { const project = sync.project if (!project) return false - return sync.data.path.directory !== project.worktree + return sdk.directory !== project.worktree }) const label = (value: string) => { @@ -46,48 +48,44 @@ export function NewSessionView(props: NewSessionViewProps) { } return ( -
-
{language.t("command.session.new")}
-
- -
- {getDirectory(projectRoot())} - {getFilename(projectRoot())} -
-
-
- - setTitle(e.currentTarget.value)} + value={store.title} + onInput={(e) => setStore("title", e.currentTarget.value)} onBlur={save} onKeyDown={keydown} onMouseDown={(e) => e.stopPropagation()} @@ -161,13 +163,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => />
- + setStore("menuOpen", open)}> diff --git a/packages/app/src/components/settings-agents.tsx b/packages/app/src/components/settings-agents.tsx deleted file mode 100644 index e68f1e59c53..00000000000 --- a/packages/app/src/components/settings-agents.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from "solid-js" -import { useLanguage } from "@/context/language" - -export const SettingsAgents: Component = () => { - const language = useLanguage() - - return ( -
-
-

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

-

{language.t("settings.agents.description")}

-
-
- ) -} diff --git a/packages/app/src/components/settings-commands.tsx b/packages/app/src/components/settings-commands.tsx deleted file mode 100644 index cf796d0aa7a..00000000000 --- a/packages/app/src/components/settings-commands.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from "solid-js" -import { useLanguage } from "@/context/language" - -export const SettingsCommands: Component = () => { - const language = useLanguage() - - return ( -
-
-

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

-

{language.t("settings.commands.description")}

-
-
- ) -} diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index c3cc2760142..42ee4092f68 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -1,17 +1,108 @@ -import { Component, createMemo, type JSX } from "solid-js" +import { Component, Show, createMemo, createResource, 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 { Tooltip } from "@opencode-ai/ui/tooltip" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" +import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" import { useSettings, monoFontFamily } from "@/context/settings" import { playSound, SOUND_OPTIONS } from "@/utils/sound" import { Link } from "./link" +let demoSoundState = { + cleanup: undefined as (() => void) | undefined, + timeout: undefined as NodeJS.Timeout | undefined, +} + +// 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 = () => { + if (demoSoundState.cleanup) { + demoSoundState.cleanup() + } + clearTimeout(demoSoundState.timeout) + demoSoundState.cleanup = undefined +} + +const playDemoSound = (src: string | undefined) => { + stopDemoSound() + if (!src) return + + demoSoundState.timeout = setTimeout(() => { + demoSoundState.cleanup = playSound(src) + }, 100) +} + export const SettingsGeneral: Component = () => { const theme = useTheme() const language = useLanguage() + const platform = usePlatform() const settings = useSettings() + const [store, setStore] = createStore({ + checking: false, + }) + + const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux") + + 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.update && platform.restart + ? [ + { + label: language.t("toast.update.action.installRestart"), + onClick: async () => { + await platform.update!() + await platform.restart!() + }, + }, + { + 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(() => Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })), ) @@ -36,257 +127,432 @@ export const SettingsGeneral: Component = () => { { value: "hack", label: "font.option.hack" }, { value: "inconsolata", label: "font.option.inconsolata" }, { value: "intel-one-mono", label: "font.option.intelOneMono" }, + { value: "iosevka", label: "font.option.iosevka" }, { value: "jetbrains-mono", label: "font.option.jetbrainsMono" }, { value: "meslo-lgs", label: "font.option.mesloLgs" }, { value: "roboto-mono", label: "font.option.robotoMono" }, { value: "source-code-pro", label: "font.option.sourceCodePro" }, { value: "ubuntu-mono", label: "font.option.ubuntuMono" }, + { value: "geist-mono", label: "font.option.geistMono" }, ] as const const fontOptionsList = [...fontOptions] - const soundOptions = [...SOUND_OPTIONS] + const noneSound = { id: "none", label: "sound.option.none", src: undefined } as const + const soundOptions = [noneSound, ...SOUND_OPTIONS] + + 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.src) + }, + onSelect: (option: (typeof soundOptions)[number] | undefined) => { + if (!option) return + if (option.id === "none") { + setEnabled(false) + stopDemoSound() + return + } + setEnabled(true) + set(option.id) + playDemoSound(option.src) + }, + variant: "secondary" as const, + size: "small" as const, + triggerVariant: "settings" as const, + }) + + const AppearanceSection = () => ( +
+

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

+ +
+ + 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" + /> + + + + {language.t("settings.general.row.theme.description")}{" "} + {language.t("common.learnMore")} + + } + > + o.value === settings.appearance.font())} + value={(o) => o.value} + label={(o) => language.t(o.label)} + onSelect={(option) => option && settings.appearance.setFont(option.value)} + variant="secondary" + size="small" + triggerVariant="settings" + triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }} + > + {(option) => ( + + {option ? language.t(option.label) : ""} + + )} + + +
+
+ ) + + const FeedSection = () => ( +
+

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

+ +
+ +
+ settings.general.setShowReasoningSummaries(checked)} + /> +
+
+ + +
+ settings.general.setShellToolPartsExpanded(checked)} + /> +
+
+ + +
+ settings.general.setEditToolPartsExpanded(checked)} + /> +
+
+
+
+ ) + + const NotificationsSection = () => ( +
+

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

+ +
+ +
+ settings.notifications.setAgent(checked)} + /> +
+
+ + +
+ settings.notifications.setPermissions(checked)} + /> +
+
+ + +
+ settings.notifications.setErrors(checked)} + /> +
+
+
+
+ ) + + const SoundsSection = () => ( +
+

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

+ +
+ + settings.sounds.permissionsEnabled(), + () => settings.sounds.permissions(), + (value) => settings.sounds.setPermissionsEnabled(value), + (id) => settings.sounds.setPermissions(id), + )} + /> + + + + o.value === language.locale())} - value={(o) => o.value} - label={(o) => o.label} - onSelect={(option) => option && language.setLocale(option.value)} - variant="secondary" - size="small" - triggerVariant="settings" - /> - - - - o.id === theme.themeId())} - value={(o) => o.id} - label={(o) => o.name} - onSelect={(option) => { - if (!option) return - theme.setTheme(option.id) - }} - onHighlight={(option) => { - if (!option) return - theme.previewTheme(option.id) - return () => theme.cancelPreview() - }} - variant="secondary" - size="small" - triggerVariant="settings" - /> - - - - - -
-
+ - {/* System notifications Section */} -
-

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

- -
- - settings.notifications.setAgent(checked)} - /> - - - - settings.notifications.setPermissions(checked)} - /> - - - - settings.notifications.setErrors(checked)} - /> - -
-
+ - {/* Sound effects Section */} -
-

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

- -
- - o.id === settings.sounds.permissions())} - value={(o) => o.id} - label={(o) => language.t(o.label)} - onHighlight={(option) => { - if (!option) return - playSound(option.src) - }} - onSelect={(option) => { - if (!option) return - settings.sounds.setPermissions(option.id) - playSound(option.src) - }} - variant="secondary" - size="small" - triggerVariant="settings" - /> - - - - o.value === actionFor(item.id))} - value={(o) => o.value} - label={(o) => o.label} - onSelect={(option) => option && setPermission(item.id, option.value)} - variant="secondary" - size="small" - triggerVariant="settings" - /> - - )} - -
-
-
-
- ) -} - -interface SettingsRowProps { - title: string - description: string - children: JSX.Element -} - -const SettingsRow: Component = (props) => { - return ( -
-
- {props.title} - {props.description} -
-
{props.children}
-
- ) -} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 7b6ca193924..a9839758b72 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -1,14 +1,249 @@ -import { Component } from "solid-js" +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" + +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.description")}

+
+
+
+

{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.tsx b/packages/app/src/components/status-popover.tsx new file mode 100644 index 00000000000..8073746c90b --- /dev/null +++ b/packages/app/src/components/status-popover.tsx @@ -0,0 +1,415 @@ +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Icon } from "@opencode-ai/ui/icon" +import { Popover } from "@opencode-ai/ui/popover" +import { Switch } from "@opencode-ai/ui/switch" +import { Tabs } from "@opencode-ai/ui/tabs" +import { showToast } from "@opencode-ai/ui/toast" +import { useNavigate } from "@solidjs/router" +import { type Accessor, createEffect, createMemo, createSignal, 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" +import { DialogSelectServer } from "./dialog-select-server" + +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) => { + const checkServerHealth = useCheckServerHealth() + const [status, setStatus] = createStore({} as Record) + + createEffect(() => { + 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 [url, setUrl] = createSignal() + const [tick, setTick] = createSignal(0) + + createEffect(() => { + tick() + let dead = false + const result = get?.() + if (!result) { + setUrl(undefined) + onCleanup(() => { + dead = true + }) + return + } + + if (result instanceof Promise) { + void result.then((next) => { + if (dead) return + setUrl(next ? normalizeServerUrl(next) : undefined) + }) + onCleanup(() => { + dead = true + }) + return + } + + setUrl(normalizeServerUrl(result)) + onCleanup(() => { + dead = true + }) + }) + + return { + key: () => { + const u = url() + if (!u) return + return ServerConnection.key({ type: "http", http: { url: u } }) + }, + refresh: () => setTick((value) => value + 1), + } +} + +const useMcpToggle = (input: { + sync: ReturnType + sdk: ReturnType + language: ReturnType +}) => { + const [loading, setLoading] = createSignal(null) + + const toggle = async (name: string) => { + if (loading()) return + setLoading(name) + + try { + const status = input.sync.data.mcp[name] + await (status?.status === "connected" + ? input.sdk.client.mcp.disconnect({ name }) + : input.sdk.client.mcp.connect({ name })) + const result = await input.sdk.client.mcp.status() + if (result.data) input.sync.set("mcp", result.data) + } catch (err) { + showToast({ + variant: "error", + title: input.language.t("common.requestFailed"), + description: err instanceof Error ? err.message : String(err), + }) + } finally { + setLoading(null) + } + } + + return { loading, toggle } +} + +export function StatusPopover() { + const sync = useSync() + const sdk = useSDK() + const server = useServer() + const platform = usePlatform() + const dialog = useDialog() + const language = useLanguage() + const navigate = useNavigate() + + 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) + const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health)) + const mcp = useMcpToggle({ sync, sdk, language }) + 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 ?? []) + const pluginCount = createMemo(() => plugins().length) + const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json")) + const overallHealthy = createMemo(() => { + const serverHealthy = server.healthy() === true + const anyMcpIssue = mcpNames().some((name) => { + const status = mcpStatus(name) + return status !== "connected" && status !== "disabled" + }) + return serverHealthy && !anyMcpIssue + }) + + 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} + > +
+ + + + {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 isBlocked = () => 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/terminal.tsx b/packages/app/src/components/terminal.tsx index 6bedcfae2ce..84090329388 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -1,19 +1,43 @@ -import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web" -import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js" +import { type HexColor, resolveThemeVariant, useTheme, withAlpha } from "@opencode-ai/ui/theme" +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 { monoFontFamily, useSettings } from "@/context/settings" -import { SerializeAddon } from "@/addons/serialize" -import { LocalPTY } from "@/context/terminal" -import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme" +import type { LocalPTY } from "@/context/terminal" +import { terminalAttr, terminalProbe } from "@/testing/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: LocalPTY) => 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 @@ -36,30 +60,167 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = { }, } +const debugTerminal = (...values: unknown[]) => { + if (!import.meta.env.DEV) return + console.debug("[terminal]", ...values) +} + +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() let container!: HTMLDivElement - const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"]) + const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"]) + const id = local.pty.id + const probe = terminalProbe(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 handleTextareaFocus: () => void - let handleTextareaBlur: () => void - let reconnect: number | undefined + 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 output: ReturnType | undefined + + 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 sdk.client.pty + .update({ + ptyID: id, + size: { cols, rows }, + }) + .catch((err) => { + debugTerminal("failed to sync terminal size", err) + }) + } const getTerminalColors = (): TerminalColors => { - const mode = theme.mode() + 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) return fallback + 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 @@ -74,239 +235,347 @@ export const Terminal = (props: TerminalProps) => { } } - const [terminalColors, setTerminalColors] = createSignal(getTerminalColors()) + 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 = getTerminalColors() - setTerminalColors(colors) + const colors = terminalColors() if (!term) return - const setOption = (term as unknown as { setOption?: (key: string, value: TerminalColors) => void }).setOption - if (!setOption) return - setOption("theme", colors) + setOptionIfSupported(term, "theme", colors) }) createEffect(() => { const font = monoFontFamily(settings.appearance.font()) if (!term) return - const setOption = (term as unknown as { setOption?: (key: string, value: string) => void }).setOption - if (!setOption) return - setOption("fontFamily", font) + 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) { + if (activeElement instanceof HTMLElement && activeElement !== container && !container.contains(activeElement)) { activeElement.blur() } focusTerminal() } - onMount(async () => { - const mod = await import("ghostty-web") - ghostty = await mod.Ghostty.load() + const handleLinkClick = (event: MouseEvent) => { + if (!event.shiftKey && !event.ctrlKey && !event.metaKey) return + if (event.altKey) return + if (event.button !== 0) return - const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`) - if (window.__OPENCODE__?.serverPassword) { - url.username = "opencode" - url.password = window.__OPENCODE__?.serverPassword - } - const socket = new WebSocket(url) - ws = socket - - const t = new mod.Terminal({ - cursorBlink: true, - cursorStyle: "bar", - fontSize: 14, - fontFamily: monoFontFamily(settings.appearance.font()), - allowTransparency: true, - theme: terminalColors(), - scrollback: 10_000, - ghostty, - }) - term = t - - const copy = () => { - const selection = t.getSelection() - if (!selection) return false - - const body = document.body - if (body) { - const textarea = document.createElement("textarea") - textarea.value = selection - textarea.setAttribute("readonly", "") - textarea.style.position = "fixed" - textarea.style.opacity = "0" - body.appendChild(textarea) - textarea.select() - const copied = document.execCommand("copy") - body.removeChild(textarea) - if (copied) return true - } + const t = term + if (!t) return - const clipboard = navigator.clipboard - if (clipboard?.writeText) { - clipboard.writeText(selection).catch(() => {}) - return true - } + const text = getHoveredLinkText(t) + if (!text) return - return false - } + event.preventDefault() + event.stopImmediatePropagation() + platform.openLink(text) + } - t.attachCustomKeyEventHandler((event) => { - const key = event.key.toLowerCase() + onMount(() => { + probe.init() + cleanups.push(() => probe.drop()) - if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") { - copy() - return true - } + const run = async () => { + const loaded = await loadGhostty() + if (disposed) return - if (event.metaKey && !event.ctrlKey && !event.altKey && key === "c") { - if (!t.hasSelection()) return true - copy() - return true + 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: monoFontFamily(settings.appearance.font()), + 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, () => { + probe.render(data) + probe.settle() + 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) - // allow for ctrl-` to toggle terminal in parent - if (event.ctrlKey && key === "`") { - return true + 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) { + document.fonts.ready.then(scheduleFit) } - return false - }) + 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)) + } - fitAddon = new mod.FitAddon() - serializeAddon = new SerializeAddon() - t.loadAddon(serializeAddon) - t.loadAddon(fitAddon) + const write = (data: string) => + new Promise((resolve) => { + if (!output) { + resolve() + return + } + output.push(data) + output.flush(resolve) + }) - t.open(container) - container.addEventListener("pointerdown", handlePointerDown) + 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() + } - handleTextareaFocus = () => { - t.options.cursorBlink = true - } - handleTextareaBlur = () => { - t.options.cursorBlink = false - } + const once = { value: false } + let closing = false - t.textarea?.addEventListener("focus", handleTextareaFocus) - t.textarea?.addEventListener("blur", handleTextareaBlur) + const url = new URL(sdk.url + `/pty/${id}/connect`) + url.searchParams.set("directory", sdk.directory) + url.searchParams.set("cursor", String(start !== undefined ? start : restore ? -1 : 0)) + url.protocol = url.protocol === "https:" ? "wss:" : "ws:" + url.username = server.current?.http.username ?? "opencode" + url.password = server.current?.http.password ?? "" - focusTerminal() + const socket = new WebSocket(url) + socket.binaryType = "arraybuffer" + ws = socket - if (local.pty.buffer) { - if (local.pty.rows && local.pty.cols) { - t.resize(local.pty.cols, local.pty.rows) + const handleOpen = () => { + probe.connect() + local.onConnect?.() + scheduleSize(t.cols, t.rows) } - t.write(local.pty.buffer, () => { - if (local.pty.scrollY) { - t.scrollToLine(local.pty.scrollY) + socket.addEventListener("open", handleOpen) + if (socket.readyState === WebSocket.OPEN) handleOpen() + + const decoder = new TextDecoder() + const handleMessage = (event: MessageEvent) => { + if (disposed) return + if (closing) 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 + } + } catch (err) { + debugTerminal("invalid websocket control frame", err) + } + return } - fitAddon.fit() - }) - } - fitAddon.observeResize() - handleResize = () => fitAddon.fit() - window.addEventListener("resize", handleResize) - t.onResize(async (size) => { - if (socket.readyState === WebSocket.OPEN) { - await sdk.client.pty - .update({ - ptyID: local.pty.id, - size: { - cols: size.cols, - rows: size.rows, - }, - }) - .catch(() => {}) + const data = typeof event.data === "string" ? event.data : "" + if (!data) return + output?.push(data) + cursor += data.length } - }) - t.onData((data) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(data) + socket.addEventListener("message", handleMessage) + + const handleError = (error: Event) => { + if (disposed) return + if (closing) return + if (once.value) return + once.value = true + console.error("WebSocket error:", error) + local.onConnectError?.(error) } - }) - t.onKey((key) => { - if (key.key == "Enter") { - props.onSubmit?.() + socket.addEventListener("error", handleError) + + const handleClose = (event: CloseEvent) => { + if (disposed) return + if (closing) return + // Normal closure (code 1000) means PTY process exited - server event handles cleanup + // For other codes (network issues, server restart), trigger error handler + if (event.code !== 1000) { + if (once.value) return + once.value = true + local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`)) + } } - }) - // t.onScroll((ydisp) => { - // console.log("Scroll position:", ydisp) - // }) - socket.addEventListener("open", () => { - local.onConnect?.() - sdk.client.pty - .update({ - ptyID: local.pty.id, - size: { - cols: t.cols, - rows: t.rows, - }, - }) - .catch(() => {}) - }) - socket.addEventListener("message", (event) => { - t.write(event.data) - }) - socket.addEventListener("error", (error) => { - if (disposed) return - console.error("WebSocket error:", error) - local.onConnectError?.(error) - }) - socket.addEventListener("close", (event) => { + socket.addEventListener("close", handleClose) + + cleanups.push(() => { + closing = true + socket.removeEventListener("open", handleOpen) + socket.removeEventListener("message", handleMessage) + socket.removeEventListener("error", handleError) + socket.removeEventListener("close", handleClose) + if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close(1000) + }) + } + + void run().catch((err) => { if (disposed) return - // Normal closure (code 1000) means PTY process exited - server event handles cleanup - // For other codes (network issues, server restart), trigger error handler - if (event.code !== 1000) { - local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`)) - } + 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 (handleResize) { - window.removeEventListener("resize", handleResize) + if (fitFrame !== undefined) cancelAnimationFrame(fitFrame) + if (sizeTimer !== undefined) clearTimeout(sizeTimer) + 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() } - container.removeEventListener("pointerdown", handlePointerDown) - term?.textarea?.removeEventListener("focus", handleTextareaFocus) - term?.textarea?.removeEventListener("blur", handleTextareaBlur) - const t = term - if (serializeAddon && props.onCleanup && t) { - const buffer = serializeAddon.serialize() - props.onCleanup({ - ...local.pty, - buffer, - rows: t.rows, - cols: t.cols, - scrollY: t.getViewportY(), - }) + if (!output) { + finalize() + return } - ws?.close() - t?.dispose() + output.flush(finalize) }) return (
{ + 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 new file mode 100644 index 00000000000..44dbbfa3a49 --- /dev/null +++ b/packages/app/src/components/titlebar-history.ts @@ -0,0 +1,57 @@ +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 index f631305b4ec..3e2374f4342 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -1,14 +1,39 @@ -import { createEffect, createMemo, Show } from "solid-js" +import { createEffect, createMemo, onCleanup, 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 { TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { useTheme } from "@opencode-ai/ui/theme" import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" +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() @@ -16,25 +41,71 @@ export function Titlebar() { const command = useCommand() const language = useLanguage() 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 reserve = createMemo( - () => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"), - ) const web = createMemo(() => platform.platform === "web") + const zoom = () => platform.webviewZoom?.() ?? 1 + const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined) - const getWin = () => { - if (platform.platform !== "desktop") return + const [history, setHistory] = createStore({ + stack: [] as string[], + index: 0, + action: undefined as "back" | "forward" | undefined, + }) - const tauri = ( - window as unknown as { - __TAURI__?: { window?: { getCurrentWindow?: () => { startDragging?: () => Promise } } } - } - ).__TAURI__ - if (!tauri?.window?.getCurrentWindow) return + const path = () => `${location.pathname}${location.search}${location.hash}` - return tauri.window.getCurrentWindow() + 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 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(() => { @@ -43,13 +114,8 @@ export function Titlebar() { const scheme = theme.colorScheme() const value = scheme === "system" ? null : scheme - const tauri = (window as unknown as { __TAURI__?: { webviewWindow?: { getCurrentWebviewWindow?: () => unknown } } }) - .__TAURI__ - const get = tauri?.webviewWindow?.getCurrentWebviewWindow - if (!get) return - - const win = get() as { setTheme?: (theme?: "light" | "dark" | null) => Promise } - if (!win.setTheme) return + const win = currentThemeWindow() + if (!win?.setTheme) return void win.setTheme(value).catch(() => undefined) }) @@ -75,26 +141,42 @@ export function Titlebar() { 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 ( -
+
-
+
@@ -103,54 +185,107 @@ export function Titlebar() {
- - + + - - -
-
-
+
+
+
+
+ +
+
+
+ +
+
+ {!tauriApi() &&
}
-
-
-
) } diff --git a/packages/app/src/context/command-keybind.test.ts b/packages/app/src/context/command-keybind.test.ts new file mode 100644 index 00000000000..4e38efd8da8 --- /dev/null +++ b/packages/app/src/context/command-keybind.test.ts @@ -0,0 +1,43 @@ +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("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("") + }) +}) diff --git a/packages/app/src/context/command.test.ts b/packages/app/src/context/command.test.ts new file mode 100644 index 00000000000..2b956287c54 --- /dev/null +++ b/packages/app/src/context/command.test.ts @@ -0,0 +1,25 @@ +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 index 39bd11a1bb0..03bd6318dab 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js" +import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -11,6 +11,7 @@ const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(na 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"]) function actionId(id: string) { if (!id.startsWith(SUGGESTED_PREFIX)) return id @@ -24,6 +25,20 @@ function normalizeKey(key: string) { 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 { @@ -47,6 +62,8 @@ export interface CommandOption { onHighlight?: () => (() => void) | void } +type CommandSource = "palette" | "keybind" | "slash" + export type CommandCatalogItem = { title: string description?: string @@ -55,6 +72,16 @@ export type CommandCatalogItem = { 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 [] @@ -150,14 +177,25 @@ export function formatKeybind(config: string): string { 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 [registrations, setRegistrations] = createSignal[]>([]) - const [suspendCount, setSuspendCount] = createSignal(0) + const [store, setStore] = createStore({ + registrations: [] as CommandRegistration[], + suspendCount: 0, + }) + const warnedDuplicates = new Set() const [catalog, setCatalog, _, catalogReady] = persisted( Persist.global("command.catalog.v1"), @@ -175,9 +213,15 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const seen = new Set() const all: CommandOption[] = [] - for (const reg of registrations()) { - for (const opt of reg()) { - if (seen.has(opt.id)) continue + 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) } @@ -221,15 +265,44 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex ] }) - const suspended = () => suspendCount() > 0 + 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 run = (id: string, source?: "palette" | "keybind" | "slash") => { + const keymap = createMemo(() => { + const map = new Map() for (const option of options()) { - if (option.id === id || option.id === "suggested." + id) { - option.onSelect?.(source) - return + 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 = () => { @@ -239,24 +312,24 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const handleKeyDown = (event: KeyboardEvent) => { if (suspended() || dialog.active) return - const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND) - if (matchKeybind(paletteKeybinds, event)) { + 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 } - for (const option of options()) { - if (option.disabled) continue - if (!option.keybind) continue - - const keybinds = parseKeybind(option.keybind) - if (matchKeybind(keybinds, event)) { - event.preventDefault() - option.onSelect?.("keybind") - return - } - } + if (!option) return + event.preventDefault() + option.onSelect?.("keybind") } onMount(() => { @@ -267,15 +340,26 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex document.removeEventListener("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(cb: () => CommandOption[]) { - const results = createMemo(cb) - setRegistrations((arr) => [results, ...arr]) - onCleanup(() => { - setRegistrations((arr) => arr.filter((x) => x !== results)) - }) - }, - trigger(id: string, source?: "palette" | "keybind" | "slash") { + register, + trigger(id: string, source?: CommandSource) { run(id, source) }, keybind(id: string) { @@ -294,7 +378,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex }, show: showPalette, keybinds(enabled: boolean) { - setSuspendCount((count) => count + (enabled ? -1 : 1)) + setStore("suspendCount", (count) => Math.max(0, count + (enabled ? -1 : 1))) }, suspended, get catalog() { diff --git a/packages/app/src/context/comments.test.ts b/packages/app/src/context/comments.test.ts new file mode 100644 index 00000000000..82fa170f2fc --- /dev/null +++ b/packages/app/src/context/comments.test.ts @@ -0,0 +1,186 @@ +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 index 12ee977e996..a97010c0af3 100644 --- a/packages/app/src/context/comments.tsx +++ b/packages/app/src/context/comments.tsx @@ -1,8 +1,10 @@ -import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js" -import { createStore } from "solid-js/store" +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 = { @@ -18,34 +20,88 @@ type CommentFocus = { file: string; id: string } const WORKSPACE_KEY = "__workspace__" const MAX_COMMENT_SESSIONS = 20 -type CommentSession = ReturnType +function sessionKey(dir: string, id: string | undefined) { + return `${dir}\n${id ?? WORKSPACE_KEY}` +} -type CommentCacheEntry = { - value: CommentSession - dispose: VoidFunction +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), + } } -function createCommentSession(dir: string, id: string | undefined) { - const legacy = `${dir}/comments${id ? "/" + id : ""}.v1` +type CommentStore = { + comments: Record +} - const [store, setStore, _, ready] = persisted( - Persist.scoped(dir, id, "comments", [legacy]), - createStore<{ - comments: Record - }>({ - comments: {}, - }), - ) +function aggregate(comments: Record) { + return Object.keys(comments) + .flatMap((file) => comments[file] ?? []) + .slice() + .sort((a, b) => a.time - b.time) +} - const [focus, setFocus] = createSignal(null) +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: crypto.randomUUID(), + id: uuid(), time: Date.now(), ...input, + selection: cloneSelection(input.selection), } batch(() => { @@ -57,25 +113,85 @@ function createCommentSession(dir: string, id: string | undefined) { } const remove = (file: string, id: string) => { - setStore("comments", file, (items) => (items ?? []).filter((x) => x.id !== id)) - setFocus((current) => (current?.id === id ? null : current)) + batch(() => { + setStore("comments", file, (items) => (items ?? []).filter((item) => item.id !== id)) + setFocus((current) => (current?.file === file && current.id === id ? null : current)) + }) } - const all = createMemo(() => { - const files = Object.keys(store.comments) - const items = files.flatMap((file) => store.comments[file] ?? []) - return items.slice().sort((a, b) => a.time - b.time) - }) + 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 { - ready, list, all, add, remove, - focus: createMemo(() => focus()), + update, + replace, + clear, + focus: () => state.focus, setFocus, - clearFocus: () => setFocus(null), + 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, } } @@ -84,44 +200,25 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont 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_COMMENT_SESSIONS) { - const first = cache.keys().next().value - if (!first) return - const entry = cache.get(first) - entry?.dispose() - cache.delete(first) - } - } + 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 = `${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: createCommentSession(dir, id), - dispose, - })) - - cache.set(key, entry) - prune() - return entry.value + const key = sessionKey(dir, id) + return cache.get(key).value } const session = createMemo(() => load(params.dir!, params.id)) @@ -132,9 +229,15 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont 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 new file mode 100644 index 00000000000..4ef5f947c7d --- /dev/null +++ b/packages/app/src/context/file-content-eviction-accounting.test.ts @@ -0,0 +1,65 @@ +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 index 5ea4993879c..99c6d2e4219 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -1,183 +1,52 @@ -import { createEffect, createMemo, createRoot, onCleanup } from "solid-js" -import { createStore, produce } from "solid-js/store" +import { batch, createEffect, createMemo, onCleanup } from "solid-js" +import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" -import type { FileContent } from "@opencode-ai/sdk/v2" import { showToast } from "@opencode-ai/ui/toast" import { useParams } from "@solidjs/router" import { getFilename } from "@opencode-ai/util/path" import { useSDK } from "./sdk" import { useSync } from "./sync" import { useLanguage } from "@/context/language" -import { Persist, persisted } from "@/utils/persist" - -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 -} - -function stripFileProtocol(input: string) { - if (!input.startsWith("file://")) return input - return input.slice("file://".length) -} - -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 +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, } -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, - } -} - -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, - } -} - -const WORKSPACE_KEY = "__workspace__" -const MAX_FILE_VIEW_SESSIONS = 20 -const MAX_VIEW_FILES = 500 - -type ViewSession = ReturnType - -type ViewCacheEntry = { - value: ViewSession - dispose: VoidFunction -} - -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("file", path, (current) => { - if (current?.scrollTop === top) return current - return { - ...(current ?? {}), - scrollTop: top, - } - }) - pruneView(path) - } - - const setScrollLeft = (path: string, left: number) => { - setView("file", path, (current) => { - if (current?.scrollLeft === left) return current - return { - ...(current ?? {}), - scrollLeft: left, - } - }) - pruneView(path) - } - - const setSelectedLines = (path: string, range: SelectedLineRange | null) => { - const next = range ? normalizeSelectedLines(range) : null - setView("file", path, (current) => { - if (current?.selectedLines === next) return current - return { - ...(current ?? {}), - selectedLines: next, - } - }) - pruneView(path) - } - - return { - ready, - scrollTop, - scrollLeft, - selectedLines, - setScrollTop, - setScrollLeft, - setSelectedLines, - } +function errorMessage(error: unknown) { + if (error instanceof Error && error.message) return error.message + if (typeof error === "string" && error) return error + return "Unknown error" } export const { use: useFile, provider: FileProvider } = createSimpleContext({ @@ -185,199 +54,217 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ gate: false, init: () => { const sdk = useSDK() - const sync = useSync() + useSync() const params = useParams() const language = useLanguage() + const layout = useLayout() - const directory = createMemo(() => sync.data.path.directory) - - function normalize(input: string) { - const root = directory() - const prefix = root.endsWith("/") ? root : root + "/" - - let path = stripQueryAndHash(stripFileProtocol(input)) - - if (path.startsWith(prefix)) { - path = path.slice(prefix.length) - } - - if (path.startsWith(root)) { - path = path.slice(root.length) - } - - if (path.startsWith("./")) { - path = path.slice(2) - } - - if (path.startsWith("/")) { - path = path.slice(1) - } - - return path - } - - function tab(input: string) { - const path = normalize(input) - return `file://${path}` - } - - function pathFromTab(tabValue: string) { - if (!tabValue.startsWith("file://")) return - return normalize(tabValue) - } + 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 viewCache = new Map() + 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 disposeViews = () => { - for (const entry of viewCache.values()) { - entry.dispose() - } - viewCache.clear() + const evictContent = (keep?: Set) => { + evictContentLru(keep, (target) => { + if (!store.file[target]) return + setStore( + "file", + target, + produce((draft) => { + draft.content = undefined + draft.loaded = false + }), + ) + }) } - const pruneViews = () => { - while (viewCache.size > MAX_FILE_VIEW_SESSIONS) { - const first = viewCache.keys().next().value - if (!first) return - const entry = viewCache.get(first) - entry?.dispose() - viewCache.delete(first) - } - } + createEffect(() => { + scope() + inflight.clear() + resetFileContentLru() + batch(() => { + setStore("file", reconcile({})) + tree.reset() + }) + }) - const loadView = (dir: string, id: string | undefined) => { - const key = `${dir}:${id ?? WORKSPACE_KEY}` - const existing = viewCache.get(key) - if (existing) { - viewCache.delete(key) - viewCache.set(key, existing) - return existing.value - } + const viewCache = createFileViewCache() + const view = createMemo(() => viewCache.load(scope(), params.id)) - const entry = createRoot((dispose) => ({ - value: createViewSession(dir, id), - dispose, - })) + const ensure = (file: string) => { + if (!file) return + if (store.file[file]) return + setStore("file", file, { path: file, name: getFilename(file) }) + } - viewCache.set(key, entry) - pruneViews() - return entry.value + const setLoading = (file: string) => { + setStore( + "file", + file, + produce((draft) => { + draft.loading = true + draft.error = undefined + }), + ) } - const view = createMemo(() => loadView(params.dir!, params.id)) + const setLoaded = (file: string, content: FileState["content"]) => { + setStore( + "file", + file, + produce((draft) => { + draft.loaded = true + draft.loading = false + draft.content = content + }), + ) + } - function ensure(path: string) { - if (!path) return - if (store.file[path]) return - setStore("file", path, { path, name: getFilename(path) }) + 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, + }) } - function load(input: string, options?: { force?: boolean }) { - const path = normalize(input) - if (!path) return Promise.resolve() + const load = (input: string, options?: { force?: boolean }) => { + const file = path.normalize(input) + if (!file) return Promise.resolve() - ensure(path) + const directory = scope() + const key = `${directory}\n${file}` + ensure(file) - const current = store.file[path] + const current = store.file[file] if (!options?.force && current?.loaded) return Promise.resolve() - const pending = inflight.get(path) + const pending = inflight.get(key) if (pending) return pending - setStore( - "file", - path, - produce((draft) => { - draft.loading = true - draft.error = undefined - }), - ) + setLoading(file) const promise = sdk.client.file - .read({ path }) + .read({ path: file }) .then((x) => { - setStore( - "file", - path, - produce((draft) => { - draft.loaded = true - draft.loading = false - draft.content = x.data - }), - ) + if (scope() !== directory) return + const content = x.data + setLoaded(file, content) + + if (!content) return + touchFileContent(file, approxBytes(content)) + evictContent(new Set([file])) }) .catch((e) => { - setStore( - "file", - path, - produce((draft) => { - draft.loading = false - draft.error = e.message - }), - ) - showToast({ - variant: "error", - title: language.t("toast.file.loadFailed.title"), - description: e.message, - }) + if (scope() !== directory) return + setLoadError(file, errorMessage(e)) }) .finally(() => { - inflight.delete(path) + inflight.delete(key) }) - inflight.set(path, promise) + 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) => { - const event = e.details - if (event.type !== "file.watcher.updated") return - const path = normalize(event.properties.file) - if (!path) return - if (path.startsWith(".git/")) return - if (!store.file[path]) return - load(path, { force: true }) + 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) => store.file[normalize(input)] - - const scrollTop = (input: string) => view().scrollTop(normalize(input)) - const scrollLeft = (input: string) => view().scrollLeft(normalize(input)) - const selectedLines = (input: string) => view().selectedLines(normalize(input)) - - const setScrollTop = (input: string, top: number) => { - const path = normalize(input) - view().setScrollTop(path, top) - } - - const setScrollLeft = (input: string, left: number) => { - const path = normalize(input) - view().setScrollLeft(path, left) + 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 } - const setSelectedLines = (input: string, range: SelectedLineRange | null) => { - const path = normalize(input) - view().setSelectedLines(path, range) + 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() - disposeViews() + viewCache.clear() }) return { ready: () => view().ready(), - normalize, - tab, - pathFromTab, + 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, @@ -386,10 +273,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ setScrollLeft, selectedLines, setSelectedLines, - searchFiles: (query: string) => - sdk.client.find.files({ query, dirs: "false" }).then((x) => (x.data ?? []).map(normalize)), - searchFilesAndDirectories: (query: string) => - sdk.client.find.files({ query, dirs: "true" }).then((x) => (x.data ?? []).map(normalize)), + 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 new file mode 100644 index 00000000000..4b724068834 --- /dev/null +++ b/packages/app/src/context/file/content-cache.ts @@ -0,0 +1,88 @@ +import type { FileContent } from "@opencode-ai/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 new file mode 100644 index 00000000000..feef6d466ef --- /dev/null +++ b/packages/app/src/context/file/path.test.ts @@ -0,0 +1,360 @@ +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 new file mode 100644 index 00000000000..53f072b6cb2 --- /dev/null +++ b/packages/app/src/context/file/path.ts @@ -0,0 +1,151 @@ +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 new file mode 100644 index 00000000000..a86051d286e --- /dev/null +++ b/packages/app/src/context/file/tree-store.ts @@ -0,0 +1,170 @@ +import { createStore, produce, reconcile } from "solid-js/store" +import type { FileNode } from "@opencode-ai/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 new file mode 100644 index 00000000000..7ce8a37c25e --- /dev/null +++ b/packages/app/src/context/file/types.ts @@ -0,0 +1,41 @@ +import type { FileContent } from "@opencode-ai/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 new file mode 100644 index 00000000000..4c060174ab8 --- /dev/null +++ b/packages/app/src/context/file/view-cache.ts @@ -0,0 +1,146 @@ +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 new file mode 100644 index 00000000000..9536b52536b --- /dev/null +++ b/packages/app/src/context/file/watcher.test.ts @@ -0,0 +1,149 @@ +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 new file mode 100644 index 00000000000..fbf71992791 --- /dev/null +++ b/packages/app/src/context/file/watcher.ts @@ -0,0 +1,53 @@ +import type { FileNode } from "@opencode-ai/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 index 7d93682bf35..c1a87b95b89 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -1,10 +1,16 @@ -import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" +import type { Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { batch, onCleanup } from "solid-js" +import z from "zod" +import { createSdkForServer } from "@/utils/server" 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: () => { @@ -12,22 +18,43 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo const platform = usePlatform() const abort = new AbortController() - const eventSdk = createOpencodeClient({ - baseUrl: server.url, + 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("No server available") + + const eventSdk = createSdkForServer({ signal: abort.signal, - fetch: platform.fetch, + 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: Array = [] + 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}` @@ -41,66 +68,163 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo if (timer) clearTimeout(timer) timer = undefined + if (queue.length === 0) return + const events = queue - queue = [] + const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined + queue = buffer + buffer = events + queue.length = 0 coalesced.clear() - if (events.length === 0) return + staleDeltas.clear() last = Date.now() batch(() => { for (const event of events) { - if (!event) continue + 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, 16 - elapsed)) + timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed)) } - const stop = () => { - flush() + 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 + 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 } void (async () => { - const events = await eventSdk.global.event() - let yielded = Date.now() - for await (const event of events.stream) { - const directory = event.directory ?? "global" - const payload = event.payload - const k = key(directory, payload) - if (k) { - const i = coalesced.get(k) - if (i !== undefined) { - queue[i] = undefined + while (!abort.signal.aborted) { + 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" + const payload = event.payload + 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, + }) } - coalesced.set(k, queue.length) + } finally { + abort.signal.removeEventListener("abort", onAbort) + attempt = undefined + clearHeartbeat() } - queue.push({ directory, payload }) - schedule() - if (Date.now() - yielded < 8) continue - yielded = Date.now() - await new Promise((resolve) => setTimeout(resolve, 0)) + if (abort.signal.aborted) return + await wait(RECONNECT_DELAY_MS) } - })() - .finally(stop) - .catch(() => undefined) + })().finally(flush) + + const onVisibility = () => { + if (typeof document === "undefined") return + if (document.visibilityState !== "visible") return + if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return + attempt?.abort() + } + if (typeof document !== "undefined") { + document.addEventListener("visibilitychange", onVisibility) + } onCleanup(() => { + if (typeof document !== "undefined") { + document.removeEventListener("visibilitychange", onVisibility) + } abort.abort() - stop() + flush() }) - const sdk = createOpencodeClient({ - baseUrl: server.url, + const sdk = createSdkForServer({ + server: server.current.http, fetch: platform.fetch, throwOnError: true, }) - return { url: server.url, client: sdk, event: emitter } + return { + url: currentServer.http.url, + client: sdk, + event: emitter, + createClient(opts: Omit[0], "server" | "fetch">) { + const s = server.current + if (!s) throw new Error("Server not available") + 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 new file mode 100644 index 00000000000..93e9c417555 --- /dev/null +++ b/packages/app/src/context/global-sync.test.ts @@ -0,0 +1,122 @@ +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 index 5f0f3d76fdb..1b6cdf530a7 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -1,333 +1,245 @@ -import { - type Message, - type Agent, - type Session, - type Part, - type Config, - type Path, - type Project, - type FileDiff, - type Todo, - type SessionStatus, - type ProviderListResponse, - type ProviderAuthResponse, - type Command, - type McpStatus, - type LspStatus, - type VcsInfo, - type PermissionRequest, - type QuestionRequest, - createOpencodeClient, +import type { + Config, + OpencodeClient, + Path, + Project, + ProviderAuthResponse, + ProviderListResponse, + Todo, } from "@opencode-ai/sdk/v2/client" -import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" -import { Binary } from "@opencode-ai/util/binary" -import { retry } from "@opencode-ai/util/retry" -import { useGlobalSDK } from "./global-sdk" -import { ErrorPage, type InitError } from "../pages/error" +import { showToast } from "@opencode-ai/ui/toast" +import { getFilename } from "@opencode-ai/util/path" import { - batch, createContext, - createEffect, getOwner, - runWithOwner, - useContext, + Match, onCleanup, onMount, - type Accessor, type ParentProps, Switch, - Match, + untrack, + useContext, } from "solid-js" -import { showToast } from "@opencode-ai/ui/toast" -import { getFilename } from "@opencode-ai/util/path" -import { usePlatform } from "./platform" +import { createStore, produce, reconcile } from "solid-js/store" import { useLanguage } from "@/context/language" import { Persist, persisted } from "@/utils/persist" - -type ProjectMeta = { - name?: string - icon?: { - override?: string - color?: string - } - commands?: { - start?: string - } -} - -type State = { - status: "loading" | "partial" | "complete" - agent: Agent[] - command: Command[] - project: string - projectMeta: ProjectMeta | undefined - icon: string | undefined - provider: ProviderListResponse - config: Config +import type { InitError } from "../pages/error" +import { useGlobalSDK } from "./global-sdk" +import { bootstrapDirectory, bootstrapGlobal } 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 { sanitizeProject } from "./global-sync/utils" +import { formatServerError } from "@/utils/server-errors" + +type GlobalStore = { + ready: boolean + error?: InitError path: Path - session: Session[] - sessionTotal: number - session_status: { - [sessionID: string]: SessionStatus - } - session_diff: { - [sessionID: string]: FileDiff[] - } - todo: { + project: Project[] + session_todo: { [sessionID: string]: Todo[] } - permission: { - [sessionID: string]: PermissionRequest[] - } - question: { - [sessionID: string]: QuestionRequest[] - } - mcp: { - [name: string]: McpStatus - } - lsp: LspStatus[] - vcs: VcsInfo | undefined - limit: number - message: { - [sessionID: string]: Message[] - } - part: { - [messageID: string]: Part[] - } -} - -type VcsCache = { - store: Store<{ value: VcsInfo | undefined }> - setStore: SetStoreFunction<{ value: VcsInfo | undefined }> - ready: Accessor -} - -type MetaCache = { - store: Store<{ value: ProjectMeta | undefined }> - setStore: SetStoreFunction<{ value: ProjectMeta | undefined }> - ready: Accessor -} - -type IconCache = { - store: Store<{ value: string | undefined }> - setStore: SetStoreFunction<{ value: string | undefined }> - ready: Accessor -} - -type ChildOptions = { - bootstrap?: boolean + provider: ProviderListResponse + provider_auth: ProviderAuthResponse + config: Config + reload: undefined | "pending" | "complete" } function createGlobalSync() { const globalSDK = useGlobalSDK() - const platform = usePlatform() const language = useLanguage() const owner = getOwner() if (!owner) throw new Error("GlobalSync must be created within owner") - const vcsCache = new Map() - const metaCache = new Map() - const iconCache = new Map() - const [projectCache, setProjectCache, , projectCacheReady] = persisted( + const sdkCache = new Map() + const booting = new Map>() + const sessionLoads = new Map>() + const sessionMeta = new Map() + + const [projectCache, setProjectCache, projectInit] = persisted( Persist.global("globalSync.project", ["globalSync.project.v1"]), createStore({ value: [] as Project[] }), ) - const sanitizeProject = (project: Project) => { - if (!project.icon?.url && !project.icon?.override) return project - return { - ...project, - icon: { - ...project.icon, - url: undefined, - override: undefined, - }, - } - } - const [globalStore, setGlobalStore] = createStore<{ - ready: boolean - error?: InitError - path: Path - project: Project[] - provider: ProviderListResponse - provider_auth: ProviderAuthResponse - config: Config - reload: undefined | "pending" | "complete" - }>({ + const [globalStore, setGlobalStore] = createStore({ ready: false, path: { state: "", config: "", worktree: "", directory: "", home: "" }, project: projectCache.value, + session_todo: {}, provider: { all: [], connected: [], default: {} }, provider_auth: {}, config: {}, reload: undefined, }) - let bootstrapQueue: string[] = [] - - createEffect(() => { - if (!projectCacheReady()) return - if (globalStore.project.length !== 0) return - const cached = projectCache.value - if (cached.length === 0) return - setGlobalStore("project", cached) - }) - createEffect(() => { - if (!projectCacheReady()) return - if (globalStore.project.length === 0 && projectCache.value.length !== 0) return - setProjectCache("value", globalStore.project.map(sanitizeProject)) - }) + let active = true + let projectWritten = false - createEffect(async () => { - if (globalStore.reload !== "complete") return - if (bootstrapQueue.length) { - for (const directory of bootstrapQueue) { - bootstrapInstance(directory) - } - bootstrap() - } - bootstrapQueue = [] - setGlobalStore("reload", undefined) + onCleanup(() => { + active = false }) - const children: Record, SetStoreFunction]> = {} - const booting = new Map>() - const sessionLoads = new Map>() - const sessionMeta = new Map() - - function ensureChild(directory: string) { - if (!directory) console.error("No directory provided") - if (!children[directory]) { - const cache = runWithOwner(owner, () => - persisted( - Persist.workspace(directory, "vcs", ["vcs.v1"]), - createStore({ value: undefined as VcsInfo | undefined }), - ), - ) - if (!cache) throw new Error("Failed to create persisted cache") - vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] }) - - const meta = runWithOwner(owner, () => - persisted( - Persist.workspace(directory, "project", ["project.v1"]), - createStore({ value: undefined as ProjectMeta | undefined }), - ), - ) - if (!meta) throw new Error("Failed to create persisted project metadata") - metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] }) - - const icon = runWithOwner(owner, () => - persisted( - Persist.workspace(directory, "icon", ["icon.v1"]), - createStore({ value: undefined as string | undefined }), - ), - ) - if (!icon) throw new Error("Failed to create persisted project icon") - iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] }) - - const init = () => { - const child = createStore({ - project: "", - projectMeta: meta[0].value, - icon: icon[0].value, - provider: { all: [], connected: [], default: {} }, - config: {}, - path: { state: "", config: "", worktree: "", directory: "", home: "" }, - status: "loading" as const, - agent: [], - command: [], - session: [], - sessionTotal: 0, - session_status: {}, - session_diff: {}, - todo: {}, - permission: {}, - question: {}, - mcp: {}, - lsp: [], - vcs: cache[0].value, - limit: 5, - message: {}, - part: {}, - }) + const cacheProjects = () => { + setProjectCache( + "value", + untrack(() => globalStore.project.map(sanitizeProject)), + ) + } - children[directory] = child + const setProjects = (next: Project[] | ((draft: Project[]) => void)) => { + projectWritten = true + if (typeof next === "function") { + setGlobalStore("project", produce(next)) + cacheProjects() + return + } + setGlobalStore("project", next) + cacheProjects() + } - createEffect(() => { - child[1]("projectMeta", meta[0].value) - }) + 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 - createEffect(() => { - child[1]("icon", icon[0].value) - }) - } + const set = ((...input: unknown[]) => { + if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) { + setProjects(input[1] as Project[] | ((draft: Project[]) => void)) + return input[1] + } + return (setGlobalStore as (...args: unknown[]) => unknown)(...input) + }) as typeof setGlobalStore + + if (projectInit instanceof Promise) { + void projectInit.then(() => { + if (!active) return + if (projectWritten) return + const cached = projectCache.value + if (cached.length === 0) return + setGlobalStore("project", cached) + }) + } - runWithOwner(owner, init) + const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => { + if (!sessionID) return + if (!todos) { + setGlobalStore( + "session_todo", + produce((draft) => { + delete draft[sessionID] + }), + ) + return } - const childStore = children[directory] - if (!childStore) throw new Error("Failed to create store") - return childStore + setGlobalStore("session_todo", sessionID, reconcile(todos, { key: "id" })) } - function child(directory: string, options: ChildOptions = {}) { - const childStore = ensureChild(directory) - const shouldBootstrap = options.bootstrap ?? true - if (shouldBootstrap && childStore[0].status === "loading") { + 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) - } - return childStore + }, + onDispose: (directory) => { + queue.clear(directory) + sessionMeta.delete(directory) + sdkCache.delete(directory) + clearSessionPrefetchDirectory(directory) + }, + }) + + 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 - const [store, setStore] = child(directory, { bootstrap: false }) + children.pin(directory) + const [store, setStore] = children.child(directory, { bootstrap: false }) const meta = sessionMeta.get(directory) - if (meta && meta.limit >= store.limit) return + 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 promise = globalSDK.client.session - .list({ directory, roots: true }) + const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) + const promise = 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) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) - - // Read the current limit at resolve-time so callers that bump the limit while - // a request is in-flight still get the expanded result. + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) const limit = store.limit - - const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory)) - if (sandboxWorkspace) { - setStore("sessionTotal", nonArchived.length) - setStore("session", reconcile(nonArchived, { key: "id" })) - sessionMeta.set(directory, { limit }) - return - } - - const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000 - // Include up to the limit, plus any updated in the last 4 hours - const sessions = nonArchived.filter((s, i) => { - if (i < limit) return true - const updated = new Date(s.time?.updated ?? s.time?.created).getTime() - return updated > fourHoursAgo + const childSessions = store.session.filter((s) => !!s.parentID) + const sessions = trimSessions([...nonArchived, ...childSessions], { + limit, + permission: store.permission, }) - // Store total session count (used for "load more" pagination) - setStore("sessionTotal", nonArchived.length) + 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({ title: language.t("toast.session.listFailed.title", { project }), description: err.message }) + showToast({ + variant: "error", + title: language.t("toast.session.listFailed.title", { project }), + description: formatServerError(err, language.t), + }) }) sessionLoads.set(directory, promise) promise.finally(() => { sessionLoads.delete(directory) + children.unpin(directory) }) return promise } @@ -337,146 +249,27 @@ function createGlobalSync() { const pending = booting.get(directory) if (pending) return pending + children.pin(directory) const promise = (async () => { - const [store, setStore] = ensureChild(directory) - const cache = vcsCache.get(directory) + const child = children.ensureChild(directory) + const cache = children.vcsCache.get(directory) if (!cache) return - const meta = metaCache.get(directory) - if (!meta) return - const sdk = createOpencodeClient({ - baseUrl: globalSDK.url, - fetch: platform.fetch, + const sdk = sdkFor(directory) + await bootstrapDirectory({ directory, - throwOnError: true, - }) - - setStore("status", "loading") - - createEffect(() => { - if (!cache.ready()) return - const cached = cache.store.value - if (!cached?.branch) return - setStore("vcs", (value) => value ?? cached) - }) - - // projectMeta is synced from persisted storage in ensureChild. - - const blockingRequests = { - project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), - provider: () => - sdk.provider.list().then((x) => { - const data = x.data! - setStore("provider", { - ...data, - all: data.all.map((provider) => ({ - ...provider, - models: Object.fromEntries( - Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"), - ), - })), - }) - }), - agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), - config: () => sdk.config.get().then((x) => setStore("config", x.data!)), - } - - try { - await Promise.all(Object.values(blockingRequests).map((p) => retry(p))) - } catch (err) { - console.error("Failed to bootstrap instance", err) - const project = getFilename(directory) - const message = err instanceof Error ? err.message : String(err) - showToast({ title: `Failed to reload ${project}`, description: message }) - setStore("status", "partial") - return - } - - if (store.status !== "complete") setStore("status", "partial") - - Promise.all([ - sdk.path.get().then((x) => setStore("path", x.data!)), - sdk.command.list().then((x) => setStore("command", x.data ?? [])), - sdk.session.status().then((x) => setStore("session_status", x.data!)), - loadSessions(directory), - sdk.mcp.status().then((x) => setStore("mcp", x.data!)), - sdk.lsp.status().then((x) => setStore("lsp", x.data!)), - sdk.vcs.get().then((x) => { - const next = x.data ?? store.vcs - setStore("vcs", next) - if (next?.branch) cache.setStore("value", next) - }), - sdk.permission.list().then((x) => { - const grouped: Record = {} - for (const perm of x.data ?? []) { - if (!perm?.id || !perm.sessionID) continue - const existing = grouped[perm.sessionID] - if (existing) { - existing.push(perm) - continue - } - grouped[perm.sessionID] = [perm] - } - - batch(() => { - for (const sessionID of Object.keys(store.permission)) { - if (grouped[sessionID]) continue - setStore("permission", sessionID, []) - } - for (const [sessionID, permissions] of Object.entries(grouped)) { - setStore( - "permission", - sessionID, - reconcile( - permissions - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), - { key: "id" }, - ), - ) - } - }) - }), - sdk.question.list().then((x) => { - const grouped: Record = {} - for (const question of x.data ?? []) { - if (!question?.id || !question.sessionID) continue - const existing = grouped[question.sessionID] - if (existing) { - existing.push(question) - continue - } - grouped[question.sessionID] = [question] - } - - batch(() => { - for (const sessionID of Object.keys(store.question)) { - if (grouped[sessionID]) continue - setStore("question", sessionID, []) - } - for (const [sessionID, questions] of Object.entries(grouped)) { - setStore( - "question", - sessionID, - reconcile( - questions - .filter((q) => !!q?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), - { key: "id" }, - ), - ) - } - }) - }), - ]).then(() => { - setStore("status", "complete") + sdk, + store: child[0], + setStore: child[1], + vcsCache: cache, + loadSessions, + translate: language.t, }) })() booting.set(directory, promise) promise.finally(() => { booting.delete(directory) + children.unpin(directory) }) return promise } @@ -486,386 +279,109 @@ function createGlobalSync() { const event = e.details if (directory === "global") { - switch (event?.type) { - case "global.disposed": { - if (globalStore.reload) return - bootstrap() - break - } - case "project.updated": { - const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id) - if (result.found) { - setGlobalStore("project", result.index, reconcile(event.properties)) - return - } - setGlobalStore( - "project", - produce((draft) => { - draft.splice(result.index, 0, event.properties) - }), - ) - break + applyGlobalEvent({ + event, + project: globalStore.project, + refresh: queue.refresh, + setGlobalProject: setProjects, + }) + if (event.type === "server.connected" || event.type === "global.disposed") { + for (const directory of Object.keys(children.children)) { + queue.push(directory) } } return } - const existing = children[directory] + const existing = children.children[directory] if (!existing) return - + children.mark(directory) const [store, setStore] = existing - switch (event.type) { - case "server.instance.disposed": { - if (globalStore.reload) { - bootstrapQueue.push(directory) - return - } - bootstrapInstance(directory) - break - } - case "session.created": { - const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) - if (result.found) { - setStore("session", result.index, reconcile(event.properties.info)) - break - } - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) - if (!event.properties.info.parentID) { - setStore("sessionTotal", store.sessionTotal + 1) - } - break - } - case "session.updated": { - const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) - if (event.properties.info.time.archived) { - if (result.found) { - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - } - if (event.properties.info.parentID) break - setStore("sessionTotal", (value) => Math.max(0, value - 1)) - break - } - if (result.found) { - setStore("session", result.index, reconcile(event.properties.info)) - break - } - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) - break - } - case "session.deleted": { - const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) - if (result.found) { - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - } - if (event.properties.info.parentID) break - setStore("sessionTotal", (value) => Math.max(0, value - 1)) - break - } - case "session.diff": - setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" })) - break - case "todo.updated": - setStore("todo", event.properties.sessionID, reconcile(event.properties.todos, { key: "id" })) - break - case "session.status": { - setStore("session_status", event.properties.sessionID, reconcile(event.properties.status)) - break - } - case "message.updated": { - const messages = store.message[event.properties.info.sessionID] - if (!messages) { - setStore("message", event.properties.info.sessionID, [event.properties.info]) - break - } - const result = Binary.search(messages, event.properties.info.id, (m) => m.id) - if (result.found) { - setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info)) - break - } - setStore( - "message", - event.properties.info.sessionID, - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) - break - } - case "message.removed": { - const messages = store.message[event.properties.sessionID] - if (!messages) break - const result = Binary.search(messages, event.properties.messageID, (m) => m.id) - if (result.found) { - setStore( - "message", - event.properties.sessionID, - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - } - break - } - case "message.part.updated": { - const part = event.properties.part - const parts = store.part[part.messageID] - if (!parts) { - setStore("part", part.messageID, [part]) - break - } - const result = Binary.search(parts, part.id, (p) => p.id) - if (result.found) { - setStore("part", part.messageID, result.index, reconcile(part)) - break - } - setStore( - "part", - part.messageID, - produce((draft) => { - draft.splice(result.index, 0, part) - }), - ) - break - } - case "message.part.removed": { - const parts = store.part[event.properties.messageID] - if (!parts) break - const result = Binary.search(parts, event.properties.partID, (p) => p.id) - if (result.found) { - setStore( - "part", - event.properties.messageID, - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - } - break - } - case "vcs.branch.updated": { - const next = { branch: event.properties.branch } - setStore("vcs", next) - const cache = vcsCache.get(directory) - if (cache) cache.setStore("value", next) - break - } - case "permission.asked": { - const sessionID = event.properties.sessionID - const permissions = store.permission[sessionID] - if (!permissions) { - setStore("permission", sessionID, [event.properties]) - break - } - - const result = Binary.search(permissions, event.properties.id, (p) => p.id) - if (result.found) { - setStore("permission", sessionID, result.index, reconcile(event.properties)) - break - } - - setStore( - "permission", - sessionID, - produce((draft) => { - draft.splice(result.index, 0, event.properties) - }), - ) - break - } - case "permission.replied": { - const permissions = store.permission[event.properties.sessionID] - if (!permissions) break - const result = Binary.search(permissions, event.properties.requestID, (p) => p.id) - if (!result.found) break - setStore( - "permission", - event.properties.sessionID, - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - break - } - case "question.asked": { - const sessionID = event.properties.sessionID - const questions = store.question[sessionID] - if (!questions) { - setStore("question", sessionID, [event.properties]) - break - } - - const result = Binary.search(questions, event.properties.id, (q) => q.id) - if (result.found) { - setStore("question", sessionID, result.index, reconcile(event.properties)) - break - } + applyDirectoryEvent({ + event, + directory, + store, + setStore, + push: queue.push, + setSessionTodo, + vcsCache: children.vcsCache.get(directory), + loadLsp: () => { + sdkFor(directory) + .lsp.status() + .then((x) => setStore("lsp", x.data ?? [])) + }, + }) + }) - setStore( - "question", - sessionID, - produce((draft) => { - draft.splice(result.index, 0, event.properties) - }), - ) - break - } - case "question.replied": - case "question.rejected": { - const questions = store.question[event.properties.sessionID] - if (!questions) break - const result = Binary.search(questions, event.properties.requestID, (q) => q.id) - if (!result.found) break - setStore( - "question", - event.properties.sessionID, - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - break - } - case "lsp.updated": { - const sdk = createOpencodeClient({ - baseUrl: globalSDK.url, - fetch: platform.fetch, - directory, - throwOnError: true, - }) - sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])) - break - } + onCleanup(unsub) + onCleanup(() => { + queue.dispose() + }) + onCleanup(() => { + for (const directory of Object.keys(children.children)) { + children.disposeDirectory(directory) } }) - onCleanup(unsub) async function bootstrap() { - const health = await globalSDK.client.global - .health() - .then((x) => x.data) - .catch(() => undefined) - if (!health?.healthy) { - setGlobalStore("error", new Error(language.t("error.globalSync.connectFailed", { url: globalSDK.url }))) - return - } - - return Promise.all([ - retry(() => - globalSDK.client.path.get().then((x) => { - setGlobalStore("path", x.data!) - }), - ), - retry(() => - globalSDK.client.config.get().then((x) => { - setGlobalStore("config", x.data!) - }), - ), - retry(() => - globalSDK.client.project.list().then(async (x) => { - const projects = (x.data ?? []) - .filter((p) => !!p?.id) - .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) - setGlobalStore("project", projects) - }), - ), - retry(() => - globalSDK.client.provider.list().then((x) => { - const data = x.data! - setGlobalStore("provider", { - ...data, - all: data.all.map((provider) => ({ - ...provider, - models: Object.fromEntries( - Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"), - ), - })), - }) - }), - ), - retry(() => - globalSDK.client.provider.auth().then((x) => { - setGlobalStore("provider_auth", x.data ?? {}) - }), - ), - ]) - .then(() => setGlobalStore("ready", true)) - .catch((e) => setGlobalStore("error", e)) + await bootstrapGlobal({ + globalSDK: globalSDK.client, + connectErrorTitle: language.t("dialog.server.add.error"), + connectErrorDescription: language.t("error.globalSync.connectFailed", { + url: globalSDK.url, + }), + requestFailedTitle: language.t("common.requestFailed"), + translate: language.t, + formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), + setGlobalStore: setBootStore, + }) } onMount(() => { - bootstrap() + void bootstrap() }) - 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) + const projectApi = { + loadSessions, + meta(directory: string, patch: ProjectMeta) { + children.projectMeta(directory, patch) + }, + icon(directory: string, value: string | undefined) { + children.projectIcon(directory, value) + }, } - 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) + 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: setGlobalStore, + set, get ready() { return globalStore.ready }, get error() { return globalStore.error }, - child, + child: children.child, bootstrap, - updateConfig: async (config: Config) => { - setGlobalStore("reload", "pending") - const response = await globalSDK.client.config.update({ config }) - setTimeout(() => { - setGlobalStore("reload", "complete") - }, 1000) - return response - }, - project: { - loadSessions, - meta: projectMeta, - icon: projectIcon, + updateConfig, + project: projectApi, + todo: { + set: setSessionTodo, }, } } @@ -876,9 +392,6 @@ export function GlobalSyncProvider(props: ParentProps) { const value = createGlobalSync() return ( - - - {props.children} diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts new file mode 100644 index 00000000000..8b1a3c48c5f --- /dev/null +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -0,0 +1,206 @@ +import type { + Config, + OpencodeClient, + Path, + PermissionRequest, + Project, + ProviderAuthResponse, + ProviderListResponse, + QuestionRequest, + Todo, +} from "@opencode-ai/sdk/v2/client" +import { showToast } from "@opencode-ai/ui/toast" +import { getFilename } from "@opencode-ai/util/path" +import { retry } from "@opencode-ai/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, normalizeProviderList } from "./utils" +import { formatServerError } from "@/utils/server-errors" + +type GlobalStore = { + ready: boolean + path: Path + project: Project[] + session_todo: { + [sessionID: string]: Todo[] + } + provider: ProviderListResponse + provider_auth: ProviderAuthResponse + config: Config + reload: undefined | "pending" | "complete" +} + +export async function bootstrapGlobal(input: { + globalSDK: OpencodeClient + connectErrorTitle: string + connectErrorDescription: string + requestFailedTitle: string + translate: (key: string, vars?: Record) => string + formatMoreCount: (count: number) => string + setGlobalStore: SetStoreFunction +}) { + const health = await input.globalSDK.global + .health() + .then((x) => x.data) + .catch(() => undefined) + if (!health?.healthy) { + showToast({ + variant: "error", + title: input.connectErrorTitle, + description: input.connectErrorDescription, + }) + input.setGlobalStore("ready", true) + return + } + + const tasks = [ + retry(() => + input.globalSDK.path.get().then((x) => { + input.setGlobalStore("path", x.data!) + }), + ), + retry(() => + input.globalSDK.global.config.get().then((x) => { + input.setGlobalStore("config", 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) + }), + ), + retry(() => + input.globalSDK.provider.list().then((x) => { + input.setGlobalStore("provider", normalizeProviderList(x.data!)) + }), + ), + retry(() => + input.globalSDK.provider.auth().then((x) => { + input.setGlobalStore("provider_auth", x.data ?? {}) + }), + ), + ] + + const results = await Promise.allSettled(tasks) + const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason) + if (errors.length) { + const message = formatServerError(errors[0], input.translate) + const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : "" + showToast({ + variant: "error", + title: input.requestFailedTitle, + description: message + more, + }) + } + 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 + }, {}) +} + +export async function bootstrapDirectory(input: { + directory: string + sdk: OpencodeClient + store: Store + setStore: SetStoreFunction + vcsCache: VcsCache + loadSessions: (directory: string) => Promise | void + translate: (key: string, vars?: Record) => string +}) { + if (input.store.status !== "complete") input.setStore("status", "loading") + + const blockingRequests = { + project: () => input.sdk.project.current().then((x) => input.setStore("project", x.data!.id)), + provider: () => + input.sdk.provider.list().then((x) => { + input.setStore("provider", normalizeProviderList(x.data!)) + }), + agent: () => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? [])), + config: () => input.sdk.config.get().then((x) => input.setStore("config", x.data!)), + } + + try { + await Promise.all(Object.values(blockingRequests).map((p) => retry(p))) + } catch (err) { + console.error("Failed to bootstrap instance", err) + const project = getFilename(input.directory) + showToast({ + variant: "error", + title: `Failed to reload ${project}`, + description: formatServerError(err, input.translate), + }) + input.setStore("status", "partial") + return + } + + if (input.store.status !== "complete") input.setStore("status", "partial") + + Promise.all([ + input.sdk.path.get().then((x) => input.setStore("path", x.data!)), + input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])), + input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)), + input.loadSessions(input.directory), + input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)), + input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)), + input.sdk.vcs.get().then((x) => { + const next = x.data ?? input.store.vcs + input.setStore("vcs", next) + if (next?.branch) input.vcsCache.setStore("value", next) + }), + input.sdk.permission.list().then((x) => { + const grouped = groupBySession( + (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), + ) + 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" }, + ), + ) + } + }) + }), + input.sdk.question.list().then((x) => { + const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) + 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" }, + ), + ) + } + }) + }), + ]).then(() => { + 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 new file mode 100644 index 00000000000..cec76ff87ec --- /dev/null +++ b/packages/app/src/context/global-sync/child-store.test.ts @@ -0,0 +1,37 @@ +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() {}, + }) + + 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 new file mode 100644 index 00000000000..e2ada244fb3 --- /dev/null +++ b/packages/app/src/context/global-sync/child-store.ts @@ -0,0 +1,270 @@ +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 "@opencode-ai/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" + +export function createChildStoreManager(input: { + owner: Owner + isBooting: (directory: string) => boolean + isLoadingSessions: (directory: string) => boolean + onBootstrap: (directory: string) => void + onDispose: (directory: string) => void +}) { + 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("Failed to create persisted cache") + 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("Failed to create persisted project metadata") + 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("Failed to create persisted project icon") + iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] }) + + const init = () => + createRoot((dispose) => { + const initialMeta = meta[0].value + const initialIcon = icon[0].value + const child = createStore({ + project: "", + projectMeta: initialMeta, + icon: initialIcon, + provider: { all: [], connected: [], default: {} }, + config: {}, + path: { state: "", config: "", worktree: "", directory: "", home: "" }, + status: "loading" as const, + agent: [], + command: [], + session: [], + sessionTotal: 0, + session_status: {}, + session_diff: {}, + todo: {}, + permission: {}, + question: {}, + mcp: {}, + 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(meta[2], () => { + if (child[0].projectMeta !== initialMeta) return + child[1]("projectMeta", meta[0].value) + }) + + 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("Failed to create store") + 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 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, + 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 new file mode 100644 index 00000000000..cf2da135cbb --- /dev/null +++ b/packages/app/src/context/global-sync/event-reducer.test.ts @@ -0,0 +1,552 @@ +import { describe, expect, test } from "bun:test" +import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@opencode-ai/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()) + const [cacheStore, setCacheStore] = createStore({ value: undefined 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" }) + expect(cacheStore.value).toEqual({ branch: "feature/test" }) + }) + + 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 new file mode 100644 index 00000000000..b8eda0573f7 --- /dev/null +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -0,0 +1,356 @@ +import { Binary } from "@opencode-ai/util/binary" +import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" +import type { + FileDiff, + Message, + Part, + PermissionRequest, + Project, + QuestionRequest, + Session, + SessionStatus, + Todo, +} from "@opencode-ai/sdk/v2/client" +import type { State, VcsCache } from "./types" +import { trimSessions } from "./session-trim" +import { dropSessionCaches } from "./session-cache" + +export function applyGlobalEvent(input: { + event: { type: string; properties?: unknown } + project: Project[] + setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => 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((draft) => { + draft[result.index] = { ...draft[result.index], ...properties } + }) + return + } + input.setGlobalProject((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: FileDiff[] } + input.setStore("session_diff", props.sessionID, reconcile(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 = (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 + 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 = { 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 new file mode 100644 index 00000000000..676a6ee17e1 --- /dev/null +++ b/packages/app/src/context/global-sync/eviction.ts @@ -0,0 +1,28 @@ +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 new file mode 100644 index 00000000000..c3468583b93 --- /dev/null +++ b/packages/app/src/context/global-sync/queue.ts @@ -0,0 +1,83 @@ +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 + 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 new file mode 100644 index 00000000000..8e11110e3d0 --- /dev/null +++ b/packages/app/src/context/global-sync/session-cache.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, test } from "bun:test" +import type { + FileDiff, + Message, + Part, + PermissionRequest, + QuestionRequest, + SessionStatus, + Todo, +} from "@opencode-ai/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 new file mode 100644 index 00000000000..0177ebbe138 --- /dev/null +++ b/packages/app/src/context/global-sync/session-cache.ts @@ -0,0 +1,62 @@ +import type { + FileDiff, + Message, + Part, + PermissionRequest, + QuestionRequest, + SessionStatus, + Todo, +} from "@opencode-ai/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 new file mode 100644 index 00000000000..3693dcb460d --- /dev/null +++ b/packages/app/src/context/global-sync/session-load.ts @@ -0,0 +1,25 @@ +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 new file mode 100644 index 00000000000..f039b02ca8a --- /dev/null +++ b/packages/app/src/context/global-sync/session-prefetch.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "bun:test" +import { + clearSessionPrefetch, + clearSessionPrefetchDirectory, + getSessionPrefetch, + runSessionPrefetch, + setSessionPrefetch, +} 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, + complete: false, + at: 123, + }) + + expect(getSessionPrefetch("/tmp/a", "ses_1")).toEqual({ limit: 200, 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, complete: true, at: 456 } + }, + }) + + const [a, b] = await Promise.all([run(), run()]) + + expect(calls).toBe(1) + expect(a).toEqual({ limit: 100, complete: true, at: 456 }) + expect(b).toEqual({ limit: 100, complete: true, at: 456 }) + }) + + test("clears a whole directory", () => { + setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_1", limit: 10, complete: true, at: 1 }) + setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_2", limit: 20, complete: false, at: 2 }) + setSessionPrefetch({ directory: "/tmp/e", sessionID: "ses_1", limit: 30, 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, complete: true, at: 3 }) + }) +}) diff --git a/packages/app/src/context/global-sync/session-prefetch.ts b/packages/app/src/context/global-sync/session-prefetch.ts new file mode 100644 index 00000000000..10877b0639f --- /dev/null +++ b/packages/app/src/context/global-sync/session-prefetch.ts @@ -0,0 +1,85 @@ +const key = (directory: string, sessionID: string) => `${directory}\n${sessionID}` + +export const SESSION_PREFETCH_TTL = 15_000 + +type Meta = { + limit: number + complete: boolean + at: number +} + +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 + complete: boolean + at?: number +}) { + cache.set(key(input.directory, input.sessionID), { + limit: input.limit, + 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 new file mode 100644 index 00000000000..be12c074b5d --- /dev/null +++ b/packages/app/src/context/global-sync/session-trim.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test" +import type { PermissionRequest, Session } from "@opencode-ai/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 new file mode 100644 index 00000000000..800ba74a684 --- /dev/null +++ b/packages/app/src/context/global-sync/session-trim.ts @@ -0,0 +1,56 @@ +import type { PermissionRequest, Session } from "@opencode-ai/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 new file mode 100644 index 00000000000..c61dc337d8b --- /dev/null +++ b/packages/app/src/context/global-sync/types.ts @@ -0,0 +1,133 @@ +import type { + Agent, + Command, + Config, + FileDiff, + LspStatus, + McpStatus, + Message, + Part, + Path, + PermissionRequest, + Project, + ProviderListResponse, + QuestionRequest, + Session, + SessionStatus, + Todo, + VcsInfo, +} from "@opencode-ai/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: ProviderListResponse + config: Config + path: Path + session: Session[] + sessionTotal: number + session_status: { + [sessionID: string]: SessionStatus + } + session_diff: { + [sessionID: string]: FileDiff[] + } + todo: { + [sessionID: string]: Todo[] + } + permission: { + [sessionID: string]: PermissionRequest[] + } + question: { + [sessionID: string]: QuestionRequest[] + } + mcp: { + [name: string]: McpStatus + } + 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.ts b/packages/app/src/context/global-sync/utils.ts new file mode 100644 index 00000000000..6b78134a611 --- /dev/null +++ b/packages/app/src/context/global-sync/utils.ts @@ -0,0 +1,25 @@ +import type { Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" + +export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) + +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 new file mode 100644 index 00000000000..058f7cc4b6c --- /dev/null +++ b/packages/app/src/context/highlights.tsx @@ -0,0 +1,233 @@ +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://opencode.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 index da10e4773ae..b1edd541c3c 100644 --- a/packages/app/src/context/language.tsx +++ b/packages/app/src/context/language.tsx @@ -17,6 +17,9 @@ import { dict as ru } from "@/i18n/ru" import { dict as ar } from "@/i18n/ar" import { dict as no } from "@/i18n/no" import { dict as br } from "@/i18n/br" +import { dict as th } from "@/i18n/th" +import { dict as bs } from "@/i18n/bs" +import { dict as tr } from "@/i18n/tr" import { dict as uiEn } from "@opencode-ai/ui/i18n/en" import { dict as uiZh } from "@opencode-ai/ui/i18n/zh" import { dict as uiZht } from "@opencode-ai/ui/i18n/zht" @@ -31,13 +34,160 @@ import { dict as uiRu } from "@opencode-ai/ui/i18n/ru" import { dict as uiAr } from "@opencode-ai/ui/i18n/ar" import { dict as uiNo } from "@opencode-ai/ui/i18n/no" import { dict as uiBr } from "@opencode-ai/ui/i18n/br" +import { dict as uiTh } from "@opencode-ai/ui/i18n/th" +import { dict as uiBs } from "@opencode-ai/ui/i18n/bs" +import { dict as uiTr } from "@opencode-ai/ui/i18n/tr" -export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br" +export type Locale = + | "en" + | "zh" + | "zht" + | "ko" + | "de" + | "es" + | "fr" + | "da" + | "ja" + | "pl" + | "ru" + | "ar" + | "no" + | "br" + | "th" + | "bs" + | "tr" type RawDictionary = typeof en & typeof uiEn type Dictionary = i18n.Flatten -const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"] +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", + "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", + 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", + tr: "language.tr", +} + +const base = i18n.flatten({ ...en, ...uiEn }) +const DICT: Record = { + en: base, + zh: { ...base, ...i18n.flatten({ ...zh, ...uiZh }) }, + zht: { ...base, ...i18n.flatten({ ...zht, ...uiZht }) }, + ko: { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }, + de: { ...base, ...i18n.flatten({ ...de, ...uiDe }) }, + es: { ...base, ...i18n.flatten({ ...es, ...uiEs }) }, + fr: { ...base, ...i18n.flatten({ ...fr, ...uiFr }) }, + da: { ...base, ...i18n.flatten({ ...da, ...uiDa }) }, + ja: { ...base, ...i18n.flatten({ ...ja, ...uiJa }) }, + pl: { ...base, ...i18n.flatten({ ...pl, ...uiPl }) }, + ru: { ...base, ...i18n.flatten({ ...ru, ...uiRu }) }, + ar: { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }, + no: { ...base, ...i18n.flatten({ ...no, ...uiNo }) }, + br: { ...base, ...i18n.flatten({ ...br, ...uiBr }) }, + th: { ...base, ...i18n.flatten({ ...th, ...uiTh }) }, + bs: { ...base, ...i18n.flatten({ ...bs, ...uiBs }) }, + tr: { ...base, ...i18n.flatten({ ...tr, ...uiTr }) }, +} + +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: "tr", match: (language) => language.startsWith("tr") }, +] + +type ParityKey = "command.session.previous.unseen" | "command.session.next.unseen" +const PARITY_CHECK: Record, Record> = { + zh, + zht, + ko, + de, + es, + fr, + da, + ja, + pl, + ru, + ar, + no, + br, + th, + bs, + tr, +} +void PARITY_CHECK function detectLocale(): Locale { if (typeof navigator !== "object") return "en" @@ -45,31 +195,18 @@ function detectLocale(): Locale { const languages = navigator.languages?.length ? navigator.languages : [navigator.language] for (const language of languages) { if (!language) continue - 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" + const normalized = language.toLowerCase() + const match = localeMatchers.find((entry) => entry.match(normalized)) + if (match) return match.locale } return "en" } +function normalizeLocale(value: string): Locale { + return LOCALES.includes(value as Locale) ? (value as Locale) : "en" +} + export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({ name: "Language", init: () => { @@ -80,81 +217,31 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont }), ) - const locale = createMemo(() => { - if (store.locale === "zh") return "zh" - if (store.locale === "zht") return "zht" - if (store.locale === "ko") return "ko" - if (store.locale === "de") return "de" - if (store.locale === "es") return "es" - if (store.locale === "fr") return "fr" - if (store.locale === "da") return "da" - if (store.locale === "ja") return "ja" - if (store.locale === "pl") return "pl" - if (store.locale === "ru") return "ru" - if (store.locale === "ar") return "ar" - if (store.locale === "no") return "no" - if (store.locale === "br") return "br" - return "en" - }) + const locale = createMemo(() => normalizeLocale(store.locale)) + console.log("locale", locale()) + const intl = createMemo(() => INTL[locale()]) - createEffect(() => { - const current = locale() - if (store.locale === current) return - setStore("locale", current) - }) - - const base = i18n.flatten({ ...en, ...uiEn }) - const dict = createMemo(() => { - if (locale() === "en") return base - if (locale() === "zh") return { ...base, ...i18n.flatten({ ...zh, ...uiZh }) } - if (locale() === "zht") return { ...base, ...i18n.flatten({ ...zht, ...uiZht }) } - if (locale() === "de") return { ...base, ...i18n.flatten({ ...de, ...uiDe }) } - if (locale() === "es") return { ...base, ...i18n.flatten({ ...es, ...uiEs }) } - if (locale() === "fr") return { ...base, ...i18n.flatten({ ...fr, ...uiFr }) } - if (locale() === "da") return { ...base, ...i18n.flatten({ ...da, ...uiDa }) } - if (locale() === "ja") return { ...base, ...i18n.flatten({ ...ja, ...uiJa }) } - if (locale() === "pl") return { ...base, ...i18n.flatten({ ...pl, ...uiPl }) } - if (locale() === "ru") return { ...base, ...i18n.flatten({ ...ru, ...uiRu }) } - if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) } - if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) } - if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) } - return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) } - }) + const dict = createMemo(() => DICT[locale()]) const t = i18n.translator(dict, i18n.resolveTemplate) - const labelKey: 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", - } - - const label = (value: Locale) => t(labelKey[value]) + 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", next) + setStore("locale", normalizeLocale(next)) }, } }, diff --git a/packages/app/src/context/layout-scroll.test.ts b/packages/app/src/context/layout-scroll.test.ts index b7962c411cd..483be150f66 100644 --- a/packages/app/src/context/layout-scroll.test.ts +++ b/packages/app/src/context/layout-scroll.test.ts @@ -1,73 +1,64 @@ -import { describe, expect, test } from "bun:test" -import { createRoot } from "solid-js" -import { createStore } from "solid-js/store" -import { makePersisted, type SyncStorage } from "@solid-primitives/storage" +import { describe, expect, test, vi } from "bun:test" import { createScrollPersistence } from "./layout-scroll" describe("createScrollPersistence", () => { - test("debounces persisted scroll writes", async () => { - const key = "layout-scroll.test" - const data = new Map() - const writes: string[] = [] - const stats = { flushes: 0 } + 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 }) + } - const storage = { - getItem: (k: string) => data.get(k) ?? null, - setItem: (k: string, v: string) => { - data.set(k, v) - if (k === key) writes.push(v) - }, - removeItem: (k: string) => { - data.delete(k) - }, - } as SyncStorage + vi.advanceTimersByTime(9) + expect(writes).toHaveLength(0) - await new Promise((resolve, reject) => { - createRoot((dispose) => { - const [raw, setRaw] = createStore({ - sessionView: {} as Record }>, - }) + vi.advanceTimersByTime(1) - const [store, setStore] = makePersisted([raw, setRaw], { name: key, storage }) + expect(writes).toHaveLength(1) + expect(writes[0]?.review).toEqual({ x: 0, y: 30 }) - const scroll = createScrollPersistence({ - debounceMs: 30, - getSnapshot: (sessionKey) => store.sessionView[sessionKey]?.scroll, - onFlush: (sessionKey, next) => { - stats.flushes += 1 + scroll.setScroll("session", "review", { x: 0, y: 30 }) + vi.advanceTimersByTime(20) - const current = store.sessionView[sessionKey] - if (!current) { - setStore("sessionView", sessionKey, { scroll: next }) - return - } - setStore("sessionView", sessionKey, "scroll", (prev) => ({ ...(prev ?? {}), ...next })) - }, - }) + expect(writes).toHaveLength(1) + scroll.dispose() + } finally { + vi.useRealTimers() + } + }) - const run = async () => { - await new Promise((r) => setTimeout(r, 0)) - writes.length = 0 + test("reseeds empty cache after persisted snapshot loads", () => { + const snapshot = { + session: {}, + } as Record> - for (const i of Array.from({ length: 100 }, (_, n) => n)) { - scroll.setScroll("session", "review", { x: 0, y: i }) - } + const scroll = createScrollPersistence({ + getSnapshot: (sessionKey) => snapshot[sessionKey], + onFlush: () => {}, + }) - await new Promise((r) => setTimeout(r, 120)) + expect(scroll.scroll("session", "review")).toBeUndefined() - expect(stats.flushes).toBeGreaterThanOrEqual(1) - expect(writes.length).toBeGreaterThanOrEqual(1) - expect(writes.length).toBeLessThanOrEqual(2) - } + snapshot.session = { + review: { x: 12, y: 34 }, + } - void run() - .then(resolve) - .catch(reject) - .finally(() => { - scroll.dispose() - dispose() - }) - }) - }) + 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 index 30b0f69044a..ef66eccd904 100644 --- a/packages/app/src/context/layout-scroll.ts +++ b/packages/app/src/context/layout-scroll.ts @@ -33,8 +33,16 @@ export function createScrollPersistence(opts: Options) { } function seed(sessionKey: string) { - if (cache[sessionKey]) return - setCache(sessionKey, clone(opts.getSnapshot(sessionKey))) + 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) { diff --git a/packages/app/src/context/layout.test.ts b/packages/app/src/context/layout.test.ts new file mode 100644 index 00000000000..582d5edbd29 --- /dev/null +++ b/packages/app/src/context/layout.test.ts @@ -0,0 +1,69 @@ +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 index 444a36d65af..78928118d72 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -1,15 +1,21 @@ import { createStore, produce } from "solid-js/store" -import { batch, createEffect, createMemo, onCleanup, onMount } from "solid-js" +import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSync } from "./global-sync" import { useGlobalSDK } from "./global-sdk" import { useServer } from "./server" +import { usePlatform } from "./platform" import { Project } from "@opencode-ai/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_PANEL_WIDTH = 344 +const DEFAULT_SESSION_WIDTH = 600 +const DEFAULT_TERMINAL_HEIGHT = 280 export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number] export function getAvatarColors(key?: string) { @@ -33,34 +39,188 @@ type SessionTabs = { 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 - if (!isRecord(sidebar)) return value - if (typeof sidebar.workspaces !== "boolean") return value - return { - ...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_PANEL_WIDTH + return { + ...fileTree, + opened: true, + width: width === 260 ? DEFAULT_PANEL_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, } } @@ -70,32 +230,44 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( createStore({ sidebar: { opened: false, - width: 344, + width: DEFAULT_PANEL_WIDTH, workspaces: {} as Record, workspacesDefault: false, }, terminal: { - height: 280, + height: DEFAULT_TERMINAL_HEIGHT, opened: false, }, review: { diffStyle: "split" as ReviewDiffStyle, panelOpened: true, }, + fileTree: { + opened: true, + width: DEFAULT_PANEL_WIDTH, + tab: "changes" as "changes" | "all", + }, session: { - width: 600, + 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 meta = { active: undefined as string | undefined, pruned: false } - const used = new Map() + 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" }, @@ -112,29 +284,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( for (const entry of SESSION_STATE_KEYS) { const target = session ? Persist.session(dir, session, entry.key) : Persist.workspace(dir, entry.key) - void removePersisted(target) + void removePersisted(target, platform) const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}` - void removePersisted({ key: legacyKey }) + void removePersisted({ key: legacyKey }, platform) } } } function prune(keep?: string) { - if (!keep) return - - const keys = new Set() - for (const key of Object.keys(store.sessionView)) keys.add(key) - for (const key of Object.keys(store.sessionTabs)) keys.add(key) - if (keys.size <= MAX_SESSION_KEYS) return - - const score = (key: string) => { - if (key === keep) return Number.MAX_SAFE_INTEGER - return used.get(key) ?? 0 - } - - const ordered = Array.from(keys).sort((a, b) => score(b) - score(a)) - const drop = ordered.slice(MAX_SESSION_KEYS) + 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( @@ -150,18 +315,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( dropSessionState(drop) for (const key of drop) { - used.delete(key) + usage.used.delete(key) } } function touch(sessionKey: string) { - meta.active = sessionKey - used.set(sessionKey, Date.now()) + usage.active = sessionKey + usage.used.set(sessionKey, Date.now()) if (!ready()) return - if (meta.pruned) return + if (usage.pruned) return - meta.pruned = true + usage.pruned = true prune(sessionKey) } @@ -170,7 +335,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( getSnapshot: (sessionKey) => store.sessionView[sessionKey]?.scroll, onFlush: (sessionKey, next) => { const current = store.sessionView[sessionKey] - const keep = meta.active ?? sessionKey + const keep = usage.active ?? sessionKey if (!current) { setStore("sessionView", sessionKey, { scroll: next }) prune(keep) @@ -182,12 +347,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, }) + const ensureKey = (key: string) => ensureSessionKey(key, touch, (sessionKey) => scroll.seed(sessionKey)) + createEffect(() => { if (!ready()) return - if (meta.pruned) return - const active = meta.active + if (usage.pruned) return + const active = usage.active if (!active) return - meta.pruned = true + usage.pruned = true prune(active) }) @@ -209,6 +376,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) const [colors, setColors] = createStore>({}) + const colorRequested = new Map() function pickAvailableColor(used: Set): AvatarColorKey { const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c)) @@ -217,7 +385,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( } function enrich(project: { worktree: string; expanded: boolean }) { - const [childStore] = globalSync.child(project.worktree) + const [childStore] = globalSync.child(project.worktree, { bootstrap: false }) const projectID = childStore.project const metadata = projectID ? globalSync.data.project.find((x) => x.id === projectID) @@ -267,17 +435,36 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( return map }) - createEffect(() => { + const rootFor = (directory: string) => { const map = roots() - if (map.size === 0) return + 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 = map.get(project.worktree) - if (!root) continue + const root = rootFor(project.worktree) + if (root === project.worktree) continue server.projects.close(project.worktree) @@ -305,13 +492,21 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( createEffect(() => { const projects = enriched() if (projects.length === 0) return + if (!globalSync.ready) return - if (globalSync.ready) { - for (const project of projects) { - if (!project.id) continue - if (project.id === "global") continue - globalSync.project.icon(project.worktree, project.icon?.override) - } + 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() @@ -322,18 +517,29 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( for (const project of projects) { if (project.icon?.color) continue - const existing = colors[project.worktree] + const worktree = project.worktree + const existing = colors[worktree] const color = existing ?? pickAvailableColor(used) if (!existing) { used.add(color) - setColors(project.worktree, 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(project.worktree, { icon: { color } }) + globalSync.project.meta(worktree, { icon: { color } }) continue } - void globalSdk.client.project.update({ projectID: project.id, directory: project.worktree, icon: { color } }) + + void globalSdk.client.project + .update({ projectID: project.id, directory: worktree, icon: { color } }) + .catch(() => { + if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) + }) } }) @@ -347,10 +553,20 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( 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 = roots().get(directory) ?? directory + const root = rootFor(directory) if (server.projects.list().find((x) => x.worktree === root)) return globalSync.project.loadSessions(root) server.projects.open(root) @@ -384,7 +600,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("sidebar", "width", width) }, workspaces(directory: string) { - return createMemo(() => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false) + return () => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false }, setWorkspaces(directory: string, value: boolean) { setStore("sidebar", "workspaces", directory, value) @@ -404,14 +620,54 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( diffStyle: createMemo(() => store.review?.diffStyle ?? "split"), setDiffStyle(diffStyle: ReviewDiffStyle) { if (!store.review) { - setStore("review", { diffStyle }) + setStore("review", { diffStyle, panelOpened: true }) return } setStore("review", "diffStyle", diffStyle) }, }, + fileTree: { + opened: createMemo(() => store.fileTree?.opened ?? true), + width: createMemo(() => store.fileTree?.width ?? DEFAULT_PANEL_WIDTH), + tab: createMemo(() => store.fileTree?.tab ?? "changes"), + setTab(tab: "changes" | "all") { + if (!store.fileTree) { + setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab }) + return + } + setStore("fileTree", "tab", tab) + }, + open() { + if (!store.fileTree) { + setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab: "changes" }) + return + } + setStore("fileTree", "opened", true) + }, + close() { + if (!store.fileTree) { + setStore("fileTree", { opened: false, width: DEFAULT_PANEL_WIDTH, tab: "changes" }) + return + } + setStore("fileTree", "opened", false) + }, + toggle() { + if (!store.fileTree) { + setStore("fileTree", { opened: true, width: DEFAULT_PANEL_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 ?? 600), + width: createMemo(() => store.session?.width ?? DEFAULT_SESSION_WIDTH), resize(width: number) { if (!store.session) { setStore("session", { width }) @@ -432,17 +688,59 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("mobileSidebar", "opened", (x) => !x) }, }, - view(sessionKey: string) { - touch(sessionKey) - scroll.seed(sessionKey) - const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} }) + 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: 280, opened: next }) + setStore("terminal", { height: DEFAULT_TERMINAL_HEIGHT, opened: next }) return } @@ -465,10 +763,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( return { scroll(tab: string) { - return scroll.scroll(sessionKey, tab) + return scroll.scroll(key(), tab) }, setScroll(tab: string, pos: SessionScroll) { - scroll.setScroll(sessionKey, tab, pos) + scroll.setScroll(key(), tab, pos) }, terminal: { opened: terminalOpened, @@ -495,105 +793,137 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, }, review: { - open: createMemo(() => s().reviewOpen), + open: createMemo(() => s().reviewOpen ?? []), setOpen(open: string[]) { - const current = store.sessionView[sessionKey] + 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", sessionKey, { + setStore("sessionView", session, { scroll: {}, - reviewOpen: open, + reviewOpen: [path], }) return } - if (same(current.reviewOpen, open)) return - setStore("sessionView", sessionKey, "reviewOpen", open) + 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) { - touch(sessionKey) - const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] }) + 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), + all: createMemo(() => tabs().all.filter((tab) => tab !== "review")), setActive(tab: string | undefined) { - if (!store.sessionTabs[sessionKey]) { - setStore("sessionTabs", sessionKey, { all: [], active: tab }) + const session = key() + const next = tab ? normalize(tab) : tab + if (!store.sessionTabs[session]) { + setStore("sessionTabs", session, { all: [], active: next }) } else { - setStore("sessionTabs", sessionKey, "active", tab) + setStore("sessionTabs", session, "active", next) } }, setAll(all: string[]) { - if (!store.sessionTabs[sessionKey]) { - setStore("sessionTabs", sessionKey, { all, active: undefined }) + 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", sessionKey, "all", all) + setStore("sessionTabs", session, "all", next) } }, async open(tab: string) { - const current = store.sessionTabs[sessionKey] ?? { all: [] } + 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 (!store.sessionTabs[sessionKey]) { - setStore("sessionTabs", sessionKey, { all: [], active: tab }) - return - } - setStore("sessionTabs", sessionKey, "active", tab) - return - } - - if (tab === "context") { - const all = [tab, ...current.all.filter((x) => x !== tab)] - if (!store.sessionTabs[sessionKey]) { - setStore("sessionTabs", sessionKey, { all, active: tab }) - return - } - setStore("sessionTabs", sessionKey, "all", all) - setStore("sessionTabs", sessionKey, "active", tab) - return - } - - if (!current.all.includes(tab)) { - if (!store.sessionTabs[sessionKey]) { - setStore("sessionTabs", sessionKey, { all: [tab], active: tab }) - return - } - setStore("sessionTabs", sessionKey, "all", [...current.all, tab]) - setStore("sessionTabs", sessionKey, "active", tab) + if (current.active !== tab) return + setStore("sessionTabs", session, "active", current.all[0]) return } - if (!store.sessionTabs[sessionKey]) { - setStore("sessionTabs", sessionKey, { all: current.all, active: tab }) + const all = current.all.filter((x) => x !== tab) + if (current.active !== tab) { + setStore("sessionTabs", session, "all", all) return } - setStore("sessionTabs", sessionKey, "active", tab) - }, - close(tab: string) { - const current = store.sessionTabs[sessionKey] - if (!current) return - const all = current.all.filter((x) => x !== tab) + const index = current.all.findIndex((f) => f === tab) + const next = current.all[index - 1] ?? current.all[index + 1] ?? all[0] batch(() => { - setStore("sessionTabs", sessionKey, "all", all) - if (current.active !== tab) return - - const index = current.all.findIndex((f) => f === tab) - const next = all[index - 1] ?? all[0] - setStore("sessionTabs", sessionKey, "active", next) + setStore("sessionTabs", session, "all", all) + setStore("sessionTabs", session, "active", next) }) }, move(tab: string, to: number) { - const current = store.sessionTabs[sessionKey] + 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", - sessionKey, + 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 index 64bfa838dde..75d1334a5a5 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,59 +1,26 @@ -import { createStore, produce, reconcile } from "solid-js/store" -import { batch, createMemo, onCleanup } from "solid-js" -import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" -import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2" +import { createStore } from "solid-js/store" +import { batch, createMemo } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { useSDK } from "./sdk" import { useSync } from "./sync" import { base64Encode } from "@opencode-ai/util/encode" import { useProviders } from "@/hooks/use-providers" -import { DateTime } from "luxon" -import { Persist, persisted } from "@/utils/persist" -import { showToast } from "@opencode-ai/ui/toast" -import { useLanguage } from "@/context/language" +import { useModels } from "@/context/models" +import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant" -export type LocalFile = FileNode & - Partial<{ - loaded: boolean - pinned: boolean - expanded: boolean - content: FileContent - selection: { startLine: number; startChar: number; endLine: number; endChar: number } - scrollTop: number - view: "raw" | "diff-unified" | "diff-split" - folded: string[] - selectedChange: number - status: FileStatus - }> -export type TextSelection = LocalFile["selection"] -export type View = LocalFile["view"] - -export type LocalModel = Omit & { - provider: Provider - latest?: boolean -} export type ModelKey = { providerID: string; modelID: string } -export type FileContext = { type: "file"; path: string; selection?: TextSelection } -export type ContextItem = FileContext - export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", init: () => { const sdk = useSDK() const sync = useSync() const providers = useProviders() - const language = useLanguage() + const connected = createMemo(() => new Set(providers.connected().map((provider) => provider.id))) function isModelValid(model: ModelKey) { const provider = providers.all().find((x) => x.id === model.providerID) - return ( - !!provider?.models[model.modelID] && - providers - .connected() - .map((p) => p.id) - .includes(model.providerID) - ) + return !!provider?.models[model.modelID] && connected().has(model.providerID) } function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) { @@ -64,8 +31,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } } + let setModel: (model: ModelKey | undefined, options?: { recent?: boolean }) => void = () => undefined + const agent = (() => { const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) + const models = useModels() + const [store, setStore] = createStore<{ current?: string }>({ @@ -84,11 +55,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ setStore("current", undefined) return } - if (name && available.some((x) => x.name === name)) { - setStore("current", name) - return - } - setStore("current", available[0].name) + const match = name ? available.find((x) => x.name === name) : undefined + const value = match ?? available[0] + if (!value) return + setStore("current", value.name) + if (!value.model) return + setModel({ + providerID: value.model.providerID, + modelID: value.model.modelID, + }) + if (value.variant) + models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant) }, move(direction: 1 | -1) { const available = list() @@ -102,113 +79,57 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const value = available[next] if (!value) return setStore("current", value.name) - if (value.model) - model.set({ - providerID: value.model.providerID, - modelID: value.model.modelID, - }) + if (!value.model) return + setModel({ + providerID: value.model.providerID, + modelID: value.model.modelID, + }) + if (value.variant) + models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant) }, } })() const model = (() => { - const [store, setStore, _, modelReady] = persisted( - Persist.global("model", ["model.v1"]), - createStore<{ - user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[] - recent: ModelKey[] - variant?: Record - }>({ - user: [], - recent: [], - variant: {}, - }), - ) + const models = useModels() const [ephemeral, setEphemeral] = createStore<{ - model: Record + model: Record }>({ model: {}, }) - const available = createMemo(() => - providers.connected().flatMap((p) => - Object.values(p.models).map((m) => ({ - ...m, - provider: p, - })), - ), - ) - - const latest = createMemo(() => - pipe( - available(), - filter((x) => Math.abs(DateTime.fromISO(x.release_date).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) => `${x.providerID}:${x.modelID}`))) - - const userVisibilityMap = 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) + const resolveConfigured = () => { + if (!sync.data.config.model) return + const [providerID, modelID] = sync.data.config.model.split("/") + const key = { providerID, modelID } + if (isModelValid(key)) return key + } - const fallbackModel = createMemo(() => { - if (sync.data.config.model) { - const [providerID, modelID] = sync.data.config.model.split("/") - if (isModelValid({ providerID, modelID })) { - return { - providerID, - modelID, - } - } + const resolveRecent = () => { + for (const item of models.recent.list()) { + if (isModelValid(item)) return item } + } - for (const item of store.recent) { - if (isModelValid(item)) { - return item + const resolveDefault = () => { + const defaults = providers.default() + for (const provider of providers.connected()) { + const configured = defaults[provider.id] + if (configured) { + const key = { providerID: provider.id, modelID: configured } + if (isModelValid(key)) return key } - } - for (const p of providers.connected()) { - if (p.id in providers.default()) { - return { - providerID: p.id, - modelID: providers.default()[p.id], - } - } + const first = Object.values(provider.models)[0] + if (!first) continue + const key = { providerID: provider.id, modelID: first.id } + if (isModelValid(key)) return key } + } - throw new Error("No default model found") + const fallbackModel = createMemo(() => { + return resolveConfigured() ?? resolveRecent() ?? resolveDefault() }) const current = createMemo(() => { @@ -220,10 +141,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ fallbackModel, ) if (!key) return undefined - return find(key) + return models.find(key) }) - const recent = createMemo(() => store.recent.map(find).filter(Boolean)) + const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean)) const cycle = (direction: 1 | -1) => { const recentList = recent() @@ -248,53 +169,52 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) } - function updateVisibility(model: ModelKey, visibility: "show" | "hide") { - const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID) - if (index >= 0) { - setStore("user", index, { visibility }) - } else { - setStore("user", store.user.length, { ...model, visibility }) - } + const set = (model: ModelKey | undefined, options?: { recent?: boolean }) => { + batch(() => { + const currentAgent = agent.current() + const next = model ?? fallbackModel() + if (currentAgent) setEphemeral("model", currentAgent.name, next) + if (model) models.setVisibility(model, true) + if (options?.recent && model) models.recent.push(model) + }) } + setModel = set + return { - ready: modelReady, + ready: models.ready, current, recent, - list, + list: models.list, cycle, - set(model: ModelKey | undefined, options?: { recent?: boolean }) { - batch(() => { - const currentAgent = agent.current() - if (currentAgent) setEphemeral("model", currentAgent.name, model ?? fallbackModel()) - if (model) updateVisibility(model, "show") - if (options?.recent && model) { - const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID) - if (uniq.length > 5) uniq.pop() - setStore("recent", uniq) - } - }) - }, + set, visible(model: ModelKey) { - const key = `${model.providerID}:${model.modelID}` - const visibility = userVisibilityMap().get(key) - if (visibility === "hide") return false - if (visibility === "show") return true - if (latestSet().has(key)) return true - // For models without valid release_date (e.g. custom models), show by default - const m = find(model) - if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true - return false + return models.visible(model) }, setVisibility(model: ModelKey, visible: boolean) { - updateVisibility(model, visible ? "show" : "hide") + models.setVisibility(model, visible) }, variant: { - current() { + configured() { + const a = agent.current() + const m = current() + if (!a || !m) return undefined + return getConfiguredAgentVariant({ + agent: { model: a.model, variant: a.variant }, + model: { providerID: m.provider.id, modelID: m.id, variants: m.variants }, + }) + }, + selected() { const m = current() if (!m) return undefined - const key = `${m.provider.id}/${m.id}` - return store.variant?.[key] + return models.variant.get({ providerID: m.provider.id, modelID: m.id }) + }, + current() { + return resolveModelVariant({ + variants: this.list(), + selected: this.selected(), + configured: this.configured(), + }) }, list() { const m = current() @@ -305,252 +225,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ set(value: string | undefined) { const m = current() if (!m) return - const key = `${m.provider.id}/${m.id}` - if (!store.variant) { - setStore("variant", { [key]: value }) - } else { - setStore("variant", key, value) - } + models.variant.set({ providerID: m.provider.id, modelID: m.id }, value) }, cycle() { const variants = this.list() if (variants.length === 0) return - const currentVariant = this.current() - if (!currentVariant) { - this.set(variants[0]) - return - } - const index = variants.indexOf(currentVariant) - if (index === -1 || index === variants.length - 1) { - this.set(undefined) - return - } - this.set(variants[index + 1]) - }, - }, - } - })() - - const file = (() => { - const [store, setStore] = createStore<{ - node: Record - }>({ - node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])), - }) - - // const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path))) - // const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b))) - - // createEffect((prev: FileStatus[]) => { - // const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path)) - // for (const p of removed) { - // setStore( - // "node", - // p.path, - // produce((draft) => { - // draft.status = undefined - // draft.view = "raw" - // }), - // ) - // load(p.path) - // } - // for (const p of sync.data.changes) { - // if (store.node[p.path] === undefined) { - // fetch(p.path).then(() => { - // if (store.node[p.path] === undefined) return - // setStore("node", p.path, "status", p) - // }) - // } else { - // setStore("node", p.path, "status", p) - // } - // } - // return sync.data.changes - // }, sync.data.changes) - - // const changed = (path: string) => { - // const node = store.node[path] - // if (node?.status) return true - // const set = changeset() - // if (set.has(path)) return true - // for (const p of set) { - // if (p.startsWith(path ? path + "/" : "")) return true - // } - // return false - // } - - // const resetNode = (path: string) => { - // setStore("node", path, { - // loaded: undefined, - // pinned: undefined, - // content: undefined, - // selection: undefined, - // scrollTop: undefined, - // folded: undefined, - // view: undefined, - // selectedChange: undefined, - // }) - // } - - const relative = (path: string) => path.replace(sync.data.path.directory + "/", "") - - const load = async (path: string) => { - const relativePath = relative(path) - await sdk.client.file - .read({ path: relativePath }) - .then((x) => { - if (!store.node[relativePath]) return - setStore( - "node", - relativePath, - produce((draft) => { - draft.loaded = true - draft.content = x.data - }), - ) - }) - .catch((e) => { - showToast({ - variant: "error", - title: language.t("toast.file.loadFailed.title"), - description: e.message, - }) - }) - } - - const fetch = async (path: string) => { - const relativePath = relative(path) - const parent = relativePath.split("/").slice(0, -1).join("/") - if (parent) { - await list(parent) - } - } - - const init = async (path: string) => { - const relativePath = relative(path) - if (!store.node[relativePath]) await fetch(path) - if (store.node[relativePath]?.loaded) return - return load(relativePath) - } - - const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => { - const relativePath = relative(path) - if (!store.node[relativePath]) await fetch(path) - // setStore("opened", (x) => { - // if (x.includes(relativePath)) return x - // return [ - // ...opened() - // .filter((x) => x.pinned) - // .map((x) => x.path), - // relativePath, - // ] - // }) - // setStore("active", relativePath) - // context.addActive() - if (options?.pinned) setStore("node", path, "pinned", true) - if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view) - if (store.node[relativePath]?.loaded) return - return load(relativePath) - } - - const list = async (path: string) => { - return sdk.client.file - .list({ path: path + "/" }) - .then((x) => { - setStore( - "node", - produce((draft) => { - x.data!.forEach((node) => { - if (node.path in draft) return - draft[node.path] = node - }) + this.set( + cycleModelVariant({ + variants, + selected: this.selected(), + configured: this.configured(), }), ) - }) - .catch(() => {}) - } - - const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!) - const searchFilesAndDirectories = (query: string) => - sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!) - - const unsub = sdk.event.listen((e) => { - const event = e.details - switch (event.type) { - case "file.watcher.updated": - const relativePath = relative(event.properties.file) - if (relativePath.startsWith(".git/")) return - if (store.node[relativePath]) load(relativePath) - break - } - }) - onCleanup(unsub) - - return { - node: async (path: string) => { - if (!store.node[path] || !store.node[path].loaded) { - await init(path) - } - return store.node[path] - }, - update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)), - open, - load, - init, - expand(path: string) { - setStore("node", path, "expanded", true) - if (store.node[path]?.loaded) return - setStore("node", path, "loaded", true) - list(path) - }, - collapse(path: string) { - setStore("node", path, "expanded", false) - }, - select(path: string, selection: TextSelection | undefined) { - setStore("node", path, "selection", selection) - }, - scroll(path: string, scrollTop: number) { - setStore("node", path, "scrollTop", scrollTop) - }, - view(path: string): View { - const n = store.node[path] - return n && n.view ? n.view : "raw" - }, - setView(path: string, view: View) { - setStore("node", path, "view", view) - }, - unfold(path: string, key: string) { - setStore("node", path, "folded", (xs) => { - const a = xs ?? [] - if (a.includes(key)) return a - return [...a, key] - }) - }, - fold(path: string, key: string) { - setStore("node", path, "folded", (xs) => (xs ?? []).filter((k) => k !== key)) - }, - folded(path: string) { - const n = store.node[path] - return n && n.folded ? n.folded : [] - }, - changeIndex(path: string) { - return store.node[path]?.selectedChange - }, - setChangeIndex(path: string, index: number | undefined) { - setStore("node", path, "selectedChange", index) - }, - // changes, - // changed, - children(path: string) { - return Object.values(store.node).filter( - (x) => - x.path.startsWith(path) && - x.path !== path && - !x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"), - ) + }, }, - searchFiles, - searchFilesAndDirectories, - relative, } })() @@ -558,7 +246,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ slug: createMemo(() => base64Encode(sdk.directory)), model, agent, - file, } return result }, diff --git a/packages/app/src/context/model-variant.test.ts b/packages/app/src/context/model-variant.test.ts new file mode 100644 index 00000000000..01b149fd267 --- /dev/null +++ b/packages/app/src/context/model-variant.test.ts @@ -0,0 +1,66 @@ +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("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") + }) +}) diff --git a/packages/app/src/context/model-variant.ts b/packages/app/src/context/model-variant.ts new file mode 100644 index 00000000000..6b7ae725640 --- /dev/null +++ b/packages/app/src/context/model-variant.ts @@ -0,0 +1,50 @@ +type AgentModel = { + providerID: string + modelID: string +} + +type Agent = { + model?: AgentModel + variant?: string +} + +type Model = AgentModel & { + variants?: Record +} + +type VariantInput = { + variants: string[] + selected: string | 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 && 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 && 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 new file mode 100644 index 00000000000..12ec8371add --- /dev/null +++ b/packages/app/src/context/models.tsx @@ -0,0 +1,163 @@ +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 index 58e7fbf83aa..04bc2fdaaaf 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -1,5 +1,5 @@ -import { createStore } from "solid-js/store" -import { createEffect, onCleanup } from "solid-js" +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" @@ -8,7 +8,8 @@ import { usePlatform } from "@/context/platform" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { Binary } from "@opencode-ai/util/binary" -import { base64Decode, base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/util/encode" +import { decode64 } from "@/utils/base64" import { EventSessionError } from "@opencode-ai/sdk/v2" import { Persist, persisted } from "@/utils/persist" import { playSound, soundSrc } from "@/utils/sound" @@ -16,7 +17,7 @@ import { playSound, soundSrc } from "@/utils/sound" type NotificationBase = { directory?: string session?: string - metadata?: any + metadata?: unknown time: number viewed: boolean } @@ -32,6 +33,21 @@ type ErrorNotification = NotificationBase & { 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 @@ -42,6 +58,53 @@ function pruneNotifications(list: Notification[]) { 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: () => { @@ -52,120 +115,257 @@ export const { use: useNotification, provider: NotificationProvider } = createSi 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 } + 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 - setStore("list", pruneNotifications(store.list)) + const list = pruneNotifications(store.list) + batch(() => { + setStore("list", list) + setIndex(reconcile(buildNotificationIndex(list), { merge: false })) + }) }) const append = (notification: Notification) => { - setStore("list", (list) => pruneNotifications([...list, 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 unsub = globalSDK.event.listen((e) => { - const directory = e.name - const event = e.details - const time = Date.now() - const activeDirectory = params.dir ? base64Decode(params.dir) : undefined - const activeSession = params.id - const viewed = (sessionID?: string) => { - if (!activeDirectory) return false - if (!activeSession) return false - if (!sessionID) return false - if (directory !== activeDirectory) return false - return sessionID === activeSession - } - switch (event.type) { - case "session.idle": { - const sessionID = event.properties.sessionID - const [syncStore] = globalSync.child(directory) - const match = Binary.search(syncStore.session, sessionID, (s) => s.id) - const session = match.found ? syncStore.session[match.index] : undefined - if (session?.parentID) break + 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()) { playSound(soundSrc(settings.sounds.agent())) + } - append({ - directory, - time, - viewed: viewed(sessionID), - type: "turn-complete", - session: sessionID, - }) + 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, - ) - } - break + const href = `/${base64Encode(directory)}/session/${sessionID}` + if (settings.notifications.agent()) { + void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href) } - case "session.error": { - const sessionID = event.properties.sessionID - const [syncStore] = globalSync.child(directory) - const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined - const session = sessionID && match?.found ? syncStore.session[match.index] : undefined - if (session?.parentID) break + }) + } + + 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()) { playSound(soundSrc(settings.sounds.errors())) + } - const error = "error" in event.properties ? event.properties.error : undefined - append({ - directory, - time, - viewed: viewed(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) - } - break + 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() }) - onCleanup(unsub) return { ready, session: { all(session: string) { - return store.list.filter((n) => n.session === session) + return index.session.all[session] ?? empty }, unseen(session: string) { - return store.list.filter((n) => n.session === session && !n.viewed) + 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) { - setStore("list", (n) => n.session === session, "viewed", true) + 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 store.list.filter((n) => n.directory === directory) + return index.project.all[directory] ?? empty }, unseen(directory: string) { - return store.list.filter((n) => n.directory === directory && !n.viewed) + 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) { - setStore("list", (n) => n.directory === directory, "viewed", true) + 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 new file mode 100644 index 00000000000..75561130055 --- /dev/null +++ b/packages/app/src/context/permission-auto-respond.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, test } from "bun:test" +import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client" +import { base64Encode } from "@opencode-ai/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 new file mode 100644 index 00000000000..b206deedff9 --- /dev/null +++ b/packages/app/src/context/permission-auto-respond.ts @@ -0,0 +1,51 @@ +import { base64Encode } from "@opencode-ai/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 index 52878ba8f80..672f84f82a6 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -1,4 +1,4 @@ -import { createMemo, onCleanup } from "solid-js" +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 "@opencode-ai/sdk/v2/client" @@ -6,7 +6,13 @@ import { Persist, persisted } from "@/utils/persist" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "./global-sync" import { useParams } from "@solidjs/router" -import { base64Decode, base64Encode } from "@opencode-ai/util/encode" +import { decode64 } from "@/utils/base64" +import { + acceptKey, + directoryAcceptKey, + isDirectoryAutoAccepting, + autoRespondsPermission, +} from "./permission-auto-respond" type PermissionRespondFn = (input: { sessionID: string @@ -15,10 +21,6 @@ type PermissionRespondFn = (input: { directory?: string }) => void -function shouldAutoAccept(perm: PermissionRequest) { - return perm.permission === "edit" -} - function isNonAllowRule(rule: unknown) { if (!rule) return false if (typeof rule === "string") return rule !== "allow" @@ -32,17 +34,14 @@ function isNonAllowRule(rule: unknown) { return false } -function hasAutoAcceptPermissionConfig(permission: unknown) { +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 - if (isNonAllowRule(config.edit)) return true - if (isNonAllowRule(config.write)) return true - - return false + return Object.values(config).some(isNonAllowRule) } export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({ @@ -53,20 +52,70 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple const globalSync = useGlobalSync() const permissionsEnabled = createMemo(() => { - const directory = params.dir ? base64Decode(params.dir) : undefined + const directory = decode64(params.dir) if (!directory) return false const [store] = globalSync.child(directory) - return hasAutoAcceptPermissionConfig(store.config.permission) + return hasPermissionPromptRules(store.config.permission) }) const [store, setStore, _, ready] = persisted( - Persist.global("permission", ["permission.v3"]), + { + ...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({ - autoAcceptEdits: {} as Record, + autoAccept: {} as Record, }), ) - const responded = new Set() + // 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(() => { @@ -75,8 +124,12 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple } function respondOnce(permission: PermissionRequest, directory?: string) { - if (responded.has(permission.id)) return - responded.add(permission.id) + 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, @@ -85,14 +138,25 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple }) } - function acceptKey(sessionID: string, directory?: string) { - if (!directory) return sessionID - return `${base64Encode(directory)}/${sessionID}` + 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 isAutoAccepting(sessionID: string, directory?: string) { + 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) - return store.autoAcceptEdits[key] ?? store.autoAcceptEdits[sessionID] ?? false + const next = (enableVersion.get(key) ?? 0) + 1 + enableVersion.set(key, next) + return next } const unsubscribe = globalSDK.event.listen((e) => { @@ -100,29 +164,60 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple if (event?.type !== "permission.asked") return const perm = event.properties - if (!isAutoAccepting(perm.sessionID, e.name)) return - if (!shouldAutoAccept(perm)) return + 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.autoAcceptEdits[key] = true - delete draft.autoAcceptEdits[sessionID] + 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 (perm.sessionID !== sessionID) continue - if (!shouldAutoAccept(perm)) continue + if (!shouldAutoRespond(perm, directory)) continue respondOnce(perm, directory) } }) @@ -130,11 +225,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple } function disable(sessionID: string, directory?: string) { - const key = directory ? acceptKey(sessionID, directory) : undefined + bumpEnableVersion(sessionID, directory) + const key = directory ? acceptKey(sessionID, directory) : sessionID setStore( produce((draft) => { - if (key) delete draft.autoAcceptEdits[key] - delete draft.autoAcceptEdits[sessionID] + draft.autoAccept[key] = false + if (!directory) return + delete draft.autoAccept[sessionID] }), ) } @@ -143,9 +240,10 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple ready, respond, autoResponds(permission: PermissionRequest, directory?: string) { - return isAutoAccepting(permission.sessionID, directory) && shouldAutoAccept(permission) + return shouldAutoRespond(permission, directory) }, isAutoAccepting, + isAutoAcceptingDirectory, toggleAutoAccept(sessionID: string, directory: string) { if (isAutoAccepting(sessionID, directory)) { disable(sessionID, directory) @@ -154,6 +252,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple 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) @@ -162,6 +267,11 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple 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 index 89056b2c845..b8ed58e343a 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -1,5 +1,13 @@ import { createSimpleContext } from "@opencode-ai/ui/context" -import { AsyncStorage, SyncStorage } from "@solid-primitives/storage" +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 } +type SaveFilePickerOptions = { title?: string; defaultPath?: string } +type UpdateInfo = { updateAvailable: boolean; version?: string } export type Platform = { /** Platform discriminator */ @@ -14,26 +22,35 @@ export type Platform = { /** 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?: { title?: string; multiple?: boolean }): Promise + openDirectoryPickerDialog?(opts?: OpenDirectoryPickerOptions): Promise /** Open native file picker dialog (Tauri only) */ - openFilePickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise + openFilePickerDialog?(opts?: OpenFilePickerOptions): Promise /** Save file picker dialog (Tauri only) */ - saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise + saveFilePickerDialog?(opts?: SaveFilePickerOptions): Promise /** Storage mechanism, defaults to localStorage */ storage?: (name?: string) => SyncStorage | AsyncStorage /** Check for updates (Tauri only) */ - checkUpdate?(): Promise<{ updateAvailable: boolean; version?: string }> + checkUpdate?(): Promise /** Install updates (Tauri only) */ update?(): Promise @@ -41,16 +58,39 @@ export type Platform = { /** Fetch override */ fetch?: typeof fetch - /** Get the configured default server URL (desktop only) */ - getDefaultServerUrl?(): Promise + /** 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 default server URL to use on app startup (desktop only) */ - setDefaultServerUrl?(url: string | null): 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 }) => { diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index 6b9d5cc0d15..fb822655911 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -1,4 +1,4 @@ -import { createStore } from "solid-js/store" +import { createStore, type SetStoreFunction } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { batch, createMemo, createRoot, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" @@ -44,6 +44,7 @@ export type FileContextItem = { selection?: FileSelection comment?: string commentID?: string + commentOrigin?: "review" | "file" preview?: string } @@ -59,27 +60,23 @@ function isSelectionEqual(a?: FileSelection, b?: FileSelection) { ) } +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++) { - const partA = promptA[i] - const partB = promptB[i] - if (partA.type !== partB.type) return false - if (partA.type === "text" && partA.content !== (partB as TextPart).content) { - return false - } - if (partA.type === "file") { - const fileA = partA as FileAttachmentPart - const fileB = partB as FileAttachmentPart - if (fileA.path !== fileB.path) return false - if (!isSelectionEqual(fileA.selection, fileB.selection)) return false - } - if (partA.type === "agent" && partA.name !== (partB as AgentPart).name) { - return false - } - if (partA.type === "image" && partA.id !== (partB as ImageAttachmentPart).id) { - return false - } + if (!isPartEqual(promptA[i], promptB[i])) return false } return true } @@ -103,6 +100,52 @@ 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 @@ -133,21 +176,7 @@ function createPromptSession(dir: string, id: string | undefined) { }), ) - function keyForItem(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)}` - } + const actions = createPromptActions(setStore) return { ready, @@ -157,27 +186,36 @@ function createPromptSession(dir: string, id: string | undefined) { context: { items: createMemo(() => store.context.items), add(item: ContextItem) { - const key = keyForItem(item) + 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(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) - }) - }, + set: actions.set, + reset: actions.reset, } } @@ -237,6 +275,10 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext( 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) => session().set(prompt, cursorPosition), reset: () => session().reset(), diff --git a/packages/app/src/context/sdk.tsx b/packages/app/src/context/sdk.tsx index aa4820c4945..bc97ea13acc 100644 --- a/packages/app/src/context/sdk.tsx +++ b/packages/app/src/context/sdk.tsx @@ -1,31 +1,49 @@ -import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" +import type { Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { onCleanup } from "solid-js" +import { type Accessor, createEffect, createMemo, onCleanup } from "solid-js" import { useGlobalSDK } from "./global-sdk" -import { usePlatform } from "./platform" + +type SDKEventMap = { + [key in Event["type"]]: Extract +} export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", - init: (props: { directory: string }) => { - const platform = usePlatform() + init: (props: { directory: Accessor }) => { const globalSDK = useGlobalSDK() - const sdk = createOpencodeClient({ - baseUrl: globalSDK.url, - fetch: platform.fetch, - directory: props.directory, - throwOnError: true, - }) - const emitter = createGlobalEmitter<{ - [key in Event["type"]]: Extract - }>() + const directory = createMemo(props.directory) + const client = createMemo(() => + globalSDK.createClient({ + directory: directory(), + throwOnError: true, + }), + ) + + const emitter = createGlobalEmitter() - const unsub = globalSDK.event.on(props.directory, (event) => { - emitter.emit(event.type, event) + createEffect(() => { + const unsub = globalSDK.event.on(directory(), (event) => { + emitter.emit(event.type, event) + }) + onCleanup(unsub) }) - onCleanup(unsub) - return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url } + 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 index 1076570928f..1171ca90536 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -1,11 +1,12 @@ -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" -import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js" import { createStore } from "solid-js/store" -import { usePlatform } from "@/context/platform" 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() @@ -14,111 +15,140 @@ export function normalizeServerUrl(input: string) { return withProtocol.replace(/\/+$/, "") } -export function serverDisplayName(url: string) { - if (!url) return "" - return url.replace(/^https?:\/\//, "").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(url: string) { - if (!url) return "" +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" - return url +} + +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: { defaultUrl: string }) => { - const platform = usePlatform() + init: (props: { defaultServer: ServerConnection.Key; servers?: Array }) => { + const checkServerHealth = useCheckServerHealth() const [store, setStore, _, ready] = persisted( Persist.global("server", ["server.v3"]), createStore({ - list: [] as string[], + list: [] as StoredServer[], projects: {} as Record, lastProject: {} as Record, }), ) - const [active, setActiveRaw] = createSignal("") - - function setActive(input: string) { - const url = normalizeServerUrl(input) - if (!url) return - setActiveRaw(url) - } - - function add(input: string) { - const url = normalizeServerUrl(input) - if (!url) return - - const fallback = normalizeServerUrl(props.defaultUrl) - if (fallback && url === fallback) { - setActiveRaw(url) - return - } + const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url) - batch(() => { - if (!store.list.includes(url)) { - setStore("list", store.list.length, url) - } - setActiveRaw(url) - }) - } + const allServers = createMemo((): Array => { + const servers = [ + ...(props.servers ?? []), + ...store.list.map((value) => + typeof value === "string" + ? { + type: "http" as const, + http: { url: value }, + } + : value, + ), + ] - function remove(input: string) { - const url = normalizeServerUrl(input) - if (!url) return + const deduped = new Map( + servers.map((value) => { + const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value } + return [ServerConnection.key(conn), conn] + }), + ) - const list = store.list.filter((x) => x !== url) - const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active() - - batch(() => { - setStore("list", list) - setActiveRaw(next) - }) - } - - createEffect(() => { - if (!ready()) return - if (active()) return - const url = normalizeServerUrl(props.defaultUrl) - if (!url) return - setActiveRaw(url) + return [...deduped.values()] }) - const isReady = createMemo(() => ready() && !!active()) - - const [healthy, setHealthy] = createSignal(undefined) - - const check = (url: string) => { - const sdk = createOpencodeClient({ - baseUrl: url, - fetch: platform.fetch, - signal: AbortSignal.timeout(3000), - }) - return sdk.global - .health() - .then((x) => x.data?.healthy === true) - .catch(() => false) - } - - createEffect(() => { - const url = active() - if (!url) return + const [state, setState] = createStore({ + active: props.defaultServer, + healthy: undefined as boolean | undefined, + }) - setHealthy(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(url) + void check(conn) .then((next) => { if (!alive) return - setHealthy(next) + setState("healthy", next) }) .finally(() => { busy = false @@ -126,30 +156,81 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( } run() - const interval = setInterval(run, 10_000) - - onCleanup(() => { + 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 + + setState("healthy", undefined) + onCleanup(startHealthPolling(current_)) }) - const origin = createMemo(() => projectsKey(active())) + const origin = createMemo(() => projectsKey(state.active)) const projectsList = createMemo(() => store.projects[origin()] ?? []) - const isLocal = createMemo(() => origin() === "local") + 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 url() { - return active() + get key() { + return state.active }, get name() { - return serverDisplayName(active()) + return serverName(current()) }, get list() { - return store.list + return allServers() + }, + get current() { + return current() }, setActive, add, diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 9211eacd2a8..b43469b5c37 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -10,14 +10,24 @@ export interface NotificationSettings { } export interface SoundSettings { + agentEnabled: boolean agent: string + permissionsEnabled: boolean permissions: string + errorsEnabled: boolean errors: string } export interface Settings { general: { autoSave: boolean + releaseNotes: boolean + showReasoningSummaries: boolean + shellToolPartsExpanded: boolean + editToolPartsExpanded: boolean + } + updates: { + startup: boolean } appearance: { fontSize: number @@ -34,6 +44,13 @@ export interface Settings { const defaultSettings: Settings = { general: { autoSave: true, + releaseNotes: true, + showReasoningSummaries: false, + shellToolPartsExpanded: true, + editToolPartsExpanded: false, + }, + updates: { + startup: true, }, appearance: { fontSize: 14, @@ -49,8 +66,11 @@ const defaultSettings: Settings = { errors: false, }, sounds: { + agentEnabled: true, agent: "staplebops-01", + permissionsEnabled: true, permissions: "staplebops-02", + errorsEnabled: true, errors: "nope-03", }, } @@ -65,17 +85,23 @@ const monoFonts: Record = { hack: `"Hack Nerd Font", "Hack Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, inconsolata: `"Inconsolata Nerd Font", "Inconsolata Nerd Font Mono","IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "intel-one-mono": `"Intel One Mono Nerd Font", "IntoneMono Nerd Font", "IntoneMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + iosevka: `"Iosevka Nerd Font", "Iosevka Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "jetbrains-mono": `"JetBrains Mono Nerd Font", "JetBrainsMono Nerd Font Mono", "JetBrainsMonoNL Nerd Font", "JetBrainsMonoNL Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "meslo-lgs": `"Meslo LGS Nerd Font", "MesloLGS Nerd Font", "MesloLGM Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "roboto-mono": `"Roboto Mono Nerd Font", "RobotoMono Nerd Font", "RobotoMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "source-code-pro": `"Source Code Pro Nerd Font", "SauceCodePro Nerd Font", "SauceCodePro Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, "ubuntu-mono": `"Ubuntu Mono Nerd Font", "UbuntuMono Nerd Font", "UbuntuMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "geist-mono": `"GeistMono Nerd Font", "GeistMono Nerd Font Mono", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, } export function monoFontFamily(font: string | undefined) { return monoFonts[font ?? defaultSettings.appearance.font] ?? monoFonts[defaultSettings.appearance.font] } +function withFallback(read: () => T | undefined, fallback: T) { + return createMemo(() => read() ?? fallback) +} + export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({ name: "Settings", init: () => { @@ -92,17 +118,48 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont return store }, general: { - autoSave: createMemo(() => store.general?.autoSave ?? defaultSettings.general.autoSave), + 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) + }, + 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) + }, + }, + updates: { + startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup), + setStartup(value: boolean) { + setStore("updates", "startup", value) + }, }, appearance: { - fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize), + fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize), setFontSize(value: number) { setStore("appearance", "fontSize", value) }, - font: createMemo(() => store.appearance?.font ?? defaultSettings.appearance.font), + font: withFallback(() => store.appearance?.font, defaultSettings.appearance.font), setFont(value: string) { setStore("appearance", "font", value) }, @@ -113,42 +170,62 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setStore("keybinds", action, keybind) }, reset(action: string) { - setStore("keybinds", action, undefined!) + 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: createMemo(() => store.permissions?.autoApprove ?? defaultSettings.permissions.autoApprove), + autoApprove: withFallback(() => store.permissions?.autoApprove, defaultSettings.permissions.autoApprove), setAutoApprove(value: boolean) { setStore("permissions", "autoApprove", value) }, }, notifications: { - agent: createMemo(() => store.notifications?.agent ?? defaultSettings.notifications.agent), + agent: withFallback(() => store.notifications?.agent, defaultSettings.notifications.agent), setAgent(value: boolean) { setStore("notifications", "agent", value) }, - permissions: createMemo(() => store.notifications?.permissions ?? defaultSettings.notifications.permissions), + permissions: withFallback(() => store.notifications?.permissions, defaultSettings.notifications.permissions), setPermissions(value: boolean) { setStore("notifications", "permissions", value) }, - errors: createMemo(() => store.notifications?.errors ?? defaultSettings.notifications.errors), + errors: withFallback(() => store.notifications?.errors, defaultSettings.notifications.errors), setErrors(value: boolean) { setStore("notifications", "errors", value) }, }, sounds: { - agent: createMemo(() => store.sounds?.agent ?? defaultSettings.sounds.agent), + 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) }, - permissions: createMemo(() => store.sounds?.permissions ?? defaultSettings.sounds.permissions), + 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) }, - errors: createMemo(() => store.sounds?.errors ?? defaultSettings.sounds.errors), + 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 new file mode 100644 index 00000000000..7deeddd6ee6 --- /dev/null +++ b/packages/app/src/context/sync-optimistic.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "bun:test" +import type { Message, Part } from "@opencode-ai/sdk/v2/client" +import { applyOptimisticAdd, applyOptimisticRemove } from "./sync" + +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): Part => ({ + 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) + }) +}) diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index 33129e1b475..db7b0638829 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -3,21 +3,120 @@ import { createStore, produce, reconcile } from "solid-js/store" import { Binary } from "@opencode-ai/util/binary" import { retry } from "@opencode-ai/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 "@opencode-ai/sdk/v2/client" +import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache" + +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) + +type OptimisticStore = { + message: Record + part: Record +} + +type OptimisticAddInput = { + sessionID: string + message: Message + parts: Part[] +} + +type OptimisticRemoveInput = { + sessionID: string + messageID: string +} + +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() - const [store, setStore] = globalSync.child(sdk.directory) - const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/") - const chunk = 400 + + 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 messagePageSize = 200 const inflight = new Map>() const inflightDiff = new Map>() const inflightTodo = new Map>() + const maxDirs = 30 + const seen = new Map>() const [meta, setMeta] = createStore({ limit: {} as Record, complete: {} as Record, @@ -25,88 +124,158 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }) 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 limitFor = (count: number) => { - if (count <= chunk) return chunk - return Math.ceil(count / chunk) * chunk + 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 hydrateMessages = (sessionID: string) => { - if (meta.limit[sessionID] !== undefined) return + const clearMeta = (directory: string, sessionIDs: string[]) => { + if (sessionIDs.length === 0) return + setMeta( + produce((draft) => { + for (const sessionID of sessionIDs) { + const key = keyFor(directory, sessionID) + delete draft.limit[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 messages = store.message[sessionID] - if (!messages) return + 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 limit = limitFor(messages.length) - setMeta("limit", sessionID, limit) - setMeta("complete", sessionID, messages.length < limit) + const fetchMessages = async (input: { client: typeof sdk.client; sessionID: string; limit: number }) => { + const messages = await retry(() => + input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }), + ) + const items = (messages.data ?? []).filter((x) => !!x?.info?.id) + const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id)) + const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) })) + return { + session, + part, + complete: session.length < input.limit, + } } - const loadMessages = async (sessionID: string, limit: number) => { - if (meta.loading[sessionID]) return + const tracked = (directory: string, sessionID: string) => seen.get(directory)?.has(sessionID) ?? false - setMeta("loading", sessionID, true) - await retry(() => sdk.client.session.messages({ sessionID, limit })) - .then((messages) => { - const items = (messages.data ?? []).filter((x) => !!x?.info?.id) - const next = items - .map((x) => x.info) - .filter((m) => !!m?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) + const loadMessages = async (input: { + directory: string + client: typeof sdk.client + setStore: Setter + sessionID: string + limit: number + }) => { + const key = keyFor(input.directory, input.sessionID) + if (meta.loading[key]) return + setMeta("loading", key, true) + await fetchMessages(input) + .then((next) => { + if (!tracked(input.directory, input.sessionID)) return batch(() => { - setStore("message", sessionID, reconcile(next, { key: "id" })) - - for (const message of items) { - setStore( - "part", - message.info.id, - reconcile( - message.parts - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), - { key: "id" }, - ), - ) + input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" })) + for (const p of next.part) { + input.setStore("part", p.id, p.part) } - - setMeta("limit", sessionID, limit) - setMeta("complete", sessionID, next.length < limit) + setMeta("limit", key, input.limit) + setMeta("complete", key, next.complete) + setSessionPrefetch({ + directory: input.directory, + sessionID: input.sessionID, + limit: input.limit, + complete: next.complete, + }) }) }) .finally(() => { - setMeta("loading", sessionID, false) + if (!tracked(input.directory, input.sessionID)) return + setMeta("loading", key, false) }) } return { - data: store, - set: setStore, + get data() { + return current()[0] + }, + get set(): Setter { + return current()[1] + }, get status() { - return store.status + return current()[0].status }, get ready() { - return store.status !== "loading" + 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 [, setStore] = target(input.directory) + setOptimisticAdd(setStore as (...args: unknown[]) => void, input) + }, + remove(input: { directory?: string; sessionID: string; messageID: string }) { + const [, setStore] = target(input.directory) + 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, @@ -115,130 +284,185 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ time: { created: Date.now() }, agent: input.agent, model: input.model, + variant: input.variant, } - setStore( - produce((draft) => { - const messages = draft.message[input.sessionID] - if (!messages) { - draft.message[input.sessionID] = [message] - } else { - const result = Binary.search(messages, input.messageID, (m) => m.id) - messages.splice(result.index, 0, message) - } - draft.part[input.messageID] = input.parts - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) - }), - ) + const [, setStore] = target() + setOptimisticAdd(setStore as (...args: unknown[]) => void, { + sessionID: input.sessionID, + message, + parts: input.parts, + }) }, - async sync(sessionID: string) { - const hasSession = getSession(sessionID) !== undefined - hydrateMessages(sessionID) - - const hasMessages = store.message[sessionID] !== undefined - if (hasSession && hasMessages) return - - const pending = inflight.get(sessionID) - if (pending) return pending - - const limit = meta.limit[sessionID] ?? chunk - - const sessionReq = hasSession - ? Promise.resolve() - : retry(() => sdk.client.session.get({ sessionID })).then((session) => { - 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 = hasMessages ? Promise.resolve() : loadMessages(sessionID, limit) - - const promise = Promise.all([sessionReq, messagesReq]) - .then(() => {}) - .finally(() => { - inflight.delete(sessionID) + 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("complete", key, seeded.complete) + setMeta("loading", key, false) }) + } - inflight.set(sessionID, promise) - return promise - }, - async diff(sessionID: string) { - if (store.session_diff[sessionID] !== undefined) return + 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("complete", key, seeded.complete) + setMeta("loading", key, false) + }) + } + } - const pending = inflightDiff.get(sessionID) - if (pending) return pending + 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 promise = retry(() => sdk.client.session.diff({ sessionID })) - .then((diff) => { - setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" })) - }) - .finally(() => { - inflightDiff.delete(sessionID) - }) + const limit = meta.limit[key] ?? messagePageSize + 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, + }) - inflightDiff.set(sessionID, promise) - return promise + await Promise.all([sessionReq, messagesReq]) + }) }, - async todo(sessionID: string) { - if (store.todo[sessionID] !== undefined) return + 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 pending = inflightTodo.get(sessionID) - if (pending) return pending + 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(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 + } - const promise = retry(() => sdk.client.session.todo({ sessionID })) - .then((todo) => { - setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" })) - }) - .finally(() => { - inflightTodo.delete(sessionID) - }) + if (cached !== undefined) { + setStore("todo", sessionID, reconcile(cached, { key: "id" })) + } - inflightTodo.set(sessionID, promise) - return promise + 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[sessionID] === undefined) return false - if (meta.complete[sessionID]) return false + if (meta.limit[key] === undefined) return false + if (meta.complete[key]) return false return true }, loading(sessionID: string) { - return meta.loading[sessionID] ?? false + const key = keyFor(sdk.directory, sessionID) + return meta.loading[key] ?? false }, - async loadMore(sessionID: string, count = chunk) { - if (meta.loading[sessionID]) return - if (meta.complete[sessionID]) return + 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 ?? messagePageSize + if (meta.loading[key]) return + if (meta.complete[key]) return - const current = meta.limit[sessionID] ?? chunk - await loadMessages(sessionID, current + count) + const currentLimit = meta.limit[key] ?? messagePageSize + await loadMessages({ + directory, + client, + setStore, + sessionID, + limit: currentLimit + step, + }) }, }, + 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 sdk.client.session.list().then((x) => { + await client.session.list().then((x) => { const sessions = (x.data ?? []) .filter((s) => !!s?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) + .sort((a, b) => cmp(a.id, b.id)) .slice(0, store.limit) setStore("session", reconcile(sessions, { key: "id" })) }) }, - more: createMemo(() => store.session.length >= store.limit), + more: createMemo(() => current()[0].session.length >= current()[0].limit), archive: async (sessionID: string) => { - await sdk.client.session.update({ sessionID, time: { archived: Date.now() } }) + 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) @@ -249,7 +473,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }, absolute, get directory() { - return store.path.directory + return current()[0].path.directory }, } }, diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts new file mode 100644 index 00000000000..6e07e031241 --- /dev/null +++ b/packages/app/src/context/terminal.test.ts @@ -0,0 +1,82 @@ +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 index 147c4f8f7ea..a2807375ff6 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -1,9 +1,10 @@ import { createStore, produce } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" -import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js" +import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { useSDK } from "./sdk" -import { Persist, persisted } from "@/utils/persist" +import type { Platform } from "./platform" +import { Persist, persisted, removePersisted } from "@/utils/persist" export type LocalPTY = { id: string @@ -13,32 +14,133 @@ export type LocalPTY = { cols?: number buffer?: string scrollY?: number - error?: boolean + cursor?: number } const WORKSPACE_KEY = "__workspace__" const MAX_TERMINAL_SESSIONS = 20 -type TerminalSession = ReturnType +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) { + const match = title.match(/^Terminal (\d+)$/) + if (!match) return + const value = Number(match[1]) + if (!Number.isFinite(value) || value <= 0) return + return value +} + +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 } -function createTerminalSession(sdk: ReturnType, dir: string, session?: string) { - const legacy = session ? [`${dir}/terminal/${session}.v1`, `${dir}/terminal.v1`] : [`${dir}/terminal.v1`] +const caches = new Set>() - const numberFromTitle = (title: string) => { - const match = title.match(/^Terminal (\d+)$/) - if (!match) return - const value = Number(match[1]) - if (!Number.isFinite(value) || value <= 0) return - return value +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() } + 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) { + 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), + { + ...Persist.workspace(dir, "terminal", legacy), + migrate: migrateTerminalState, + }, createStore<{ active?: string all: LocalPTY[] @@ -47,65 +149,61 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess }), ) - const unsub = sdk.event.on("pty.exited", (event) => { - const id = event.properties.id - if (!store.all.some((x) => x.id === id)) return + 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", - store.all.filter((x) => x.id !== id), + produce((draft) => { + draft.splice(index, 1) + }), ) - if (store.active === id) { - const remaining = store.all.filter((x) => x.id !== id) - setStore("active", remaining[0]?.id) - } }) - }) - onCleanup(unsub) - - const meta = { migrated: false } - - createEffect(() => { - if (!ready()) return - if (meta.migrated) return - meta.migrated = true + } - setStore("all", (all) => { - const next = all.map((pty) => { - const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined - if (direct !== undefined) return pty - const parsed = numberFromTitle(pty.title) - if (parsed === undefined) return pty - return { ...pty, titleNumber: parsed } - }) - if (next.every((pty, index) => pty === all[index])) return all - return next - }) + const unsub = sdk.event.on("pty.exited", (event: { properties: { id: string } }) => { + removeExited(event.properties.id) }) + onCleanup(unsub) return { ready, - all: createMemo(() => Object.values(store.all)), + all: createMemo(() => store.all), active: createMemo(() => store.active), + clear() { + batch(() => { + setStore("active", undefined) + setStore("all", []) + }) + }, new() { - 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] - }), - ) - - const nextNumber = - Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find( - (number) => !existingTitleNumbers.has(number), - ) ?? 1 + const nextNumber = pickNextTerminalNumber() sdk.client.pty .create({ title: `Terminal ${nextNumber}` }) - .then((pty) => { + .then((pty: { data?: { id?: string; title?: string } }) => { const id = pty.data?.id if (!id) return const newTerminal = { @@ -113,20 +211,18 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess title: pty.data?.title ?? "Terminal", titleNumber: nextNumber, } - setStore("all", (all) => { - const newAll = [...all, newTerminal] - return newAll - }) + setStore("all", store.all.length, newTerminal) setStore("active", id) }) - .catch((e) => { - console.error("Failed to create terminal", e) + .catch((error: unknown) => { + console.error("Failed to create terminal", error) }) }, update(pty: Partial & { id: string }) { const index = store.all.findIndex((x) => x.id === pty.id) - if (index !== -1) { - setStore("all", index, (existing) => ({ ...existing, ...pty })) + const previous = index >= 0 ? store.all[index] : undefined + if (index >= 0) { + setStore("all", index, (item) => ({ ...item, ...pty })) } sdk.client.pty .update({ @@ -134,10 +230,26 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess title: pty.title, size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, }) - .catch((e) => { - console.error("Failed to update terminal", e) + .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) }) }, + 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) { const index = store.all.findIndex((x) => x.id === id) const pty = store.all[index] @@ -146,18 +258,30 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess .create({ title: pty.title, }) - .catch((e) => { - console.error("Failed to clone terminal", e) + .catch((error: unknown) => { + console.error("Failed to clone terminal", error) return undefined }) if (!clone?.data) return - setStore("all", index, { - ...pty, - ...clone.data, + + const active = store.active === pty.id + + batch(() => { + setStore("all", index, { + id: clone.data.id, + title: clone.data.title ?? pty.title, + titleNumber: pty.titleNumber, + // New PTY process, so start clean. + buffer: undefined, + cursor: undefined, + scrollY: undefined, + rows: undefined, + cols: undefined, + }) + if (active) { + setStore("active", clone.data.id) + } }) - if (store.active === pty.id) { - setStore("active", clone.data.id) - } }, open(id: string) { setStore("active", id) @@ -175,18 +299,24 @@ function createTerminalSession(sdk: ReturnType, dir: string, sess setStore("active", store.all[prevIndex]?.id) }, async close(id: string) { - batch(() => { - const filtered = store.all.filter((x) => x.id !== id) - if (store.active === id) { - const index = store.all.findIndex((f) => f.id === id) - const next = index > 0 ? index - 1 : 0 - setStore("active", filtered[next]?.id) - } - setStore("all", filtered) - }) + 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((e) => { - console.error("Failed to close terminal", e) + await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => { + console.error("Failed to close terminal", error) }) }, move(id: string, to: number) { @@ -210,6 +340,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont const params = useParams() const cache = new Map() + caches.add(cache) + onCleanup(() => caches.delete(cache)) + const disposeAll = () => { for (const entry of cache.values()) { entry.dispose() @@ -229,8 +362,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont } } - const load = (dir: string, session?: string) => { - const key = `${dir}:${WORKSPACE_KEY}` + 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) @@ -239,7 +373,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont } const entry = createRoot((dispose) => ({ - value: createTerminalSession(sdk, dir, session), + value: createWorkspaceTerminalSession(sdk, dir, legacySessionID), dispose, })) @@ -248,7 +382,20 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont return entry.value } - const workspace = createMemo(() => load(params.dir!, params.id)) + 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(), @@ -256,6 +403,8 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont 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), open: (id: string) => workspace().open(id), close: (id: string) => workspace().close(id), diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index df8547636b4..c62baccba50 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -1,76 +1,141 @@ // @refresh reload + +import { iife } from "@opencode-ai/util/iife" import { render } from "solid-js/web" import { AppBaseProviders, AppInterface } from "@/app" -import { Platform, PlatformProvider } from "@/context/platform" +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 root = document.getElementById("root") -if (import.meta.env.DEV && !(root instanceof HTMLElement)) { - const locale = (() => { - 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 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 message = locale === "zh" ? (zh[key] ?? en[key]) : en[key] - throw new Error(message) + 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_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_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(url: string) { - window.open(url, "_blank") - }, - restart: async () => { - window.location.reload() - }, - 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 - - await Promise.resolve() - .then(() => { - const notification = new Notification(title, { - body: description ?? "", - icon: "https://opencode.ai/favicon-96x96-v3.png", - }) - notification.onclick = () => { - window.focus() - if (href) { - window.history.pushState(null, "", href) - window.dispatchEvent(new PopStateEvent("popstate")) - } - notification.close() - } - }) - .catch(() => undefined) + openLink, + back, + forward, + restart, + notify, + getDefaultServer: async () => { + const stored = readDefaultServerUrl() + return stored ? ServerConnection.Key.make(stored) : null }, + setDefaultServer: writeDefaultServerUrl, } -render( - () => ( - - - - - - ), - root!, -) +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 index ad575e93b4a..89721f34f29 100644 --- a/packages/app/src/env.d.ts +++ b/packages/app/src/env.d.ts @@ -1,3 +1,5 @@ +import "solid-js" + interface ImportMetaEnv { readonly VITE_OPENCODE_SERVER_HOST: string readonly VITE_OPENCODE_SERVER_PORT: string @@ -6,3 +8,11 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv } + +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 index 4a73fa05588..a25f8b4b252 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -1,31 +1,44 @@ import { useGlobalSync } from "@/context/global-sync" -import { base64Decode } from "@opencode-ai/util/encode" +import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" import { createMemo } from "solid-js" -export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] +export const popularProviders = [ + "opencode", + "opencode-go", + "anthropic", + "github-copilot", + "openai", + "google", + "openrouter", + "vercel", +] +const popularProviderSet = new Set(popularProviders) export function useProviders() { const globalSync = useGlobalSync() const params = useParams() - const currentDirectory = createMemo(() => base64Decode(params.dir ?? "")) - const providers = createMemo(() => { - if (currentDirectory()) { - const [projectStore] = globalSync.child(currentDirectory()) + const dir = createMemo(() => decode64(params.dir) ?? "") + const providers = () => { + if (dir()) { + const [projectStore] = globalSync.child(dir()) return projectStore.provider } return globalSync.data.provider - }) - const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id))) - const paid = createMemo(() => - connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)), - ) - const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id))) + } return { - all: createMemo(() => providers().all), - default: createMemo(() => providers().default), - popular, - connected, - paid, + 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 index 08c69ea62c9..c9b92db501d 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "سمة", "command.category.language": "لغة", "command.category.file": "ملف", + "command.category.context": "سياق", "command.category.terminal": "محطة طرفية", "command.category.model": "نموذج", "command.category.mcp": "MCP", @@ -15,11 +16,9 @@ export const dict = { "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": "اتصال بموفر", @@ -27,22 +26,24 @@ export const dict = { "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.file.open.description": "البحث في الملفات والأوامر", + "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": "إنشاء علامة تبويب جديدة للمحطة الطرفية", @@ -62,8 +63,12 @@ export const dict = { "command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق", "command.model.variant.cycle": "تغيير جهد التفكير", "command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي", - "command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا", - "command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا", + "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": "إعادة", @@ -76,30 +81,32 @@ export const dict = { "command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة", "command.session.unshare": "إلغاء مشاركة الجلسة", "command.session.unshare.description": "إيقاف مشاركة هذه الجلسة", - - "palette.search.placeholder": "البحث في الملفات والأوامر", + "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.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode", "dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين", - - "dialog.provider.viewAll": "عرض جميع الموفرين", - + "dialog.provider.viewAll": "عرض المزيد من الموفرين", "provider.connect.title": "اتصال {{provider}}", "provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max", "provider.connect.selectMethod": "حدد طريقة تسجيل الدخول لـ {{provider}}.", @@ -134,7 +141,44 @@ export const dict = { "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", @@ -151,23 +195,27 @@ export const dict = { "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.submit": "إرسال", "common.save": "حفظ", "common.saving": "جارٍ الحفظ...", "common.default": "افتراضي", "common.attachment": "مرفق", - "prompt.placeholder.shell": "أدخل أمر shell...", "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": "إصلاح الاختبارات المعطلة", @@ -193,11 +241,13 @@ export const dict = { "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": "إزالة الملف النشط من السياق", @@ -206,7 +256,6 @@ export const dict = { "prompt.attachment.remove": "إزالة المرفق", "prompt.action.send": "إرسال", "prompt.action.stop": "توقف", - "prompt.toast.pasteUnsupported.title": "لصق غير مدعوم", "prompt.toast.pasteUnsupported.description": "يمكن لصق الصور أو ملفات PDF فقط هنا.", "prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً", @@ -216,21 +265,19 @@ export const dict = { "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": "تبديل خادم OpenCode الذي يتصل به هذا التطبيق.", "dialog.server.search.placeholder": "البحث في الخوادم", @@ -240,7 +287,7 @@ export const dict = { "dialog.server.add.placeholder": "http://localhost:4096", "dialog.server.add.error": "تعذر الاتصال بالخادم", "dialog.server.add.checking": "جارٍ التحقق...", - "dialog.server.add.button": "إضافة", + "dialog.server.add.button": "إضافة خادم", "dialog.server.default.title": "الخادم الافتراضي", "dialog.server.default.description": "الاتصال بهذا الخادم عند بدء تشغيل التطبيق بدلاً من بدء خادم محلي. يتطلب إعادة التشغيل.", @@ -248,7 +295,12 @@ export const dict = { "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": "أيقونة", @@ -257,7 +309,9 @@ export const dict = { "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": "النظام", @@ -265,10 +319,8 @@ export const dict = { "context.breakdown.assistant": "المساعد", "context.breakdown.tool": "استدعاءات الأداة", "context.breakdown.other": "أخرى", - "context.systemPrompt.title": "موجه النظام", "context.rawMessages.title": "الرسائل الخام", - "context.stats.session": "جلسة", "context.stats.messages": "رسائل", "context.stats.provider": "موفر", @@ -285,62 +337,59 @@ export const dict = { "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": "الإنجليزية", - "language.zh": "الصينية (المبسطة)", - "language.zht": "الصينية (التقليدية)", - "language.ko": "الكورية", - "language.de": "الألمانية", - "language.es": "الإسبانية", - "language.fr": "الفرنسية", - "language.ja": "اليابانية", - "language.da": "الدانماركية", - "language.ru": "الروسية", - "language.pl": "البولندية", + "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": "النرويجية", - "language.br": "البرتغالية (البرازيل)", - + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "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": "الآن يتم عرض عدة 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": "نسخة جديدة من OpenCode ({{version}}) متاحة الآن للتثبيت.", "toast.update.action.installRestart": "تثبيت وإعادة تشغيل", "toast.update.action.notYet": "ليس الآن", - "error.page.title": "حدث خطأ ما", "error.page.description": "حدث خطأ أثناء تحميل التطبيق.", "error.page.details.label": "تفاصيل الخطأ", @@ -351,12 +400,10 @@ export const dict = { "error.page.report.prefix": "يرجى الإبلاغ عن هذا الخطأ لفريق OpenCode", "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", @@ -376,44 +423,58 @@ export const dict = { "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.files.selectToOpen": "اختر ملفًا لفتحه", + "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.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": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.", @@ -425,10 +486,8 @@ export const dict = { "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": "محطة طرفية", @@ -436,7 +495,6 @@ export const dict = { "terminal.close": "إغلاق المحطة الطرفية", "terminal.connectionLost.title": "فقد الاتصال", "terminal.connectionLost.description": "انقطع اتصال المحطة الطرفية. يمكن أن يحدث هذا عند إعادة تشغيل الخادم.", - "common.closeTab": "إغلاق علامة التبويب", "common.dismiss": "رفض", "common.requestFailed": "فشل الطلب", @@ -450,7 +508,6 @@ export const dict = { "common.edit": "تحرير", "common.loadMore": "تحميل المزيد", "common.key.esc": "ESC", - "sidebar.menu.toggle": "تبديل القائمة", "sidebar.nav.projectsAndSessions": "المشاريع والجلسات", "sidebar.settings": "الإعدادات", @@ -462,15 +519,21 @@ export const dict = { "sidebar.gettingStarted.line2": "قم بتوصيل أي موفر لاستخدام النماذج، بما في ذلك Claude و GPT و Gemini وما إلى ذلك.", "sidebar.project.recentSessions": "الجلسات الحديثة", "sidebar.project.viewAllSessions": "عرض جميع الجلسات", - + "sidebar.project.clearNotifications": "مسح الإشعارات", + "app.name.desktop": "OpenCode 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": "تشغيل خادم OpenCode داخل 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": "تغيير لغة العرض لـ OpenCode", "settings.general.row.appearance.title": "المظهر", @@ -479,17 +542,40 @@ export const dict = { "settings.general.row.theme.description": "تخصيص سمة OpenCode.", "settings.general.row.font.title": "الخط", "settings.general.row.font.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.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": "التحقق تلقائيًا من التحديثات عند تشغيل OpenCode", + "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": "أنت تستخدم أحدث إصدار من OpenCode.", "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": "بلا", "sound.option.alert01": "تنبيه 01", "sound.option.alert02": "تنبيه 02", "sound.option.alert03": "تنبيه 03", @@ -535,21 +621,18 @@ export const dict = { "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": "تم إعادة تعيين الاختصارات", @@ -560,16 +643,21 @@ export const dict = { "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": "الوكلاء", @@ -578,16 +666,13 @@ export const dict = { "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": "تحرير", @@ -601,9 +686,9 @@ export const dict = { "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.title": "مهمة", "settings.permissions.tool.task.description": "تشغيل الوكلاء الفرعيين", - "settings.permissions.tool.skill.title": "Skill", + "settings.permissions.tool.skill.title": "مهارة", "settings.permissions.tool.skill.description": "تحميل مهارة بالاسم", "settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة", @@ -621,12 +706,10 @@ export const dict = { "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": "صندوق رمل", @@ -637,6 +720,7 @@ export const dict = { "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": "لم يتم اكتشاف تغييرات غير مدمجة.", @@ -651,4 +735,18 @@ export const dict = { "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 الأساسي.", } diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index af9a71b16fd..951edf0a5c0 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -8,6 +8,7 @@ export const dict = { "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", @@ -15,11 +16,9 @@ export const dict = { "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", @@ -27,22 +26,24 @@ export const dict = { "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.file.open.description": "Buscar arquivos e comandos", + "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", @@ -62,8 +63,12 @@ export const dict = { "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.permissions.autoaccept.enable": "Aceitar edições automaticamente", - "command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente", + "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", @@ -76,30 +81,32 @@ export const dict = { "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 e comandos", + "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.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode", "dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares", - - "dialog.provider.viewAll": "Ver todos os provedores", - + "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}}.", @@ -134,7 +141,44 @@ export const dict = { "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", @@ -151,23 +195,27 @@ export const dict = { "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.submit": "Enviar", "common.save": "Salvar", "common.saving": "Salvando...", "common.default": "Padrão", "common.attachment": "anexo", - "prompt.placeholder.shell": "Digite comando do shell...", "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", @@ -193,11 +241,13 @@ export const dict = { "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": "Solte imagens ou PDFs 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", @@ -206,7 +256,6 @@ export const dict = { "prompt.attachment.remove": "Remover anexo", "prompt.action.send": "Enviar", "prompt.action.stop": "Parar", - "prompt.toast.pasteUnsupported.title": "Colagem não suportada", "prompt.toast.pasteUnsupported.description": "Somente imagens ou PDFs podem ser colados aqui.", "prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo", @@ -216,21 +265,19 @@ export const dict = { "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}} de {{total}} habilitados", + "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 OpenCode este aplicativo se conecta.", "dialog.server.search.placeholder": "Buscar servidores", @@ -248,7 +295,12 @@ export const dict = { "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", @@ -260,7 +312,6 @@ export const dict = { "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.', @@ -269,10 +320,8 @@ export const dict = { "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", @@ -289,62 +338,59 @@ export const dict = { "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": "Inglês", - "language.zh": "Chinês (Simplificado)", - "language.zht": "Chinês (Tradicional)", - "language.ko": "Coreano", - "language.de": "Alemão", - "language.es": "Espanhol", - "language.fr": "Francês", - "language.ja": "Japonês", - "language.da": "Dinamarquês", - "language.ru": "Russo", - "language.pl": "Polonês", - "language.ar": "Árabe", - "language.no": "Norueguês", + "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": "ไทย", "toast.language.title": "Idioma", "toast.language.description": "Alterado para {{language}}", - "toast.theme.title": "Tema alterado", "toast.scheme.title": "Esquema de cores", - - "toast.permissions.autoaccept.on.title": "Aceitando edições automaticamente", - "toast.permissions.autoaccept.on.description": "Permissões de edição e escrita serão aprovadas automaticamente", - "toast.permissions.autoaccept.off.title": "Parou de aceitar edições automaticamente", - "toast.permissions.autoaccept.off.description": "Permissões de edição e escrita exigirão aprovação", - + "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 OpenCode ({{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", @@ -355,12 +401,10 @@ export const dict = { "error.page.report.prefix": "Por favor, reporte este erro para a equipe do OpenCode", "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", @@ -382,44 +426,58 @@ export const dict = { "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.noChanges": "Sem alterações", + "session.files.selectToOpen": "Selecione um arquivo para abrir", + "session.files.all": "Todos os arquivos", + "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.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.", @@ -433,10 +491,8 @@ export const dict = { "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", @@ -445,7 +501,6 @@ export const dict = { "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", @@ -459,7 +514,6 @@ export const dict = { "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", @@ -471,15 +525,21 @@ export const dict = { "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": "OpenCode 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 OpenCode 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 OpenCode", "settings.general.row.appearance.title": "Aparência", @@ -488,17 +548,40 @@ export const dict = { "settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.", "settings.general.row.font.title": "Fonte", "settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código", + "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.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 OpenCode 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 OpenCode.", "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": "Nenhum", "sound.option.alert01": "Alerta 01", "sound.option.alert02": "Alerta 02", "sound.option.alert03": "Alerta 03", @@ -544,7 +627,6 @@ export const dict = { "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", @@ -553,14 +635,12 @@ export const dict = { "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", @@ -571,16 +651,21 @@ export const dict = { "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", @@ -589,16 +674,13 @@ export const dict = { "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", @@ -632,12 +714,10 @@ export const dict = { "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", @@ -648,6 +728,7 @@ export const dict = { "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.", @@ -662,4 +743,18 @@ export const dict = { "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.", } diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts new file mode 100644 index 00000000000..e8bdcde596e --- /dev/null +++ b/packages/app/src/i18n/bs.ts @@ -0,0 +1,837 @@ +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.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje OpenCode", + "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 OpenCode-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 OpenCode-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 OpenCode-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.submit": "Pošalji", + "common.save": "Sačuvaj", + "common.saving": "Čuvanje...", + "common.default": "Podrazumijevano", + "common.attachment": "prilog", + + "prompt.placeholder.shell": "Unesi shell naredbu...", + "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": "Spusti slike ili PDF-ove ovdje", + "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žano lijepljenje", + "prompt.toast.pasteUnsupported.description": "Ovdje se mogu zalijepiti samo slike ili PDF-ovi.", + "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 OpenCode 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.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": "ไทย", + + "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 OpenCode-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 OpenCode 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: OpenCode 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.noChanges": "Nema izmjena", + + "session.files.selectToOpen": "Odaberi datoteku za otvaranje", + "session.files.all": "Sve datoteke", + "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.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": "OpenCode 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": "OpenCode 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 OpenCode 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 OpenCode-u", + "settings.general.row.appearance.title": "Izgled", + "settings.general.row.appearance.description": "Prilagodi kako OpenCode izgleda na tvom uređaju", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Prilagodi temu OpenCode-a.", + "settings.general.row.font.title": "Font", + "settings.general.row.font.description": "Prilagodi monospace font koji se koristi u blokovima koda", + + "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.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 OpenCode 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 OpenCode-a.", + "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": "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.todoread.title": "Čitanje liste zadataka", + "settings.permissions.tool.todoread.description": "Čitanje liste zadataka", + "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.", +} diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index b1e3f59c37c..5ea52a5c92c 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -8,6 +8,7 @@ export const dict = { "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", @@ -15,6 +16,7 @@ export const dict = { "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", @@ -23,8 +25,11 @@ export const dict = { "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", @@ -39,8 +44,12 @@ export const dict = { "command.session.new": "Ny session", "command.file.open": "Åbn fil", - "command.file.open.description": "Søg i filer og kommandoer", + "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", @@ -60,8 +69,12 @@ export const dict = { "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.permissions.autoaccept.enable": "Accepter ændringer automatisk", - "command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer", + "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", @@ -75,7 +88,7 @@ export const dict = { "command.session.unshare": "Stop deling af session", "command.session.unshare.description": "Stop med at dele denne session", - "palette.search.placeholder": "Søg i filer og kommandoer", + "palette.search.placeholder": "Søg i filer, kommandoer og sessioner", "palette.empty": "Ingen resultater fundet", "palette.group.commands": "Kommandoer", "palette.group.files": "Filer", @@ -85,7 +98,15 @@ export const dict = { "dialog.provider.group.popular": "Populære", "dialog.provider.group.other": "Andre", "dialog.provider.tag.recommended": "Anbefalet", - "dialog.provider.anthropic.note": "Forbind med Claude Pro/Max eller API-nøgle", + "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", @@ -96,7 +117,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode", "dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere", - "dialog.provider.viewAll": "Vis alle udbydere", + "dialog.provider.viewAll": "Vis flere udbydere", "provider.connect.title": "Forbind {{provider}}", "provider.connect.title.anthropicProMax": "Log ind med Claude Pro/Max", @@ -115,6 +136,7 @@ export const dict = { "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", @@ -132,13 +154,70 @@ export const dict = { "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.submit": "Indsend", "common.save": "Gem", "common.saving": "Gemmer...", @@ -147,7 +226,11 @@ export const dict = { "prompt.placeholder.shell": "Indtast shell-kommando...", "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", @@ -179,7 +262,10 @@ export const dict = { "prompt.popover.emptyResults": "Ingen matchende resultater", "prompt.popover.emptyCommands": "Ingen matchende kommandoer", "prompt.dropzone.label": "Slip billeder eller PDF'er 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", @@ -198,11 +284,15 @@ export const dict = { "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", @@ -222,7 +312,7 @@ export const dict = { "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", + "dialog.server.add.button": "Tilføj 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.", @@ -231,6 +321,13 @@ export const dict = { "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", @@ -240,6 +337,9 @@ export const dict = { "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.', @@ -257,10 +357,10 @@ export const dict = { "context.stats.provider": "Udbyder", "context.stats.model": "Model", "context.stats.limit": "Kontekstgrænse", - "context.stats.totalTokens": "Total Tokens", + "context.stats.totalTokens": "Samlede tokens", "context.stats.usage": "Forbrug", - "context.stats.inputTokens": "Input Tokens", - "context.stats.outputTokens": "Output Tokens", + "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", @@ -275,20 +375,22 @@ export const dict = { "context.usage.clickToView": "Klik for at se kontekst", "context.usage.view": "Se kontekstforbrug", - "language.en": "Engelsk", - "language.zh": "Kinesisk (forenklet)", - "language.zht": "Kinesisk (traditionelt)", - "language.ko": "Koreansk", - "language.de": "Tysk", - "language.es": "Spansk", - "language.fr": "Fransk", - "language.ja": "Japansk", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", "language.da": "Dansk", - "language.ru": "Russisk", - "language.pl": "Polsk", - "language.ar": "Arabisk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", "language.no": "Norsk", - "language.br": "Portugisisk (Brasilien)", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "Sprog", "toast.language.description": "Skiftede til {{language}}", @@ -296,16 +398,24 @@ export const dict = { "toast.theme.title": "Tema skiftet", "toast.scheme.title": "Farveskema", - "toast.permissions.autoaccept.on.title": "Accepterer ændringer automatisk", - "toast.permissions.autoaccept.on.description": "Redigerings- og skrivetilladelser vil automatisk blive godkendt", - "toast.permissions.autoaccept.off.title": "Stoppede automatisk accept af ændringer", - "toast.permissions.autoaccept.off.description": "Redigerings- og skrivetilladelser vil kræve godkendelse", + "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!", @@ -339,6 +449,7 @@ export const dict = { "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:", @@ -380,15 +491,26 @@ export const dict = { "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.noChanges": "Ingen ændringer", + "session.files.selectToOpen": "Vælg en fil at åbne", + "session.files.all": "Alle 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.new.title": "Byg hvad som helst", "session.new.worktree.main": "Hovedgren", "session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})", "session.new.worktree.create": "Opret nyt worktree", @@ -396,6 +518,19 @@ export const dict = { "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": @@ -420,6 +555,8 @@ export const dict = { "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", @@ -433,6 +570,8 @@ export const dict = { "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", @@ -443,14 +582,23 @@ export const dict = { "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": "OpenCode 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 OpenCode-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 OpenCode", @@ -461,6 +609,87 @@ export const dict = { "settings.general.row.font.title": "Skrifttype", "settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke", + "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.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 OpenCode 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 OpenCode.", + + "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": "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", @@ -496,6 +725,13 @@ export const dict = { "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", @@ -563,6 +799,7 @@ export const dict = { "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.", @@ -577,4 +814,18 @@ export const dict = { "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.", } diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 9b7d1e5e6af..a6cf8045c09 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -12,39 +12,42 @@ export const dict = { "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.file.open.description": "Dateien und Befehle durchsuchen", + "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", @@ -64,8 +67,12 @@ export const dict = { "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.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren", - "command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen", + "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", @@ -78,30 +85,32 @@ export const dict = { "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 und Befehle durchsuchen", + "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.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode", "dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen", - - "dialog.provider.viewAll": "Alle Anbieter anzeigen", - + "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.", @@ -119,6 +128,7 @@ export const dict = { "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", @@ -135,25 +145,82 @@ export const dict = { "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.submit": "Absenden", "common.save": "Speichern", "common.saving": "Speichert...", "common.default": "Standard", "common.attachment": "Anhang", - "prompt.placeholder.shell": "Shell-Befehl eingeben...", "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", @@ -179,11 +246,13 @@ export const dict = { "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 oder PDFs 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", @@ -192,7 +261,6 @@ export const dict = { "prompt.attachment.remove": "Anhang entfernen", "prompt.action.send": "Senden", "prompt.action.stop": "Stopp", - "prompt.toast.pasteUnsupported.title": "Nicht unterstütztes Einfügen", "prompt.toast.pasteUnsupported.description": "Hier können nur Bilder oder PDFs eingefügt werden.", "prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell", @@ -203,21 +271,19 @@ export const dict = { "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 OpenCode-Server, mit dem sich diese App verbindet.", "dialog.server.search.placeholder": "Server durchsuchen", @@ -227,7 +293,7 @@ export const dict = { "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": "Hinzufügen", + "dialog.server.add.button": "Server hinzufügen", "dialog.server.default.title": "Standardserver", "dialog.server.default.description": "Beim App-Start mit diesem Server verbinden, anstatt einen lokalen Server zu starten. Erfordert Neustart.", @@ -235,7 +301,12 @@ export const dict = { "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", @@ -244,7 +315,10 @@ export const dict = { "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.', @@ -253,10 +327,8 @@ export const dict = { "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", @@ -273,62 +345,59 @@ export const dict = { "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": "Englisch", - "language.zh": "Chinesisch (Vereinfacht)", - "language.zht": "Chinesisch (Traditionell)", - "language.ko": "Koreanisch", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", "language.de": "Deutsch", - "language.es": "Spanisch", - "language.fr": "Französisch", - "language.ja": "Japanisch", - "language.da": "Dänisch", - "language.ru": "Russisch", - "language.pl": "Polnisch", - "language.ar": "Arabisch", - "language.no": "Norwegisch", - "language.br": "Portugiesisch (Brasilien)", - + "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": "ไทย", "toast.language.title": "Sprache", "toast.language.description": "Zu {{language}} gewechselt", - "toast.theme.title": "Thema gewechselt", "toast.scheme.title": "Farbschema", - - "toast.permissions.autoaccept.on.title": "Änderungen werden automatisch akzeptiert", - "toast.permissions.autoaccept.on.description": "Bearbeitungs- und Schreibrechte werden automatisch genehmigt", - "toast.permissions.autoaccept.off.title": "Automatische Annahme von Änderungen gestoppt", - "toast.permissions.autoaccept.off.description": "Bearbeitungs- und Schreibrechte erfordern Genehmigung", - + "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 OpenCode ({{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", @@ -339,12 +408,10 @@ export const dict = { "error.page.report.prefix": "Bitte melden Sie diesen Fehler dem OpenCode-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", @@ -367,43 +434,58 @@ export const dict = { "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.noChanges": "Keine Änderungen", + "session.files.selectToOpen": "Datei zum Öffnen auswählen", + "session.files.all": "Alle 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.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.", @@ -417,16 +499,16 @@ export const dict = { "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", @@ -439,7 +521,8 @@ export const dict = { "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", @@ -451,15 +534,21 @@ export const dict = { "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": "OpenCode 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": "OpenCode-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 OpenCode ändern", "settings.general.row.appearance.title": "Erscheinungsbild", @@ -468,7 +557,85 @@ export const dict = { "settings.general.row.theme.description": "Das Thema von OpenCode anpassen.", "settings.general.row.font.title": "Schriftart", "settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen", - + "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.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 OpenCode 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 OpenCode.", + "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": "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", @@ -477,14 +644,12 @@ export const dict = { "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", @@ -495,16 +660,21 @@ export const dict = { "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", @@ -513,16 +683,13 @@ export const dict = { "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", @@ -556,12 +723,10 @@ export const dict = { "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", @@ -572,6 +737,7 @@ export const dict = { "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.", @@ -586,4 +752,18 @@ export const dict = { "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.", } satisfies Partial> diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index ed00ff18be7..c87e7cb9dbb 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -8,6 +8,7 @@ export const dict = { "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", @@ -27,6 +28,8 @@ export const dict = { "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", @@ -41,8 +44,12 @@ export const dict = { "command.session.new": "New session", "command.file.open": "Open file", - "command.file.open.description": "Search files and commands", + "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", @@ -62,8 +69,12 @@ export const dict = { "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.permissions.autoaccept.enable": "Auto-accept edits", - "command.permissions.autoaccept.disable": "Stop auto-accepting edits", + "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", @@ -77,7 +88,7 @@ export const dict = { "command.session.unshare": "Unshare session", "command.session.unshare.description": "Stop sharing this session", - "palette.search.placeholder": "Search files and commands", + "palette.search.placeholder": "Search files, commands, and sessions", "palette.empty": "No results found", "palette.group.commands": "Commands", "palette.group.files": "Files", @@ -87,18 +98,27 @@ export const dict = { "dialog.provider.group.popular": "Popular", "dialog.provider.group.other": "Other", "dialog.provider.tag.recommended": "Recommended", - "dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key", + "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 OpenCode", "dialog.model.unpaid.addMore.title": "Add more models from popular providers", - "dialog.provider.viewAll": "View all providers", + "dialog.provider.viewAll": "Show more providers", "provider.connect.title": "Connect {{provider}}", "provider.connect.title.anthropicProMax": "Login with Claude Pro/Max", @@ -135,6 +155,46 @@ export const dict = { "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", @@ -153,10 +213,14 @@ export const dict = { "model.tooltip.context": "Context limit {{limit}}", "common.search.placeholder": "Search", - "common.goBack": "Go back", + "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.submit": "Submit", "common.save": "Save", "common.saving": "Saving...", @@ -165,7 +229,11 @@ export const dict = { "prompt.placeholder.shell": "Enter shell command...", "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", "prompt.example.1": "Fix a TODO in the codebase", @@ -197,12 +265,15 @@ export const dict = { "prompt.popover.emptyResults": "No matching results", "prompt.popover.emptyCommands": "No matching commands", "prompt.dropzone.label": "Drop images or PDFs 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": "Attach file", + "prompt.action.attachFile": "Add file", "prompt.attachment.remove": "Remove attachment", "prompt.action.send": "Send", "prompt.action.stop": "Stop", @@ -216,11 +287,15 @@ export const dict = { "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", @@ -235,12 +310,17 @@ export const dict = { "dialog.server.description": "Switch which OpenCode server this app connects to.", "dialog.server.search.placeholder": "Search servers", "dialog.server.empty": "No servers yet", - "dialog.server.add.title": "Add a server", - "dialog.server.add.url": "Server URL", + "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", + "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.password": "Password (optional)", + "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.", @@ -249,6 +329,13 @@ export const dict = { "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", + "dialog.project.edit.title": "Edit project", "dialog.project.edit.name": "Name", "dialog.project.edit.icon": "Icon", @@ -261,6 +348,11 @@ export const dict = { "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", @@ -296,19 +388,22 @@ export const dict = { "context.usage.view": "View context usage", "language.en": "English", - "language.zh": "Chinese (Simplified)", - "language.zht": "Chinese (Traditional)", - "language.ko": "Korean", - "language.de": "German", - "language.es": "Spanish", - "language.fr": "French", - "language.ja": "Japanese", - "language.da": "Danish", - "language.ru": "Russian", - "language.pl": "Polish", - "language.ar": "Arabic", - "language.no": "Norwegian", - "language.br": "Portuguese (Brazil)", + "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}}", @@ -316,15 +411,24 @@ export const dict = { "toast.theme.title": "Theme switched", "toast.scheme.title": "Color scheme", - "toast.permissions.autoaccept.on.title": "Auto-accepting edits", - "toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved", - "toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits", - "toast.permissions.autoaccept.off.description": "Edit and write permissions will require approval", + "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", @@ -338,6 +442,7 @@ export const dict = { "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 OpenCode ({{version}}) is now available to install.", @@ -359,8 +464,10 @@ export const dict = { "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}}`?", + "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}}", @@ -400,8 +507,19 @@ export const dict = { "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.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable", + "session.review.noChanges": "No changes", + + "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", @@ -409,7 +527,16 @@ export const dict = { "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.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", @@ -417,6 +544,19 @@ export const dict = { "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", + + "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": @@ -446,6 +586,7 @@ export const dict = { "common.closeTab": "Close tab", "common.dismiss": "Dismiss", + "common.moreCountSuffix": " (+{{count}} more)", "common.requestFailed": "Request failed", "common.moreOptions": "More options", "common.learnMore": "Learn more", @@ -458,6 +599,11 @@ export const dict = { "common.loadMore": "Load more", "common.key.esc": "ESC", + "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", @@ -469,14 +615,24 @@ export const dict = { "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", + + "app.name.desktop": "OpenCode 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 OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Appearance", "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 OpenCode", @@ -486,17 +642,45 @@ export const dict = { "settings.general.row.theme.description": "Customise how OpenCode is themed.", "settings.general.row.font.title": "Font", "settings.general.row.font.description": "Customise the mono font used in code blocks", + "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.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 OpenCode 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 OpenCode.", "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": "None", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", @@ -578,6 +762,15 @@ export const dict = { "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", @@ -644,6 +837,7 @@ export const dict = { "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.", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 8de149d2f5a..77ef7970c43 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -8,12 +8,14 @@ export const dict = { "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", @@ -23,8 +25,11 @@ export const dict = { "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", @@ -39,8 +44,12 @@ export const dict = { "command.session.new": "Nueva sesión", "command.file.open": "Abrir archivo", - "command.file.open.description": "Buscar archivos y comandos", + "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", @@ -60,8 +69,12 @@ export const dict = { "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.permissions.autoaccept.enable": "Aceptar ediciones automáticamente", - "command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente", + "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", @@ -75,7 +88,7 @@ export const dict = { "command.session.unshare": "Dejar de compartir sesión", "command.session.unshare.description": "Dejar de compartir esta sesión", - "palette.search.placeholder": "Buscar archivos y comandos", + "palette.search.placeholder": "Buscar archivos, comandos y sesiones", "palette.empty": "No se encontraron resultados", "palette.group.commands": "Comandos", "palette.group.files": "Archivos", @@ -85,7 +98,15 @@ export const dict = { "dialog.provider.group.popular": "Popular", "dialog.provider.group.other": "Otro", "dialog.provider.tag.recommended": "Recomendado", - "dialog.provider.anthropic.note": "Conectar con Claude Pro/Max o clave API", + "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", @@ -96,7 +117,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode", "dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares", - "dialog.provider.viewAll": "Ver todos los proveedores", + "dialog.provider.viewAll": "Ver más proveedores", "provider.connect.title": "Conectar {{provider}}", "provider.connect.title.anthropicProMax": "Iniciar sesión con Claude Pro/Max", @@ -115,6 +136,7 @@ export const dict = { "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", @@ -132,13 +154,71 @@ export const dict = { "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.submit": "Enviar", "common.save": "Guardar", "common.saving": "Guardando...", @@ -147,7 +227,11 @@ export const dict = { "prompt.placeholder.shell": "Introduce comando de shell...", "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", @@ -179,7 +263,10 @@ export const dict = { "prompt.popover.emptyResults": "Sin resultados coincidentes", "prompt.popover.emptyCommands": "Sin comandos coincidentes", "prompt.dropzone.label": "Suelta imágenes o PDFs 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", @@ -198,11 +285,15 @@ export const dict = { "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", @@ -222,7 +313,7 @@ export const dict = { "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", + "dialog.server.add.button": "Añadir 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.", @@ -231,6 +322,13 @@ export const dict = { "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", @@ -239,6 +337,10 @@ export const dict = { "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": @@ -275,20 +377,22 @@ export const dict = { "context.usage.clickToView": "Haz clic para ver contexto", "context.usage.view": "Ver uso del contexto", - "language.en": "Inglés", - "language.zh": "Chino (simplificado)", - "language.zht": "Chino (tradicional)", - "language.ko": "Coreano", - "language.de": "Alemán", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", "language.es": "Español", - "language.fr": "Francés", - "language.ja": "Japonés", - "language.da": "Danés", - "language.ru": "Ruso", - "language.pl": "Polaco", - "language.ar": "Árabe", - "language.no": "Noruego", - "language.br": "Portugués (Brasil)", + "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": "ไทย", "toast.language.title": "Idioma", "toast.language.description": "Cambiado a {{language}}", @@ -296,15 +400,24 @@ export const dict = { "toast.theme.title": "Tema cambiado", "toast.scheme.title": "Esquema de color", - "toast.permissions.autoaccept.on.title": "Aceptando ediciones automáticamente", - "toast.permissions.autoaccept.on.description": "Los permisos de edición y escritura serán aprobados automáticamente", - "toast.permissions.autoaccept.off.title": "Se dejó de aceptar ediciones automáticamente", - "toast.permissions.autoaccept.off.description": "Los permisos de edición y escritura requerirán aprobación", + "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", @@ -339,6 +452,7 @@ export const dict = { "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:", @@ -381,15 +495,28 @@ export const dict = { "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.noChanges": "Sin cambios", + + "session.files.selectToOpen": "Selecciona un archivo para abrir", + "session.files.all": "Todos los 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.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", @@ -397,6 +524,19 @@ export const dict = { "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": @@ -420,6 +560,9 @@ export const dict = { "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", @@ -433,7 +576,9 @@ export const dict = { "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", @@ -444,14 +589,24 @@ export const dict = { "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": "OpenCode 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 OpenCode 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 OpenCode", @@ -460,7 +615,90 @@ export const dict = { "settings.general.row.theme.title": "Tema", "settings.general.row.theme.description": "Personaliza el tema de OpenCode.", "settings.general.row.font.title": "Fuente", - "settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código", + "settings.general.row.font.description": "Personaliza la fuente monoespaciada usada en bloques de código", + + "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.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 OpenCode", + "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 OpenCode.", + "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": "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": @@ -499,6 +737,13 @@ export const dict = { "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", @@ -566,6 +811,7 @@ export const dict = { "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.", @@ -580,4 +826,18 @@ export const dict = { "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.", } diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 8ac4d5dd402..c887f9ee8b2 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -8,39 +8,42 @@ export const dict = { "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.file.open.description": "Rechercher des fichiers et des commandes", + "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", @@ -60,8 +63,12 @@ export const dict = { "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.permissions.autoaccept.enable": "Accepter automatiquement les modifications", - "command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications", + "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", @@ -74,30 +81,32 @@ export const dict = { "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 et des commandes", + "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.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode", "dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires", - - "dialog.provider.viewAll": "Voir tous les fournisseurs", - + "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}}.", @@ -115,6 +124,7 @@ export const dict = { "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", @@ -131,25 +141,81 @@ export const dict = { "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.submit": "Soumettre", "common.save": "Enregistrer", "common.saving": "Enregistrement...", "common.default": "Défaut", "common.attachment": "pièce jointe", - "prompt.placeholder.shell": "Entrez une commande shell...", "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", @@ -175,11 +241,13 @@ export const dict = { "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 ou des PDF 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", @@ -188,7 +256,6 @@ export const dict = { "prompt.attachment.remove": "Supprimer la pièce jointe", "prompt.action.send": "Envoyer", "prompt.action.stop": "Arrêter", - "prompt.toast.pasteUnsupported.title": "Collage non supporté", "prompt.toast.pasteUnsupported.description": "Seules les images ou les PDF peuvent être collés ici.", "prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle", @@ -198,21 +265,19 @@ export const dict = { "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 OpenCode auquel cette application se connecte.", "dialog.server.search.placeholder": "Rechercher des serveurs", @@ -222,7 +287,7 @@ export const dict = { "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", + "dialog.server.add.button": "Ajouter un 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.", @@ -230,7 +295,12 @@ export const dict = { "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", @@ -239,7 +309,10 @@ export const dict = { "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.", @@ -248,10 +321,8 @@ export const dict = { "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", @@ -268,65 +339,60 @@ export const dict = { "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": "Anglais", - "language.zh": "Chinois (simplifié)", - "language.zht": "Chinois (traditionnel)", - "language.ko": "Coréen", - "language.de": "Allemand", - "language.es": "Espagnol", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", "language.fr": "Français", - "language.ja": "Japonais", - "language.da": "Danois", - "language.ru": "Russe", - "language.pl": "Polonais", - "language.ar": "Arabe", - "language.no": "Norvégien", - "language.br": "Portugais (Brésil)", - + "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": "ไทย", "toast.language.title": "Langue", "toast.language.description": "Passé à {{language}}", - "toast.theme.title": "Thème changé", "toast.scheme.title": "Schéma de couleurs", - - "toast.permissions.autoaccept.on.title": "Acceptation auto des modifications", - "toast.permissions.autoaccept.on.description": - "Les permissions de modification et d'écriture seront automatiquement approuvées", - "toast.permissions.autoaccept.off.title": "Arrêt acceptation auto des modifications", - "toast.permissions.autoaccept.off.description": - "Les permissions de modification et d'écriture nécessiteront une approbation", - + "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'OpenCode ({{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", @@ -337,13 +403,11 @@ export const dict = { "error.page.report.prefix": "Veuillez signaler cette erreur à l'équipe OpenCode", "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", @@ -366,43 +430,58 @@ export const dict = { "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.files.selectToOpen": "Sélectionnez un fichier à ouvrir", + "session.files.all": "Tous les fichiers", + "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.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.", @@ -416,16 +495,16 @@ export const dict = { "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é", @@ -438,7 +517,8 @@ export const dict = { "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", @@ -451,15 +531,21 @@ export const dict = { "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": "OpenCode 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 OpenCode 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 OpenCode", "settings.general.row.appearance.title": "Apparence", @@ -468,7 +554,85 @@ export const dict = { "settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.", "settings.general.row.font.title": "Police", "settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code", - + "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.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'OpenCode", + "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'OpenCode.", + "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": "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", @@ -477,14 +641,12 @@ export const dict = { "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", @@ -495,16 +657,21 @@ export const dict = { "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", @@ -513,16 +680,13 @@ export const dict = { "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", @@ -557,12 +721,10 @@ export const dict = { "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", @@ -573,6 +735,7 @@ export const dict = { "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.", @@ -587,4 +750,18 @@ export const dict = { "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.", } diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 821cda762e9..9ddb6baf4a7 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -8,39 +8,42 @@ export const dict = { "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.file.open.description": "ファイルとコマンドを検索", + "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": "新しいターミナルタブを作成", @@ -60,8 +63,12 @@ export const dict = { "command.agent.cycle.reverse.description": "前のエージェントに切り替え", "command.model.variant.cycle": "思考レベルの切り替え", "command.model.variant.cycle.description": "次の思考レベルに切り替え", - "command.permissions.autoaccept.enable": "編集を自動承認", - "command.permissions.autoaccept.disable": "編集の自動承認を停止", + "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": "やり直す", @@ -74,30 +81,32 @@ export const dict = { "command.session.share.description": "このセッションを共有しURLをクリップボードにコピー", "command.session.unshare": "セッションの共有を停止", "command.session.unshare.description": "このセッションの共有を停止", - - "palette.search.placeholder": "ファイルとコマンドを検索", + "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.unpaid.freeModels.title": "OpenCodeが提供する無料モデル", "dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加", - - "dialog.provider.viewAll": "すべてのプロバイダーを表示", - + "dialog.provider.viewAll": "さらにプロバイダーを表示", "provider.connect.title": "{{provider}}を接続", "provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン", "provider.connect.selectMethod": "{{provider}}のログイン方法を選択してください。", @@ -114,6 +123,7 @@ export const dict = { "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": "このリンク", @@ -130,25 +140,81 @@ export const dict = { "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.submit": "送信", "common.save": "保存", "common.saving": "保存中...", "common.default": "デフォルト", "common.attachment": "添付ファイル", - "prompt.placeholder.shell": "シェルコマンドを入力...", "prompt.placeholder.normal": '何でも聞いてください... "{{example}}"', - "prompt.mode.shell": "Shell", + "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": "壊れたテストを修正", @@ -174,11 +240,13 @@ export const dict = { "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": "コンテキストからアクティブなファイルを削除", @@ -187,7 +255,6 @@ export const dict = { "prompt.attachment.remove": "添付ファイルを削除", "prompt.action.send": "送信", "prompt.action.stop": "停止", - "prompt.toast.pasteUnsupported.title": "サポートされていない貼り付け", "prompt.toast.pasteUnsupported.description": "ここでは画像またはPDFのみ貼り付け可能です。", "prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択", @@ -197,21 +264,19 @@ export const dict = { "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": "このアプリが接続するOpenCodeサーバーを切り替えます。", "dialog.server.search.placeholder": "サーバーを検索", @@ -221,7 +286,7 @@ export const dict = { "dialog.server.add.placeholder": "http://localhost:4096", "dialog.server.add.error": "サーバーに接続できませんでした", "dialog.server.add.checking": "確認中...", - "dialog.server.add.button": "追加", + "dialog.server.add.button": "サーバーを追加", "dialog.server.default.title": "デフォルトサーバー", "dialog.server.default.description": "ローカルサーバーを起動する代わりに、アプリ起動時にこのサーバーに接続します。再起動が必要です。", @@ -229,7 +294,12 @@ export const dict = { "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": "アイコン", @@ -238,7 +308,10 @@ export const dict = { "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": "システム", @@ -246,10 +319,8 @@ export const dict = { "context.breakdown.assistant": "アシスタント", "context.breakdown.tool": "ツール呼び出し", "context.breakdown.other": "その他", - "context.systemPrompt.title": "システムプロンプト", "context.rawMessages.title": "生のメッセージ", - "context.stats.session": "セッション", "context.stats.messages": "メッセージ", "context.stats.provider": "プロバイダー", @@ -266,62 +337,59 @@ export const dict = { "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": "英語", - "language.zh": "中国語(簡体字)", - "language.zht": "中国語(繁体字)", - "language.ko": "韓国語", - "language.de": "ドイツ語", - "language.es": "スペイン語", - "language.fr": "フランス語", + "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.da": "デンマーク語", - "language.ru": "ロシア語", - "language.pl": "ポーランド語", - "language.ar": "アラビア語", - "language.no": "ノルウェー語", - "language.br": "ポルトガル語(ブラジル)", - + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "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.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": "OpenCodeの新しいバージョン ({{version}}) がインストール可能です。", "toast.update.action.installRestart": "インストールして再起動", "toast.update.action.notYet": "今はしない", - "error.page.title": "問題が発生しました", "error.page.description": "アプリケーションの読み込み中にエラーが発生しました。", "error.page.details.label": "エラー詳細", @@ -332,12 +400,10 @@ export const dict = { "error.page.report.prefix": "このエラーをOpenCodeチームに報告してください: ", "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エラー", @@ -358,43 +424,58 @@ export const dict = { "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.noChanges": "変更なし", + "session.files.selectToOpen": "開くファイルを選択", + "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": "ToDo", + "session.todo.collapse": "折りたたむ", + "session.todo.expand": "展開", + "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": "このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。", @@ -408,16 +489,16 @@ export const dict = { "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": "リクエスト失敗", @@ -430,7 +511,8 @@ export const dict = { "common.close": "閉じる", "common.edit": "編集", "common.loadMore": "さらに読み込む", - + "common.key.esc": "ESC", + "sidebar.menu.toggle": "メニューを切り替え", "sidebar.nav.projectsAndSessions": "プロジェクトとセッション", "sidebar.settings": "設定", "sidebar.help": "ヘルプ", @@ -441,15 +523,21 @@ export const dict = { "sidebar.gettingStarted.line2": "プロバイダーを接続して、Claude、GPT、Geminiなどのモデルを使用できます。", "sidebar.project.recentSessions": "最近のセッション", "sidebar.project.viewAllSessions": "すべてのセッションを表示", - + "sidebar.project.clearNotifications": "通知をクリア", + "app.name.desktop": "OpenCode 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環境でOpenCodeサーバーを実行します。", "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": "OpenCodeの表示言語を変更します", "settings.general.row.appearance.title": "外観", @@ -458,7 +546,85 @@ export const dict = { "settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。", "settings.general.row.font.title": "フォント", "settings.general.row.font.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.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": "OpenCode の起動時に自動でアップデートを確認します", + "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": "OpenCode は最新バージョンです。", + "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": "なし", + "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": "エージェントが完了したか、注意が必要な場合にシステム通知を表示します", @@ -466,14 +632,12 @@ export const dict = { "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": "ショートカットをリセットしました", @@ -484,16 +648,21 @@ export const dict = { "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": "エージェント", @@ -502,16 +671,13 @@ export const dict = { "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": "編集", @@ -534,22 +700,20 @@ export const dict = { "settings.permissions.tool.todoread.description": "Todoリストの読み込み", "settings.permissions.tool.todowrite.title": "Todo書き込み", "settings.permissions.tool.todowrite.description": "Todoリストの更新", - "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.title": "Web取得", "settings.permissions.tool.webfetch.description": "URLからコンテンツを取得", - "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.title": "Web検索", "settings.permissions.tool.websearch.description": "ウェブを検索", - "settings.permissions.tool.codesearch.title": "Code Search", + "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.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": "サンドボックス", @@ -560,6 +724,7 @@ export const dict = { "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": "未マージの変更は検出されませんでした。", @@ -574,4 +739,18 @@ export const dict = { "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 互換のプロバイダーを追加します。", } diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 69a5fd65889..1e35106d1bc 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -12,39 +12,42 @@ export const dict = { "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.file.open.description": "파일 및 명령어 검색", + "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": "새 터미널 탭 생성", @@ -64,8 +67,12 @@ export const dict = { "command.agent.cycle.reverse.description": "이전 에이전트로 전환", "command.model.variant.cycle": "생각 수준 순환", "command.model.variant.cycle.description": "다음 생각 수준으로 전환", - "command.permissions.autoaccept.enable": "편집 자동 수락", - "command.permissions.autoaccept.disable": "편집 자동 수락 중지", + "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": "다시 실행", @@ -78,30 +85,32 @@ export const dict = { "command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사", "command.session.unshare": "세션 공유 중지", "command.session.unshare.description": "이 세션 공유 중지", - - "palette.search.placeholder": "파일 및 명령어 검색", + "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.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델", "dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가", - - "dialog.provider.viewAll": "모든 공급자 보기", - + "dialog.provider.viewAll": "더 많은 공급자 보기", "provider.connect.title": "{{provider}} 연결", "provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인", "provider.connect.selectMethod": "{{provider}} 로그인 방법 선택", @@ -117,9 +126,10 @@ export const dict = { "provider.connect.opencodeZen.line1": "OpenCode Zen은 코딩 에이전트를 위해 최적화된 신뢰할 수 있는 엄선된 모델에 대한 액세스를 제공합니다.", "provider.connect.opencodeZen.line2": "단일 API 키로 Claude, GPT, Gemini, GLM 등 다양한 모델에 액세스할 수 있습니다.", - "provider.connect.opencodeZen.visit.prefix": "", - "provider.connect.opencodeZen.visit.suffix": "를 방문하여 API 키를 받으세요.", - "provider.connect.oauth.code.visit.prefix": "", + "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": "를 방문하여 인증 코드를 받아 계정을 연결하고 OpenCode에서 {{provider}} 모델을 사용하세요.", @@ -127,32 +137,88 @@ export const dict = { "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.prefix": "다음 ", "provider.connect.oauth.auto.visit.link": "이 링크", "provider.connect.oauth.auto.visit.suffix": "를 방문하고 아래 코드를 입력하여 계정을 연결하고 OpenCode에서 {{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.submit": "제출", "common.save": "저장", "common.saving": "저장 중...", "common.default": "기본값", "common.attachment": "첨부 파일", - "prompt.placeholder.shell": "셸 명령어 입력...", "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": "고장 난 테스트 수정", @@ -178,11 +244,13 @@ export const dict = { "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": "컨텍스트에서 활성 파일 제거", @@ -191,7 +259,6 @@ export const dict = { "prompt.attachment.remove": "첨부 파일 제거", "prompt.action.send": "전송", "prompt.action.stop": "중지", - "prompt.toast.pasteUnsupported.title": "지원되지 않는 붙여넣기", "prompt.toast.pasteUnsupported.description": "이미지나 PDF만 붙여넣을 수 있습니다.", "prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택", @@ -201,21 +268,19 @@ export const dict = { "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": "이 앱이 연결할 OpenCode 서버를 전환합니다.", "dialog.server.search.placeholder": "서버 검색", @@ -225,7 +290,7 @@ export const dict = { "dialog.server.add.placeholder": "http://localhost:4096", "dialog.server.add.error": "서버에 연결할 수 없습니다", "dialog.server.add.checking": "확인 중...", - "dialog.server.add.button": "추가", + "dialog.server.add.button": "서버 추가", "dialog.server.default.title": "기본 서버", "dialog.server.default.description": "로컬 서버를 시작하는 대신 앱 실행 시 이 서버에 연결합니다. 다시 시작해야 합니다.", @@ -233,7 +298,12 @@ export const dict = { "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": "아이콘", @@ -242,7 +312,9 @@ export const dict = { "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": "시스템", @@ -250,10 +322,8 @@ export const dict = { "context.breakdown.assistant": "어시스턴트", "context.breakdown.tool": "도구 호출", "context.breakdown.other": "기타", - "context.systemPrompt.title": "시스템 프롬프트", "context.rawMessages.title": "원시 메시지", - "context.stats.session": "세션", "context.stats.messages": "메시지", "context.stats.provider": "공급자", @@ -270,62 +340,59 @@ export const dict = { "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": "영어", - "language.zh": "중국어 (간체)", - "language.zht": "중국어 (번체)", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", "language.ko": "한국어", - "language.de": "독일어", - "language.es": "스페인어", - "language.fr": "프랑스어", - "language.ja": "일본어", - "language.da": "덴마크어", - "language.ru": "러시아어", - "language.pl": "폴란드어", - "language.ar": "아랍어", - "language.no": "노르웨이어", - "language.br": "포르투갈어 (브라질)", - + "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": "ไทย", "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.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": "OpenCode의 새 버전({{version}})을 설치할 수 있습니다.", "toast.update.action.installRestart": "설치 및 다시 시작", "toast.update.action.notYet": "나중에", - "error.page.title": "문제가 발생했습니다", "error.page.description": "애플리케이션을 로드하는 동안 오류가 발생했습니다.", "error.page.details.label": "오류 세부 정보", @@ -336,12 +403,10 @@ export const dict = { "error.page.report.prefix": "이 오류를 OpenCode 팀에 제보해 주세요: ", "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 오류", @@ -361,43 +426,58 @@ export const dict = { "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.noChanges": "변경 없음", + "session.files.selectToOpen": "열 파일을 선택하세요", + "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.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": @@ -410,16 +490,16 @@ export const dict = { "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": "요청 실패", @@ -432,7 +512,8 @@ export const dict = { "common.close": "닫기", "common.edit": "편집", "common.loadMore": "더 불러오기", - + "common.key.esc": "ESC", + "sidebar.menu.toggle": "메뉴 토글", "sidebar.nav.projectsAndSessions": "프로젝트 및 세션", "sidebar.settings": "설정", "sidebar.help": "도움말", @@ -443,15 +524,21 @@ export const dict = { "sidebar.gettingStarted.line2": "Claude, GPT, Gemini 등을 포함한 모델을 사용하려면 공급자를 연결하세요.", "sidebar.project.recentSessions": "최근 세션", "sidebar.project.viewAllSessions": "모든 세션 보기", - + "sidebar.project.clearNotifications": "알림 지우기", + "app.name.desktop": "OpenCode 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 내부에서 OpenCode 서버를 실행합니다.", "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": "OpenCode 표시 언어 변경", "settings.general.row.appearance.title": "모양", @@ -460,21 +547,97 @@ export const dict = { "settings.general.row.theme.description": "OpenCode 테마 사용자 지정", "settings.general.row.font.title": "글꼴", "settings.general.row.font.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.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": "OpenCode를 실행할 때 업데이트를 자동으로 확인합니다", + "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": "현재 최신 버전의 OpenCode를 사용 중입니다.", + "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": "없음", + "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": "단축키 초기화됨", @@ -485,16 +648,21 @@ export const dict = { "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": "에이전트", @@ -503,16 +671,13 @@ export const dict = { "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": "편집", @@ -545,12 +710,10 @@ export const dict = { "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": "샌드박스", @@ -561,6 +724,7 @@ export const dict = { "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": "병합되지 않은 변경 사항이 감지되지 않았습니다.", @@ -575,4 +739,18 @@ export const dict = { "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 호환 공급자를 추가합니다.", } diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 5a82ec1e04b..d9dac8ee550 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -11,6 +11,7 @@ export const dict = { "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", @@ -30,6 +31,8 @@ export const dict = { "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", @@ -44,8 +47,12 @@ export const dict = { "command.session.new": "Ny sesjon", "command.file.open": "Åpne fil", - "command.file.open.description": "Søk i filer og kommandoer", + "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", @@ -65,8 +72,12 @@ export const dict = { "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.permissions.autoaccept.enable": "Godta endringer automatisk", - "command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk", + "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", @@ -80,7 +91,7 @@ export const dict = { "command.session.unshare": "Slutt å dele sesjon", "command.session.unshare.description": "Slutt å dele denne sesjonen", - "palette.search.placeholder": "Søk i filer og kommandoer", + "palette.search.placeholder": "Søk i filer, kommandoer og sesjoner", "palette.empty": "Ingen resultater funnet", "palette.group.commands": "Kommandoer", "palette.group.files": "Filer", @@ -90,7 +101,15 @@ export const dict = { "dialog.provider.group.popular": "Populære", "dialog.provider.group.other": "Andre", "dialog.provider.tag.recommended": "Anbefalt", - "dialog.provider.anthropic.note": "Koble til med Claude Pro/Max eller API-nøkkel", + "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", @@ -101,7 +120,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode", "dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører", - "dialog.provider.viewAll": "Vis alle 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", @@ -138,6 +157,46 @@ export const dict = { "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", @@ -157,9 +216,12 @@ export const dict = { "common.search.placeholder": "Søk", "common.goBack": "Gå tilbake", + "common.goForward": "Navigate forward", "common.loading": "Laster", "common.loading.ellipsis": "...", "common.cancel": "Avbryt", + "common.connect": "Koble til", + "common.disconnect": "Koble fra", "common.submit": "Send inn", "common.save": "Lagre", "common.saving": "Lagrer...", @@ -168,7 +230,11 @@ export const dict = { "prompt.placeholder.shell": "Skriv inn shell-kommando...", "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", @@ -200,7 +266,10 @@ export const dict = { "prompt.popover.emptyResults": "Ingen matchende resultater", "prompt.popover.emptyCommands": "Ingen matchende kommandoer", "prompt.dropzone.label": "Slipp bilder eller PDF-er 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", @@ -219,11 +288,15 @@ export const dict = { "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", @@ -243,7 +316,7 @@ export const dict = { "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", + "dialog.server.add.button": "Legg til 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.", @@ -252,6 +325,13 @@ export const dict = { "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", @@ -260,6 +340,9 @@ export const dict = { "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.', @@ -295,20 +378,22 @@ export const dict = { "context.usage.clickToView": "Klikk for å se kontekst", "context.usage.view": "Se kontekstforbruk", - "language.en": "Engelsk", - "language.zh": "Kinesisk (forenklet)", - "language.zht": "Kinesisk (tradisjonell)", - "language.ko": "Koreansk", - "language.de": "Tysk", - "language.es": "Spansk", - "language.fr": "Fransk", - "language.ja": "Japansk", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", "language.da": "Dansk", - "language.ru": "Russisk", - "language.pl": "Polsk", - "language.ar": "Arabisk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", "language.no": "Norsk", - "language.br": "Portugisisk (Brasil)", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "Språk", "toast.language.description": "Byttet til {{language}}", @@ -316,15 +401,24 @@ export const dict = { "toast.theme.title": "Tema byttet", "toast.scheme.title": "Fargevalg", - "toast.permissions.autoaccept.on.title": "Godtar endringer automatisk", - "toast.permissions.autoaccept.on.description": "Redigerings- og skrivetillatelser vil bli godkjent automatisk", - "toast.permissions.autoaccept.off.title": "Sluttet å godta endringer automatisk", - "toast.permissions.autoaccept.off.description": "Redigerings- og skrivetillatelser vil kreve godkjenning", + "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", @@ -359,6 +453,7 @@ export const dict = { "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:", @@ -400,8 +495,16 @@ export const dict = { "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.noChanges": "Ingen endringer", + + "session.files.selectToOpen": "Velg en fil å åpne", + "session.files.all": "Alle 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", @@ -409,7 +512,11 @@ export const dict = { "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.new.title": "Bygg hva som helst", "session.new.worktree.main": "Hovedgren", "session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})", "session.new.worktree.create": "Opprett nytt worktree", @@ -417,6 +524,19 @@ export const dict = { "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": @@ -451,6 +571,7 @@ export const dict = { "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", @@ -468,14 +589,24 @@ export const dict = { "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": "OpenCode 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 OpenCode-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 OpenCode", @@ -486,6 +617,87 @@ export const dict = { "settings.general.row.font.title": "Skrift", "settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker", + "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.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 OpenCode 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 OpenCode.", + "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": "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", @@ -521,6 +733,13 @@ export const dict = { "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", @@ -573,6 +792,11 @@ export const dict = { "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", @@ -583,6 +807,7 @@ export const dict = { "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.", @@ -597,4 +822,18 @@ export const dict = { "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.", } satisfies Partial> diff --git a/packages/app/src/i18n/parity.test.ts b/packages/app/src/i18n/parity.test.ts new file mode 100644 index 00000000000..c06a55ab171 --- /dev/null +++ b/packages/app/src/i18n/parity.test.ts @@ -0,0 +1,32 @@ +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 index 9d95424dd60..b63fe5ee409 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -8,6 +8,7 @@ export const dict = { "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", @@ -15,11 +16,9 @@ export const dict = { "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ę", @@ -27,22 +26,24 @@ export const dict = { "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.file.open.description": "Szukaj plików i poleceń", + "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", @@ -62,8 +63,12 @@ export const dict = { "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.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji", - "command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji", + "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", @@ -76,30 +81,32 @@ export const dict = { "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 i poleceń", + "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.anthropic.note": "Połącz z Claude Pro/Max lub kluczem API", - + "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.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode", "dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców", - - "dialog.provider.viewAll": "Zobacz wszystkich 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}}.", @@ -134,7 +141,45 @@ export const dict = { "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", @@ -151,23 +196,27 @@ export const dict = { "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.submit": "Prześlij", "common.save": "Zapisz", "common.saving": "Zapisywanie...", "common.default": "Domyślny", "common.attachment": "załącznik", - "prompt.placeholder.shell": "Wpisz polecenie terminala...", "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", @@ -193,11 +242,13 @@ export const dict = { "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ść obrazy lub pliki PDF tutaj", + "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", @@ -206,7 +257,6 @@ export const dict = { "prompt.attachment.remove": "Usuń załącznik", "prompt.action.send": "Wyślij", "prompt.action.stop": "Zatrzymaj", - "prompt.toast.pasteUnsupported.title": "Nieobsługiwane wklejanie", "prompt.toast.pasteUnsupported.description": "Tylko obrazy lub pliki PDF mogą być tutaj wklejane.", "prompt.toast.modelAgentRequired.title": "Wybierz agenta i model", @@ -216,21 +266,19 @@ export const dict = { "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 OpenCode, z którym łączy się ta aplikacja.", "dialog.server.search.placeholder": "Szukaj serwerów", @@ -240,7 +288,7 @@ export const dict = { "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", + "dialog.server.add.button": "Dodaj 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.", @@ -248,7 +296,12 @@ export const dict = { "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", @@ -257,7 +310,9 @@ export const dict = { "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", @@ -265,10 +320,8 @@ export const dict = { "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", @@ -285,61 +338,59 @@ export const dict = { "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": "Angielski", - "language.zh": "Chiński", - "language.ko": "Koreański", - "language.de": "Niemiecki", - "language.es": "Hiszpański", - "language.fr": "Francuski", - "language.ja": "Japoński", - "language.da": "Duński", + "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": "Rosyjski", - "language.ar": "Arabski", - "language.no": "Norweski", - "language.br": "Portugalski (Brazylia)", - + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "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.permissions.autoaccept.on.title": "Automatyczne akceptowanie edycji", - "toast.permissions.autoaccept.on.description": "Uprawnienia do edycji i zapisu będą automatycznie zatwierdzane", - "toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie edycji", - "toast.permissions.autoaccept.off.description": "Uprawnienia do edycji i zapisu będą wymagały zatwierdzenia", - + "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 OpenCode ({{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", @@ -350,12 +401,10 @@ export const dict = { "error.page.report.prefix": "Proszę zgłosić ten błąd do zespołu OpenCode", "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", @@ -365,8 +414,7 @@ export const dict = { "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": - 'Serwer MCP "{{name}}" nie powiódł się. Uwaga, OpenCode nie obsługuje jeszcze uwierzytelniania MCP.', + "error.chain.mcpFailed": 'MCP server "{{name}}" failed. Note, OpenCode 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ę.', @@ -377,44 +425,58 @@ export const dict = { "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.noChanges": "Brak zmian", + "session.files.selectToOpen": "Wybierz plik do otwarcia", + "session.files.all": "Wszystkie pliki", + "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.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.", @@ -428,10 +490,8 @@ export const dict = { "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", @@ -440,7 +500,6 @@ export const dict = { "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ę", @@ -454,7 +513,6 @@ export const dict = { "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", @@ -466,15 +524,21 @@ export const dict = { "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": "OpenCode 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 OpenCode 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 OpenCode", "settings.general.row.appearance.title": "Wygląd", @@ -483,17 +547,40 @@ export const dict = { "settings.general.row.theme.description": "Dostosuj motyw OpenCode.", "settings.general.row.font.title": "Czcionka", "settings.general.row.font.description": "Dostosuj czcionkę mono używaną w blokach kodu", + "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.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 OpenCode", + "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 OpenCode.", "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": "Brak", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", @@ -539,7 +626,6 @@ export const dict = { "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", @@ -548,14 +634,12 @@ export const dict = { "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", @@ -566,16 +650,21 @@ export const dict = { "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", @@ -584,16 +673,13 @@ export const dict = { "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", @@ -626,12 +712,10 @@ export const dict = { "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", @@ -642,6 +726,7 @@ export const dict = { "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.", @@ -656,4 +741,18 @@ export const dict = { "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.", } diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index dd399e27cf4..aadb926d270 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -8,6 +8,7 @@ export const dict = { "command.category.theme": "Тема", "command.category.language": "Язык", "command.category.file": "Файл", + "command.category.context": "Контекст", "command.category.terminal": "Терминал", "command.category.model": "Модель", "command.category.mcp": "MCP", @@ -27,6 +28,8 @@ export const dict = { "command.settings.open": "Открыть настройки", "command.session.previous": "Предыдущая сессия", "command.session.next": "Следующая сессия", + "command.session.previous.unseen": "Предыдущая непрочитанная сессия", + "command.session.next.unseen": "Следующая непрочитанная сессия", "command.session.archive": "Архивировать сессию", "command.palette": "Палитра команд", @@ -41,8 +44,12 @@ export const dict = { "command.session.new": "Новая сессия", "command.file.open": "Открыть файл", - "command.file.open.description": "Поиск файлов и команд", + "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": "Создать новую вкладку терминала", @@ -62,8 +69,12 @@ export const dict = { "command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту", "command.model.variant.cycle": "Цикл режимов мышления", "command.model.variant.cycle.description": "Переключиться к следующему уровню усилий", - "command.permissions.autoaccept.enable": "Авто-принятие изменений", - "command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений", + "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": "Повторить", @@ -77,7 +88,7 @@ export const dict = { "command.session.unshare": "Отменить публикацию", "command.session.unshare.description": "Прекратить публикацию сессии", - "palette.search.placeholder": "Поиск файлов и команд", + "palette.search.placeholder": "Поиск файлов, команд и сессий", "palette.empty": "Ничего не найдено", "palette.group.commands": "Команды", "palette.group.files": "Файлы", @@ -87,7 +98,15 @@ export const dict = { "dialog.provider.group.popular": "Популярные", "dialog.provider.group.other": "Другие", "dialog.provider.tag.recommended": "Рекомендуемые", - "dialog.provider.anthropic.note": "Подключитесь с помощью Claude Pro/Max или API ключа", + "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": "Поиск моделей", @@ -98,7 +117,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode", "dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров", - "dialog.provider.viewAll": "Посмотреть всех провайдеров", + "dialog.provider.viewAll": "Показать больше провайдеров", "provider.connect.title": "Подключить {{provider}}", "provider.connect.title.anthropicProMax": "Войти с помощью Claude Pro/Max", @@ -135,6 +154,46 @@ export const dict = { "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", @@ -154,9 +213,12 @@ export const dict = { "common.search.placeholder": "Поиск", "common.goBack": "Назад", + "common.goForward": "Вперёд", "common.loading": "Загрузка", "common.loading.ellipsis": "...", "common.cancel": "Отмена", + "common.connect": "Подключить", + "common.disconnect": "Отключить", "common.submit": "Отправить", "common.save": "Сохранить", "common.saving": "Сохранение...", @@ -165,7 +227,11 @@ export const dict = { "prompt.placeholder.shell": "Введите команду оболочки...", "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 в коде", @@ -197,7 +263,10 @@ export const dict = { "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": "Удалить активный файл из контекста", @@ -216,11 +285,15 @@ export const dict = { "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": "требуется авторизация", @@ -240,7 +313,7 @@ export const dict = { "dialog.server.add.placeholder": "http://localhost:4096", "dialog.server.add.error": "Не удалось подключиться к серверу", "dialog.server.add.checking": "Проверка...", - "dialog.server.add.button": "Добавить", + "dialog.server.add.button": "Добавить сервер", "dialog.server.default.title": "Сервер по умолчанию", "dialog.server.default.description": "Подключаться к этому серверу при запуске приложения вместо запуска локального сервера. Требуется перезапуск.", @@ -249,6 +322,13 @@ export const dict = { "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": "Иконка", @@ -258,6 +338,10 @@ export const dict = { "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": 'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.', @@ -293,18 +377,22 @@ export const dict = { "context.usage.clickToView": "Нажмите для просмотра контекста", "context.usage.view": "Показать использование контекста", - "language.en": "Английский", - "language.zh": "Китайский", - "language.ko": "Корейский", - "language.de": "Немецкий", - "language.es": "Испанский", - "language.fr": "Французский", - "language.ja": "Японский", - "language.da": "Датский", + "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": "Норвежский", - "language.br": "Португальский (Бразилия)", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", "toast.language.title": "Язык", "toast.language.description": "Переключено на {{language}}", @@ -312,16 +400,24 @@ export const dict = { "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.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 скопирован в буфер обмена!", @@ -355,6 +451,7 @@ export const dict = { "Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?", "error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?", + "directory.error.invalidUrl": "Недопустимая директория в URL.", "error.chain.unknown": "Неизвестная ошибка", "error.chain.causedBy": "Причина:", @@ -398,8 +495,14 @@ export const dict = { "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.files.selectToOpen": "Выберите файл, чтобы открыть", + "session.files.all": "Все файлы", + "session.files.binaryContent": "Двоичный файл (содержимое не может быть отображено)", "session.messages.renderEarlier": "Показать предыдущие сообщения", "session.messages.loadingEarlier": "Загрузка предыдущих сообщений...", "session.messages.loadEarlier": "Загрузить предыдущие сообщения", @@ -407,7 +510,11 @@ export const dict = { "session.messages.jumpToLatest": "Перейти к последнему", "session.context.addToContext": "Добавить {{selection}} в контекст", + "session.todo.title": "Задачи", + "session.todo.collapse": "Свернуть", + "session.todo.expand": "Развернуть", + "session.new.title": "Создавайте что угодно", "session.new.worktree.main": "Основная ветка", "session.new.worktree.mainWithBranch": "Основная ветка ({{branch}})", "session.new.worktree.create": "Создать новый worktree", @@ -415,6 +522,19 @@ export const dict = { "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": @@ -468,14 +588,23 @@ export const dict = { "Подключите любого провайдера для использования моделей, включая Claude, GPT, Gemini и др.", "sidebar.project.recentSessions": "Недавние сессии", "sidebar.project.viewAllSessions": "Посмотреть все сессии", + "sidebar.project.clearNotifications": "Очистить уведомления", + "app.name.desktop": "OpenCode 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": "Запускать сервер OpenCode внутри 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": "Изменить язык отображения OpenCode", @@ -485,17 +614,43 @@ export const dict = { "settings.general.row.theme.description": "Настройте оформление OpenCode.", "settings.general.row.font.title": "Шрифт", "settings.general.row.font.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.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": "Автоматически проверять обновления при запуске OpenCode", + "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": "Вы используете последнюю версию OpenCode.", "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": "Нет", "sound.option.alert01": "Alert 01", "sound.option.alert02": "Alert 02", "sound.option.alert03": "Alert 03", @@ -578,6 +733,13 @@ export const dict = { "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": "Агенты", @@ -645,6 +807,7 @@ export const dict = { "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": "Неслитые изменения не обнаружены.", @@ -659,4 +822,18 @@ export const dict = { "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.", } diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts new file mode 100644 index 00000000000..6a25a356a96 --- /dev/null +++ b/packages/app/src/i18n/th.ts @@ -0,0 +1,829 @@ +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.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย OpenCode", + "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}} ใน OpenCode", + "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}} ใน OpenCode", + "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}} ใน OpenCode", + "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.submit": "ส่ง", + "common.save": "บันทึก", + "common.saving": "กำลังบันทึก...", + "common.default": "ค่าเริ่มต้น", + "common.attachment": "ไฟล์แนบ", + + "prompt.placeholder.shell": "ป้อนคำสั่งเชลล์...", + "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": "สลับเซิร์ฟเวอร์ OpenCode ที่แอปนี้เชื่อมต่อด้วย", + "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.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": "ไทย", + + "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": "เวอร์ชันใหม่ของ OpenCode ({{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": "โปรดรายงานข้อผิดพลาดนี้ให้ทีม OpenCode", + "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}}" ล้มเหลว โปรดทราบว่า OpenCode ยังไม่รองรับการตรวจสอบสิทธิ์ 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.files.selectToOpen": "เลือกไฟล์เพื่อเปิด", + "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.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": "OpenCode รวมถึงโมเดลฟรีเพื่อให้คุณเริ่มต้นได้ทันที", + "sidebar.gettingStarted.line2": "เชื่อมต่อผู้ให้บริการใด ๆ เพื่อใช้โมเดล รวมถึง Claude, GPT, Gemini ฯลฯ", + "sidebar.project.recentSessions": "เซสชันล่าสุด", + "sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด", + "sidebar.project.clearNotifications": "ล้างการแจ้งเตือน", + + "app.name.desktop": "OpenCode 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": "เรียกใช้เซิร์ฟเวอร์ OpenCode ภายใน 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": "เปลี่ยนภาษาที่แสดงสำหรับ OpenCode", + "settings.general.row.appearance.title": "รูปลักษณ์", + "settings.general.row.appearance.description": "ปรับแต่งวิธีการที่ OpenCode มีลักษณะบนอุปกรณ์ของคุณ", + "settings.general.row.theme.title": "ธีม", + "settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม", + "settings.general.row.font.title": "ฟอนต์", + "settings.general.row.font.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.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": "ตรวจสอบการอัปเดตโดยอัตโนมัติเมื่อ OpenCode เปิดใช้งาน", + "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": "คุณกำลังใช้งาน OpenCode เวอร์ชันล่าสุด", + + "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": "ไม่มี", + "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.todoread.title": "อ่านรายการงาน", + "settings.permissions.tool.todoread.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 หลัก", +} diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts new file mode 100644 index 00000000000..50e55983247 --- /dev/null +++ b/packages/app/src/i18n/tr.ts @@ -0,0 +1,850 @@ +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": "OpenCode 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 OpenCode'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 OpenCode'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 OpenCode'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.submit": "Gönder", + "common.save": "Kaydet", + "common.saving": "Kaydediliyor...", + "common.default": "Varsayılan", + "common.attachment": "ek", + + "prompt.placeholder.shell": "Kabuk komutu girin...", + "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": "Görsel veya PDF'leri 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 yapıştırma", + "prompt.toast.pasteUnsupported.description": "Buraya sadece görsel veya PDF yapıştırılabilir.", + "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 OpenCode 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.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": "OpenCode'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ı OpenCode 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: OpenCode 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.noChanges": "Değişiklik yok", + + "session.files.selectToOpen": "Açmak için bir dosya seçin", + "session.files.all": "Tüm dosyalar", + "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.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": "OpenCode ü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": "OpenCode 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": "OpenCode 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": "OpenCode'un görünüm dilini değiştirin", + "settings.general.row.appearance.title": "Görünüm", + "settings.general.row.appearance.description": "OpenCode'un cihazınızdaki görünümünü özelleştirin", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.", + "settings.general.row.font.title": "Yazı Tipi", + "settings.general.row.font.description": "Kod bloklarında kullanılan monospace yazı tipini özelleştirin", + "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.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": "OpenCode 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": "OpenCode'un en son sürümünü kullanıyorsunuz.", + + "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": "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.todoread.title": "Görev Oku", + "settings.permissions.tool.todoread.description": "Görev listesini oku", + "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.", +} satisfies Partial> diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 079c96baafb..1f88a822235 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -12,60 +12,96 @@ export const dict = { "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.set": "使用主题:{{theme}}", "command.theme.scheme.cycle": "切换配色方案", - "command.theme.scheme.set": "使用配色方案: {{scheme}}", + "command.theme.scheme.set": "使用配色方案:{{scheme}}", "command.language.cycle": "切换语言", - "command.language.set": "使用语言: {{language}}", + "command.language.set": "使用语言:{{language}}", "command.session.new": "新建会话", + "command.file.open": "打开文件", - "command.file.open.description": "搜索文件和命令", + + "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.permissions.autoaccept.enable": "自动接受编辑", - "command.permissions.autoaccept.disable": "停止自动接受编辑", + + "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": "重做", @@ -79,7 +115,7 @@ export const dict = { "command.session.unshare": "取消分享会话", "command.session.unshare.description": "停止分享此会话", - "palette.search.placeholder": "搜索文件和命令", + "palette.search.placeholder": "搜索文件、命令和会话", "palette.empty": "未找到结果", "palette.group.commands": "命令", "palette.group.files": "文件", @@ -89,18 +125,25 @@ export const dict = { "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.unpaid.freeModels.title": "OpenCode 提供的免费模型", "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", - "dialog.provider.viewAll": "查看全部提供商", + "dialog.provider.viewAll": "查看更多提供商", "provider.connect.title": "连接 {{provider}}", "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录", @@ -108,7 +151,7 @@ export const dict = { "provider.connect.method.apiKey": "API 密钥", "provider.connect.status.inProgress": "正在授权...", "provider.connect.status.waiting": "等待授权...", - "provider.connect.status.failed": "授权失败: {{error}}", + "provider.connect.status.failed": "授权失败:{{error}}", "provider.connect.apiKey.description": "输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。", "provider.connect.apiKey.label": "{{provider}} API 密钥", @@ -117,6 +160,7 @@ export const dict = { "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": "此链接", @@ -132,13 +176,71 @@ export const dict = { "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.submit": "提交", "common.save": "保存", "common.saving": "保存中...", @@ -147,39 +249,44 @@ export const dict = { "prompt.placeholder.shell": "输入 shell 命令...", "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.2": "这个项目的技术栈是什么?", "prompt.example.3": "修复失败的测试", "prompt.example.4": "解释认证是如何工作的", "prompt.example.5": "查找并修复安全漏洞", "prompt.example.6": "为用户服务添加单元测试", "prompt.example.7": "重构这个函数,让它更易读", - "prompt.example.8": "这个错误是什么意思?", + "prompt.example.8": "这个错误是什么意思?", "prompt.example.9": "帮我调试这个问题", "prompt.example.10": "生成 API 文档", "prompt.example.11": "优化数据库查询", "prompt.example.12": "添加输入校验", "prompt.example.13": "创建一个新的组件用于...", - "prompt.example.14": "我该如何部署这个项目?", + "prompt.example.14": "我该如何部署这个项目?", "prompt.example.15": "审查我的代码并给出最佳实践建议", "prompt.example.16": "为这个函数添加错误处理", "prompt.example.17": "解释这个正则表达式", "prompt.example.18": "把它转换成 TypeScript", "prompt.example.19": "在整个代码库中添加日志", - "prompt.example.20": "哪些依赖已经过期?", + "prompt.example.20": "哪些依赖已经过期?", "prompt.example.21": "帮我写一个迁移脚本", "prompt.example.22": "为这个接口实现缓存", "prompt.example.23": "给这个列表添加分页", "prompt.example.24": "创建一个 CLI 命令用于...", - "prompt.example.25": "这里的环境变量是怎么工作的?", - + "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": "从上下文移除活动文件", @@ -188,7 +295,6 @@ export const dict = { "prompt.attachment.remove": "移除附件", "prompt.action.send": "发送", "prompt.action.stop": "停止", - "prompt.toast.pasteUnsupported.title": "不支持的粘贴", "prompt.toast.pasteUnsupported.description": "这里只能粘贴图片或 PDF 文件。", "prompt.toast.modelAgentRequired.title": "请选择智能体和模型", @@ -198,11 +304,16 @@ export const dict = { "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": "需要授权", @@ -222,13 +333,19 @@ export const dict = { "dialog.server.add.placeholder": "http://localhost:4096", "dialog.server.add.error": "无法连接到服务器", "dialog.server.add.checking": "检查中...", - "dialog.server.add.button": "添加", + "dialog.server.add.button": "添加服务器", "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": "名称", @@ -238,6 +355,9 @@ export const dict = { "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 的大致拆分。“其他”包含工具定义和开销。", @@ -246,10 +366,8 @@ export const dict = { "context.breakdown.assistant": "助手", "context.breakdown.tool": "工具调用", "context.breakdown.other": "其他", - "context.systemPrompt.title": "系统提示词", "context.rawMessages.title": "原始消息", - "context.stats.session": "会话", "context.stats.messages": "消息数", "context.stats.provider": "提供商", @@ -266,57 +384,57 @@ export const dict = { "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": "英语", + "language.en": "English", "language.zh": "简体中文", - "language.zht": "繁体中文", - "language.ko": "韩语", - "language.de": "德语", - "language.es": "西班牙语", - "language.fr": "法语", - "language.ja": "日语", - "language.da": "丹麦语", - "language.ru": "俄语", - "language.pl": "波兰语", - "language.ar": "阿拉伯语", - "language.no": "挪威语", - "language.br": "葡萄牙语(巴西)", + "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": "ไทย", "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.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": "OpenCode 有新版本 ({{version}}) 可安装。", "toast.update.action.installRestart": "安装并重启", @@ -331,38 +449,37 @@ export const dict = { "error.page.action.updateTo": "更新到 {{version}}", "error.page.report.prefix": "请将此错误报告给 OpenCode 团队", "error.page.report.discord": "在 Discord 上", - "error.page.version": "版本: {{version}}", - - "error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html? 或者 id 属性拼写错了?", + "error.page.version": "版本:{{version}}", + "error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?", + "error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", - "error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", + "directory.error.invalidUrl": "URL 中的目录无效。", "error.chain.unknown": "未知错误", - "error.chain.causedBy": "原因:", + "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.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}}" 启动失败。注意: OpenCode 暂不支持 MCP 认证。', - "error.chain.providerAuthFailed": "提供商认证失败 ({{provider}}): {{message}}", + "error.chain.providerAuthFailed": "提供商认证失败({{provider}}):{{message}}", "error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。', "error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)", - "error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C): {{message}}", + "error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C):{{message}}", "error.chain.configDirectoryTypo": '{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。', - "error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}", + "error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}", "error.chain.configInvalid": "配置文件 {{path}} 无效", - "error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效: {{message}}", + "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": "发生错误", @@ -376,22 +493,43 @@ export const dict = { "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.files.selectToOpen": "选择要打开的文件", + "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.new.title": "构建任何东西", "session.new.worktree.main": "主分支", - "session.new.worktree.mainWithBranch": "主分支 ({{branch}})", + "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": "此会话已在网页上公开。任何拥有链接的人都可以访问。", @@ -409,10 +547,13 @@ export const dict = { "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": "忽略", @@ -426,7 +567,9 @@ export const dict = { "common.close": "关闭", "common.edit": "编辑", "common.loadMore": "加载更多", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "切换菜单", "sidebar.nav.projectsAndSessions": "项目和会话", "sidebar.settings": "设置", "sidebar.help": "帮助", @@ -437,15 +580,26 @@ export const dict = { "sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。", "sidebar.project.recentSessions": "最近会话", "sidebar.project.viewAllSessions": "查看全部会话", + "sidebar.project.clearNotifications": "清除通知", + + "app.name.desktop": "OpenCode 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 环境中运行 OpenCode 服务器。", + "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": "更改 OpenCode 的显示语言", "settings.general.row.appearance.title": "外观", @@ -454,6 +608,85 @@ export const dict = { "settings.general.row.theme.description": "自定义 OpenCode 的主题。", "settings.general.row.font.title": "字体", "settings.general.row.font.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.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": "在 OpenCode 启动时自动检查更新", + "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": "你正在使用最新版本的 OpenCode。", + + "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": "无", + "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": "当智能体完成或需要注意时显示系统通知", @@ -461,7 +694,6 @@ export const dict = { "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": "权限", @@ -479,7 +711,6 @@ export const dict = { "settings.shortcuts.pressKeys": "按下按键", "settings.shortcuts.search.placeholder": "搜索快捷键", "settings.shortcuts.search.empty": "未找到快捷键", - "settings.shortcuts.group.general": "通用", "settings.shortcuts.group.session": "会话", "settings.shortcuts.group.navigation": "导航", @@ -489,12 +720,23 @@ export const dict = { "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 设置将在此处可配置。", @@ -502,11 +744,9 @@ export const dict = { "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": "编辑", @@ -519,9 +759,9 @@ export const dict = { "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.title": "任务", "settings.permissions.tool.task.description": "启动子智能体", - "settings.permissions.tool.skill.title": "Skill", + "settings.permissions.tool.skill.title": "技能", "settings.permissions.tool.skill.description": "按名称加载技能", "settings.permissions.tool.lsp.title": "LSP", "settings.permissions.tool.lsp.description": "运行语言服务器查询", @@ -529,20 +769,20 @@ export const dict = { "settings.permissions.tool.todoread.description": "读取待办列表", "settings.permissions.tool.todowrite.title": "更新待办", "settings.permissions.tool.todowrite.description": "更新待办列表", - "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.title": "网页获取", "settings.permissions.tool.webfetch.description": "从 URL 获取内容", - "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.title": "网页搜索", "settings.permissions.tool.websearch.description": "搜索网页", - "settings.permissions.tool.codesearch.title": "Code Search", + "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.title": "死循环", "settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用", "session.delete.failed.title": "删除会话失败", "session.delete.title": "删除会话", - "session.delete.confirm": '删除会话 "{{name}}"?', + "session.delete.confirm": '删除会话 "{{name}}"?', "session.delete.button": "删除会话", "workspace.new": "新建工作区", @@ -555,18 +795,33 @@ export const dict = { "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.confirm": '删除工作区 "{{name}}"?', "workspace.delete.button": "删除工作区", "workspace.reset.title": "重置工作区", - "workspace.reset.confirm": '重置工作区 "{{name}}"?', + "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 兼容的提供商。", } satisfies Partial> diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 35b25dd1dc2..a75e8ef47a6 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -12,6 +12,7 @@ export const dict = { "command.category.theme": "主題", "command.category.language": "語言", "command.category.file": "檔案", + "command.category.context": "上下文", "command.category.terminal": "終端機", "command.category.model": "模型", "command.category.mcp": "MCP", @@ -19,6 +20,7 @@ export const dict = { "command.category.permissions": "權限", "command.category.workspace": "工作區", + "command.category.settings": "設定", "theme.scheme.system": "系統", "theme.scheme.light": "淺色", "theme.scheme.dark": "深色", @@ -27,8 +29,11 @@ export const dict = { "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": "命令面板", @@ -43,8 +48,12 @@ export const dict = { "command.session.new": "新增工作階段", "command.file.open": "開啟檔案", - "command.file.open.description": "搜尋檔案和命令", + "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": "建立新的終端機標籤頁", @@ -64,8 +73,12 @@ export const dict = { "command.agent.cycle.reverse.description": "切換到上一個代理程式", "command.model.variant.cycle": "循環思考強度", "command.model.variant.cycle.description": "切換到下一個強度等級", - "command.permissions.autoaccept.enable": "自動接受編輯", - "command.permissions.autoaccept.disable": "停止自動接受編輯", + "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": "重做", @@ -79,7 +92,7 @@ export const dict = { "command.session.unshare": "取消分享工作階段", "command.session.unshare.description": "停止分享此工作階段", - "palette.search.placeholder": "搜尋檔案和命令", + "palette.search.placeholder": "搜尋檔案、命令和工作階段", "palette.empty": "找不到結果", "palette.group.commands": "命令", "palette.group.files": "檔案", @@ -89,7 +102,15 @@ export const dict = { "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": "搜尋模型", @@ -100,7 +121,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型", "dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型", - "dialog.provider.viewAll": "查看全部提供者", + "dialog.provider.viewAll": "查看更多提供者", "provider.connect.title": "連線 {{provider}}", "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登入", @@ -134,13 +155,70 @@ export const dict = { "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.submit": "提交", "common.save": "儲存", "common.saving": "儲存中...", @@ -149,7 +227,11 @@ export const dict = { "prompt.placeholder.shell": "輸入 shell 命令...", "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", @@ -181,7 +263,10 @@ export const dict = { "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": "從上下文移除目前檔案", @@ -200,11 +285,15 @@ export const dict = { "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": "需要授權", @@ -224,7 +313,7 @@ export const dict = { "dialog.server.add.placeholder": "http://localhost:4096", "dialog.server.add.error": "無法連線到伺服器", "dialog.server.add.checking": "檢查中...", - "dialog.server.add.button": "新增", + "dialog.server.add.button": "新增伺服器", "dialog.server.default.title": "預設伺服器", "dialog.server.default.description": "應用程式啟動時連線此伺服器,而不是啟動本地伺服器。需要重新啟動。", "dialog.server.default.none": "未選擇伺服器", @@ -232,6 +321,13 @@ export const dict = { "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": "圖示", @@ -241,6 +337,9 @@ export const dict = { "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": "系統", @@ -275,31 +374,47 @@ export const dict = { "context.usage.clickToView": "點擊查看上下文", "context.usage.view": "檢視上下文用量", - "language.en": "英語", - "language.zh": "簡體中文", + "language.en": "English", + "language.zh": "简体中文", "language.zht": "繁體中文", - "language.ko": "韓語", - "language.ru": "俄語", - "language.ar": "阿拉伯語", - "language.no": "挪威語", - "language.br": "葡萄牙語(巴西)", + "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": "ไทย", "toast.language.title": "語言", "toast.language.description": "已切換到 {{language}}", "toast.theme.title": "主題已切換", - "toast.scheme.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.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": "分享連結已複製到剪貼簿", @@ -332,6 +447,7 @@ export const dict = { "error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html? 或者 id 屬性拼錯了?", "error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?", + "directory.error.invalidUrl": "URL 中的目錄無效。", "error.chain.unknown": "未知錯誤", "error.chain.causedBy": "原因:", @@ -372,15 +488,26 @@ export const dict = { "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.files.selectToOpen": "選取要開啟的檔案", + "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.new.title": "建構任何東西", "session.new.worktree.main": "主分支", "session.new.worktree.mainWithBranch": "主分支 ({{branch}})", "session.new.worktree.create": "建立新的 worktree", @@ -388,6 +515,19 @@ export const dict = { "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": "此工作階段已在網頁上公開。任何擁有連結的人都可以存取。", @@ -410,6 +550,8 @@ export const dict = { "terminal.title.numbered": "終端機 {{number}}", "terminal.close": "關閉終端機", + "terminal.connectionLost.title": "連線中斷", + "terminal.connectionLost.description": "終端機連線已中斷。這可能會在伺服器重新啟動時發生。", "common.closeTab": "關閉標籤頁", "common.dismiss": "忽略", "common.requestFailed": "要求失敗", @@ -423,6 +565,8 @@ export const dict = { "common.edit": "編輯", "common.loadMore": "載入更多", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "切換選單", "sidebar.nav.projectsAndSessions": "專案與工作階段", "sidebar.settings": "設定", "sidebar.help": "說明", @@ -433,14 +577,23 @@ export const dict = { "sidebar.gettingStarted.line2": "連線任意提供者即可使用更多模型,如 Claude、GPT、Gemini 等。", "sidebar.project.recentSessions": "最近工作階段", "sidebar.project.viewAllSessions": "查看全部工作階段", + "sidebar.project.clearNotifications": "清除通知", + "app.name.desktop": "OpenCode Desktop", "settings.section.desktop": "桌面", + "settings.section.server": "伺服器", "settings.tab.general": "一般", "settings.tab.shortcuts": "快速鍵", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on 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": "變更 OpenCode 的顯示語言", @@ -451,6 +604,85 @@ export const dict = { "settings.general.row.font.title": "字型", "settings.general.row.font.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.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": "在 OpenCode 啟動時自動檢查更新", + "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": "你正在使用最新版本的 OpenCode。", + + "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": "無", + "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": "權限", @@ -485,6 +717,13 @@ export const dict = { "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": "代理程式", @@ -551,6 +790,7 @@ export const dict = { "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": "未偵測到未合併的變更。", @@ -565,4 +805,18 @@ export const dict = { "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 相容的提供者。", } satisfies Partial> diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 3d7b9db7af9..9e231e2d285 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -1,57 +1,29 @@ @import "@opencode-ai/ui/styles/tailwind"; -:root { - a { - cursor: default; +@layer components { + [data-component="getting-started"] { + container-type: inline-size; + container-name: getting-started; } -} - -[data-component="markdown"] ul { - list-style: disc outside; - padding-left: 1.5rem; -} - -[data-component="markdown"] ol { - list-style: decimal outside; - padding-left: 1.5rem; -} -[data-component="markdown"] li > p:first-child { - display: inline; - margin: 0; -} - -[data-component="markdown"] li > p + p { - display: block; - margin-top: 0.5rem; -} - -*[data-tauri-drag-region] { - app-region: drag; -} - -.session-scroller::-webkit-scrollbar { - width: 10px !important; - height: 10px !important; -} - -.session-scroller::-webkit-scrollbar-track { - background: transparent !important; - border-radius: 5px !important; -} + [data-component="getting-started-actions"] { + display: flex; + flex-direction: column; + gap: 0.75rem; /* gap-3 */ + } -.session-scroller::-webkit-scrollbar-thumb { - background: var(--border-weak-base) !important; - border-radius: 5px !important; - border: 3px solid transparent !important; - background-clip: padding-box !important; -} + [data-component="getting-started-actions"] > [data-component="button"] { + width: 100%; + } -.session-scroller::-webkit-scrollbar-thumb:hover { - background: var(--border-weak-base) !important; -} + @container getting-started (min-width: 17rem) { + [data-component="getting-started-actions"] { + flex-direction: row; + align-items: center; + } -.session-scroller { - scrollbar-width: thin !important; - scrollbar-color: var(--border-weak-base) transparent !important; + [data-component="getting-started-actions"] > [data-component="button"] { + width: auto; + } + } } diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index df3181133e9..6c870dfa4d0 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -1,2 +1,5 @@ -export { PlatformProvider, type Platform } from "./context/platform" export { AppBaseProviders, AppInterface } from "./app" +export { useCommand } from "./context/command" +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 index dca02489a8a..fdf321f2dc3 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,57 +1,93 @@ -import { createMemo, Show, type ParentProps } from "solid-js" -import { useNavigate, useParams } from "@solidjs/router" -import { SDKProvider, useSDK } from "@/context/sdk" +import { batch, createEffect, createMemo, Show, type ParentProps } from "solid-js" +import { createStore } from "solid-js/store" +import { useLocation, useNavigate, useParams } from "@solidjs/router" +import { SDKProvider } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" import { LocalProvider } from "@/context/local" +import { useGlobalSDK } from "@/context/global-sdk" -import { base64Decode } from "@opencode-ai/util/encode" import { DataProvider } from "@opencode-ai/ui/context" -import { iife } from "@opencode-ai/util/iife" -import type { QuestionAnswer } from "@opencode-ai/sdk/v2" +import { base64Encode } from "@opencode-ai/util/encode" +import { decode64 } from "@/utils/base64" +import { showToast } from "@opencode-ai/ui/toast" +import { useLanguage } from "@/context/language" +function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { + const navigate = useNavigate() + const sync = useSync() + const slug = createMemo(() => base64Encode(props.directory)) + + return ( + navigate(`/${slug()}/session/${sessionID}`)} + onSessionHref={(sessionID: string) => `/${slug()}/session/${sessionID}`} + > + {props.children} + + ) +} export default function Layout(props: ParentProps) { const params = useParams() const navigate = useNavigate() - const directory = createMemo(() => { - return base64Decode(params.dir!) - }) - return ( - - - - {iife(() => { - const sync = useSync() - const sdk = useSDK() - const respond = (input: { - sessionID: string - permissionID: string - response: "once" | "always" | "reject" - }) => sdk.client.permission.respond(input) - - const replyToQuestion = (input: { requestID: string; answers: QuestionAnswer[] }) => - sdk.client.question.reply(input) + const location = useLocation() + const language = useLanguage() + const globalSDK = useGlobalSDK() + const directory = createMemo(() => decode64(params.dir) ?? "") + const [state, setState] = createStore({ invalid: "", resolved: "" }) - const rejectQuestion = (input: { requestID: string }) => sdk.client.question.reject(input) + createEffect(() => { + if (!params.dir) return + const raw = directory() + if (!raw) { + if (state.invalid === params.dir) return + setState("invalid", params.dir) + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: language.t("directory.error.invalidUrl"), + }) + navigate("/", { replace: true }) + return + } - const navigateToSession = (sessionID: string) => { - navigate(`/${params.dir}/session/${sessionID}`) - } + const current = params.dir + globalSDK + .createClient({ + directory: raw, + throwOnError: true, + }) + .path.get() + .then((x) => { + if (params.dir !== current) return + const next = x.data?.directory ?? raw + batch(() => { + setState("invalid", "") + setState("resolved", next) + }) + if (next === raw) return + const path = location.pathname.slice(current.length + 1) + navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true }) + }) + .catch(() => { + if (params.dir !== current) return + batch(() => { + setState("invalid", "") + setState("resolved", raw) + }) + }) + }) - return ( - - {props.children} - - ) - })} - - + return ( + + {(resolved) => ( + + + {props.children} + + + )} ) } diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx index 6d6faf6fa3b..a30d86d1809 100644 --- a/packages/app/src/pages/error.tsx +++ b/packages/app/src/pages/error.tsx @@ -13,6 +13,17 @@ export type InitError = { } 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 ( @@ -112,9 +123,7 @@ function formatInitError(error: InitError, t: Translator): string { } case "ConfigInvalidError": { const issues = Array.isArray(data.issues) - ? data.issues.map( - (issue: { message: string; path: string[] }) => "↳ " + issue.message + " " + issue.path.join("."), - ) + ? 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 : safeJson(data.path) @@ -139,14 +148,14 @@ function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessag if (isInitError(error)) { const message = formatInitError(error, t) if (depth > 0 && parentMessage === message) return "" - const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" + 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${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" + 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() @@ -190,11 +199,11 @@ function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessag if (typeof error === "string") { if (depth > 0 && parentMessage === error) return "" - const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" return indent + error } - const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : "" + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" return indent + safeJson(error) } @@ -212,20 +221,35 @@ export const ErrorPage: Component = (props) => { 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) - const result = await platform.checkUpdate() - setStore("checking", false) - if (result.updateAvailable && result.version) setStore("version", result.version) + 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.update || !platform.restart) return - await platform.update() - await platform.restart() + await platform + .update() + .then(() => platform.restart!()) + .then(() => setStore("actionError", undefined)) + .catch((err) => { + setStore("actionError", formatError(err, language.t)) + }) } return ( @@ -266,6 +290,9 @@ export const ErrorPage: Component = (props) => {
+ + {(message) =>

{message()}

} +
{language.t("error.page.report.prefix")} diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 4007129b5cc..ba3a2b94270 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -1,4 +1,4 @@ -import { createMemo, For, Match, Show, Switch } from "solid-js" +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" @@ -23,6 +23,19 @@ export default function Home() { 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) @@ -67,9 +80,7 @@ export default function Home() {
{server.name} @@ -84,11 +95,7 @@ export default function Home() {
    - (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) - .slice(0, 5)} - > + {(project) => (
{language.t("home.empty.description")}
-
-
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 12e11e724a0..da857a60312 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -4,58 +4,55 @@ import { createMemo, createSignal, For, - Match, on, onCleanup, onMount, ParentProps, Show, - Switch, untrack, - type Accessor, - type JSX, } from "solid-js" -import { A, useNavigate, useParams } from "@solidjs/router" -import { useLayout, getAvatarColors, LocalProject } from "@/context/layout" +import { useNavigate, useParams } from "@solidjs/router" +import { useLayout, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { Persist, persisted } from "@/utils/persist" -import { base64Decode, base64Encode } from "@opencode-ai/util/encode" -import { Avatar } from "@opencode-ai/ui/avatar" +import { base64Encode } from "@opencode-ai/util/encode" +import { decode64 } from "@/utils/base64" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" -import { InlineInput } from "@opencode-ai/ui/inline-input" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { HoverCard } from "@opencode-ai/ui/hover-card" -import { MessageNav } from "@opencode-ai/ui/message-nav" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Collapsible } from "@opencode-ai/ui/collapsible" -import { DiffChanges } from "@opencode-ai/ui/diff-changes" -import { Spinner } from "@opencode-ai/ui/spinner" import { Dialog } from "@opencode-ai/ui/dialog" import { getFilename } from "@opencode-ai/util/path" -import { Session, type Message, type TextPart } from "@opencode-ai/sdk/v2/client" +import { Session, type Message } from "@opencode-ai/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, - createSortable, -} from "@thisbeyond/solid-dnd" +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, + SESSION_PREFETCH_TTL, + setSessionPrefetch, +} from "@/context/global-sync/session-prefetch" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" import { Binary } from "@opencode-ai/util/binary" import { retry } from "@opencode-ai/util/retry" import { playSound, soundSrc } 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" @@ -63,39 +60,57 @@ import { DialogSelectProvider } from "@/components/dialog-select-provider" import { DialogSelectServer } from "@/components/dialog-select-server" import { DialogSettings } from "@/components/dialog-settings" import { useCommand, type CommandOption } from "@/context/command" -import { ConstrainDragXAxis } from "@/utils/solid-dnd" -import { navStart } from "@/utils/perf" +import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogEditProject } from "@/components/dialog-edit-project" +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({ - lastSession: {} as { [directory: string]: string }, + 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 - const xlQuery = window.matchMedia("(min-width: 1280px)") - const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches) - const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches) - xlQuery.addEventListener("change", handleViewportChange) - onCleanup(() => xlQuery.removeEventListener("change", handleViewportChange)) const params = useParams() - const [autoselect, setAutoselect] = createSignal(!params.dir) const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() const layout = useLayout() @@ -106,12 +121,13 @@ export default function Layout(props: ParentProps) { 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 initialDir = params.dir + const initialDirectory = decode64(params.dir) const availableThemeEntries = createMemo(() => Object.entries(theme.themes())) const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"] const colorSchemeKey: Record = { @@ -120,130 +136,175 @@ export default function Layout(props: ParentProps) { dark: "theme.scheme.dark", } const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme]) - - const [editor, setEditor] = createStore({ - active: "" as string, - value: "", + const currentDir = createMemo(() => decode64(params.dir) ?? "") + + const [state, setState] = createStore({ + autoselect: !initialDirectory, + busyWorkspaces: {} as Record, + hoverSession: undefined as string | undefined, + hoverProject: undefined as string | undefined, + scrollSessionKey: undefined as string | undefined, + nav: undefined as HTMLElement | undefined, }) - const [busyWorkspaces, setBusyWorkspaces] = createSignal>(new Set()) + + const editor = createInlineEditorController() const setBusy = (directory: string, value: boolean) => { const key = workspaceKey(directory) - setBusyWorkspaces((prev) => { - const next = new Set(prev) - if (value) next.add(key) - else next.delete(key) - return next - }) + if (value) { + setState("busyWorkspaces", key, true) + return + } + setState( + "busyWorkspaces", + produce((draft) => { + delete draft[key] + }), + ) } - const isBusy = (directory: string) => busyWorkspaces().has(workspaceKey(directory)) - const editorRef = { current: undefined as HTMLInputElement | undefined } + const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)] + const navLeave = { current: undefined as number | undefined } + const [sortNow, setSortNow] = createSignal(Date.now()) + const [sizing, setSizing] = createSignal(false) + let sizet: number | undefined + let sortNowInterval: ReturnType | undefined + const sortNowTimeout = setTimeout( + () => { + setSortNow(Date.now()) + sortNowInterval = setInterval(() => setSortNow(Date.now()), 60_000) + }, + 60_000 - (Date.now() % 60_000), + ) - const [hoverSession, setHoverSession] = createSignal() + 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) + setState("hoverSession", undefined) + }, + }) - const autoselecting = createMemo(() => { - if (params.dir) return false - if (initialDir) return false - if (!autoselect()) return false - if (!pageReady()) return true - if (!layoutReady()) return true - const list = layout.projects.list() - if (list.length === 0) return false - return true + onCleanup(() => { + 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() }) - const editorOpen = (id: string) => editor.active === id - const editorValue = () => editor.value + onMount(() => { + const stop = () => setSizing(false) + window.addEventListener("pointerup", stop) + window.addEventListener("pointercancel", stop) + window.addEventListener("blur", stop) + onCleanup(() => { + window.removeEventListener("pointerup", stop) + window.removeEventListener("pointercancel", stop) + window.removeEventListener("blur", stop) + }) + }) - const openEditor = (id: string, value: string) => { - if (!id) return - setEditor({ active: id, value }) + 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 setHoverSession = (id: string | undefined) => setState("hoverSession", id) - const closeEditor = () => setEditor({ active: "", value: "" }) + const disarm = () => { + if (navLeave.current === undefined) return + clearTimeout(navLeave.current) + navLeave.current = undefined + } - const saveEditor = (callback: (next: string) => void) => { - const next = editor.value.trim() - if (!next) { - closeEditor() - return - } - closeEditor() - callback(next) + const arm = () => { + if (layout.sidebar.opened()) return + if (state.hoverProject === undefined) return + disarm() + navLeave.current = window.setTimeout(() => { + navLeave.current = undefined + setHoverProject(undefined) + setState("hoverSession", undefined) + }, 300) } - const editorKeyDown = (event: KeyboardEvent, callback: (next: string) => void) => { - if (event.key === "Enter") { - event.preventDefault() - saveEditor(callback) + const [peek, setPeek] = createSignal(undefined) + const [peeked, setPeeked] = createSignal(false) + let peekt: number | undefined + + const hoverProjectData = createMemo(() => { + const id = state.hoverProject + 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 + } + setPeek(p) + setPeeked(true) return } - if (event.key === "Escape") { - event.preventDefault() - closeEditor() - } - } - const InlineEditor = (props: { - id: string - value: Accessor - onSave: (next: string) => void - class?: string - displayClass?: string - editing?: boolean - stopPropagation?: boolean - openOnDblClick?: boolean - }) => { - 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()) - } + setPeeked(false) + if (peek() === undefined) return + if (peekt !== undefined) clearTimeout(peekt) + peekt = window.setTimeout(() => { + peekt = undefined + setPeek(undefined) + }, 180) + }) - return ( - - {props.value()} - - } - > - { - editorRef.current = el - requestAnimationFrame(() => 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} - /> - - ) + createEffect(() => { + if (!layout.sidebar.opened()) return + setHoverProject(undefined) + }) + + const autoselecting = createMemo(() => { + if (params.dir) return false + if (!state.autoselect) return false + if (!pageReady()) return true + if (!layoutReady()) return true + const list = layout.projects.list() + if (list.length > 0) return true + return !!server.projects.last() + }) + + 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 + setState("hoverSession", undefined) + setHoverProject(undefined) + } + + const navigateWithSidebarReset = (href: string) => { + clearSidebarHoverState() + navigate(href) + layout.mobileSidebar.hide() } function cycleTheme(direction = 1) { @@ -291,172 +352,198 @@ export default function Layout(props: ParentProps) { setLocale(next) } - onMount(() => { - if (!platform.checkUpdate || !platform.update || !platform.restart) return - - let toastId: number | undefined - - async function pollUpdate() { - const { updateAvailable, version } = await platform.checkUpdate!() - if (updateAvailable && toastId === undefined) { - 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.update!() - await platform.restart!() + const useUpdatePolling = () => + onMount(() => { + if (!platform.checkUpdate || !platform.update || !platform.restart) 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.update!() + await platform.restart!() + }, }, - }, - { - label: language.t("toast.update.action.notYet"), - onClick: "dismiss", - }, - ], + { + label: language.t("toast.update.action.notYet"), + onClick: "dismiss", + }, + ], + }) }) - } - } - pollUpdate() - const interval = setInterval(pollUpdate, 10 * 60 * 1000) - onCleanup(() => clearInterval(interval)) - }) + createEffect(() => { + if (!settings.ready()) return - onMount(() => { - const toastBySession = new Map() - const alertedAtBySession = new Map() - const cooldownMs = 5000 - - const unsub = globalSDK.event.listen((e) => { - 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) - 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") { - playSound(soundSrc(settings.sounds.permissions())) - if (settings.notifications.permissions()) { - void platform.notify(title, description, href) + if (!settings.updates.startup()) { + if (interval === undefined) return + clearInterval(interval) + interval = undefined + return } - } - if (e.details.type === "question.asked") { - if (settings.notifications.agent()) { - void platform.notify(title, description, href) - } - } + if (interval !== undefined) return + void pollUpdate() + interval = setInterval(pollUpdate, 10 * 60 * 1000) + }) - const currentDir = params.dir ? base64Decode(params.dir) : undefined - const currentSession = params.id - if (directory === currentDir && props.sessionID === currentSession) return - if (directory === currentDir && session?.parentID === currentSession) return - - const existingToastId = toastBySession.get(sessionKey) - if (existingToastId !== undefined) toaster.dismiss(existingToastId) - - 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", - }, - ], + onCleanup(() => { + if (interval === undefined) return + clearInterval(interval) }) - toastBySession.set(sessionKey, toastId) }) - onCleanup(unsub) - - createEffect(() => { - const currentDir = params.dir ? base64Decode(params.dir) : undefined - const currentSession = params.id - if (!currentDir || !currentSession) return - const sessionKey = `${currentDir}:${currentSession}` - const toastId = toastBySession.get(sessionKey) - if (toastId !== undefined) { + + 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 [store] = globalSync.child(currentDir) - const childSessions = store.session.filter((s) => s.parentID === currentSession) - for (const child of childSessions) { - const childKey = `${currentDir}:${child.id}` - const childToastId = toastBySession.get(childKey) - if (childToastId !== undefined) { - toaster.dismiss(childToastId) - toastBySession.delete(childKey) - alertedAtBySession.delete(childKey) + + const unsub = globalSDK.event.listen((e) => { + if (e.details?.type === "worktree.ready") { + setBusy(e.name, false) + WorktreeState.ready(e.name) + return } - } - }) - }) - function sortSessions(a: Session, b: Session) { - const now = Date.now() - const oneMinuteAgo = now - 60 * 1000 - 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.localeCompare(b.id) - if (aRecent && !bRecent) return -1 - if (!aRecent && bRecent) return 1 - return bUpdated - aUpdated - } + 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()) { + playSound(soundSrc(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 (directory === currentDir() && props.sessionID === currentSession) return + if (directory === 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}`) + } + }) + }) - const [scrollSessionKey, setScrollSessionKey] = createSignal(undefined) + useUpdatePolling() + useSDKNotificationToasts() function scrollToSession(sessionId: string, sessionKey: string) { if (!scrollContainerRef) return - if (scrollSessionKey() === sessionKey) 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) { - setScrollSessionKey(sessionKey) + setState("scrollSessionKey", sessionKey) return } - setScrollSessionKey(sessionKey) + setState("scrollSessionKey", sessionKey) element.scrollIntoView({ block: "nearest", behavior: "smooth" }) } const currentProject = createMemo(() => { - const directory = params.dir ? base64Decode(params.dir) : undefined + const directory = currentDir() if (!directory) return const projects = layout.projects.list() @@ -467,7 +554,7 @@ export default function Layout(props: ParentProps) { const direct = projects.find((p) => p.worktree === directory) if (direct) return direct - const [child] = globalSync.child(directory) + const [child] = globalSync.child(directory, { bootstrap: false }) const id = child.project if (!id) return @@ -478,45 +565,34 @@ export default function Layout(props: ParentProps) { return projects.find((p) => p.worktree === root) }) - createEffect( - on( - () => ({ ready: pageReady(), project: currentProject() }), - (value) => { - if (!value.ready) return - const project = value.project - if (!project) return - const last = server.projects.last() - if (last === project.worktree) return - server.projects.touch(project.worktree) - }, - { defer: true }, - ), - ) - createEffect( on( () => ({ ready: pageReady(), layoutReady: layoutReady(), dir: params.dir, list: layout.projects.list() }), (value) => { if (!value.ready) return if (!value.layoutReady) return - if (!autoselect()) return - if (initialDir) return + if (!state.autoselect) return if (value.dir) return - if (value.list.length === 0) return const last = server.projects.last() + + if (value.list.length === 0) { + if (!last) return + setState("autoselect", false) + openProject(last, false) + navigateToProject(last) + return + } + const next = value.list.find((project) => project.worktree === last) ?? value.list[0] if (!next) return - setAutoselect(false) + setState("autoselect", false) openProject(next.worktree, false) navigateToProject(next.worktree) }, - { defer: true }, ), ) - const workspaceKey = (directory: string) => directory.replace(/[\\/]+$/, "") - const workspaceName = (directory: string, projectId?: string, branch?: string) => { const key = workspaceKey(directory) const direct = store.workspaceName[key] ?? store.workspaceName[directory] @@ -528,48 +604,36 @@ export default function Layout(props: ParentProps) { const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => { const key = workspaceKey(directory) - setStore("workspaceName", (prev) => ({ ...(prev ?? {}), [key]: next })) + setStore("workspaceName", key, next) if (!projectId) return if (!branch) return - setStore("workspaceBranchName", projectId, (prev) => ({ ...(prev ?? {}), [branch]: next })) + 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 isWorkspaceEditing = () => editor.active.startsWith("workspace:") - const workspaceSetting = createMemo(() => { const project = currentProject() if (!project) return false + if (project.vcs !== "git") return false return layout.sidebar.workspaces(project.worktree)() }) - createEffect(() => { - if (!pageReady()) return - if (!layoutReady()) return + const visibleSessionDirs = createMemo(() => { const project = currentProject() - if (!project) return - - const dirs = [project.worktree, ...(project.sandboxes ?? [])] - const existing = store.workspaceOrder[project.worktree] - if (!existing) { - setStore("workspaceOrder", project.worktree, dirs) - return - } - - const keep = existing.filter((d) => dirs.includes(d)) - const missing = dirs.filter((d) => !existing.includes(d)) - const merged = [...keep, ...missing] - - if (merged.length !== existing.length) { - setStore("workspaceOrder", project.worktree, merged) - return - } - - if (merged.some((d, i) => d !== existing[i])) { - setStore("workspaceOrder", project.worktree, merged) - } + 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 = directory === activeDir + return expanded || active + }) }) createEffect(() => { @@ -580,36 +644,23 @@ export default function Layout(props: ParentProps) { if (!expanded) continue const project = projects.find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) if (!project) continue - if (layout.sidebar.workspaces(project.worktree)()) continue + if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue setStore("workspaceExpanded", directory, false) } }) const currentSessions = createMemo(() => { - const project = currentProject() - if (!project) return [] as Session[] - if (workspaceSetting()) { - const dirs = workspaceIds(project) - const activeDir = params.dir ? base64Decode(params.dir) : "" - const result: Session[] = [] - for (const dir of dirs) { - const expanded = store.workspaceExpanded[dir] ?? dir === project.worktree - const active = dir === activeDir - if (!expanded && !active) continue - const [dirStore] = globalSync.child(dir, { bootstrap: true }) - const dirSessions = dirStore.session - .filter((session) => session.directory === dirStore.path.directory) - .filter((session) => !session.parentID && !session.time?.archived) - .toSorted(sortSessions) - result.push(...dirSessions) - } - return result + 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) } - const [projectStore] = globalSync.child(project.worktree) - return projectStore.session - .filter((session) => session.directory === projectStore.path.directory) - .filter((session) => !session.parentID && !session.time?.archived) - .toSorted(sortSessions) + return result }) type PrefetchQueue = { @@ -619,20 +670,58 @@ export default function Layout(props: ParentProps) { running: number } - const prefetchChunk = 600 - const prefetchConcurrency = 1 - const prefetchPendingLimit = 6 + 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: directory === params.dir && params.id ? [params.id] : undefined, + }) + } + + createEffect(() => { + const active = new Set(visibleSessionDirs()) + for (const directory of [...prefetchedByDir.keys()]) { + if (active.has(directory)) continue + prefetchedByDir.delete(directory) + } + }) + createEffect(() => { params.dir globalSDK.url prefetchToken.value += 1 - for (const q of prefetchQueues.values()) { + 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) } }) @@ -650,39 +739,85 @@ export default function Layout(props: ParentProps) { 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 [, setStore] = globalSync.child(directory) - - return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk })) - .then((messages) => { - if (prefetchToken.value !== token) return - - const items = (messages.data ?? []).filter((x) => !!x?.info?.id) - const next = items - .map((x) => x.info) - .filter((m) => !!m?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) - - batch(() => { - setStore("message", sessionID, reconcile(next, { key: "id" })) - - for (const message of items) { - setStore( - "part", - message.info.id, - reconcile( - message.parts - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), - { key: "id" }, - ), + 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 meta = { + limit: prefetchChunk, + complete: sorted.length < prefetchChunk, + 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, ) - } - }) - }) - .catch(() => undefined) + + 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) => { @@ -709,12 +844,30 @@ export default function Layout(props: ParentProps) { const directory = session.directory if (!directory) return - const [store] = globalSync.child(directory) - if (store.message[session.id] !== undefined) return + const [store] = globalSync.child(directory, { bootstrap: false }) + const cached = untrack(() => { + if (store.message[session.id] === undefined) return false + const info = getSessionPrefetch(directory, session.id) + if (!info) return false + return Date.now() - info.at < SESSION_PREFETCH_TTL + }) + if (cached) return const q = queueFor(directory) if (q.inflight.has(session.id)) return - if (q.pendingSet.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) @@ -729,27 +882,29 @@ export default function Layout(props: ParentProps) { pumpPrefetch(directory) } - createEffect(() => { - const sessions = currentSessions() - const id = params.id - - if (!id) { - const first = sessions[0] - if (first) prefetchSession(first) + 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 second = sessions[1] - if (second) prefetchSession(second) - return + const prev = sessions[index - offset] + if (prev) prefetchSession(prev, offset === 1 ? "high" : "low") } + } - const index = sessions.findIndex((s) => s.id === id) + 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 - const next = sessions[index + 1] - if (next) prefetchSession(next) + if (!params.id) { + const first = sessions[index] + if (first) prefetchSession(first, "high") + } - const prev = sessions[index - 1] - if (prev) prefetchSession(prev) + warm(sessions, index) }) function navigateSessionByOffset(offset: number) { @@ -768,29 +923,34 @@ export default function Layout(props: ParentProps) { const session = sessions[targetIndex] if (!session) return - const next = sessions[(targetIndex + 1) % sessions.length] - const prev = sessions[(targetIndex - 1 + sessions.length) % sessions.length] + prefetchSession(session, "high") + warm(sessions, targetIndex) - if (offset > 0) { - if (next) prefetchSession(next, "high") - if (prev) prefetchSession(prev) - } + navigateToSession(session) + } - if (offset < 0) { - if (prev) prefetchSession(prev, "high") - if (next) prefetchSession(next) - } + function navigateSessionByUnseen(offset: number) { + const sessions = currentSessions() + if (sessions.length === 0) return - if (import.meta.env.DEV) { - navStart({ - dir: base64Encode(session.directory), - from: params.id, - to: session.id, - trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup", - }) + 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 } - navigateToSession(session) - queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`)) } async function archiveSession(session: Session) { @@ -819,50 +979,7 @@ export default function Layout(props: ParentProps) { } } - async function deleteSession(session: Session) { - const [store, setStore] = globalSync.child(session.directory) - const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived) - const index = sessions.findIndex((s) => s.id === session.id) - const nextSession = sessions[index + 1] ?? sessions[index - 1] - - const result = await globalSDK.client.session - .delete({ directory: session.directory, sessionID: session.id }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("session.delete.failed.title"), - description: errorMessage(err), - }) - return false - }) - - if (!result) return - - setStore( - produce((draft) => { - const removed = new Set([session.id]) - const collect = (parentID: string) => { - for (const item of draft.session) { - if (item.parentID !== parentID) continue - removed.add(item.id) - collect(item.id) - } - } - collect(session.id) - draft.session = draft.session.filter((s) => !removed.has(s.id)) - }), - ) - - if (session.id === params.id) { - if (nextSession) { - navigate(`/${params.dir}/session/${nextSession.id}`) - } else { - navigate(`/${params.dir}/session`) - } - } - } - - command.register(() => { + command.register("layout", () => { const commands: CommandOption[] = [ { id: "sidebar.toggle", @@ -911,6 +1028,20 @@ export default function Layout(props: ParentProps) { 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"), @@ -923,9 +1054,44 @@ export default function Layout(props: ParentProps) { }, }, { - id: "theme.cycle", - title: language.t("command.theme.cycle"), - category: language.t("command.category.theme"), + 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), }, @@ -996,18 +1162,140 @@ export default function Layout(props: ParentProps) { dialog.show(() => ) } - function navigateToProject(directory: string | undefined) { + function projectRoot(directory: string) { + const project = layout.projects + .list() + .find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) + if (project) return project.worktree + + const known = Object.entries(store.workspaceOrder).find( + ([root, dirs]) => root === directory || dirs.includes(directory), + ) + 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 touchProjectRoute() { + const root = currentProject()?.worktree + if (!root) return + if (server.projects.last() !== root) server.projects.touch(root) + return root + } + + 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 - server.projects.touch(directory) - const lastSession = store.lastSession[directory] - navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`) - layout.mobileSidebar.hide() + 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 - navigate(`/${base64Encode(session.directory)}/session/${session.id}`) - layout.mobileSidebar.hide() + navigateWithSidebarReset(`/${base64Encode(session.directory)}/session/${session.id}`) } function openProject(directory: string, navigate = true) { @@ -1015,7 +1303,36 @@ export default function Layout(props: ParentProps) { if (navigate) navigateToProject(directory) } - const displayName = (project: LocalProject) => project.name || getFilename(project.worktree) + const handleDeepLinks = (urls: string[]) => { + if (!server.isLocal()) return + + for (const directory of collectOpenProjectDeepLinks(urls)) { + openProject(directory) + } + + for (const link of collectNewSessionDeepLinks(urls)) { + 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)) + window.addEventListener(deepLinkEvent, handler as EventListener) + onCleanup(() => window.removeEventListener(deepLinkEvent, handler as EventListener)) + }) async function renameProject(project: LocalProject, next: string) { const current = displayName(project) @@ -1030,15 +1347,6 @@ export default function Layout(props: ParentProps) { globalSync.project.meta(project.worktree, { name }) } - async function renameSession(session: Session, next: string) { - if (next === session.title) return - await globalSDK.client.session.update({ - directory: session.directory, - sessionID: session.id, - title: next, - }) - } - const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => { const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory) if (current === next) return @@ -1046,13 +1354,42 @@ export default function Layout(props: ParentProps) { } function closeProject(directory: string) { - const index = layout.projects.list().findIndex((x) => x.worktree === directory) - const next = layout.projects.list()[index + 1] + const list = layout.projects.list() + const index = list.findIndex((x) => x.worktree === directory) + const active = currentProject()?.worktree === directory + 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) - if (next) navigateToProject(next.worktree) - else navigate("/") + 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) => dialog.show(() => ) + async function chooseProject() { function resolve(result: string | string[] | null) { if (Array.isArray(result)) { @@ -1079,29 +1416,26 @@ export default function Layout(props: ParentProps) { } } - 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 deleteWorkspace = async (root: string, directory: string, leaveDeletedWorkspace = false) => { + if (directory === root) return - const deleteWorkspace = async (directory: string) => { - const current = currentProject() - if (!current) return - if (directory === current.worktree) 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: current.worktree, worktreeRemoveInput: { directory } }) + .remove({ directory: root, worktreeRemoveInput: { directory } }) .then((x) => x.data) .catch((err) => { showToast({ title: language.t("workspace.delete.failed.title"), - description: errorMessage(err), + description: errorMessage(err, language.t("common.requestFailed")), }) return false }) @@ -1110,18 +1444,40 @@ export default function Layout(props: ParentProps) { 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(current.worktree) + 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 && base64Decode(params.dir) === directory) { - navigateToProject(current.worktree) + if (params.dir && projectRoot(nextCurrent) === root && !valid) { + navigateWithSidebarReset(`/${base64Encode(root)}/session`) } } - const resetWorkspace = async (directory: string) => { - const current = currentProject() - if (!current) return - if (directory === current.worktree) return + const resetWorkspace = async (root: string, directory: string) => { + if (directory === root) return setBusy(directory, true) const progress = showToast({ @@ -1131,18 +1487,25 @@ export default function Layout(props: ParentProps) { }) const dismiss = () => toaster.dismiss(progress) - const sessions = await globalSDK.client.session + 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: current.worktree, worktreeResetInput: { directory } }) + .reset({ directory: root, worktreeResetInput: { directory } }) .then((x) => x.data) .catch((err) => { showToast({ title: language.t("workspace.reset.failed.title"), - description: errorMessage(err), + description: errorMessage(err, language.t("common.requestFailed")), }) return false }) @@ -1168,8 +1531,6 @@ export default function Layout(props: ParentProps) { ), ) - await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) - setBusy(directory, false) dismiss() @@ -1193,34 +1554,7 @@ export default function Layout(props: ParentProps) { }) } - function DialogDeleteSession(props: { session: Session }) { - const handleDelete = async () => { - await deleteSession(props.session) - dialog.close() - } - - return ( - -
-
- - {language.t("session.delete.confirm", { name: props.session.title })} - -
-
- - -
-
-
- ) - } - - function DialogDeleteWorkspace(props: { directory: string }) { + function DialogDeleteWorkspace(props: { root: string; directory: string }) { const name = createMemo(() => getFilename(props.directory)) const [data, setData] = createStore({ status: "loading" as "loading" | "ready" | "error", @@ -1228,12 +1562,6 @@ export default function Layout(props: ParentProps) { }) onMount(() => { - const current = currentProject() - if (!current) { - setData({ status: "error", dirty: false }) - return - } - globalSDK.client.file .status({ directory: props.directory }) .then((x) => { @@ -1246,9 +1574,13 @@ export default function Layout(props: ParentProps) { }) }) - const handleDelete = async () => { - await deleteWorkspace(props.directory) + 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 = () => { @@ -1280,7 +1612,7 @@ export default function Layout(props: ParentProps) { ) } - function DialogResetWorkspace(props: { directory: string }) { + function DialogResetWorkspace(props: { root: string; directory: string }) { const name = createMemo(() => getFilename(props.directory)) const [state, setState] = createStore({ status: "loading" as "loading" | "ready" | "error", @@ -1298,12 +1630,6 @@ export default function Layout(props: ParentProps) { } onMount(() => { - const current = currentProject() - if (!current) { - setState({ status: "error", dirty: false }) - return - } - globalSDK.client.file .status({ directory: props.directory }) .then((x) => { @@ -1319,7 +1645,7 @@ export default function Layout(props: ParentProps) { const handleReset = () => { dialog.close() - void resetWorkspace(props.directory) + void resetWorkspace(props.root, props.directory) } const archivedCount = () => state.sessions.length @@ -1362,57 +1688,80 @@ export default function Layout(props: ParentProps) { ) } + const activeRoute = { + session: "", + sessionProject: "", + } + createEffect( on( - () => ({ ready: pageReady(), dir: params.dir, id: params.id }), - (value) => { - if (!value.ready) return - const dir = value.dir - const id = value.id - if (!dir || !id) return - const directory = base64Decode(dir) - setStore("lastSession", directory, id) - notification.session.markViewed(id) - const expanded = untrack(() => store.workspaceExpanded[directory]) - if (expanded === false) { - setStore("workspaceExpanded", directory, true) + () => [pageReady(), params.dir, params.id, currentProject()?.worktree] as const, + ([ready, dir, id]) => { + if (!ready || !dir) { + activeRoute.session = "" + activeRoute.sessionProject = "" + return } - requestAnimationFrame(() => scrollToSession(id, `${directory}:${id}`)) + + const directory = decode64(dir) + if (!directory) return + + const root = touchProjectRoute() ?? activeProjectRoot(directory) + + if (!id) { + activeRoute.session = "" + activeRoute.sessionProject = "" + return + } + + const session = `${dir}/${id}` + if (session !== activeRoute.session) { + activeRoute.session = session + activeRoute.sessionProject = syncSessionRoute(directory, id, root) + return + } + + if (root === activeRoute.sessionProject) return + activeRoute.sessionProject = rememberSessionRoute(directory, id, root) }, - { defer: true }, ), ) createEffect(() => { - const project = currentProject() - if (!project) return + const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48 + document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`) + }) - if (workspaceSetting()) { - const activeDir = params.dir ? base64Decode(params.dir) : "" - const dirs = [project.worktree, ...(project.sandboxes ?? [])] - for (const directory of dirs) { - const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree - const active = directory === activeDir - if (!expanded && !active) continue - globalSync.project.loadSessions(directory) - } - return - } + const loadedSessionDirs = new Set() - globalSync.project.loadSessions(project.worktree) - }) + createEffect( + on( + visibleSessionDirs, + (dirs) => { + if (dirs.length === 0) { + loadedSessionDirs.clear() + return + } - function getDraggableId(event: unknown): string | undefined { - if (typeof event !== "object" || event === null) return undefined - if (!("draggable" in event)) return undefined - const draggable = (event as { draggable?: { id?: unknown } }).draggable - if (!draggable) return undefined - return typeof draggable.id === "string" ? draggable.id : undefined - } + const next = new Set(dirs) + for (const directory of next) { + if (loadedSessionDirs.has(directory)) continue + 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) } @@ -1434,19 +1783,27 @@ export default function Layout(props: ParentProps) { function workspaceIds(project: LocalProject | undefined) { if (!project) return [] - const dirs = [project.worktree, ...(project.sandboxes ?? [])] + const local = project.worktree + const dirs = [local, ...(project.sandboxes ?? [])] const active = currentProject() - const directory = active?.worktree === project.worktree && params.dir ? base64Decode(params.dir) : undefined - const next = directory && directory !== project.worktree && !dirs.includes(directory) ? [...dirs, directory] : dirs - - const existing = store.workspaceOrder[project.worktree] - if (!existing) return next - - const keep = existing.filter((d) => next.includes(d)) - const missing = next.filter((d) => !existing.includes(d)) - return [...keep, ...missing] + const directory = active?.worktree === project.worktree ? currentDir() : undefined + const extra = directory && directory !== local && !dirs.includes(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 @@ -1457,7 +1814,7 @@ export default function Layout(props: ParentProps) { const { draggable, droppable } = event if (!draggable || !droppable) return - const project = currentProject() + const project = sidebarProject() if (!project) return const ids = workspaceIds(project) @@ -1470,1089 +1827,532 @@ export default function Layout(props: ParentProps) { const [item] = result.splice(fromIndex, 1) if (!item) return result.splice(toIndex, 0, item) - setStore("workspaceOrder", project.worktree, result) + setStore( + "workspaceOrder", + project.worktree, + result.filter((directory) => workspaceKey(directory) !== workspaceKey(project.worktree)), + ) } function handleWorkspaceDragEnd() { setStore("activeWorkspace", undefined) } - const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => { - const notification = useNotification() - const notifications = createMemo(() => notification.project.unseen(props.project.worktree)) - const hasError = createMemo(() => notifications().some((n) => n.type === "error")) - const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) - const mask = "radial-gradient(circle 5px at calc(100% - 4px) 4px, transparent 5px, black 5.5px)" - const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" + 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 + }) - return ( -
-
- 0 && props.notify - ? { "-webkit-mask-image": mask, "mask-image": mask } - : undefined - } - /> -
- 0 && props.notify}> -
- -
- ) - } + if (!created?.directory) return - const SessionItem = (props: { - session: Session - slug: string - mobile?: boolean - dense?: boolean - popover?: boolean - }): JSX.Element => { - const notification = useNotification() - const notifications = createMemo(() => notification.session.unseen(props.session.id)) - const hasError = createMemo(() => notifications().some((n) => n.type === "error")) - const [sessionStore] = globalSync.child(props.session.directory) - const hasPermissions = createMemo(() => { - const permissions = sessionStore.permission?.[props.session.id] ?? [] - if (permissions.length > 0) return true - const childSessions = sessionStore.session.filter((s) => s.parentID === props.session.id) - for (const child of childSessions) { - const childPermissions = sessionStore.permission?.[child.id] ?? [] - if (childPermissions.length > 0) return true - } - return false - }) - const isWorking = createMemo(() => { - if (hasPermissions()) return false - const status = sessionStore.session_status[props.session.id] - return status?.type === "busy" || status?.type === "retry" - }) + setWorkspaceName(created.directory, created.branch, project.id, created.branch) - const tint = createMemo(() => { - const messages = sessionStore.message[props.session.id] - if (!messages) return undefined - const user = messages - .slice() - .reverse() - .find((m) => m.role === "user") - if (!user?.agent) return undefined - - const agent = sessionStore.agent.find((a) => a.name === user.agent) - return agent?.color - }) + const local = project.worktree + const key = workspaceKey(created.directory) + const root = workspaceKey(local) - const hoverMessages = createMemo(() => - sessionStore.message[props.session.id]?.filter((message) => message.role === "user"), - ) - const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined) - const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened()) - const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) - const isActive = createMemo(() => props.session.id === params.id) - const [menuOpen, setMenuOpen] = createSignal(false) - const [pendingRename, setPendingRename] = createSignal(false) - - const messageLabel = (message: Message) => { - const parts = sessionStore.part[message.id] ?? [] - const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored) - return text?.text + 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] + }) - const item = ( - prefetchSession(props.session, "high")} - onFocus={() => prefetchSession(props.session, "high")} - onClick={() => setHoverSession(undefined)} - > -
-
- }> - - - - -
- - -
- - 0}> -
- - -
- props.session.title} - onSave={(next) => renameSession(props.session, next)} - class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - stopPropagation - /> - - {(summary) => ( -
- -
- )} -
-
-
- ) - - return ( -
- - {item} - - } - > - setHoverSession(open ? props.session.id : undefined)} - > - {language.t("session.messages.loading")}
} - > -
- { - if (!isActive()) { - sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`) - navigate(`${props.slug}/session/${props.session.id}`) - return - } - window.history.replaceState(null, "", `#message-${message.id}`) - window.dispatchEvent(new HashChangeEvent("hashchange")) - }} - size="normal" - class="w-60" - /> -
- - - -
- - - - - - { - if (!pendingRename()) return - event.preventDefault() - setPendingRename(false) - openEditor(`session:${props.session.id}`, props.session.title) - }} - > - { - setPendingRename(true) - setMenuOpen(false) - }} - > - {language.t("common.rename")} - - archiveSession(props.session)}> - {language.t("common.archive")} - - - dialog.show(() => )}> - {language.t("common.delete")} - - - - -
-
- ) - } - - const NewSessionItem = (props: { slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => { - const label = language.t("command.session.new") - const tooltip = () => props.mobile || !layout.sidebar.opened() - const item = ( - setHoverSession(undefined)} - > -
-
- -
- - {label} - -
-
- ) - - return ( -
- - {item} - - } - > - {item} - -
- ) + globalSync.child(created.directory) + navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`) } - const SessionSkeleton = (props: { count?: number }): JSX.Element => { - const items = Array.from({ length: props.count ?? 4 }, (_, index) => index) - return ( -
- - {() =>
} - -
- ) + const workspaceSidebarCtx: WorkspaceSidebarContext = { + currentDir, + navList: currentSessions, + sidebarExpanded, + sidebarHovering, + nav: () => state.nav, + hoverSession: () => state.hoverSession, + setHoverSession, + 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 ProjectDragOverlay = (): JSX.Element => { - const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeProject)) - return ( - - {(p) => ( -
- -
- )} -
- ) + const projectSidebarCtx: ProjectSidebarContext = { + currentDir, + sidebarOpened: () => layout.sidebar.opened(), + sidebarHovering, + hoverProject: () => state.hoverProject, + nav: () => state.nav, + onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event), + onProjectMouseLeave: (worktree) => aim.leave(worktree), + onProjectFocus: (worktree) => aim.activate(worktree), + navigateToProject, + openSidebar: () => layout.sidebar.open(), + closeProject, + showEditProjectDialog, + toggleProjectWorkspaces, + workspacesEnabled: (project) => project.vcs === "git" && layout.sidebar.workspaces(project.worktree)(), + workspaceIds, + workspaceLabel, + sessionProps: { + navList: currentSessions, + sidebarExpanded, + sidebarHovering, + nav: () => state.nav, + hoverSession: () => state.hoverSession, + setHoverSession, + clearHoverProjectSoon, + prefetchSession, + archiveSession, + }, + setHoverSession, } - const WorkspaceDragOverlay = (): JSX.Element => { - const label = createMemo(() => { - const project = currentProject() - if (!project) return - const directory = store.activeWorkspace - if (!directory) return - - const [workspaceStore] = globalSync.child(directory) - const kind = - directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") - const name = workspaceLabel(directory, workspaceStore.vcs?.branch, project.id) - return `${kind} : ${name}` + const SidebarPanel = (panelProps: { project: LocalProject | undefined; mobile?: boolean; merged?: boolean }) => { + const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened())) + const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened()) + const popover = createMemo(() => !!panelProps.mobile || panelProps.merged === false || layout.sidebar.opened()) + const projectName = createMemo(() => { + const project = panelProps.project + if (!project) return "" + return project.name || getFilename(project.worktree) }) - - return ( - - {(value) => ( -
{value()}
- )} -
- ) - } - - const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => { - const sortable = createSortable(props.directory) - const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false }) - const [menuOpen, setMenuOpen] = createSignal(false) - const [pendingRename, setPendingRename] = createSignal(false) - const slug = createMemo(() => base64Encode(props.directory)) - const sessions = createMemo(() => - workspaceStore.session - .filter((session) => session.directory === workspaceStore.path.directory) - .filter((session) => !session.parentID && !session.time?.archived) - .toSorted(sortSessions), + const projectId = createMemo(() => panelProps.project?.id ?? "") + const workspaces = createMemo(() => workspaceIds(panelProps.project)) + const unseenCount = createMemo(() => + workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), ) - const local = createMemo(() => props.directory === props.project.worktree) - const active = createMemo(() => { - const current = params.dir ? base64Decode(params.dir) : "" - return current === props.directory - }) - const workspaceValue = createMemo(() => { - const branch = workspaceStore.vcs?.branch - const name = branch ?? getFilename(props.directory) - return workspaceName(props.directory, props.project.id, branch) ?? name - }) - const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local()) - const boot = createMemo(() => open() || active()) - const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0) - const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length) - const busy = createMemo(() => isBusy(props.directory)) - const loadMore = async () => { - if (!local()) return - setWorkspaceStore("limit", (limit) => limit + 5) - await globalSync.project.loadSessions(props.directory) - } - - const workspaceEditActive = createMemo(() => editorOpen(`workspace:${props.directory}`)) - - const openWrapper = (value: boolean) => { - setStore("workspaceExpanded", props.directory, value) - if (value) return - if (editorOpen(`workspace:${props.directory}`)) closeEditor() - } - - createEffect(() => { - if (!boot()) return - globalSync.child(props.directory, { bootstrap: true }) + const clearNotifications = () => + workspaces() + .filter((directory) => notification.project.unseenCount(directory) > 0) + .forEach((directory) => notification.project.markViewed(directory)) + const workspacesEnabled = createMemo(() => { + const project = panelProps.project + if (!project) return false + if (project.vcs !== "git") return false + return layout.sidebar.workspaces(project.worktree)() }) - - const header = () => ( -
-
- }> - - -
- - {local() ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")} : - - - {workspaceStore.vcs?.branch ?? getFilename(props.directory)} - - } - > - { - const trimmed = next.trim() - if (!trimmed) return - renameWorkspace(props.directory, trimmed, props.project.id, workspaceStore.vcs?.branch) - setEditor("value", workspaceValue()) - }} - class="text-14-medium text-text-base min-w-0 truncate" - displayClass="text-14-medium text-text-base min-w-0 truncate" - editing={workspaceEditActive()} - stopPropagation={false} - openOnDblClick={false} - /> - - -
- ) + const homedir = createMemo(() => globalSync.data.path.home) return (
- -
-
-
- - {header()} - - } - > -
{header()}
-
-
- - - + + {(p) => ( + <> +
+
+
+ renameProject(p(), next)} + class="text-14-medium text-text-strong truncate" + displayClass="text-14-medium text-text-strong truncate" + stopPropagation + /> + + + + {p().worktree.replace(homedir(), "~")} + +
+ + + - { - if (!pendingRename()) return - event.preventDefault() - setPendingRename(false) - openEditor(`workspace:${props.directory}`, workspaceValue()) - }} - > + + showEditProjectDialog(p())}> + {language.t("common.edit")} + { - setPendingRename(true) - setMenuOpen(false) - }} + data-action="project-workspaces-toggle" + data-project={base64Encode(p().worktree)} + disabled={p().vcs !== "git" && !layout.sidebar.workspaces(p().worktree)()} + onSelect={() => toggleProjectWorkspaces(p())} > - {language.t("common.rename")} + + {layout.sidebar.workspaces(p().worktree)() + ? language.t("sidebar.workspaces.disable") + : language.t("sidebar.workspaces.enable")} + dialog.show(() => )} + data-action="project-clear-notifications" + data-project={base64Encode(p().worktree)} + disabled={unseenCount() === 0} + onSelect={clearNotifications} > - {language.t("common.reset")} + + {language.t("sidebar.project.clearNotifications")} + + dialog.show(() => )} + data-action="project-close-menu" + data-project={base64Encode(p().worktree)} + onSelect={() => closeProject(p().worktree)} > - {language.t("common.delete")} + {language.t("common.close")}
-
-
- - - - - -
- ) - } - - const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => { - const sortable = createSortable(props.project.worktree) - const selected = createMemo(() => { - const current = params.dir ? base64Decode(params.dir) : "" - return props.project.worktree === current || props.project.sandboxes?.includes(current) - }) - - const workspaces = createMemo(() => workspaceIds(props.project).slice(0, 2)) - const workspaceEnabled = createMemo(() => layout.sidebar.workspaces(props.project.worktree)()) - const [open, setOpen] = createSignal(false) - - const label = (directory: string) => { - const [data] = globalSync.child(directory) - const kind = - directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") - const name = workspaceLabel(directory, data.vcs?.branch, props.project.id) - return `${kind} : ${name}` - } - - const sessions = (directory: string) => { - const [data] = globalSync.child(directory) - return data.session - .filter((session) => session.directory === data.path.directory) - .filter((session) => !session.parentID && !session.time?.archived) - .toSorted(sortSessions) - .slice(0, 2) - } - - const projectSessions = () => { - const [data] = globalSync.child(props.project.worktree) - return data.session - .filter((session) => session.directory === data.path.directory) - .filter((session) => !session.parentID && !session.time?.archived) - .toSorted(sortSessions) - .slice(0, 2) - } - const projectName = () => props.project.name || getFilename(props.project.worktree) - const trigger = ( - - ) - - return ( - // @ts-ignore -
- { - setOpen(value) - if (value) setHoverSession(undefined) - }} - > -
-
-
{displayName(props.project)}
- - { - event.stopPropagation() - setOpen(false) - closeProject(props.project.worktree) - }} - /> - -
-
{language.t("sidebar.project.recentSessions")}
-
- - {(session) => ( - - )} - - } - > - - {(directory) => ( -
-
-
- -
- {label(directory)} +
+ +
+ +
+
+
- - {(session) => ( - + } + > + <> +
+ +
+
+ + + +
{ + 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 LocalWorkspace = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => { - const [workspaceStore, setWorkspaceStore] = globalSync.child(props.project.worktree) - const slug = createMemo(() => base64Encode(props.project.worktree)) - const sessions = createMemo(() => - workspaceStore.session - .filter((session) => session.directory === workspaceStore.path.directory) - .filter((session) => !session.parentID && !session.time?.archived) - .toSorted(sortSessions), - ) - const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0) - const hasMore = createMemo(() => workspaceStore.sessionTotal > workspaceStore.session.length) - const loadMore = async () => { - setWorkspaceStore("limit", (limit) => limit + 5) - await globalSync.project.loadSessions(props.project.worktree) - } - - return ( -
{ - if (!props.mobile) scrollContainerRef = el - }} - class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar" - style={{ "overflow-anchor": "none" }} - > - -
- ) - } - - const SidebarContent = (sidebarProps: { mobile?: boolean }) => { - const expanded = () => sidebarProps.mobile || layout.sidebar.opened() - - const sync = useGlobalSync() - const project = createMemo(() => currentProject()) - const projectName = createMemo(() => { - const current = project() - if (!current) return "" - return current.name || getFilename(current.worktree) - }) - const projectId = createMemo(() => project()?.id ?? "") - const workspaces = createMemo(() => workspaceIds(project())) - - const createWorkspace = async () => { - const current = project() - if (!current) return - - const created = await globalSDK.client.worktree - .create({ directory: current.worktree }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("workspace.create.failed.title"), - description: errorMessage(err), - }) - return undefined - }) - - if (!created?.directory) return - - globalSync.child(created.directory) - navigate(`/${base64Encode(created.directory)}/session`) - } - - command.register(() => [ - { - id: "workspace.new", - title: language.t("workspace.new"), - category: language.t("command.category.workspace"), - keybind: "mod+shift+w", - disabled: !layout.sidebar.workspaces(project()?.worktree ?? "")(), - onSelect: createWorkspace, - }, - ]) - - const homedir = createMemo(() => sync.data.path.home) + ) + } + /> + ) - return ( -
-
-
- + +
+
+
+
+ +
setSizing(true)}> + { + setSizing(true) + if (sizet !== undefined) clearTimeout(sizet) + sizet = window.setTimeout(() => setSizing(false), 120) + layout.sidebar.resize(w) + }} + onCollapse={layout.sidebar.close} /> - -
- - - - -
-
- - - - - platform.openLink("https://opencode.ai/desktop-feedback")} - aria-label={language.t("sidebar.help")} - /> - -
-
+
+ + - -
- - {(p) => ( - <> -
-
-
- project() && renameProject(project()!, next)} - class="text-16-medium text-text-strong truncate" - displayClass="text-16-medium text-text-strong truncate" - stopPropagation - /> + +
+
{ + if (e.target === e.currentTarget) layout.mobileSidebar.hide() + }} + /> + +
- - - - - dialog.show(() => )}> - {language.t("common.edit")} - - layout.sidebar.toggleWorkspaces(p.worktree)}> - - {layout.sidebar.workspaces(p.worktree)() - ? language.t("sidebar.workspaces.disable") - : language.t("sidebar.workspaces.enable")} - - - - closeProject(p.worktree)}> - {language.t("common.close")} - - - - -
-
+
+
+ }> + {props.children} + +
+
- -
- - - -
-
- -
- - } - > - <> -
- - - -
-
- - - -
{ - if (!sidebarProps.mobile) scrollContainerRef = el - }} - class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar" - style={{ "overflow-anchor": "none" }} - > - - - {(directory) => ( - - )} - - -
- - - -
-
- -
- - )} - - 0 && providers.paid().length === 0}> -
-
-
-
{language.t("sidebar.gettingStarted.title")}
-
{language.t("sidebar.gettingStarted.line1")}
-
{language.t("sidebar.gettingStarted.line2")}
-
- -
-
-
-
- -
- ) - } +
{ + disarm() + aim.reset() + }} + onPointerDown={disarm} + onMouseLeave={() => { + arm() + }} + > + + {(project) => } + +
- return ( -
- -
- -
-
{ - if (e.target === e.currentTarget) layout.mobileSidebar.hide() - }} - /> -
- -
- }> - {props.children} - -
+ {import.meta.env.DEV && }
diff --git a/packages/app/src/pages/layout/deep-links.ts b/packages/app/src/pages/layout/deep-links.ts new file mode 100644 index 00000000000..5dca421f749 --- /dev/null +++ b/packages/app/src/pages/layout/deep-links.ts @@ -0,0 +1,50 @@ +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 & { + __OPENCODE__?: { + deepLinks?: string[] + } +} + +export const drainPendingDeepLinks = (target: OpenCodeWindow) => { + const pending = target.__OPENCODE__?.deepLinks ?? [] + if (pending.length === 0) return [] + if (target.__OPENCODE__) target.__OPENCODE__.deepLinks = [] + return pending +} diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts new file mode 100644 index 00000000000..9dbc6c72d2f --- /dev/null +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, test } from "bun:test" +import { + collectNewSessionDeepLinks, + collectOpenProjectDeepLinks, + drainPendingDeepLinks, + parseDeepLink, + parseNewSessionDeepLink, +} from "./deep-links" +import { type Session } from "@opencode-ai/sdk/v2/client" +import { + 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 = { + __OPENCODE__: { + deepLinks: ["opencode://open-project?directory=/a"], + }, + } as unknown as Window & { __OPENCODE__?: { 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("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 new file mode 100644 index 00000000000..be4ce9f5742 --- /dev/null +++ b/packages/app/src/pages/layout/helpers.ts @@ -0,0 +1,92 @@ +import { getFilename } from "@opencode-ai/util/path" +import { type Session } from "@opencode-ai/sdk/v2/client" + +export const workspaceKey = (directory: string) => { + const drive = directory.match(/^([A-Za-z]:)[\\/]+$/) + if (drive) return `${drive[1]}${directory.includes("\\") ? "\\" : "/"}` + if (/^[\\/]+$/.test(directory)) return directory.includes("\\") ? "\\" : "/" + return directory.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 sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) => + store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now)) + +export const latestRootSession = (stores: { session: Session[]; path: { directory: string } }[], now: number) => + stores + .flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory))) + .sort(sortSessions(now))[0] + +export function hasProjectPermissions( + request: Record, + include: (item: T) => boolean = () => true, +) { + return Object.values(request).some((list) => list?.some(include)) +} + +export const childMapByParent = (sessions: Session[]) => { + const map = new Map() + for (const session of sessions) { + if (!session.parentID) continue + const existing = map.get(session.parentID) + if (existing) { + existing.push(session.id) + continue + } + map.set(session.parentID, [session.id]) + } + return map +} + +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 new file mode 100644 index 00000000000..4189e4a72a0 --- /dev/null +++ b/packages/app/src/pages/layout/inline-editor.tsx @@ -0,0 +1,126 @@ +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 new file mode 100644 index 00000000000..b6c8fedb155 --- /dev/null +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -0,0 +1,430 @@ +import type { Message, Session, TextPart, UserMessage } from "@opencode-ai/sdk/v2/client" +import { Avatar } from "@opencode-ai/ui/avatar" +import { HoverCard } from "@opencode-ai/ui/hover-card" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { MessageNav } from "@opencode-ai/ui/message-nav" +import { Spinner } from "@opencode-ai/ui/spinner" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { base64Encode } from "@opencode-ai/util/encode" +import { getFilename } from "@opencode-ai/util/path" +import { A, useNavigate, useParams } from "@solidjs/router" +import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js" +import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch" +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 { agentColor } from "@/utils/agent" +import { sessionPermissionRequest } from "../session/composer/session-request-tree" +import { hasProjectPermissions } from "./helpers" + +const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" + +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 + popover?: boolean + children: Map + sidebarExpanded: Accessor + sidebarHovering: Accessor + nav: Accessor + hoverSession: Accessor + setHoverSession: (id: string | undefined) => void + 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 + setHoverSession: (id: string | undefined) => void + clearHoverProjectSoon: () => void + sidebarOpened: Accessor + warmHover: () => void + warmPress: () => void + warmFocus: () => void + cancelHoverPrefetch: () => void +}): JSX.Element => ( + { + props.setHoverSession(undefined) + if (props.sidebarOpened()) return + props.clearHoverProjectSoon() + }} + > +
+
+ }> + + + + +
+ + +
+ + 0}> +
+ + +
+ + {props.session.title} + +
+
+) + +const SessionHoverPreview = (props: { + mobile?: boolean + nav: Accessor + hoverSession: Accessor + session: Session + sidebarHovering: Accessor + hoverReady: Accessor + hoverMessages: Accessor + language: ReturnType + isActive: Accessor + slug: string + setHoverSession: (id: string | undefined) => void + messageLabel: (message: Message) => string | undefined + onMessageSelect: (message: Message) => void + trigger: JSX.Element +}): JSX.Element => ( + props.setHoverSession(open ? props.session.id : undefined)} + > + {props.language.t("session.messages.loading")}
} + > +
+ +
+ + +) + +export const SessionItem = (props: SessionItemProps): JSX.Element => { + const params = useParams() + const navigate = useNavigate() + 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 status = sessionStore.session_status[props.session.id] + return status?.type === "busy" || status?.type === "retry" + }) + + const tint = createMemo(() => { + const messages = sessionStore.message[props.session.id] + if (!messages) return undefined + let user: Message | undefined + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i] + if (message.role !== "user") continue + user = message + break + } + if (!user?.agent) return undefined + + const agent = sessionStore.agent.find((a) => a.name === user.agent) + return agentColor(user.agent, agent?.color) + }) + + const hoverMessages = createMemo(() => + sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"), + ) + const hoverReady = createMemo(() => { + if (sessionStore.message[props.session.id] === undefined) return false + if (props.session.id === params.id) return true + const info = getSessionPrefetch(props.session.directory, props.session.id) + if (!info) return false + return Date.now() - info.at < SESSION_PREFETCH_TTL + }) + const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded()) + const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) + const isActive = createMemo(() => 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 hoverPrefetch = { + current: undefined as ReturnType | undefined, + } + const cancelHoverPrefetch = () => { + if (hoverPrefetch.current === undefined) return + clearTimeout(hoverPrefetch.current) + hoverPrefetch.current = undefined + } + const scheduleHoverPrefetch = () => { + warm(1, "high") + if (hoverPrefetch.current !== undefined) return + hoverPrefetch.current = setTimeout(() => { + hoverPrefetch.current = undefined + warm(2, "low") + }, 80) + } + + onCleanup(cancelHoverPrefetch) + + const messageLabel = (message: Message) => { + const parts = sessionStore.part[message.id] ?? [] + const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored) + return text?.text + } + const item = ( + warm(2, "high")} + warmFocus={() => warm(2, "high")} + cancelHoverPrefetch={cancelHoverPrefetch} + /> + ) + + return ( +
+ + {item} + + } + > + { + if (!isActive()) + layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id) + + navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`) + }} + trigger={item} + /> + + +
+ + { + event.preventDefault() + event.stopPropagation() + void props.archiveSession(props.session) + }} + /> + +
+
+ ) +} + +export const NewSessionItem = (props: { + slug: string + mobile?: boolean + dense?: boolean + sidebarExpanded: Accessor + clearHoverProjectSoon: () => void + setHoverSession: (id: string | undefined) => void +}): JSX.Element => { + const layout = useLayout() + const language = useLanguage() + const label = language.t("command.session.new") + const tooltip = () => props.mobile || !props.sidebarExpanded() + const item = ( + { + props.setHoverSession(undefined) + 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 new file mode 100644 index 00000000000..a26bc183118 --- /dev/null +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -0,0 +1,395 @@ +import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" +import { createStore } from "solid-js/store" +import { base64Encode } from "@opencode-ai/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 { childMapByParent, displayName, sortedRootSessions } from "./helpers" + +export type ProjectSidebarContext = { + currentDir: Accessor + sidebarOpened: Accessor + sidebarHovering: Accessor + hoverProject: Accessor + nav: Accessor + onProjectMouseEnter: (worktree: string, event: MouseEvent) => void + onProjectMouseLeave: (worktree: string) => void + onProjectFocus: (worktree: string) => 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 + setHoverSession: (id: string | undefined) => void +} + +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 + nav: Accessor + 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 (!props.overlay()) return + if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return + 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={() => { + if (props.selected()) { + props.setSuppressHover(true) + layout.sidebar.toggle() + return + } + props.setSuppressHover(false) + 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> + projectChildren: Accessor> + workspaceSessions: (directory: string) => ReturnType + workspaceChildren: (directory: string) => Map + setOpen: (value: boolean) => void + ctx: ProjectSidebarContext + language: ReturnType +}): JSX.Element => ( +
+
+
{displayName(props.project)}
+
+
{props.language.t("sidebar.project.recentSessions")}
+
+ + {(session) => ( + + )} + + } + > + + {(directory) => { + const sessions = createMemo(() => props.workspaceSessions(directory)) + const children = createMemo(() => props.workspaceChildren(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.project.worktree === props.ctx.currentDir() || + props.project.sandboxes?.includes(props.ctx.currentDir()) === true, + ) + 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({ + open: false, + menu: false, + suppressHover: false, + }) + + const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) + const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) + const active = createMemo( + () => state.menu || (preview() ? state.open : overlay() && props.ctx.hoverProject() === props.project.worktree), + ) + + createEffect(() => { + if (preview()) return + if (!state.open) return + setState("open", false) + }) + + createEffect(() => { + if (!selected()) return + if (!state.open) return + setState("open", false) + }) + + 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 projectChildren = createMemo(() => childMapByParent(projectStore().session)) + const workspaceSessions = (directory: string) => { + const [data] = globalSync.child(directory, { bootstrap: false }) + return sortedRootSessions(data, props.sortNow()) + } + const workspaceChildren = (directory: string) => { + const [data] = globalSync.child(directory, { bootstrap: false }) + return childMapByParent(data.session) + } + 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) => setState("open", value)} + setSuppressHover={(value) => setState("suppressHover", value)} + language={language} + /> + ) + + return ( + // @ts-ignore +
+ + { + if (state.menu) return + if (value && state.suppressHover) return + setState("open", value) + if (value) props.ctx.setHoverSession(undefined) + }} + > + setState("open", value)} + ctx={props.ctx} + language={language} + /> + + +
+ ) +} diff --git a/packages/app/src/pages/layout/sidebar-shell.tsx b/packages/app/src/pages/layout/sidebar-shell.tsx new file mode 100644 index 00000000000..ca36af2a421 --- /dev/null +++ b/packages/app/src/pages/layout/sidebar-shell.tsx @@ -0,0 +1,125 @@ +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 new file mode 100644 index 00000000000..48c63e5478d --- /dev/null +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -0,0 +1,502 @@ +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/util/encode" +import { getFilename } from "@opencode-ai/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 "@opencode-ai/sdk/v2/client" +import { type LocalProject } from "@/context/layout" +import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" +import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" +import { childMapByParent, sortedRootSessions } from "./helpers" + +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 + nav: Accessor + hoverSession: Accessor + setHoverSession: (id: string | undefined) => void + 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 + setHoverSession: WorkspaceSidebarContext["setHoverSession"] + 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.setHoverSession(undefined) + props.clearHoverProjectSoon() + props.navigateToNewSession() + }} + /> + + +
+) + +const WorkspaceSessionList = (props: { + slug: Accessor + mobile?: boolean + popover?: boolean + ctx: WorkspaceSidebarContext + showNew: Accessor + loading: Accessor + sessions: Accessor + children: Accessor> + hasMore: Accessor + loadMore: () => Promise + language: ReturnType +}): JSX.Element => ( + +) + +export const SortableWorkspace = (props: { + ctx: WorkspaceSidebarContext + directory: string + project: LocalProject + sortNow: Accessor + mobile?: boolean + popover?: 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 children = createMemo(() => childMapByParent(workspaceStore.session)) + const local = createMemo(() => props.directory === props.project.worktree) + const active = createMemo(() => props.ctx.currentDir() === 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 booted = createMemo((prev) => prev || workspaceStore.status === "complete", false) + const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length) + const busy = createMemo(() => props.ctx.isBusy(props.directory)) + const wasBusy = createMemo((prev) => prev || busy(), false) + const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy()) + const touch = createMediaQuery("(hover: none)") + const showNew = createMemo(() => !loading() && (touch() || sessions().length === 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} + setHoverSession={props.ctx.setHoverSession} + clearHoverProjectSoon={props.ctx.clearHoverProjectSoon} + navigateToNewSession={() => navigate(`/${slug()}/session`)} + /> +
+
+
+ + + + +
+
+ ) +} + +export const LocalWorkspace = (props: { + ctx: WorkspaceSidebarContext + project: LocalProject + sortNow: Accessor + mobile?: boolean + popover?: 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 children = createMemo(() => childMapByParent(workspace().store.session)) + const booted = createMemo((prev) => prev || workspace().store.status === "complete", false) + const loading = createMemo(() => !booted() && sessions().length === 0) + const hasMore = createMemo(() => workspace().store.sessionTotal > sessions().length) + 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} + children={children} + hasMore={hasMore} + loadMore={loadMore} + language={language} + /> +
+ ) +} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 9470a032f72..7642ac165e4 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,248 +1,356 @@ +import type { Project, UserMessage } from "@opencode-ai/sdk/v2" +import { useDialog } from "@opencode-ai/ui/context/dialog" import { - For, - Index, onCleanup, - onMount, Show, Match, Switch, createMemo, createEffect, + createComputed, on, - createSignal, + onMount, + untrack, } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" -import { Dynamic } from "solid-js/web" import { useLocal } from "@/context/local" import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" import { createStore } from "solid-js/store" -import { PromptInput } from "@/components/prompt-input" -import { SessionContextUsage } from "@/components/session-context-usage" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { Select } from "@opencode-ai/ui/select" import { Tabs } from "@opencode-ai/ui/tabs" -import { useCodeComponent } from "@opencode-ai/ui/context/code" -import { SessionTurn } from "@opencode-ai/ui/session-turn" import { createAutoScroll } from "@opencode-ai/ui/hooks" -import { SessionReview } from "@opencode-ai/ui/session-review" -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 { useSync } from "@/context/sync" -import { useTerminal, type LocalPTY } from "@/context/terminal" -import { useLayout } from "@/context/layout" -import { Terminal } from "@/components/terminal" -import { checksum, base64Encode, base64Decode } from "@opencode-ai/util/encode" -import { getFilename } from "@opencode-ai/util/path" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { DialogSelectFile } from "@/components/dialog-select-file" -import { DialogSelectModel } from "@/components/dialog-select-model" -import { DialogSelectMcp } from "@/components/dialog-select-mcp" -import { DialogFork } from "@/components/dialog-fork" -import { useCommand } from "@/context/command" +import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" +import { Button } from "@opencode-ai/ui/button" +import { showToast } from "@opencode-ai/ui/toast" +import { base64Encode, checksum } from "@opencode-ai/util/encode" +import { useNavigate, 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 { useNavigate, useParams } from "@solidjs/router" -import { UserMessage } from "@opencode-ai/sdk/v2" -import type { FileDiff } from "@opencode-ai/sdk/v2/client" -import { useSDK } from "@/context/sdk" +import { useLayout } from "@/context/layout" import { usePrompt } from "@/context/prompt" -import { useComments, type LineComment } from "@/context/comments" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { useTerminal } from "@/context/terminal" +import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer" +import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } 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 { resetSessionModel, 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 { extractPromptFromParts } from "@/utils/prompt" -import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" -import { usePermission } from "@/context/permission" -import { showToast } from "@opencode-ai/ui/toast" -import { - SessionHeader, - SessionContextTab, - SortableTab, - FileVisual, - SortableTerminalTab, - NewSessionView, -} from "@/components/session" -import { usePlatform } from "@/context/platform" -import { navMark, navParams } from "@/utils/perf" import { same } from "@/utils/same" +import { formatServerError } from "@/utils/server-errors" + +const emptyUserMessages: UserMessage[] = [] + +type SessionHistoryWindowInput = { + sessionID: () => string | undefined + messagesReady: () => boolean + visibleUserMessages: () => UserMessage[] + historyMore: () => boolean + historyLoading: () => boolean + loadMore: (sessionID: string) => Promise + userScrolled: () => boolean + scroller: () => HTMLDivElement | undefined +} -type DiffStyle = "unified" | "split" +/** + * 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 handoff = { - prompt: "", - terminals: [] as string[], - files: {} as Record, -} + const initialTurnStart = (len: number) => (len > turnInit ? len - turnInit : 0) -interface SessionReviewTabProps { - diffs: () => FileDiff[] - view: () => ReturnType["view"]> - diffStyle: DiffStyle - onDiffStyleChange?: (style: DiffStyle) => void - onViewFile?: (file: string) => void - onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void - comments?: LineComment[] - focusedComment?: { file: string; id: string } | null - onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void - classes?: { - root?: string - header?: string - container?: string + 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 }) } -} -function SessionReviewTab(props: SessionReviewTabProps) { - let scroll: HTMLDivElement | undefined - let frame: number | undefined - let pending: { x: number; y: number } | undefined + const renderedUserMessages = createMemo( + () => { + const msgs = input.visibleUserMessages() + const start = turnStart() + if (start <= 0) return msgs + return msgs.slice(start) + }, + emptyUserMessages, + { + equals: same, + }, + ) - const sdk = useSDK() + 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 - const readFile = (path: string) => { - return sdk.client.file - .read({ path }) - .then((x) => x.data) - .catch(() => undefined) + preserveScroll(() => setTurnStart(nextStart)) } - const restoreScroll = () => { - const el = scroll - if (!el) return + /** 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 + + if (start > 0) setTurnStart(0) - const s = props.view().scroll("review") - if (!s) return + if (!input.historyMore() || input.historyLoading()) return - if (el.scrollTop !== s.y) el.scrollTop = s.y - if (el.scrollLeft !== s.x) el.scrollLeft = s.x + await input.loadMore(id) + if (input.sessionID() !== id) return + + const afterVisible = input.visibleUserMessages().length + const growth = afterVisible - beforeVisible + if (state.prefetchNoGrowth) setState("prefetchNoGrowth", 0) + if (growth <= 0) return + if (turnStart() !== 0) return + + const target = Math.min(afterVisible, Math.max(beforeVisible, renderedUserMessages().length) + turnBatch) + const nextStart = Math.max(0, afterVisible - target) + preserveScroll(() => setTurnStart(nextStart)) } - const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { - pending = { - x: event.currentTarget.scrollLeft, - y: event.currentTarget.scrollTop, + /** 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) } - if (frame !== undefined) return - frame = requestAnimationFrame(() => { - frame = undefined + const start = turnStart() + const beforeVisible = input.visibleUserMessages().length + const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages().length - const next = pending - pending = undefined - if (!next) return + await input.loadMore(id) + if (input.sessionID() !== id) return - props.view().setScroll("review", next) - }) + const afterVisible = input.visibleUserMessages().length + const growth = afterVisible - beforeVisible + + if (opts?.prefetch) { + setState("prefetchNoGrowth", growth > 0 ? 0 : state.prefetchNoGrowth + 1) + } else if (growth > 0 && state.prefetchNoGrowth) { + setState("prefetchNoGrowth", 0) + } + + if (growth <= 0) return + if (turnStart() !== start) return + + const reveal = !opts?.prefetch + const currentRendered = renderedUserMessages().length + const base = Math.max(beforeRendered, currentRendered) + const target = reveal ? Math.min(afterVisible, base + turnBatch) : base + const nextStart = Math.max(0, afterVisible - target) + preserveScroll(() => setTurnStart(nextStart)) + } + + 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( - () => props.diffs().length, + input.sessionID, () => { - requestAnimationFrame(restoreScroll) + setState({ prefetchUntil: 0, prefetchNoGrowth: 0 }) }, { defer: true }, ), ) - onCleanup(() => { - if (frame === undefined) return - cancelAnimationFrame(frame) - }) - - return ( - { - scroll = el - restoreScroll() - }} - onScroll={handleScroll} - onDiffRendered={() => requestAnimationFrame(restoreScroll)} - open={props.view().review.open()} - onOpenChange={props.view().review.setOpen} - classes={{ - root: props.classes?.root ?? "pb-40", - header: props.classes?.header ?? "px-6", - container: props.classes?.container ?? "px-6", - }} - diffs={props.diffs()} - diffStyle={props.diffStyle} - onDiffStyleChange={props.onDiffStyleChange} - onViewFile={props.onViewFile} - readFile={readFile} - onLineComment={props.onLineComment} - comments={props.comments} - focusedComment={props.focusedComment} - onFocusedCommentChange={props.onFocusedCommentChange} - /> + 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 terminal = useTerminal() const dialog = useDialog() - const codeComponent = useCodeComponent() - const command = useCommand() const language = useLanguage() - const platform = usePlatform() - const params = useParams() const navigate = useNavigate() const sdk = useSDK() const prompt = usePrompt() const comments = useComments() - const permission = usePermission() - const [pendingMessage, setPendingMessage] = createSignal(undefined) - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - const tabs = createMemo(() => layout.tabs(sessionKey())) - const view = createMemo(() => layout.view(sessionKey())) - - if (import.meta.env.DEV) { - createEffect( - on( - () => [params.dir, params.id] as const, - ([dir, id], prev) => { - if (!id) return - navParams({ dir, from: prev?.[1], to: id }) - }, - ), - ) + const terminal = useTerminal() + const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() + const { params, sessionKey, tabs, view } = useSessionLayout() - createEffect(() => { - const id = params.id - if (!id) return - if (!prompt.ready()) return - navMark({ dir: params.dir, to: id, name: "storage:prompt-ready" }) + createEffect(() => { + if (!untrack(() => prompt.ready())) return + prompt.ready() + untrack(() => { + if (params.id || !prompt.ready()) 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 }) }) + }) - createEffect(() => { - const id = params.id - if (!id) return - if (!terminal.ready()) return - navMark({ dir: params.dir, to: id, name: "storage:terminal-ready" }) - }) + const [ui, setUi] = createStore({ + git: false, + pendingMessage: undefined as string | undefined, + restoring: undefined as string | undefined, + reviewSnap: false, + scrollGesture: 0, + scroll: { + overflow: false, + bottom: true, + }, + }) - createEffect(() => { - const id = params.id - if (!id) return - if (!file.ready()) return - navMark({ dir: params.dir, to: id, name: "storage:file-view-ready" }) - }) + const composer = createSessionComposerState() - createEffect(() => { - const id = params.id - if (!id) return - if (sync.data.message[id] === undefined) return - navMark({ dir: params.dir, to: id, name: "session:data-ready" }) - }) - } + 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 @@ -261,43 +369,26 @@ export default function Page() { return next } - const openTab = (value: string) => { - const next = normalizeTab(value) - tabs().open(next) - - const path = file.pathFromTab(next) - if (path) file.load(path) + const openReviewPanel = () => { + if (!view().reviewPanel.opened()) view().reviewPanel.open() } - createEffect(() => { - const active = tabs().active() - if (!active) return - - const path = file.pathFromTab(active) - if (path) file.load(path) - }) - - createEffect(() => { - const current = tabs().all() - if (current.length === 0) return - - const next = normalizeTabs(current) - if (same(current, next)) return - - tabs().setAll(next) - - const active = tabs().active() - if (!active) return - if (!active.startsWith("file://")) return - - const normalized = normalizeTab(active) - if (active === normalized) return - tabs().setActive(normalized) - }) - const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) - const reviewCount = createMemo(() => info()?.summary?.files ?? 0) + const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) + const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length)) const hasReview = createMemo(() => reviewCount() > 0) + const reviewTab = createMemo(() => isDesktop()) + const tabState = createSessionTabs({ + tabs, + pathFromTab: file.pathFromTab, + normalizeTab, + review: reviewTab, + hasReview, + }) + const contextOpen = tabState.contextOpen + const openedTabs = tabState.openedTabs + 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(() => { @@ -315,88 +406,222 @@ export default function Page() { if (!id) return false return sync.session.history.loading(id) }) - const emptyUserMessages: UserMessage[] = [] - const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages) - const visibleUserMessages = createMemo(() => { - const revert = revertMessageID() - if (!revert) return userMessages() - return userMessages().filter((m) => m.id < revert) - }, emptyUserMessages) + + 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) file.load(path) + }) + createEffect( on( () => lastUserMessage()?.id, () => { const msg = lastUserMessage() if (!msg) return - if (msg.agent) local.agent.set(msg.agent) - if (msg.model) local.model.set(msg.model) + 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 (!next.id) resetSessionModel(local) }, + { defer: true }, ), ) const [store, setStore] = createStore({ - activeDraggable: undefined as string | undefined, - activeTerminalDraggable: undefined as string | undefined, - expanded: {} as Record, messageId: undefined as string | undefined, - turnStart: 0, - mobileTab: "session" as "session" | "review", + mobileTab: "session" as "session" | "changes", + changes: "session" as "session" | "turn", newSessionWorktree: "main", - promptHeight: 0, + deferRender: false, }) - const renderedUserMessages = createMemo(() => { - const msgs = visibleUserMessages() - const start = store.turnStart - if (start <= 0) return msgs - if (start >= msgs.length) return emptyUserMessages - return msgs.slice(start) - }, emptyUserMessages) + 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 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(() => lastUserMessage()?.summary?.diffs ?? []) + const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs())) const newSessionWorktree = createMemo(() => { if (store.newSessionWorktree === "create") return "create" const project = sync.project - if (project && sync.data.path.directory !== project.worktree) return sync.data.path.directory + if (project && sdk.directory !== project.worktree) return sdk.directory return "main" }) - const activeMessage = createMemo(() => { - if (!store.messageId) return lastUserMessage() - const found = visibleUserMessages()?.find((m) => m.id === store.messageId) - return found ?? lastUserMessage() - }) 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 = activeMessage() - const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1 - const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset - if (targetIndex < 0 || targetIndex >= msgs.length) 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") } - const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) const diffsReady = createMemo(() => { const id = params.id if (!id) return true if (!hasReview()) return true return sync.data.session_diff[id] !== undefined }) + const reviewEmptyKey = createMemo(() => { + const project = sync.project + if (project && !project.vcs) return "session.review.noVcs" + if (sync.data.config.snapshot === false) return "session.review.noSnapshot" + return "session.review.empty" + }) + + 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]) + } + + function initGit() { + if (ui.git) return + setUi("git", true) + void sdk.client.project + .initGit() + .then((x) => { + if (!x.data) return + upsert(x.data) + }) + .catch((err) => { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: formatServerError(err, language.t), + }) + }) + .finally(() => { + setUi("git", false) + }) + } - const idle = { type: "idle" as const } 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 [scrollGesture, setScrollGesture] = createSignal(0) const scrollGestureWindowMs = 250 const markScrollGesture = (target?: EventTarget | null) => { @@ -407,65 +632,45 @@ export default function Page() { const nested = el?.closest("[data-scrollable]") if (nested && nested !== root) return - setScrollGesture(Date.now()) + setUi("scrollGesture", Date.now()) } - const hasScrollGesture = () => Date.now() - scrollGesture() < scrollGestureWindowMs - - createEffect(() => { - if (!params.id) return - sync.session.sync(params.id) - }) - - const [autoCreated, setAutoCreated] = createSignal(false) - - createEffect(() => { - if (!view().terminal.opened()) { - setAutoCreated(false) - return - } - if (!terminal.ready() || terminal.all().length !== 0 || autoCreated()) return - terminal.new() - setAutoCreated(true) - }) + const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs createEffect( - on( - () => terminal.all().length, - (count, prevCount) => { - if (prevCount !== undefined && prevCount > 0 && count === 0) { - if (view().terminal.opened()) { - view().terminal.toggle() - } - } - }, - ), - ) + on([() => sdk.directory, () => params.id] as const, ([, id]) => { + if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame) + if (refreshTimer !== undefined) window.clearTimeout(refreshTimer) + refreshFrame = undefined + refreshTimer = undefined + if (!id) return - createEffect( - on( - () => terminal.active(), - (activeId) => { - if (!activeId || !view().terminal.opened()) return - // Immediately remove focus - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur() - } - const wrapper = document.getElementById(`terminal-wrapper-${activeId}`) - const element = wrapper?.querySelector('[data-component="terminal"]') as HTMLElement - if (!element) return - - // Find and focus the ghostty textarea (the actual input element) - const textarea = element.querySelector("textarea") as HTMLTextAreaElement - if (textarea) { - textarea.focus() - return - } - // Fallback: focus container and dispatch pointer event - element.focus() - element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true })) - }, - ), + const cached = untrack(() => sync.data.message[id] !== undefined) + const stale = !cached + ? false + : (() => { + const info = getSessionPrefetch(sdk.directory, id) + if (!info) return true + return Date.now() - info.at > SESSION_PREFETCH_TTL + })() + const todos = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined) + + untrack(() => { + void sync.session.sync(id) + }) + + refreshFrame = requestAnimationFrame(() => { + refreshFrame = undefined + refreshTimer = window.setTimeout(() => { + refreshTimer = undefined + if (params.id !== id) return + untrack(() => { + if (stale) void sync.session.sync(id, { force: true }) + void sync.session.todo(id, todos ? { force: true } : undefined) + }) + }, 0) + }) + }), ) createEffect( @@ -480,38 +685,33 @@ export default function Page() { ), ) - const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle) - createEffect( on( - () => params.id, + sessionKey, () => { setStore("messageId", undefined) - setStore("expanded", {}) + setStore("changes", "session") + setUi("pendingMessage", undefined) }, { defer: true }, ), ) - createEffect(() => { - const id = lastUserMessage()?.id - if (!id) return - setStore("expanded", id, status().type !== "idle") - }) + 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 - const start = Math.max(1, Math.min(selection.startLine, selection.endLine)) - const end = Math.max(selection.startLine, selection.endLine) - const lines = content.split("\n").slice(start - 1, end) - if (lines.length === 0) return undefined - return lines.slice(0, 2).join("\n") - } - - const addSelectionToContext = (path: string, selection: FileSelection) => { - const preview = selectionPreview(path, selection) - prompt.context.add({ type: "file", path, selection, preview }) + return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine }) } const addCommentToContext = (input: { @@ -519,6 +719,7 @@ export default function Page() { selection: SelectedLineRange comment: string preview?: string + origin?: "review" | "file" }) => { const selection = selectionFromLines(input.selection) const preview = input.preview ?? selectionPreview(input.file, selection) @@ -533,352 +734,63 @@ export default function Page() { selection, comment: input.comment, commentID: saved.id, + commentOrigin: input.origin, preview, }) } - command.register(() => [ - { - id: "session.new", - title: "New session", - category: "Session", - keybind: "mod+shift+s", - slash: "new", - onSelect: () => navigate(`/${params.dir}/session`), - }, - { - id: "file.open", - title: "Open file", - description: "Search files and commands", - category: "File", - keybind: "mod+p", - slash: "open", - onSelect: () => dialog.show(() => ), - }, - { - id: "context.addSelection", - title: "Add selection to context", - description: "Add selected lines from the current file", - category: "Context", - keybind: "mod+shift+l", - disabled: (() => { - const active = tabs().active() - if (!active) return true - const path = file.pathFromTab(active) - if (!path) return true - return file.selectedLines(path) == null - })(), - onSelect: () => { - const active = tabs().active() - if (!active) return - const path = file.pathFromTab(active) - if (!path) return + 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 range = file.selectedLines(path) - if (!range) { - showToast({ - title: "No line selection", - description: "Select a line range in a file tab first.", - }) - return - } + const removeCommentFromContext = (input: { id: string; file: string }) => { + comments.remove(input.file, input.id) + prompt.context.removeComment(input.file, input.id) + } - addSelectionToContext(path, selectionFromLines(range)) - }, - }, - { - id: "terminal.toggle", - title: "Toggle terminal", - description: "", - category: "View", - keybind: "ctrl+`", - slash: "terminal", - onSelect: () => view().terminal.toggle(), - }, - { - id: "review.toggle", - title: "Toggle review", - description: "", - category: "View", - keybind: "mod+shift+r", - onSelect: () => view().reviewPanel.toggle(), - }, - { - id: "terminal.new", - title: language.t("command.terminal.new"), - description: language.t("command.terminal.new.description"), - category: language.t("command.category.terminal"), - keybind: "ctrl+alt+t", - onSelect: () => { - if (terminal.all().length > 0) terminal.new() - view().terminal.open() - }, - }, - { - id: "steps.toggle", - title: "Toggle steps", - description: "Show or hide steps for the current message", - category: "View", - keybind: "mod+e", - slash: "steps", - disabled: !params.id, - onSelect: () => { - const msg = activeMessage() - if (!msg) return - setStore("expanded", msg.id, (open: boolean | undefined) => !open) - }, - }, - { - id: "message.previous", - title: "Previous message", - description: "Go to the previous user message", - category: "Session", - keybind: "mod+arrowup", - disabled: !params.id, - onSelect: () => navigateMessageByOffset(-1), - }, - { - id: "message.next", - title: "Next message", - description: "Go to the next user message", - category: "Session", - keybind: "mod+arrowdown", - disabled: !params.id, - onSelect: () => navigateMessageByOffset(1), - }, - { - id: "model.choose", - title: "Choose model", - description: "Select a different model", - category: "Model", - keybind: "mod+'", - slash: "model", - onSelect: () => dialog.show(() => ), - }, - { - id: "mcp.toggle", - title: "Toggle MCPs", - description: "Toggle MCPs", - category: "MCP", - keybind: "mod+;", - slash: "mcp", - onSelect: () => dialog.show(() => ), - }, - { - id: "agent.cycle", - title: "Cycle agent", - description: "Switch to the next agent", - category: "Agent", - keybind: "mod+.", - slash: "agent", - onSelect: () => local.agent.move(1), - }, - { - id: "agent.cycle.reverse", - title: "Cycle agent backwards", - description: "Switch to the previous agent", - category: "Agent", - keybind: "shift+mod+.", - onSelect: () => local.agent.move(-1), - }, - { - id: "model.variant.cycle", - title: "Cycle thinking effort", - description: "Switch to the next effort level", - category: "Model", - keybind: "shift+mod+d", - onSelect: () => { - local.model.variant.cycle() - }, - }, - { - id: "permissions.autoaccept", - title: - params.id && permission.isAutoAccepting(params.id, sdk.directory) - ? "Stop auto-accepting edits" - : "Auto-accept edits", - category: "Permissions", - keybind: "mod+shift+a", - disabled: !params.id || !permission.permissionsEnabled(), - onSelect: () => { - const sessionID = params.id - if (!sessionID) return - permission.toggleAutoAccept(sessionID, sdk.directory) - showToast({ - title: permission.isAutoAccepting(sessionID, sdk.directory) - ? "Auto-accepting edits" - : "Stopped auto-accepting edits", - description: permission.isAutoAccepting(sessionID, sdk.directory) - ? "Edit and write permissions will be automatically approved" - : "Edit and write permissions will require approval", - }) - }, - }, - { - id: "session.undo", - title: "Undo", - description: "Undo the last message", - category: "Session", - slash: "undo", - disabled: !params.id || visibleUserMessages().length === 0, - onSelect: async () => { - const sessionID = params.id - if (!sessionID) return - if (status()?.type !== "idle") { - await sdk.client.session.abort({ sessionID }).catch(() => {}) - } - const revert = info()?.revert?.messageID - // Find the last user message that's not already reverted - const message = userMessages().findLast((x) => !revert || x.id < revert) - if (!message) return - await sdk.client.session.revert({ sessionID, messageID: message.id }) - // Restore the prompt from the reverted message - const parts = sync.data.part[message.id] - if (parts) { - const restored = extractPromptFromParts(parts, { directory: sdk.directory }) - prompt.set(restored) - } - // Navigate to the message before the reverted one (which will be the new last visible message) - const priorMessage = userMessages().findLast((x) => x.id < message.id) - setActiveMessage(priorMessage) - }, - }, - { - id: "session.redo", - title: "Redo", - description: "Redo the last undone message", - category: "Session", - slash: "redo", - disabled: !params.id || !info()?.revert?.messageID, - onSelect: async () => { - const sessionID = params.id - if (!sessionID) return - const revertMessageID = info()?.revert?.messageID - if (!revertMessageID) return - const nextMessage = userMessages().find((x) => x.id > revertMessageID) - if (!nextMessage) { - // Full unrevert - restore all messages and navigate to last - await sdk.client.session.unrevert({ sessionID }) - prompt.reset() - // Navigate to the last message (the one that was at the revert point) - const lastMsg = userMessages().findLast((x) => x.id >= revertMessageID) - setActiveMessage(lastMsg) - return - } - // Partial redo - move forward to next message - await sdk.client.session.revert({ sessionID, messageID: nextMessage.id }) - // Navigate to the message before the new revert point - const priorMsg = userMessages().findLast((x) => x.id < nextMessage.id) - setActiveMessage(priorMsg) - }, - }, - { - id: "session.compact", - title: "Compact session", - description: "Summarize the session to reduce context size", - category: "Session", - slash: "compact", - disabled: !params.id || visibleUserMessages().length === 0, - onSelect: async () => { - const sessionID = params.id - if (!sessionID) return - const model = local.model.current() - if (!model) { - showToast({ - title: "No model selected", - description: "Connect a provider to summarize this session", - }) - return - } - await sdk.client.session.summarize({ - sessionID, - modelID: model.id, - providerID: model.provider.id, - }) - }, - }, - { - id: "session.fork", - title: "Fork from message", - description: "Create a new session from a previous message", - category: "Session", - slash: "fork", - disabled: !params.id || visibleUserMessages().length === 0, - onSelect: () => dialog.show(() => ), - }, - ...(sync.data.config.share !== "disabled" - ? [ - { - id: "session.share", - title: "Share session", - description: "Share this session and copy the URL to clipboard", - category: "Session", - slash: "share", - disabled: !params.id || !!info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .share({ sessionID: params.id }) - .then((res) => { - navigator.clipboard.writeText(res.data!.share!.url).catch(() => - showToast({ - title: "Failed to copy URL to clipboard", - variant: "error", - }), - ) - }) - .then(() => - showToast({ - title: "Session shared", - description: "Share URL copied to clipboard!", - variant: "success", - }), - ) - .catch(() => - showToast({ - title: "Failed to share session", - description: "An error occurred while sharing the session", - variant: "error", - }), - ) - }, - }, - { - id: "session.unshare", - title: "Unshare session", - description: "Stop sharing this session", - category: "Session", - slash: "unshare", - disabled: !params.id || !info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .unshare({ sessionID: params.id }) - .then(() => - showToast({ - title: "Session unshared", - description: "Session unshared successfully!", - variant: "success", - }), - ) - .catch(() => - showToast({ - title: "Failed to unshare session", - description: "An error occurred while unsharing the session", - variant: "error", - }), - ) - }, - }, - ] - : []), - ]) + 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 activeElement = document.activeElement as HTMLElement | undefined + 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 = /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(activeElement.tagName) || activeElement.isContentEditable + const isInput = isEditableTarget(activeElement) if (isProtected || isInput) return } if (dialog.active) return @@ -888,8 +800,11 @@ export default function Page() { return } - // Don't autofocus chat if terminal panel is open - if (view().terminal.opened()) return + // Prefer the open terminal over the composer when it can take focus + if (view().terminal.opened()) { + const id = terminal.active() + if (id && 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") { @@ -898,521 +813,593 @@ export default function Page() { } if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { + if (composer.blocked()) return inputRef?.focus() } } - const handleDragStart = (event: unknown) => { - const id = getDraggableId(event) - if (!id) return - setStore("activeDraggable", id) + const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") + + 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 handleDragOver = (event: DragEvent) => { - const { draggable, droppable } = event - if (draggable && droppable) { - const currentTabs = tabs().all() - const fromIndex = currentTabs?.indexOf(draggable.id.toString()) - const toIndex = currentTabs?.indexOf(droppable.id.toString()) - if (fromIndex !== toIndex && toIndex !== undefined) { - tabs().move(draggable.id.toString(), toIndex) - } + const focusInput = () => 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 changesOptions = ["session", "turn"] as const + const changesOptionsList = [...changesOptions] + + const changesTitle = () => { + if (!hasReview()) { + return null } + + 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("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 new file mode 100644 index 00000000000..c3571f3ffce --- /dev/null +++ b/packages/app/src/pages/session/helpers.ts @@ -0,0 +1,191 @@ +import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js" +import { createStore } from "solid-js/store" +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 +} + +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) 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(() => { + window.addEventListener("pointerup", stop) + window.addEventListener("pointercancel", stop) + window.addEventListener("blur", stop) + onCleanup(() => { + window.removeEventListener("pointerup", stop) + window.removeEventListener("pointercancel", stop) + window.removeEventListener("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 new file mode 100644 index 00000000000..b2af4bb8342 --- /dev/null +++ b/packages/app/src/pages/session/message-gesture.test.ts @@ -0,0 +1,62 @@ +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 new file mode 100644 index 00000000000..731cb1bdeb6 --- /dev/null +++ b/packages/app/src/pages/session/message-gesture.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000000..2857f4b01d6 --- /dev/null +++ b/packages/app/src/pages/session/message-id-from-hash.ts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000000..50f9b452a13 --- /dev/null +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -0,0 +1,841 @@ +import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js" +import { createStore, produce } from "solid-js/store" +import { useNavigate } from "@solidjs/router" +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 type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2" +import { showToast } from "@opencode-ai/ui/toast" +import { Binary } from "@opencode-ai/util/binary" +import { getFilename } from "@opencode-ai/util/path" +import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" +import { SessionContextUsage } from "@/components/session-context-usage" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLanguage } from "@/context/language" +import { useSessionKey } from "@/pages/session/session-layout" +import { useSettings } from "@/context/settings" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" + +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 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 } + 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 sdk = useSDK() + const sync = useSync() + const settings = useSettings() + const dialog = useDialog() + const language = useLanguage() + const { params, sessionKey } = useSessionKey() + + 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(() => !!pending() || sessionStatus().type !== "idle") + + const [slot, setSlot] = createStore({ + open: false, + show: false, + fade: false, + }) + + let f: number | undefined + const clear = () => { + if (f !== undefined) window.clearTimeout(f) + f = undefined + } + + onCleanup(clear) + createEffect( + on( + working, + (on, prev) => { + clear() + if (on) { + setSlot({ open: true, show: true, fade: false }) + return + } + if (prev) { + setSlot({ open: false, show: true, fade: true }) + f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260) + return + } + setSlot({ open: false, show: false, fade: false }) + }, + { defer: true }, + ), + ) + 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 parentID = createMemo(() => info()?.parentID) + 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, + saving: false, + menuOpen: false, + pendingRename: false, + }) + let titleRef: HTMLInputElement | undefined + + const errorMessage = (err: unknown) => { + if (err && typeof err === "object" && "data" in err) { + const data = (err as { data?: { message?: string } }).data + if (data?.message) return data.message + } + if (err instanceof Error) return err.message + return language.t("common.requestFailed") + } + + createEffect( + on( + sessionKey, + () => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }), + { defer: true }, + ), + ) + + const openTitleEditor = () => { + if (!sessionID()) return + setTitle({ editing: true, draft: titleValue() ?? "" }) + requestAnimationFrame(() => { + titleRef?.focus() + titleRef?.select() + }) + } + + const closeTitleEditor = () => { + if (title.saving) return + setTitle({ editing: false, saving: false }) + } + + const saveTitleEditor = async () => { + const id = sessionID() + if (!id) return + if (title.saving) return + + const next = title.draft.trim() + if (!next || next === (titleValue() ?? "")) { + setTitle({ editing: false, saving: false }) + return + } + + setTitle("saving", true) + await sdk.client.session + .update({ sessionID: id, title: next }) + .then(() => { + sync.set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === id) + if (index !== -1) draft.session[index].title = next + }), + ) + setTitle({ editing: false, saving: false }) + }) + .catch((err) => { + setTitle("saving", false) + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }) + } + + const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => { + if (params.id !== sessionID) return + if (parentID) { + navigate(`/${params.dir}/session/${parentID}`) + return + } + if (nextSessionID) { + navigate(`/${params.dir}/session/${nextSessionID}`) + return + } + navigate(`/${params.dir}/session`) + } + + const archiveSession = async (sessionID: string) => { + const session = sync.session.get(sessionID) + if (!session) return + + const sessions = sync.data.session ?? [] + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + await sdk.client.session + .update({ sessionID, time: { archived: Date.now() } }) + .then(() => { + sync.set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === sessionID) + if (index !== -1) draft.session.splice(index, 1) + }), + ) + navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) + }) + .catch((err) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }) + } + + const deleteSession = async (sessionID: string) => { + const session = sync.session.get(sessionID) + if (!session) return false + + const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived) + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + const result = await sdk.client.session + .delete({ sessionID }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: language.t("session.delete.failed.title"), + description: errorMessage(err), + }) + return false + }) + + if (!result) return false + + sync.set( + produce((draft) => { + const removed = new Set([sessionID]) + + const byParent = new Map() + for (const item of draft.session) { + const parentID = item.parentID + if (!parentID) continue + const existing = byParent.get(parentID) + if (existing) { + existing.push(item.id) + continue + } + byParent.set(parentID, [item.id]) + } + + const stack = [sessionID] + while (stack.length) { + const parentID = stack.pop() + if (!parentID) continue + + const children = byParent.get(parentID) + if (!children) continue + + for (const child of children) { + if (removed.has(child)) continue + removed.add(child) + stack.push(child) + } + } + + draft.session = draft.session.filter((s) => !removed.has(s.id)) + }), + ) + + navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) + return true + } + + const navigateParent = () => { + const id = parentID() + if (!id) return + navigate(`/${params.dir}/session/${id}`) + } + + function DialogDeleteSession(props: { sessionID: string }) { + const name = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new")) + const handleDelete = async () => { + await deleteSession(props.sessionID) + dialog.close() + } + + return ( + +
+
+ + {language.t("session.delete.confirm", { name: name() })} + +
+
+ + +
+
+
+ ) + } + + return ( + {props.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", + }} + > +
+ +
+
+
+ + + +
+ + + + {titleValue()} + + } + > + { + titleRef = el + }} + value={title.draft} + disabled={title.saving} + class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]" + style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }} + onInput={(event) => setTitle("draft", event.currentTarget.value)} + onKeyDown={(event) => { + event.stopPropagation() + if (event.key === "Enter") { + event.preventDefault() + void saveTitleEditor() + return + } + if (event.key === "Escape") { + event.preventDefault() + closeTitleEditor() + } + }} + onBlur={closeTitleEditor} + /> + + +
+
+ + {(id) => ( +
+ + setTitle("menuOpen", open)} + > + + + { + if (!title.pendingRename) return + event.preventDefault() + setTitle("pendingRename", false) + openTitleEditor() + }} + > + { + setTitle("pendingRename", true) + setTitle("menuOpen", false) + }} + > + {language.t("common.rename")} + + void archiveSession(id())}> + {language.t("common.archive")} + + + dialog.show(() => )} + > + {language.t("common.delete")} + + + + +
+ )} +
+
+
+
+ +
+ 0 || props.historyMore}> +
+ +
+
+ + {(messageID) => { + const active = createMemo(() => activeMessageID() === messageID) + const queued = createMemo(() => { + if (active()) return false + const activeID = activeMessageID() + if (activeID) return messageID > activeID + return false + }) + const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], { + equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), + }) + 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 new file mode 100644 index 00000000000..c073e621472 --- /dev/null +++ b/packages/app/src/pages/session/review-tab.tsx @@ -0,0 +1,170 @@ +import { createEffect, onCleanup, type JSX } from "solid-js" +import type { FileDiff } from "@opencode-ai/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" + +export interface SessionReviewTabProps { + title?: JSX.Element + empty?: JSX.Element + diffs: () => FileDiff[] + 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 + 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) + if (scroll) { + scroll.removeEventListener("wheel", handleInteraction, { capture: true }) + scroll.removeEventListener("mousewheel", handleInteraction, { capture: true }) + scroll.removeEventListener("pointerdown", handleInteraction, { capture: true }) + scroll.removeEventListener("touchstart", handleInteraction, { capture: true }) + scroll.removeEventListener("keydown", handleInteraction, { capture: true }) + } + }) + + return ( + { + scroll = el + el.addEventListener("wheel", handleInteraction, { passive: true, capture: true }) + el.addEventListener("mousewheel", handleInteraction, { passive: true, capture: true }) + el.addEventListener("pointerdown", handleInteraction, { passive: true, capture: true }) + el.addEventListener("touchstart", handleInteraction, { passive: true, capture: true }) + el.addEventListener("keydown", handleInteraction, { passive: true, 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} + 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 new file mode 100644 index 00000000000..113411150da --- /dev/null +++ b/packages/app/src/pages/session/session-layout.ts @@ -0,0 +1,20 @@ +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 new file mode 100644 index 00000000000..5f554dcd36a --- /dev/null +++ b/packages/app/src/pages/session/session-model-helpers.test.ts @@ -0,0 +1,158 @@ +import { describe, expect, test } from "bun:test" +import type { UserMessage } from "@opencode-ai/sdk/v2" +import { resetSessionModel, syncSessionModel } from "./session-model-helpers" + +const message = (input?: Partial>) => + ({ + id: "msg", + sessionID: "session", + role: "user", + time: { created: 1 }, + agent: input?.agent ?? "build", + model: input?.model ?? { providerID: "anthropic", modelID: "claude-sonnet-4" }, + variant: input?.variant, + }) as UserMessage + +describe("syncSessionModel", () => { + test("restores the last message model and variant", () => { + const calls: unknown[] = [] + + syncSessionModel( + { + agent: { + current() { + return undefined + }, + set(value) { + calls.push(["agent", value]) + }, + }, + model: { + set(value) { + calls.push(["model", value]) + }, + current() { + return { id: "claude-sonnet-4", provider: { id: "anthropic" } } + }, + variant: { + set(value) { + calls.push(["variant", value]) + }, + }, + }, + }, + message({ variant: "high" }), + ) + + expect(calls).toEqual([ + ["agent", "build"], + ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }], + ["variant", "high"], + ]) + }) + + test("skips variant when the model falls back", () => { + const calls: unknown[] = [] + + syncSessionModel( + { + agent: { + current() { + return undefined + }, + set(value) { + calls.push(["agent", value]) + }, + }, + model: { + set(value) { + calls.push(["model", value]) + }, + current() { + return { id: "gpt-5", provider: { id: "openai" } } + }, + variant: { + set(value) { + calls.push(["variant", value]) + }, + }, + }, + }, + message({ variant: "high" }), + ) + + expect(calls).toEqual([ + ["agent", "build"], + ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }], + ]) + }) +}) + +describe("resetSessionModel", () => { + test("restores the current agent defaults", () => { + const calls: unknown[] = [] + + resetSessionModel({ + agent: { + current() { + return { + model: { providerID: "anthropic", modelID: "claude-sonnet-4" }, + variant: "high", + } + }, + set() {}, + }, + model: { + set(value) { + calls.push(["model", value]) + }, + current() { + return undefined + }, + variant: { + set(value) { + calls.push(["variant", value]) + }, + }, + }, + }) + + expect(calls).toEqual([ + ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }], + ["variant", "high"], + ]) + }) + + test("clears the variant when the agent has none", () => { + const calls: unknown[] = [] + + resetSessionModel({ + agent: { + current() { + return { + model: { providerID: "anthropic", modelID: "claude-sonnet-4" }, + } + }, + set() {}, + }, + model: { + set(value) { + calls.push(["model", value]) + }, + current() { + return undefined + }, + variant: { + set(value) { + calls.push(["variant", value]) + }, + }, + }, + }) + + expect(calls).toEqual([ + ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }], + ["variant", undefined], + ]) + }) +}) diff --git a/packages/app/src/pages/session/session-model-helpers.ts b/packages/app/src/pages/session/session-model-helpers.ts new file mode 100644 index 00000000000..7600f16d5c7 --- /dev/null +++ b/packages/app/src/pages/session/session-model-helpers.ts @@ -0,0 +1,48 @@ +import type { UserMessage } from "@opencode-ai/sdk/v2" +import { batch } from "solid-js" + +type Local = { + agent: { + current(): + | { + model?: UserMessage["model"] + variant?: string + } + | undefined + set(name: string | undefined): void + } + model: { + set(model: UserMessage["model"] | undefined): void + current(): + | { + id: string + provider: { id: string } + } + | undefined + variant: { + set(value: string | undefined): void + } + } +} + +export const resetSessionModel = (local: Local) => { + const agent = local.agent.current() + if (!agent) return + batch(() => { + local.model.set(agent.model) + local.model.variant.set(agent.variant) + }) +} + +export const syncSessionModel = (local: Local, msg: UserMessage) => { + batch(() => { + local.agent.set(msg.agent) + local.model.set(msg.model) + }) + + const model = local.model.current() + if (!model) return + if (model.provider.id !== msg.model.providerID) return + if (model.id !== msg.model.modelID) return + local.model.variant.set(msg.variant) +} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx new file mode 100644 index 00000000000..3b8b0c96bfe --- /dev/null +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -0,0 +1,455 @@ +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 { 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 { DialogSelectFile } from "@/components/dialog-select-file" +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 { 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: { + reviewPanel: () => JSX.Element + activeDiff?: string + focusReviewDiff: (path: string) => void + reviewSnap: boolean + size: Sizing +}) { + const layout = useLayout() + const sync = useSync() + const file = useFile() + const language = useLanguage() + const command = useCommand() + const dialog = useDialog() + const { params, sessionKey, tabs, view } = useSessionLayout() + + const isDesktop = createMediaQuery("(min-width: 768px)") + + const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) + const fileOpen = createMemo(() => isDesktop() && 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 info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) + const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) + const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length)) + const hasReview = createMemo(() => reviewCount() > 0) + const diffsReady = createMemo(() => { + const id = params.id + if (!id) return true + if (!hasReview()) return true + return sync.data.session_diff[id] !== undefined + }) + + const reviewEmptyKey = createMemo(() => { + if (sync.project && !sync.project.vcs) return "session.review.noVcs" + if (sync.data.config.snapshot === false) return "session.review.noSnapshot" + return "session.review.noChanges" + }) + + const diffFiles = createMemo(() => diffs().map((d) => d.file)) + const kinds = createMemo(() => { + const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { + if (!a) return b + if (a === b) return a + return "mix" as const + } + + const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "") + + const out = new Map() + for (const diff of diffs()) { + const file = normalize(diff.file) + const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix" + + out.set(file, kind) + + const parts = file.split("/") + for (const [idx] of parts.slice(0, -1).entries()) { + const dir = parts.slice(0, idx + 1).join("/") + if (!dir) continue + out.set(dir, merge(out.get(dir), kind)) + } + } + return out + }) + + const 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, + }) + 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 new file mode 100644 index 00000000000..6d336769b10 --- /dev/null +++ b/packages/app/src/pages/session/terminal-label.ts @@ -0,0 +1,16 @@ +export const terminalTabLabel = (input: { + title?: string + titleNumber?: number + t: (key: string, vars?: Record) => string +}) => { + const title = input.title ?? "" + const number = input.titleNumber ?? 0 + const match = title.match(/^Terminal (\d+)$/) + const parsed = match ? Number(match[1]) : undefined + const isDefaultTitle = Number.isFinite(number) && number > 0 && Number.isFinite(parsed) && parsed === 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 new file mode 100644 index 00000000000..43eeec32f21 --- /dev/null +++ b/packages/app/src/pages/session/terminal-panel.test.ts @@ -0,0 +1,25 @@ +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 new file mode 100644 index 00000000000..e78ebecfc41 --- /dev/null +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -0,0 +1,316 @@ +import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" +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 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() + window.addEventListener("resize", sync) + port?.addEventListener("resize", sync) + onCleanup(() => { + window.removeEventListener("resize", sync) + port?.removeEventListener("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 = [120, 240].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) => ( + pty.id === id)}> + {(pty) => ( +
+ terminal.trim(id)} + onCleanup={terminal.update} + onConnectError={() => terminal.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 new file mode 100644 index 00000000000..f5a4c05764b --- /dev/null +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -0,0 +1,495 @@ +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 { usePrompt } from "@/context/prompt" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { useTerminal } from "@/context/terminal" +import { DialogSelectFile } from "@/components/dialog-select-file" +import { DialogSelectModel } from "@/components/dialog-select-model" +import { DialogSelectMcp } from "@/components/dialog-select-mcp" +import { DialogFork } from "@/components/dialog-fork" +import { showToast } from "@opencode-ai/ui/toast" +import { findLast } from "@opencode-ai/util/array" +import { createSessionTabs } from "@/pages/session/helpers" +import { extractPromptFromParts } from "@/utils/prompt" +import { UserMessage } from "@opencode-ai/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 prompt = usePrompt() + const sdk = useSDK() + 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 = () => { + const id = params.id + if (!id) return false + return Math.max(info()?.summary?.files ?? 0, (sync.data.session_diff[id] ?? []).length) > 0 + } + 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 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) + } + command.register("session", () => { + const share = + sync.data.config.share === "disabled" + ? [] + : [ + 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: async () => { + if (!params.id) return + + const write = (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 Promise.resolve(true) + } + + const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard + if (!clipboard?.writeText) return Promise.resolve(false) + return clipboard.writeText(value).then( + () => true, + () => false, + ) + } + + const copy = async (url: string, existing: boolean) => { + const ok = await write(url) + if (!ok) { + 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 existing = info()?.share?.url + if (existing) { + await copy(existing, true) + return + } + + const url = await sdk.client.session + .share({ sessionID: params.id }) + .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 copy(url, false) + }, + }), + 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: async () => { + if (!params.id) return + await sdk.client.session + .unshare({ sessionID: params.id }) + .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", + }), + ) + }, + }), + ] + + return [ + sessionCommand({ + id: "session.new", + title: language.t("command.session.new"), + keybind: "mod+shift+s", + slash: "new", + onSelect: () => navigate(`/${params.dir}/session`), + }), + fileCommand({ + id: "file.open", + title: language.t("command.file.open"), + description: language.t("palette.search.placeholder"), + keybind: "mod+p", + slash: "open", + onSelect: () => dialog.show(() => ), + }), + fileCommand({ + id: "tab.close", + title: language.t("command.tab.close"), + keybind: "mod+w", + disabled: !closableTab(), + onSelect: () => { + const tab = closableTab() + if (!tab) return + tabs().close(tab) + }, + }), + contextCommand({ + id: "context.addSelection", + title: language.t("command.context.addSelection"), + description: language.t("command.context.addSelection.description"), + keybind: "mod+shift+l", + disabled: !canAddSelectionContext(), + onSelect: () => { + 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)) + }, + }), + 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(), + }), + 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, + }), + terminalCommand({ + id: "terminal.new", + title: language.t("command.terminal.new"), + description: language.t("command.terminal.new.description"), + keybind: "ctrl+alt+t", + onSelect: () => { + if (terminal.all().length > 0) terminal.new() + view().terminal.open() + }, + }), + sessionCommand({ + id: "message.previous", + title: language.t("command.message.previous"), + description: language.t("command.message.previous.description"), + keybind: "mod+arrowup", + 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+arrowdown", + disabled: !params.id, + onSelect: () => navigateMessageByOffset(1), + }), + modelCommand({ + id: "model.choose", + title: language.t("command.model.choose"), + description: language.t("command.model.choose.description"), + keybind: "mod+'", + slash: "model", + onSelect: () => dialog.show(() => ), + }), + mcpCommand({ + id: "mcp.toggle", + title: language.t("command.mcp.toggle"), + description: language.t("command.mcp.toggle.description"), + keybind: "mod+;", + slash: "mcp", + onSelect: () => dialog.show(() => ), + }), + 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), + }), + 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(), + }), + 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: () => { + 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"), + }) + }, + }), + 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: 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 priorMessage = findLast(userMessages(), (x) => x.id < message.id) + setActiveMessage(priorMessage) + }, + }), + 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: async () => { + const sessionID = params.id + if (!sessionID) return + const revertMessageID = info()?.revert?.messageID + if (!revertMessageID) return + const nextMessage = userMessages().find((x) => x.id > revertMessageID) + if (!nextMessage) { + await sdk.client.session.unrevert({ sessionID }) + prompt.reset() + const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID) + setActiveMessage(lastMsg) + return + } + await sdk.client.session.revert({ sessionID, messageID: nextMessage.id }) + const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id) + setActiveMessage(priorMsg) + }, + }), + 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: 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, + }) + }, + }), + 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: () => dialog.show(() => ), + }), + ...share, + ] + }) +} 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 new file mode 100644 index 00000000000..7f3389baaac --- /dev/null +++ b/packages/app/src/pages/session/use-session-hash-scroll.test.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 00000000000..5fadb1f22a0 --- /dev/null +++ b/packages/app/src/pages/session/use-session-hash-scroll.ts @@ -0,0 +1,197 @@ +import type { UserMessage } from "@opencode-ai/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[] + 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")) + }) + + 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 index 47a8fbec7bf..035e323c040 100644 --- a/packages/app/src/sst-env.d.ts +++ b/packages/app/src/sst-env.d.ts @@ -1,6 +1,8 @@ /* This file is auto-generated by SST. Do not edit. */ /* tslint:disable */ /* eslint-disable */ +/* biome-ignore-all lint: auto-generated */ + /// interface ImportMetaEnv { diff --git a/packages/app/src/testing/terminal.ts b/packages/app/src/testing/terminal.ts new file mode 100644 index 00000000000..aa197440455 --- /dev/null +++ b/packages/app/src/testing/terminal.ts @@ -0,0 +1,64 @@ +export const terminalAttr = "data-pty-id" + +export type TerminalProbeState = { + connected: boolean + rendered: string + settled: number +} + +export type E2EWindow = Window & { + __opencode_e2e?: { + terminal?: { + enabled?: boolean + terminals?: Record + } + } +} + +const seed = (): TerminalProbeState => ({ + connected: false, + rendered: "", + settled: 0, +}) + +const root = () => { + if (typeof window === "undefined") return + const state = (window as E2EWindow).__opencode_e2e?.terminal + if (!state?.enabled) return + state.terminals ??= {} + return state.terminals +} + +export const terminalProbe = (id: string) => { + const set = (next: Partial) => { + const terms = root() + if (!terms) return + terms[id] = { ...(terms[id] ?? seed()), ...next } + } + + return { + init() { + set(seed()) + }, + connect() { + set({ connected: true }) + }, + render(data: string) { + const terms = root() + if (!terms) return + const prev = terms[id] ?? seed() + terms[id] = { ...prev, rendered: prev.rendered + data } + }, + settle() { + const terms = root() + if (!terms) return + const prev = terms[id] ?? seed() + terms[id] = { ...prev, settled: prev.settled + 1 } + }, + drop() { + const terms = root() + if (!terms) return + delete terms[id] + }, + } +} diff --git a/packages/app/src/theme-preload.test.ts b/packages/app/src/theme-preload.test.ts new file mode 100644 index 00000000000..00d7da23948 --- /dev/null +++ b/packages/app/src/theme-preload.test.ts @@ -0,0 +1,46 @@ +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 new file mode 100644 index 00000000000..7c2c81e7468 --- /dev/null +++ b/packages/app/src/utils/agent.ts @@ -0,0 +1,11 @@ +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)", +} + +export function agentColor(name: string, custom?: string) { + if (custom) return custom + return defaults[name] ?? defaults[name.toLowerCase()] +} diff --git a/packages/app/src/utils/aim.ts b/packages/app/src/utils/aim.ts new file mode 100644 index 00000000000..23471959e16 --- /dev/null +++ b/packages/app/src/utils/aim.ts @@ -0,0 +1,138 @@ +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 new file mode 100644 index 00000000000..c1f9d88c6e9 --- /dev/null +++ b/packages/app/src/utils/base64.ts @@ -0,0 +1,10 @@ +import { base64Decode } from "@opencode-ai/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 new file mode 100644 index 00000000000..99e87fc81c7 --- /dev/null +++ b/packages/app/src/utils/comment-note.ts @@ -0,0 +1,88 @@ +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/dom.ts b/packages/app/src/utils/dom.ts deleted file mode 100644 index 4f3724c7c95..00000000000 --- a/packages/app/src/utils/dom.ts +++ /dev/null @@ -1,51 +0,0 @@ -export function getCharacterOffsetInLine(lineElement: Element, targetNode: Node, offset: number): number { - const r = document.createRange() - r.selectNodeContents(lineElement) - r.setEnd(targetNode, offset) - return r.toString().length -} - -export function getNodeOffsetInLine(lineElement: Element, charIndex: number): { node: Node; offset: number } | null { - const walker = document.createTreeWalker(lineElement, NodeFilter.SHOW_TEXT, null) - let remaining = Math.max(0, charIndex) - let lastText: Node | null = null - let lastLen = 0 - let node: Node | null - while ((node = walker.nextNode())) { - const len = node.textContent?.length || 0 - lastText = node - lastLen = len - if (remaining <= len) return { node, offset: remaining } - remaining -= len - } - if (lastText) return { node: lastText, offset: lastLen } - if (lineElement.firstChild) return { node: lineElement.firstChild, offset: 0 } - return null -} - -export function getSelectionInContainer( - container: HTMLElement, -): { sl: number; sch: number; el: number; ech: number } | null { - const s = window.getSelection() - if (!s || s.rangeCount === 0) return null - const r = s.getRangeAt(0) - const sc = r.startContainer - const ec = r.endContainer - const getLineElement = (n: Node) => - (n.nodeType === Node.TEXT_NODE ? (n.parentElement as Element) : (n as Element))?.closest(".line") - const sle = getLineElement(sc) - const ele = getLineElement(ec) - if (!sle || !ele) return null - if (!container.contains(sle as Node) || !container.contains(ele as Node)) return null - const cc = container.querySelector("code") as HTMLElement | null - if (!cc) return null - const lines = Array.from(cc.querySelectorAll(".line")) - const sli = lines.indexOf(sle as Element) - const eli = lines.indexOf(ele as Element) - if (sli === -1 || eli === -1) return null - const sl = sli + 1 - const el = eli + 1 - const sch = getCharacterOffsetInLine(sle as Element, sc, r.startOffset) - const ech = getCharacterOffsetInLine(ele as Element, ec, r.endOffset) - return { sl, sch, el, ech } -} diff --git a/packages/app/src/utils/index.ts b/packages/app/src/utils/index.ts deleted file mode 100644 index d87053269df..00000000000 --- a/packages/app/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./dom" diff --git a/packages/app/src/utils/notification-click.test.ts b/packages/app/src/utils/notification-click.test.ts new file mode 100644 index 00000000000..fa81b0e0251 --- /dev/null +++ b/packages/app/src/utils/notification-click.test.ts @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000000..316b2782062 --- /dev/null +++ b/packages/app/src/utils/notification-click.ts @@ -0,0 +1,13 @@ +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/perf.ts b/packages/app/src/utils/perf.ts deleted file mode 100644 index 0ecbc33ffc1..00000000000 --- a/packages/app/src/utils/perf.ts +++ /dev/null @@ -1,135 +0,0 @@ -type Nav = { - id: string - dir?: string - from?: string - to: string - trigger?: string - start: number - marks: Record - logged: boolean - timer?: ReturnType -} - -const dev = import.meta.env.DEV - -const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}` - -const now = () => performance.now() - -const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2) - -const navs = new Map() -const pending = new Map() -const active = new Map() - -const required = [ - "session:params", - "session:data-ready", - "session:first-turn-mounted", - "storage:prompt-ready", - "storage:terminal-ready", - "storage:file-view-ready", -] - -function flush(id: string, reason: "complete" | "timeout") { - if (!dev) return - const nav = navs.get(id) - if (!nav) return - if (nav.logged) return - - nav.logged = true - if (nav.timer) clearTimeout(nav.timer) - - const baseName = nav.marks["navigate:start"] !== undefined ? "navigate:start" : "session:params" - const base = nav.marks[baseName] ?? nav.start - - const ms = Object.fromEntries( - Object.entries(nav.marks) - .slice() - .sort(([a], [b]) => a.localeCompare(b)) - .map(([name, t]) => [name, Math.round((t - base) * 100) / 100]), - ) - - console.log( - "perf.session-nav " + - JSON.stringify({ - type: "perf.session-nav.v0", - id: nav.id, - dir: nav.dir, - from: nav.from, - to: nav.to, - trigger: nav.trigger, - base: baseName, - reason, - ms, - }), - ) - - navs.delete(id) -} - -function maybeFlush(id: string) { - if (!dev) return - const nav = navs.get(id) - if (!nav) return - if (nav.logged) return - if (!required.every((name) => nav.marks[name] !== undefined)) return - flush(id, "complete") -} - -function ensure(id: string, data: Omit) { - const existing = navs.get(id) - if (existing) return existing - - const nav: Nav = { - ...data, - marks: {}, - logged: false, - } - nav.timer = setTimeout(() => flush(id, "timeout"), 5000) - navs.set(id, nav) - return nav -} - -export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) { - if (!dev) return - - const id = uid() - const start = now() - const nav = ensure(id, { ...input, id, start }) - nav.marks["navigate:start"] = start - - pending.set(key(input.dir, input.to), id) - return id -} - -export function navParams(input: { dir?: string; from?: string; to: string }) { - if (!dev) return - - const k = key(input.dir, input.to) - const pendingId = pending.get(k) - if (pendingId) pending.delete(k) - const id = pendingId ?? uid() - - const start = now() - const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" }) - nav.marks["session:params"] = start - - active.set(k, id) - maybeFlush(id) - return id -} - -export function navMark(input: { dir?: string; to: string; name: string }) { - if (!dev) return - - const id = active.get(key(input.dir, input.to)) - if (!id) return - - const nav = navs.get(id) - if (!nav) return - if (nav.marks[input.name] !== undefined) return - - nav.marks[input.name] = now() - maybeFlush(id) -} diff --git a/packages/app/src/utils/persist.test.ts b/packages/app/src/utils/persist.test.ts new file mode 100644 index 00000000000..673acd224d2 --- /dev/null +++ b/packages/app/src/utils/persist.test.ts @@ -0,0 +1,115 @@ +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 index 70884977c75..bee2f3e7d1f 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -1,4 +1,4 @@ -import { usePlatform } from "@/context/platform" +import { Platform, usePlatform } from "@/context/platform" import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage" import { checksum } from "@opencode-ai/util/encode" import { createResource, type Accessor } from "solid-js" @@ -17,8 +17,61 @@ type PersistTarget = { const LEGACY_STORAGE = "default.dat" const GLOBAL_STORAGE = "opencode.global.dat" const LOCAL_PREFIX = "opencode." -const fallback = { disabled: false } -const cache = new Map() +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) { @@ -63,9 +116,11 @@ function evict(storage: Storage, keep: string, value: string) { 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 @@ -78,6 +133,7 @@ function evict(storage: Storage, keep: string, value: string) { 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 @@ -85,13 +141,16 @@ function write(storage: Storage, key: string, value: string) { try { storage.removeItem(key) + cacheDelete(key) storage.setItem(key, value) + cacheSet(key, value) return true } catch (error) { if (!quota(error)) throw error } - return evict(storage, key, value) + const ok = evict(storage, key, value) + return ok } function snapshot(value: unknown) { @@ -136,77 +195,114 @@ function parse(value: string) { } } +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" + 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 = cache.get(name) - if (fallback.disabled && cached !== undefined) return cached - - const stored = localStorage.getItem(name) + 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 - cache.set(name, stored) + cacheSet(name, stored) return stored }, setItem: (key, value) => { const name = item(key) - cache.set(name, value) - if (fallback.disabled) return + if (fallbackDisabled(scope)) return try { if (write(localStorage, name, value)) return } catch { - fallback.disabled = true + fallbackSet(scope) return } - fallback.disabled = true + fallbackSet(scope) }, removeItem: (key) => { const name = item(key) - cache.delete(name) - if (fallback.disabled) return - localStorage.removeItem(name) + cacheDelete(name) + if (fallbackDisabled(scope)) return + try { + localStorage.removeItem(name) + } catch { + fallbackSet(scope) + } }, } } function localStorageDirect(): SyncStorage { + const scope = "direct" return { getItem: (key) => { - const cached = cache.get(key) - if (fallback.disabled && cached !== undefined) return cached - - const stored = localStorage.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 - cache.set(key, stored) + cacheSet(key, stored) return stored }, setItem: (key, value) => { - cache.set(key, value) - if (fallback.disabled) return + if (fallbackDisabled(scope)) return try { if (write(localStorage, key, value)) return } catch { - fallback.disabled = true + fallbackSet(scope) return } - fallback.disabled = true + fallbackSet(scope) }, removeItem: (key) => { - cache.delete(key) - if (fallback.disabled) return - localStorage.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 } @@ -223,9 +319,8 @@ export const Persist = { }, } -export function removePersisted(target: { storage?: string; key: string }) { - const platform = usePlatform() - const isDesktop = platform.platform === "desktop" && !!platform.storage +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) @@ -272,12 +367,11 @@ export function persisted( getItem: (key) => { const raw = current.getItem(key) if (raw !== null) { - const parsed = parse(raw) - if (parsed === undefined) return raw - - const migrated = config.migrate ? config.migrate(parsed) : parsed - const merged = merge(defaults, migrated) - const next = JSON.stringify(merged) + const next = normalize(defaults, raw, config.migrate) + if (next === undefined) { + current.removeItem(key) + return null + } if (raw !== next) current.setItem(key, next) return next } @@ -286,16 +380,13 @@ export function persisted( const legacyRaw = legacyStore.getItem(legacyKey) if (legacyRaw === null) continue - current.setItem(key, legacyRaw) + const next = normalize(defaults, legacyRaw, config.migrate) + if (next === undefined) { + legacyStore.removeItem(legacyKey) + continue + } + current.setItem(key, next) legacyStore.removeItem(legacyKey) - - const parsed = parse(legacyRaw) - if (parsed === undefined) return legacyRaw - - const migrated = config.migrate ? config.migrate(parsed) : parsed - const merged = merge(defaults, migrated) - const next = JSON.stringify(merged) - if (legacyRaw !== next) current.setItem(key, next) return next } @@ -319,12 +410,11 @@ export function persisted( getItem: async (key) => { const raw = await current.getItem(key) if (raw !== null) { - const parsed = parse(raw) - if (parsed === undefined) return raw - - const migrated = config.migrate ? config.migrate(parsed) : parsed - const merged = merge(defaults, migrated) - const next = JSON.stringify(merged) + 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 } @@ -335,16 +425,13 @@ export function persisted( const legacyRaw = await legacyStore.getItem(legacyKey) if (legacyRaw === null) continue - await current.setItem(key, legacyRaw) + 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) - - const parsed = parse(legacyRaw) - if (parsed === undefined) return legacyRaw - - const migrated = config.migrate ? config.migrate(parsed) : parsed - const merged = merge(defaults, migrated) - const next = JSON.stringify(merged) - if (legacyRaw !== next) await current.setItem(key, next) return next } diff --git a/packages/app/src/utils/runtime-adapters.test.ts b/packages/app/src/utils/runtime-adapters.test.ts new file mode 100644 index 00000000000..9f408b8eb7e --- /dev/null +++ b/packages/app/src/utils/runtime-adapters.test.ts @@ -0,0 +1,62 @@ +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", () => { + class SpeechCtor {} + 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 new file mode 100644 index 00000000000..4c74da5dc1d --- /dev/null +++ b/packages/app/src/utils/runtime-adapters.ts @@ -0,0 +1,39 @@ +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/scoped-cache.test.ts b/packages/app/src/utils/scoped-cache.test.ts new file mode 100644 index 00000000000..0c6189dafe5 --- /dev/null +++ b/packages/app/src/utils/scoped-cache.test.ts @@ -0,0 +1,69 @@ +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 new file mode 100644 index 00000000000..224c363c1eb --- /dev/null +++ b/packages/app/src/utils/scoped-cache.ts @@ -0,0 +1,104 @@ +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 new file mode 100644 index 00000000000..1f53bb8cf66 --- /dev/null +++ b/packages/app/src/utils/server-errors.test.ts @@ -0,0 +1,131 @@ +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 new file mode 100644 index 00000000000..2c3a8c54dbb --- /dev/null +++ b/packages/app/src/utils/server-errors.ts @@ -0,0 +1,80 @@ +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 new file mode 100644 index 00000000000..50082dcf35d --- /dev/null +++ b/packages/app/src/utils/server-health.test.ts @@ -0,0 +1,123 @@ +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 new file mode 100644 index 00000000000..45a323c7be7 --- /dev/null +++ b/packages/app/src/utils/server-health.ts @@ -0,0 +1,91 @@ +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 + +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) => checkServerHealth(http, fetcher) +} diff --git a/packages/app/src/utils/server.ts b/packages/app/src/utils/server.ts new file mode 100644 index 00000000000..17f4a3adcec --- /dev/null +++ b/packages/app/src/utils/server.ts @@ -0,0 +1,22 @@ +import { createOpencodeClient } from "@opencode-ai/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 ?? "opencode"}:${server.password}`)}`, + } + })() + + return createOpencodeClient({ + ...config, + headers: { ...config.headers, ...auth }, + baseUrl: server.url, + }) +} diff --git a/packages/app/src/utils/solid-dnd.tsx b/packages/app/src/utils/solid-dnd.tsx index a634be4b486..8e30a033aec 100644 --- a/packages/app/src/utils/solid-dnd.tsx +++ b/packages/app/src/utils/solid-dnd.tsx @@ -1,55 +1,49 @@ import { useDragDropContext } from "@thisbeyond/solid-dnd" -import { JSXElement } from "solid-js" 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 (typeof event !== "object" || event === null) return undefined - if (!("draggable" in event)) return undefined - const draggable = (event as { draggable?: { id?: unknown } }).draggable + if (!isDragEvent(event)) return undefined + const draggable = event.draggable if (!draggable) return undefined return typeof draggable.id === "string" ? draggable.id : undefined } -export const ConstrainDragXAxis = (): JSXElement => { - const context = useDragDropContext() - if (!context) return <> - const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context - const transformer: Transformer = { - id: "constrain-x-axis", - order: 100, - callback: (transform) => ({ ...transform, x: 0 }), - } - 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 <> -} +const createTransformer = (id: string, axis: "x" | "y"): Transformer => ({ + id, + order: 100, + callback: (transform) => (axis === "x" ? { ...transform, x: 0 } : { ...transform, y: 0 }), +}) -export const ConstrainDragYAxis = (): JSXElement => { +const createAxisConstraint = (axis: "x" | "y", transformerId: string) => (): JSXElement => { const context = useDragDropContext() - if (!context) return <> + if (!context) return null const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context - const transformer: Transformer = { - id: "constrain-y-axis", - order: 100, - callback: (transform) => ({ ...transform, y: 0 }), - } - 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) + 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 }) - return <> + 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 index d5e606c6750..6dea812ec81 100644 --- a/packages/app/src/utils/sound.ts +++ b/packages/app/src/utils/sound.ts @@ -106,5 +106,12 @@ export function soundSrc(id: string | undefined) { export function playSound(src: string | undefined) { if (typeof Audio === "undefined") return if (!src) return - void new Audio(src).play().catch(() => undefined) + const audio = new Audio(src) + audio.play().catch(() => undefined) + + // Return a cleanup function to pause the sound. + return () => { + audio.pause() + audio.currentTime = 0 + } } diff --git a/packages/app/src/utils/speech.ts b/packages/app/src/utils/speech.ts deleted file mode 100644 index 921e0a159bc..00000000000 --- a/packages/app/src/utils/speech.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { createSignal, onCleanup } from "solid-js" - -// Minimal types to avoid relying on non-standard DOM typings -type RecognitionResult = { - 0: { transcript: string } - isFinal: boolean -} - -type RecognitionEvent = { - results: RecognitionResult[] - resultIndex: number -} - -interface Recognition { - continuous: boolean - interimResults: boolean - lang: string - start: () => void - stop: () => void - onresult: ((e: RecognitionEvent) => void) | null - onerror: ((e: { error: string }) => void) | null - onend: (() => void) | null - onstart: (() => void) | null -} - -const COMMIT_DELAY = 250 - -const appendSegment = (base: string, addition: string) => { - const trimmed = addition.trim() - if (!trimmed) return base - if (!base) return trimmed - const needsSpace = /\S$/.test(base) && !/^[,.;!?]/.test(trimmed) - return `${base}${needsSpace ? " " : ""}${trimmed}` -} - -const extractSuffix = (committed: string, hypothesis: string) => { - const cleanHypothesis = hypothesis.trim() - if (!cleanHypothesis) return "" - const baseTokens = committed.trim() ? committed.trim().split(/\s+/) : [] - const hypothesisTokens = cleanHypothesis.split(/\s+/) - let index = 0 - while ( - index < baseTokens.length && - index < hypothesisTokens.length && - baseTokens[index] === hypothesisTokens[index] - ) { - index += 1 - } - if (index < baseTokens.length) return "" - return hypothesisTokens.slice(index).join(" ") -} - -export function createSpeechRecognition(opts?: { - lang?: string - onFinal?: (text: string) => void - onInterim?: (text: string) => void -}) { - const hasSupport = - typeof window !== "undefined" && - Boolean((window as any).webkitSpeechRecognition || (window as any).SpeechRecognition) - - const [isRecording, setIsRecording] = createSignal(false) - const [committed, setCommitted] = createSignal("") - const [interim, setInterim] = createSignal("") - - let recognition: Recognition | undefined - let shouldContinue = false - let committedText = "" - let sessionCommitted = "" - let pendingHypothesis = "" - let lastInterimSuffix = "" - let shrinkCandidate: string | undefined - let commitTimer: number | undefined - - const cancelPendingCommit = () => { - if (commitTimer === undefined) return - clearTimeout(commitTimer) - commitTimer = undefined - } - - const commitSegment = (segment: string) => { - const nextCommitted = appendSegment(committedText, segment) - if (nextCommitted === committedText) return - committedText = nextCommitted - setCommitted(committedText) - if (opts?.onFinal) opts.onFinal(segment.trim()) - } - - const promotePending = () => { - if (!pendingHypothesis) return - const suffix = extractSuffix(sessionCommitted, pendingHypothesis) - if (!suffix) { - pendingHypothesis = "" - return - } - sessionCommitted = appendSegment(sessionCommitted, suffix) - commitSegment(suffix) - pendingHypothesis = "" - lastInterimSuffix = "" - shrinkCandidate = undefined - setInterim("") - if (opts?.onInterim) opts.onInterim("") - } - - const applyInterim = (suffix: string, hypothesis: string) => { - cancelPendingCommit() - pendingHypothesis = hypothesis - lastInterimSuffix = suffix - shrinkCandidate = undefined - setInterim(suffix) - if (opts?.onInterim) { - opts.onInterim(suffix ? appendSegment(committedText, suffix) : "") - } - if (!suffix) return - const snapshot = hypothesis - commitTimer = window.setTimeout(() => { - if (pendingHypothesis !== snapshot) return - const currentSuffix = extractSuffix(sessionCommitted, pendingHypothesis) - if (!currentSuffix) return - sessionCommitted = appendSegment(sessionCommitted, currentSuffix) - commitSegment(currentSuffix) - pendingHypothesis = "" - lastInterimSuffix = "" - shrinkCandidate = undefined - setInterim("") - if (opts?.onInterim) opts.onInterim("") - }, COMMIT_DELAY) - } - - if (hasSupport) { - const Ctor: new () => Recognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition - - recognition = new Ctor() - recognition.continuous = false - recognition.interimResults = true - recognition.lang = opts?.lang || (typeof navigator !== "undefined" ? navigator.language : "en-US") - - recognition.onresult = (event: RecognitionEvent) => { - if (!event.results.length) return - - let aggregatedFinal = "" - let latestHypothesis = "" - - for (let i = 0; i < event.results.length; i += 1) { - const result = event.results[i] - const transcript = (result[0]?.transcript || "").trim() - if (!transcript) continue - if (result.isFinal) { - aggregatedFinal = appendSegment(aggregatedFinal, transcript) - } else { - latestHypothesis = transcript - } - } - - if (aggregatedFinal) { - cancelPendingCommit() - const finalSuffix = extractSuffix(sessionCommitted, aggregatedFinal) - if (finalSuffix) { - sessionCommitted = appendSegment(sessionCommitted, finalSuffix) - commitSegment(finalSuffix) - } - pendingHypothesis = "" - lastInterimSuffix = "" - shrinkCandidate = undefined - setInterim("") - if (opts?.onInterim) opts.onInterim("") - return - } - - cancelPendingCommit() - - if (!latestHypothesis) { - shrinkCandidate = undefined - applyInterim("", "") - return - } - - const suffix = extractSuffix(sessionCommitted, latestHypothesis) - - if (!suffix) { - if (!lastInterimSuffix) { - shrinkCandidate = undefined - applyInterim("", latestHypothesis) - return - } - if (shrinkCandidate === "") { - applyInterim("", latestHypothesis) - return - } - shrinkCandidate = "" - pendingHypothesis = latestHypothesis - return - } - - if (lastInterimSuffix && suffix.length < lastInterimSuffix.length) { - if (shrinkCandidate === suffix) { - applyInterim(suffix, latestHypothesis) - return - } - shrinkCandidate = suffix - pendingHypothesis = latestHypothesis - return - } - - shrinkCandidate = undefined - applyInterim(suffix, latestHypothesis) - } - - recognition.onerror = (e: { error: string }) => { - cancelPendingCommit() - lastInterimSuffix = "" - shrinkCandidate = undefined - if (e.error === "no-speech" && shouldContinue) { - setInterim("") - if (opts?.onInterim) opts.onInterim("") - setTimeout(() => { - try { - recognition?.start() - } catch {} - }, 150) - return - } - shouldContinue = false - setIsRecording(false) - } - - recognition.onstart = () => { - sessionCommitted = "" - pendingHypothesis = "" - cancelPendingCommit() - lastInterimSuffix = "" - shrinkCandidate = undefined - setInterim("") - if (opts?.onInterim) opts.onInterim("") - setIsRecording(true) - } - - recognition.onend = () => { - cancelPendingCommit() - lastInterimSuffix = "" - shrinkCandidate = undefined - setIsRecording(false) - if (shouldContinue) { - setTimeout(() => { - try { - recognition?.start() - } catch {} - }, 150) - } - } - } - - const start = () => { - if (!recognition) return - shouldContinue = true - sessionCommitted = "" - pendingHypothesis = "" - cancelPendingCommit() - lastInterimSuffix = "" - shrinkCandidate = undefined - setInterim("") - try { - recognition.start() - } catch {} - } - - const stop = () => { - if (!recognition) return - shouldContinue = false - promotePending() - cancelPendingCommit() - lastInterimSuffix = "" - shrinkCandidate = undefined - setInterim("") - if (opts?.onInterim) opts.onInterim("") - try { - recognition.stop() - } catch {} - } - - onCleanup(() => { - shouldContinue = false - promotePending() - cancelPendingCommit() - lastInterimSuffix = "" - shrinkCandidate = undefined - setInterim("") - if (opts?.onInterim) opts.onInterim("") - try { - recognition?.stop() - } catch {} - }) - - return { - isSupported: () => hasSupport, - isRecording, - committed, - interim, - start, - stop, - } -} diff --git a/packages/app/src/utils/terminal-writer.test.ts b/packages/app/src/utils/terminal-writer.test.ts new file mode 100644 index 00000000000..c49702e39b1 --- /dev/null +++ b/packages/app/src/utils/terminal-writer.test.ts @@ -0,0 +1,64 @@ +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 new file mode 100644 index 00000000000..083f51de471 --- /dev/null +++ b/packages/app/src/utils/terminal-writer.ts @@ -0,0 +1,65 @@ +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 new file mode 100644 index 00000000000..d183e10807d --- /dev/null +++ b/packages/app/src/utils/time.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 00000000000..e6b4e282409 --- /dev/null +++ b/packages/app/src/utils/uuid.test.ts @@ -0,0 +1,78 @@ +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 new file mode 100644 index 00000000000..7b964068c86 --- /dev/null +++ b/packages/app/src/utils/uuid.ts @@ -0,0 +1,12 @@ +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 new file mode 100644 index 00000000000..8161e7ad836 --- /dev/null +++ b/packages/app/src/utils/worktree.test.ts @@ -0,0 +1,46 @@ +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 new file mode 100644 index 00000000000..581afd5535e --- /dev/null +++ b/packages/app/src/utils/worktree.ts @@ -0,0 +1,73 @@ +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/sst-env.d.ts b/packages/app/sst-env.d.ts index b6a7e9066ef..64441936d7a 100644 --- a/packages/app/sst-env.d.ts +++ b/packages/app/sst-env.d.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ /* deno-fmt-ignore-file */ +/* biome-ignore-all lint: auto-generated */ /// diff --git a/packages/console/app/package.json b/packages/console/app/package.json index dca6f8477cd..2cbc8f75756 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,13 +1,13 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.33", + "version": "1.2.24", "type": "module", "license": "MIT", "scripts": { "typecheck": "tsgo --noEmit", "dev": "vite dev --host 0.0.0.0", "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev", - "build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json", + "build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json ./.output/public/tui.json", "start": "vite start" }, "dependencies": { diff --git a/packages/console/app/script/generate-sitemap.ts b/packages/console/app/script/generate-sitemap.ts index 6cbffcb8516..89bca6bac59 100755 --- a/packages/console/app/script/generate-sitemap.ts +++ b/packages/console/app/script/generate-sitemap.ts @@ -3,6 +3,7 @@ import { readdir, writeFile } from "fs/promises" import { join, dirname } from "path" import { fileURLToPath } from "url" import { config } from "../src/config.js" +import { LOCALES, route } from "../src/lib/language.js" const __dirname = dirname(fileURLToPath(import.meta.url)) const BASE_URL = config.baseUrl @@ -25,14 +26,17 @@ async function getMainRoutes(): Promise { { path: "/enterprise", priority: 0.8, changefreq: "weekly" }, { path: "/brand", priority: 0.6, changefreq: "monthly" }, { path: "/zen", priority: 0.8, changefreq: "weekly" }, + { path: "/go", priority: 0.8, changefreq: "weekly" }, ] - for (const route of staticRoutes) { - routes.push({ - url: `${BASE_URL}${route.path}`, - priority: route.priority, - changefreq: route.changefreq, - }) + for (const item of staticRoutes) { + for (const locale of LOCALES) { + routes.push({ + url: `${BASE_URL}${route(locale, item.path)}`, + priority: item.priority, + changefreq: item.changefreq, + }) + } } return routes @@ -50,11 +54,13 @@ async function getDocsRoutes(): Promise { const slug = file.replace(".mdx", "") const path = slug === "index" ? "/docs/" : `/docs/${slug}` - routes.push({ - url: `${BASE_URL}${path}`, - priority: slug === "index" ? 0.9 : 0.7, - changefreq: "weekly", - }) + for (const locale of LOCALES) { + routes.push({ + url: `${BASE_URL}${route(locale, path)}`, + priority: slug === "index" ? 0.9 : 0.7, + changefreq: "weekly", + }) + } } } catch (error) { console.error("Error reading docs directory:", error) diff --git a/packages/console/app/src/app.tsx b/packages/console/app/src/app.tsx index cde2f01876f..1f1d0066ecc 100644 --- a/packages/console/app/src/app.tsx +++ b/packages/console/app/src/app.tsx @@ -6,19 +6,36 @@ import { Favicon } from "@opencode-ai/ui/favicon" import { Font } from "@opencode-ai/ui/font" import "@ibm/plex/css/ibm-plex.css" import "./app.css" +import { LanguageProvider } from "~/context/language" +import { I18nProvider, useI18n } from "~/context/i18n" +import { strip } from "~/lib/language" + +function AppMeta() { + const i18n = useI18n() + return ( + <> + opencode + + + + + ) +} export default function App() { return ( ( - - opencode - - - - {props.children} - + + + + + {props.children} + + + )} > diff --git a/packages/console/app/src/asset/brand/opencode-logo-dark-square.png b/packages/console/app/src/asset/brand/opencode-logo-dark-square.png new file mode 100644 index 00000000000..673c7e3a20f Binary files /dev/null and b/packages/console/app/src/asset/brand/opencode-logo-dark-square.png differ diff --git a/packages/console/app/src/asset/brand/opencode-logo-dark-square.svg b/packages/console/app/src/asset/brand/opencode-logo-dark-square.svg new file mode 100644 index 00000000000..6a67f62717b --- /dev/null +++ b/packages/console/app/src/asset/brand/opencode-logo-dark-square.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/console/app/src/asset/brand/opencode-logo-light-square.png b/packages/console/app/src/asset/brand/opencode-logo-light-square.png new file mode 100644 index 00000000000..5c710474abc Binary files /dev/null and b/packages/console/app/src/asset/brand/opencode-logo-light-square.png differ diff --git a/packages/console/app/src/asset/brand/opencode-logo-light-square.svg b/packages/console/app/src/asset/brand/opencode-logo-light-square.svg new file mode 100644 index 00000000000..a738ad87dbb --- /dev/null +++ b/packages/console/app/src/asset/brand/opencode-logo-light-square.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/console/app/src/asset/brand/preview-opencode-logo-dark-square.png b/packages/console/app/src/asset/brand/preview-opencode-logo-dark-square.png new file mode 100644 index 00000000000..604ad7aa7a8 Binary files /dev/null and b/packages/console/app/src/asset/brand/preview-opencode-logo-dark-square.png differ diff --git a/packages/console/app/src/asset/brand/preview-opencode-logo-light-square.png b/packages/console/app/src/asset/brand/preview-opencode-logo-light-square.png new file mode 100644 index 00000000000..3964d852844 Binary files /dev/null and b/packages/console/app/src/asset/brand/preview-opencode-logo-light-square.png differ diff --git a/packages/console/app/src/asset/go-ornate-dark.svg b/packages/console/app/src/asset/go-ornate-dark.svg new file mode 100644 index 00000000000..9b617c6777f --- /dev/null +++ b/packages/console/app/src/asset/go-ornate-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/console/app/src/asset/go-ornate-light.svg b/packages/console/app/src/asset/go-ornate-light.svg new file mode 100644 index 00000000000..79991973d6d --- /dev/null +++ b/packages/console/app/src/asset/go-ornate-light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/console/app/src/component/email-signup.tsx b/packages/console/app/src/component/email-signup.tsx index 65f81b5fc6d..bd33e92006a 100644 --- a/packages/console/app/src/component/email-signup.tsx +++ b/packages/console/app/src/component/email-signup.tsx @@ -2,6 +2,7 @@ import { action, useSubmission } from "@solidjs/router" import dock from "../asset/lander/dock.png" import { Resource } from "@opencode-ai/console-resource" import { Show } from "solid-js" +import { useI18n } from "~/context/i18n" const emailSignup = action(async (formData: FormData) => { "use server" @@ -23,22 +24,21 @@ const emailSignup = action(async (formData: FormData) => { export function EmailSignup() { const submission = useSubmission(emailSignup) + const i18n = useI18n() return (
-

Be the first to know when we release new products

-

Join the waitlist for early access.

+

{i18n.t("email.title")}

+

{i18n.t("email.subtitle")}

- +
-
- Almost done, check your inbox and confirm your email address -
+
{i18n.t("email.success")}
{submission.error}
diff --git a/packages/console/app/src/component/footer.tsx b/packages/console/app/src/component/footer.tsx index 27f8ddd65f1..0ea370ac789 100644 --- a/packages/console/app/src/component/footer.tsx +++ b/packages/console/app/src/component/footer.tsx @@ -2,12 +2,22 @@ import { createAsync } from "@solidjs/router" import { createMemo } from "solid-js" import { github } from "~/lib/github" import { config } from "~/config" +import { useLanguage } from "~/context/language" +import { useI18n } from "~/context/i18n" export function Footer() { + const language = useLanguage() + const i18n = useI18n() + const community = createMemo(() => { + const locale = language.locale() + return locale === "zh" || locale === "zht" + ? ({ key: "footer.feishu", link: language.route("/feishu") } as const) + : ({ key: "footer.discord", link: language.route("/discord") } as const) + }) const githubData = createAsync(() => github()) const starCount = createMemo(() => githubData()?.stars - ? new Intl.NumberFormat("en-US", { + ? new Intl.NumberFormat(language.tag(language.locale()), { notation: "compact", compactDisplay: "short", }).format(githubData()!.stars!) @@ -18,20 +28,20 @@ export function Footer() { ) diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index 7bfcc782508..1e129d59085 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -19,6 +19,8 @@ import { createStore } from "solid-js/store" import { github } from "~/lib/github" import { createEffect, onCleanup } from "solid-js" import { config } from "~/config" +import { useI18n } from "~/context/i18n" +import { useLanguage } from "~/context/language" import "./header-context-menu.css" const isDarkMode = () => window.matchMedia("(prefers-color-scheme: dark)").matches @@ -34,14 +36,17 @@ const fetchSvgContent = async (svgPath: string): Promise => { } } -export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { +export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: boolean }) { const navigate = useNavigate() + const i18n = useI18n() + const language = useLanguage() const githubData = createAsync(() => github()) const starCount = createMemo(() => githubData()?.stars ? new Intl.NumberFormat("en-US", { notation: "compact", compactDisplay: "short", + maximumFractionDigits: 0, }).format(githubData()?.stars!) : config.github.starsFormatted.compact, ) @@ -118,9 +123,9 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { return (
@@ -130,62 +135,66 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { style={`left: ${store.contextMenuPosition.x}px; top: ${store.contextMenuPosition.y}px;`} > -