diff --git a/.changeset/fix-anthropic-custom-model-id-fallback.md b/.changeset/fix-anthropic-custom-model-id-fallback.md new file mode 100644 index 0000000000..c84d0134ad --- /dev/null +++ b/.changeset/fix-anthropic-custom-model-id-fallback.md @@ -0,0 +1,11 @@ +--- +"zoo-code": patch +--- + +Fix Anthropic provider silently replacing a custom/unrecognized `apiModelId` with the hardcoded default model. + +`AnthropicHandler.getModel()` coerced any `apiModelId` not present in the static `anthropicModels` table down to `anthropicDefaultModelId` ("claude-sonnet-4-5"), and that coerced id was what actually got sent as `model` in the API request -- silently ignoring a user-configured custom model name (e.g. a custom Anthropic-compatible deployment or proxy). This produced confusing "model does not exist" errors for the default model instead of the model the user actually selected (#418). + +The same fallback also affected capability lookups used to build the `thinking` request parameter: an unrecognized id fell back to the default model's info, which can be from an older model generation with a different API contract, causing the request to use the legacy `thinking: {type: "enabled", budget_tokens}` shape and get rejected with a 400 by models that require `{type: "adaptive"}`. + +The model id sent to the API now always honors a user-configured `apiModelId`. For unrecognized values, capabilities are best-effort guessed by matching known model-family substrings (mirroring the existing `BedrockHandler.guessModelInfoFromId` heuristic) instead of defaulting to `anthropicDefaultModelId`'s info. diff --git a/.changeset/fix-litellm-model-desync.md b/.changeset/fix-litellm-model-desync.md new file mode 100644 index 0000000000..3598784912 --- /dev/null +++ b/.changeset/fix-litellm-model-desync.md @@ -0,0 +1,28 @@ +--- +"zoo-code": patch +--- + +Fix LiteLLM provider cache key collision, credential priority, and model-selection fallback to non-existent default. + +Two bugs are addressed: + +1. **Cache key collision**: All URL-scoped providers (LiteLLM, Ollama, LM Studio, Poe, DeepSeek, + Requesty) previously shared one cache entry keyed only on the provider name. Switching between + profiles backed by different servers silently served the wrong model list and the stale list + persisted across VS Code restarts via the disk cache. Fixed with a compound cache key: + URL-scoped providers use `provider:baseUrl`; key-scoped providers (LiteLLM, Poe, Requesty) + additionally include a short, irreversible discriminator derived from the API key + (`provider:baseUrl:`) so that two different API keys on the same server never share + a cache entry (relevant when the server enforces per-key model allowlists). Both the discriminator + and the on-disk filename digest are derived via truncated PBKDF2 so neither can be reversed to + identify the API key written to the cache filename. The `RouterProvider.getModel()` cold-start + fallback is also corrected to pass the full options so it resolves the same compound key. + +2. **Silent fallback to hardcoded default**: When the LiteLLM model list was empty (due to the + collision above, a failed sync, or a transient error), `useSelectedModel` reset the configured + model ID to `claude-3-7-sonnet-20250219` -- a model that typically does not exist on user + LiteLLM servers. Four sub-fixes: preserve the configured model ID when the list is empty; + invalidate the React Query router-models cache after a successful "Sync Models" click; pass the + current LiteLLM credentials in the debounced `requestRouterModels` message; and correct the + credential priority in `webviewMessageHandler.ts` so that message values (current unsaved field + state) take precedence over stale saved config, matching the pattern already used for DeepSeek. diff --git a/.changeset/improve-apply-diff-prompt.md b/.changeset/improve-apply-diff-prompt.md new file mode 100644 index 0000000000..4f8dc24c1e --- /dev/null +++ b/.changeset/improve-apply-diff-prompt.md @@ -0,0 +1,5 @@ +--- +"zoo-code": patch +--- + +Enhance the `apply_diff` tool description and parameter instructions to recommend `:start_line:` with exact syntax and emphasize copy-paste exact matching requirements, improving success rates for Gemini Flash and other smaller/faster models. diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index 78eb63dd7f..a5770440ed 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -13,6 +13,11 @@ on: type: boolean default: false +# Least privilege: the release job escalates to contents: write via its own +# job-level permissions block. +permissions: + contents: read + jobs: # Build CLI for each platform. build: diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 76abccf103..6dc7b33d9d 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -9,7 +9,56 @@ on: merge_group: types: [checks_requested] +# Least privilege: every job below escalates only where it needs to. +permissions: + contents: read + jobs: + dependency-review: + runs-on: ubuntu-latest + # Only meaningful for PRs — validates the dependency diff of the pull + # request against GitHub's advisory database before merge. + if: github.event_name == 'pull_request' + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + # This job never pushes — don't persist the GITHUB_TOKEN. + persist-credentials: false + - name: Dependency review + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + + invisible-chars: + runs-on: ubuntu-latest + # Reject invisible / homoglyph Unicode that GitHub's diff UI renders + # invisibly and most editors hide. These compile fine, which is the + # risk: identifier-splitting, string-literal injection, and the + # "Trojan Source" bidi-override attack (U+202A-U+202E). Scanning raw + # bytes catches them in strings, identifiers, and comments alike. + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + # This job never pushes — don't persist the GITHUB_TOKEN. + persist-credentials: false + - name: Reject invisible / homoglyph Unicode + run: | + # zero-width (U+200B-200F), word joiner (U+2060), BOM (U+FEFF), + # bidi overrides (U+202A-202E), soft hyphen (U+00AD). + # Covers source, release-adjacent executable scripts + # (*.sh / *.cjs / *.cts / *.mts), and the executable shell + # blocks inside GitHub workflow/action YAML. + if grep -rnP '[\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}\x{FEFF}\x{00AD}]' \ + --include='*.ts' --include='*.tsx' --include='*.js' --include='*.mjs' \ + --include='*.cjs' --include='*.cts' --include='*.mts' --include='*.sh' \ + --include='*.yml' --include='*.yaml' \ + --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=out \ + --exclude-dir=coverage --exclude-dir=.turbo --exclude-dir=.vinxi \ + src webview-ui packages apps .github; then + echo "::error::Found invisible or homoglyph Unicode characters (zero-width / bidi-override / BOM / soft hyphen)" + exit 1 + fi + check-translations: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a7ca23bd9d..3b9007610c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -7,6 +7,11 @@ on: schedule: - cron: '24 19 * * 3' +# Least privilege: the analyze job escalates to security-events: write via its +# own job-level permissions block. +permissions: + contents: read + jobs: analyze: name: Analyze (${{ matrix.language }}) @@ -47,7 +52,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3 + uses: github/codeql-action/init@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -75,6 +80,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3 + uses: github/codeql-action/analyze@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6a1838c37a..eba2426d02 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,6 +7,9 @@ on: merge_group: types: [checks_requested] +permissions: + contents: read + jobs: e2e-mock: runs-on: ubuntu-latest @@ -34,6 +37,22 @@ jobs: - name: Install xvfb if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true' run: sudo apt-get install -y xvfb + + - name: Get VS Code version from package.json + if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true' + id: vscode-ver + run: | + VERSION=$(node -p 'require("./apps/vscode-e2e/package.json").devDependencies["@types/vscode"]') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Cache VS Code test binary + if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true' + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: | + apps/vscode-e2e/.vscode-test/ + key: vscode-test-${{ runner.os }}-${{ steps.vscode-ver.outputs.version }}-v1 + - name: Run mocked E2E tests id: run-e2e # merge_group and workflow_dispatch always run; cache skip is pull_request only diff --git a/.github/workflows/label-pr-review-state.yml b/.github/workflows/label-pr-review-state.yml index 57ea7015d8..d79b77bc70 100644 --- a/.github/workflows/label-pr-review-state.yml +++ b/.github/workflows/label-pr-review-state.yml @@ -2,11 +2,16 @@ name: Label PR review state on: schedule: - - cron: '0 * * * *' # hourly + - cron: '0 * * * *' # hourly fallback workflow_dispatch: + pull_request: + types: [opened, reopened, ready_for_review, synchronize, review_requested] + pull_request_review: + types: [submitted, dismissed] permissions: pull-requests: write + checks: read concurrency: group: label-pr-review-state @@ -21,71 +26,230 @@ jobs: with: script: | const { owner, repo } = context.repo; - const stateLabels = ['awaiting-author', 'awaiting-review']; - const failures = []; + const stateLabels = ['awaiting-author', 'awaiting-review', 'has-conflicts']; + + // When triggered by a single PR event, only reconcile that PR. + // The hourly schedule and workflow_dispatch reconcile all open PRs. + let prs; + const prNumber = context.payload.pull_request?.number; + if (prNumber) { + const { data: pr } = await github.rest.pulls.get({ + owner, repo, pull_number: prNumber, + }); + prs = [pr]; + } else { + prs = await github.paginate(github.rest.pulls.list, { + owner, repo, state: 'open', per_page: 100, + }); + } + + // Only a `pull_request`/`pull_request_review` run that was itself triggered + // FROM a fork gets a read-only GITHUB_TOKEN — label mutations there 403 with + // "Resource not accessible by integration". `schedule` and `workflow_dispatch` + // runs always execute in the base repo's context with a read/write token, even + // when the PR they're reconciling happens to come from a fork, so they must NOT + // be skipped or fork PRs would never get stale labels cleaned up. + // See: https://docs.github.com/en/actions/concepts/security/github_token + const isReadOnlyRun = Boolean(context.payload.pull_request) && + context.payload.pull_request.head?.repo?.owner?.login !== owner; + + function isForkPR(pr) { + return pr.head?.repo?.owner?.login && pr.head.repo.owner.login !== owner; + } + + // Strips stateLabels from a PR, optionally keeping one. + // Also removes stale-awaiting-author when not keeping awaiting-author. + // Only skipped when this run's own token is read-only (see isReadOnlyRun) — + // schedule/workflow_dispatch runs reconcile fork PRs normally. + async function reconcileLabels(pr, desiredLabel) { + if (isReadOnlyRun && isForkPR(pr)) { + core.info(`PR #${pr.number}: fork PR on a read-only run — skipping label mutation`); + return; + } + const currentLabels = new Set(pr.labels.map(l => l.name)); + for (const label of stateLabels) { + if (label !== desiredLabel && currentLabels.has(label)) { + try { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr.number, name: label, + }); + } catch (err) { + if (err.status !== 404) throw err; // 404 = already gone, benign + } + } + } + if (desiredLabel && !currentLabels.has(desiredLabel)) { + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr.number, labels: [desiredLabel], + }); + } + if (desiredLabel !== 'awaiting-author' && currentLabels.has('stale-awaiting-author')) { + try { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr.number, name: 'stale-awaiting-author', + }); + } catch (err) { + if (err.status !== 404) throw err; + } + } + } - const prs = await github.paginate(github.rest.pulls.list, { - owner, repo, state: 'open', per_page: 100, - }); + // Fetch required status check names from the branch ruleset. + // Uses the public /rules/branches endpoint — no admin token needed. + // Falls back to blocking on all checks if the endpoint is unavailable. + let requiredCheckNames = null; + try { + const { data: rules } = await github.request( + 'GET /repos/{owner}/{repo}/rules/branches/{branch}', + { owner, repo, branch: 'main' }, + ); + const statusRule = rules.find(r => r.type === 'required_status_checks'); + if (statusRule) { + requiredCheckNames = new Set( + statusRule.parameters.required_status_checks.map(c => c.context), + ); + core.info(`Required checks: ${[...requiredCheckNames].join(', ')}`); + } + } catch (err) { + core.warning(`Could not fetch branch rules, falling back to all checks: ${err.message}`); + } + + const failures = []; for (const pr of prs) { try { + // Draft PRs never get a state label. + if (pr.draft) { + core.info(`PR #${pr.number}: draft — stripping state labels`); + await reconcileLabels(pr, null); + continue; + } + + // `mergeable`/`mergeable_state` are only returned by the single-PR GET + // endpoint, and are computed asynchronously by GitHub — a PR fetched via + // pulls.list (schedule/workflow_dispatch runs) never has them, and even a + // single-PR fetch can return `null`/"unknown" if the merge check hasn't + // finished yet. Re-fetch the single PR to get a fresh value, and treat + // "unknown" as not-yet-computed rather than as conflicting. + const prDetail = prNumber + ? pr + : (await github.rest.pulls.get({ owner, repo, pull_number: pr.number })).data; + + if (prDetail.mergeable === false && prDetail.mergeable_state === 'dirty') { + core.info(`PR #${pr.number}: has merge conflicts — labeling has-conflicts`); + await reconcileLabels(pr, 'has-conflicts'); + continue; + } + + // Check CI status for required checks on the PR's head commit only. + // Scoping to required checks avoids advisory checks (e.g. codecov/patch) + // incorrectly blocking label assignment on otherwise-ready PRs. + const [checkRuns, commitStatusRes] = await Promise.all([ + github.paginate(github.rest.checks.listForRef, { + owner, repo, ref: pr.head.sha, per_page: 100, + }), + github.rest.repos.getCombinedStatusForRef({ + owner, repo, ref: pr.head.sha, + }), + ]); + + // Filter to required checks only (or all checks if rules unavailable). + // Always exclude this workflow's own run to avoid self-referential loops. + const relevantRuns = checkRuns.filter(run => { + if (run.name === 'Reconcile PR review state labels') return false; + return requiredCheckNames ? requiredCheckNames.has(run.name) : true; + }); + + // For commit statuses (external CIs), there's no per-status name filtering + // available from getCombinedStatusForRef — it aggregates all statuses. + // If required checks are known, we only use commitStatus as a signal when + // no required check runs exist for this ref (i.e. pure status-based CI). + const useCommitStatus = !requiredCheckNames || relevantRuns.length === 0; + + core.debug(`PR #${pr.number}: ${relevantRuns.length} required check run(s), commit status=${commitStatusRes.data.state} (used=${useCommitStatus})`); + for (const run of relevantRuns) { + core.debug(` check: "${run.name}" status=${run.status} conclusion=${run.conclusion}`); + } + + const ciPending = relevantRuns.some( + run => run.status === 'queued' || run.status === 'in_progress', + ) || (useCommitStatus && commitStatusRes.data.state === 'pending'); + + const ciFailed = !ciPending && ( + relevantRuns.some( + run => run.status === 'completed' && + run.conclusion !== 'success' && + run.conclusion !== 'skipped' && + run.conclusion !== 'neutral', + ) || (useCommitStatus && ( + commitStatusRes.data.state === 'failure' || + commitStatusRes.data.state === 'error' + )) + ); + + // While CI is running or has failed, remove state labels and move on. + // CI failure is its own signal; the label would add noise, not clarity. + if (ciPending || ciFailed) { + core.info(`PR #${pr.number}: CI ${ciPending ? 'pending' : 'failed'} — stripping state labels`); + await reconcileLabels(pr, null); + continue; + } + + // CI is passing. Now determine review state. const reviews = await github.paginate(github.rest.pulls.listReviews, { owner, repo, pull_number: pr.number, per_page: 100, }); - // Reviews are returned chronologically, so later entries replace - // each reviewer's earlier decision. + // Reduce to each reviewer's latest meaningful state. + // Reviews are returned oldest-first, so last-write-wins yields the latest state. + // COMMENTED and DISMISSED are treated as neutral — they do not + // block the PR or indicate the author needs to act. const latest = new Map(); for (const r of reviews) { - if (r.state !== 'COMMENTED') { + if (r.state !== 'COMMENTED' && r.state !== 'DISMISSED') { latest.set(r.user.login, r); } } - const changeRequestReviewers = [...latest.entries()] - .filter(([, review]) => review.state === 'CHANGES_REQUESTED') - .map(([login]) => login); const requestedReviewers = new Set( - pr.requested_reviewers.map(reviewer => reviewer.login), + pr.requested_reviewers.map(r => r.login), ); - let desiredLabel = null; - if (changeRequestReviewers.length > 0) { - desiredLabel = changeRequestReviewers.every( - reviewer => requestedReviewers.has(reviewer), - ) + const changeRequesters = [...latest.entries()] + .filter(([, r]) => r.state === 'CHANGES_REQUESTED') + .map(([login]) => login); + + let desiredLabel; + if (changeRequesters.length > 0) { + // If every change-requester has been re-requested for review, + // the author has addressed feedback and re-opened it for review. + desiredLabel = changeRequesters.every(login => requestedReviewers.has(login)) ? 'awaiting-review' : 'awaiting-author'; - } + } else { + // No outstanding change requests: awaiting first review, or all approved. + // awaiting-review if: there are pending requested reviewers, or nobody + // has given a meaningful review yet. null (approved) if everyone approved. + const allApproved = latest.size > 0 && + [...latest.values()].every(r => r.state === 'APPROVED') && + requestedReviewers.size === 0; - const currentLabels = new Set(pr.labels.map(label => label.name)); - for (const label of stateLabels) { - if (label !== desiredLabel && currentLabels.has(label)) { - await github.rest.issues.removeLabel({ - owner, repo, issue_number: pr.number, name: label, - }); - } + desiredLabel = allApproved ? null : 'awaiting-review'; } - if (desiredLabel && !currentLabels.has(desiredLabel)) { - await github.rest.issues.addLabels({ - owner, repo, issue_number: pr.number, labels: [desiredLabel], - }); - } + core.info( + `PR #${pr.number}: CI passing, reviews=${latest.size}, ` + + `changeRequesters=[${changeRequesters.join(',')}], ` + + `requestedReviewers=[${[...requestedReviewers].join(',')}] → ${desiredLabel ?? '(none)'}` + ); - if ( - desiredLabel !== 'awaiting-author' && - currentLabels.has('stale-awaiting-author') - ) { - await github.rest.issues.removeLabel({ - owner, repo, issue_number: pr.number, - name: 'stale-awaiting-author', - }); - } + await reconcileLabels(pr, desiredLabel); } catch (error) { - failures.push(`#${pr.number}: ${error.message}`); - core.error(`Failed to reconcile PR #${pr.number}: ${error.message}`); + const detail = error.status + ? `${error.message} (HTTP ${error.status}${error.response?.data?.message ? `: ${error.response.data.message}` : ''})` + : error.message; + failures.push(`#${pr.number}: ${detail}`); + core.error(`Failed to reconcile PR #${pr.number}: ${detail}`); } } diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml index 9448d2e400..93fd732762 100644 --- a/.github/workflows/marketplace-publish.yml +++ b/.github/workflows/marketplace-publish.yml @@ -6,6 +6,11 @@ on: - "v*.*.*" workflow_dispatch: +# Least privilege: publish-stable escalates to contents: write via its own +# job-level permissions block. +permissions: + contents: read + jobs: check-pr-approval: runs-on: ubuntu-latest diff --git a/.github/workflows/nightly-publish.yml b/.github/workflows/nightly-publish.yml index 2487a694f9..b395a4019e 100644 --- a/.github/workflows/nightly-publish.yml +++ b/.github/workflows/nightly-publish.yml @@ -41,7 +41,21 @@ jobs: exit 1 fi + - name: Skip if this push is a release merge + id: release-check + run: | + commit_subject=$(git log -1 --format=%s) + skip=false + + if [[ "$commit_subject" =~ ^chore:\ prepare\ v[0-9]+\.[0-9]+\.[0-9]+\ release ]]; then + echo "Commit '${commit_subject}' looks like a release-prep merge; skipping nightly pre-release." + skip=true + fi + + echo "skip=${skip}" >> "$GITHUB_OUTPUT" + - name: Set pre-release version + if: steps.release-check.outputs.skip != 'true' id: version env: RUN_NUMBER: ${{ github.run_number }} @@ -61,6 +75,7 @@ jobs: EOF - name: Build workspace packages + if: steps.release-check.outputs.skip != 'true' env: PKG_RELEASE_CHANNEL: prerelease run: | @@ -68,6 +83,7 @@ jobs: pnpm --filter @roo-code/vscode-webview build - name: Package pre-release VSIX + if: steps.release-check.outputs.skip != 'true' env: POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} PKG_RELEASE_CHANNEL: prerelease @@ -76,6 +92,7 @@ jobs: pnpm --filter ./src exec vsce package --pre-release --no-dependencies --out ../bin - name: Verify VSIX contents + if: steps.release-check.outputs.skip != 'true' env: VERSION_NUMBER: ${{ steps.version.outputs.number }} run: | @@ -87,6 +104,7 @@ jobs: grep -q "extension/webview-ui/audio/celebration.wav" /tmp/zoo-code-vsix-contents.txt - name: Validate packaged manifest identity + if: steps.release-check.outputs.skip != 'true' env: VERSION_NUMBER: ${{ steps.version.outputs.number }} run: | @@ -98,12 +116,36 @@ jobs: test "$artifact_name" = "zoo-code" test "$artifact_publisher" = "ZooCodeOrganization" - # Open VSX is intentionally excluded: it has no pre-release channel concept, - # so pre-release builds would surface as the latest stable version for all users. - name: Publish pre-release to VS Code Marketplace + if: steps.release-check.outputs.skip != 'true' env: VSCE_PAT: ${{ secrets.VSCE_PAT }} VERSION_NUMBER: ${{ steps.version.outputs.number }} run: | - npx @vscode/vsce publish --pre-release --packagePath "bin/zoo-code-${VERSION_NUMBER}.vsix" - echo "Published ZooCodeOrganization.zoo-code ${VERSION_NUMBER} as a VS Code Marketplace pre-release" + npx @vscode/vsce publish --pre-release --skip-duplicate --packagePath "bin/zoo-code-${VERSION_NUMBER}.vsix" + echo "Published or skipped existing ZooCodeOrganization.zoo-code ${VERSION_NUMBER} as a VS Code Marketplace pre-release" + + # The VSIX built above with `vsce package --pre-release` already carries the + # Microsoft.VisualStudio.Code.PreRelease manifest property, which is what Open + # VSX reads to flag the version. `ovsx publish` ignores --pre-release for an + # already-packaged .vsix (it only applies when ovsx does the packaging itself), + # so it's intentionally omitted here. + # + # Open VSX's "latest" alias resolves to the highest semver across stable and + # pre-release alike (pre-release only breaks ties at equal major.minor.patch), + # so a nightly build can transiently become "latest" until the next stable + # release outranks it. This mirrors how Marketplace pre-release users already + # track the newest published version, so it's an accepted trade-off here too. + - name: Publish pre-release to Open VSX Registry + if: steps.release-check.outputs.skip != 'true' + env: + OVSX_PAT: ${{ secrets.OVSX_PAT }} + VERSION_NUMBER: ${{ steps.version.outputs.number }} + run: | + set -o pipefail + publish_output=$(pnpm exec ovsx publish "bin/zoo-code-${VERSION_NUMBER}.vsix" --skip-duplicate 2>&1 | tee /dev/stderr) + if echo "$publish_output" | grep -q "is already published"; then + echo "ZooCodeOrganization.zoo-code ${VERSION_NUMBER} was already published to Open VSX; skipped." + else + echo "Published ZooCodeOrganization.zoo-code ${VERSION_NUMBER} as an Open VSX pre-release" + fi diff --git a/.github/workflows/release-validation.yml b/.github/workflows/release-validation.yml index 7d7ea410b3..0603b71277 100644 --- a/.github/workflows/release-validation.yml +++ b/.github/workflows/release-validation.yml @@ -16,6 +16,9 @@ on: - "releases/**" - ".github/workflows/release-validation.yml" +permissions: + contents: read + jobs: validate-release: runs-on: ubuntu-latest diff --git a/.vscode/settings.json b/.vscode/settings.json index 6eb636c982..9d429ef11f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,9 @@ "dist": true // set this to false to include "dist" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off", - "vitest.disableWorkspaceWarning": true + "js/ts.tsc.autoDetect": "off", + // Surface invisible (zero-width) and ambiguous/homoglyph Unicode in the + // editor. Pairs with the code-qa.yml invisible-chars CI check. + "editor.unicodeHighlight.invisibleCharacters": true, + "editor.unicodeHighlight.ambiguousCharacters": true } diff --git a/AGENTS.md b/AGENTS.md index f291691537..dea0a8f795 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ This repo is a long-running fork of [`Zoo-Code-Org/Zoo-Code`](https://github.com ## Other guidance - Settings View Pattern: When working on `SettingsView`, inputs must bind to the local `cachedState`, NOT the live `useExtensionState()`. The `cachedState` acts as a buffer for user edits, isolating them from the `ContextProxy` source-of-truth until the user explicitly clicks "Save". Wiring inputs directly to the live state causes race conditions. +- Changesets: Do NOT create `.changeset` files for each commit or code change. Changesets are managed separately by maintainers and should not be generated by agents during normal development. ## Test Placement Guidance diff --git a/CHANGELOG.md b/CHANGELOG.md index 217abe9b9a..5d0cb7f707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,77 @@ # Zoo Code Changelog +## [3.68.0] + +### Minor Changes + +- Add Friendli provider with GLM-5.2 support for another hosted way to use the latest GLM model (#722 by @Lee-Si-Yoon, PR #721 by @Lee-Si-Yoon) +- Add native thinking/reasoning support for Ollama models to preserve reasoning output end-to-end (#831 by @navedmerchant, PR #832 by @navedmerchant) +- Fix(anthropic): honor custom `apiModelId` selections instead of silently defaulting to `claude-sonnet-4-5` (#418 by @tatianadenel-devops, #843 by @grizmin, PR #842 by @grizmin) +- Fix(ollama): correctly handle tool results and prevent premature context condensing (#847 by @navedmerchant, PR #848 by @navedmerchant) +- Improve Anthropic Vertex Claude content block handling for more reliable responses (#788 by @daewoongoh, PR #789 by @daewoongoh) +- Fix(task-lifecycle): preserve the parent-child link when a delegated subtask is interrupted (#560 by @edelauna, PR #787 by @edelauna) +- Refactor: remove the deprecated `openai-error-handler` shim and use the shared `error-handler` directly (#766 by @daewoongoh, PR #767 by @daewoongoh) +- Feat(nightly-publish): publish Open VSX pre-releases and skip nightly publish on release merges (#784 by @edelauna, PR #790 by @edelauna) +- Fix(ci): don't skip fork-PR label reconciliation on scheduled and manual runs (PR #234 by @app/roomote) +- Fix(label-pr-review-state): detect merge conflicts and label PRs with `has-conflicts` (PR #269 by @app/roomote) +- Chore(deps): update the `github/codeql-action` digest to `411c4c9` (PR #803 by @app/renovate) +- Chore(deps): update `@types/react` to `v18.3.31` (PR #805 by @app/renovate) +- Chore(deps): update `axios` to `v1.18.1` (PR #806 by @app/renovate) +- Chore: merge the v3.66.0 release preparation branch into `main` (PR #795 by @navedmerchant) + +## [3.66.0] + +### Minor Changes + +- Add Claude Sonnet 5 support across Anthropic, Bedrock, and Vertex providers (#777 by @navedmerchant, PR #778 by @navedmerchant) +- Upgrade Semble to v0.4.1 with flattened result parsing and localized status messages (#733 by @navedmerchant, PR #734 by @navedmerchant) +- Add task-lifecycle status transition guard and startup delegation reconciliation to prevent invalid task state transitions (#366 by @edelauna, PR #692 by @edelauna) +- Fix: LiteLLM cache key collision and silent fallback to a non-existent default model (#638 by @awschmeder, PR #647 by @awschmeder) +- Fix: reliable auto context condensing for the VS Code Language Model API (#714 by @simurg79, PR #710 by @simurg79) +- Fix(ThinkingBudget): support `xhigh` and all extended reasoning effort values (#713 by @6rz6, PR #774 by @edelauna) +- Fix(deepseek): round-trip `reasoning_content` in thinking mode to prevent 400 errors (#201 by @leosdad, PR #775 by @edelauna) +- Fix(gemini): base64-encode `thoughtSignature` bypass token to fix the Vertex AI empty-response loop (#536 by @edelauna, PR #776 by @edelauna) +- Fix: provider cache reset after settings import (#689 by @JunyongParkDev, PR #726 by @JunyongParkDev) +- Fix(delegation): atomically serialize `reopenParentFromDelegation` (#365 by @edelauna, PR #725 by @edelauna) +- Fix: shell default profile name type guard (#686 by @daewoongoh, PR #687 by @daewoongoh) +- chore(security): dependency-review, invisible-char detection, and least-privilege workflow permissions (#782 by @edelauna, PR #783 by @edelauna) +- chore: upgrade `@anthropic-ai/sdk` to 0.104.1 and `@anthropic-ai/vertex-sdk` to 0.17.1 (#438 by @p12tic, PR #600 by @p12tic) +- chore: enforce no-floating-promises in core/task/ (PR #253 by @0xMink) +- ci: improve PR label reconciliation with CI gating and event triggers (PR #228 by @app/roomote) +- fix(deps): update AI SDKs and providers (PR #744 by @app/renovate) +- chore(deps): update build, lint, and test tooling (PR #745 by @app/renovate) +- chore(deps): update dependency mermaid to v11.16.0 (PR #742 by @app/renovate) +- chore(deps): update dependency posthog-js to v1.393.5 (PR #746 by @app/renovate) +- chore(deps): update dependency ajv to v8.20.0 (PR #747 by @app/renovate) +- chore(deps): update dependency react-use to v17.6.1 (PR #740 by @app/renovate) +- chore(deps): update dependency reconnecting-eventsource to v1.6.5 (PR #741 by @app/renovate) +- chore(deps): update dependency pdf-parse to v1.1.4 (PR #739 by @app/renovate) +- chore(deps): update dependency ovsx to v0.10.12 (PR #738 by @app/renovate) +- chore(deps): update dependency only-allow to v1.2.2 (PR #737 by @app/renovate) + +## [3.64.0] + +### Minor Changes + +- Add Rules Management UI — new Rules tab in Settings to create, delete, and open global and workspace Zoo rules (#660 by @ivanarifin, PR #657 by @ivanarifin) +- Add completion change review actions — "See New Changes" and "Restore Changes" buttons after task completion let you inspect and undo changes from the latest prompt (#661 by @ivanarifin, PR #633 by @ivanarifin) +- Add kimi-k2p7-code model on Fireworks provider (PR #599 by @p12tic) +- feat: add abort signal core plumbing — threads AbortSignal through the API metadata layer for future provider-level cancellation (#434 by @easonLiangWorldedtech, PR #674 by @easonLiangWorldedtech) +- feat: add TaskSemaphore utility for parallel task coordination (#362 by @edelauna, PR #675 by @edelauna) +- feat(experiments): register PARALLEL_TOOL_EXECUTION feature flag (internal-only) (#363 by @edelauna, PR #678 by @edelauna) +- Add Roo Code history import to the About page (PR #141 by @roomote) +- Fix: configurable relaxed diff thresholds and diagnostics reduce "edit unsuccessful" errors (#452 by @DannyVarodBlueVine, PR #470 by @nigeldelviero) +- Fix: auto-closing edited files is now opt-in — the setting defaults to off (#719 by @edelauna, PR #720 by @edelauna) +- Fix(diff-view): make auto-closing edited files opt-in, fixing setting that could not be unchecked (#667 by @navedmerchant, PR #668 by @navedmerchant) +- Fix(delegation): serialize delegateParentAndOpenChild with atomicReadAndUpdate to prevent race conditions (#364 by @edelauna, #365 by @edelauna, PR #691 by @edelauna) +- Fix(ask_followup_question): report non-array follow_up suggestions as a type error (#511 by @nh2, PR #662 by @nh2) +- Fix: parse Gemma 4 `` reasoning tags alongside `` (#323 by @sagidM, PR #324 by @sagidM) +- docs(prompt): enhance apply_diff tool instructions to improve Gemini model success rate (#611 by @awschmeder, PR #619 by @awschmeder) +- chore(deps): update undici to v6.27.0 [security] (PR #659 by @renovate) +- chore(deps): update @types/node, @vscode/test-cli, execa, axios (PR #669, #670, #671, #673 by @renovate) +- test(mcp): fix McpHub Windows command wrapping test ordering (PR #632 by @HappyLiang12) +- fix(McpHub): resolve flaky McpHub.spec.ts tests after Vitest 4 upgrade (PR #666 by @edelauna) + ## [3.62.0] ### Minor Changes diff --git a/FORK.md b/FORK.md index 6690d6ac70..2cee803188 100644 --- a/FORK.md +++ b/FORK.md @@ -18,83 +18,103 @@ across merges. `AGENTS.md` only points here. 3. Run the [post-merge checklist](#post-merge-checklist). 4. Fast-forward `local/daily-driver`. -> **Limitation:** the file lists here come from our fork's *non-merge* commits +> **Limitation:** the file lists here come from our fork's _non-merge_ commits > (`git log --no-merges upstream/main..local/daily-driver`). Conflicts that were already resolved -> *inside* past merge commits are not captured. When a new area starts conflicting, add a row. +> _inside_ past merge commits are not captured. When a new area starts conflicting, add a row. + +> **Tag-topology gotcha (learned in the v3.68.0 sync):** our local `vX.Y.Z` release tags +> **shadow** upstream's — ours point at our own release-prep commits (no PR-number suffix), while +> upstream's real tags carry a `(#NNN)` suffix on the release commit. So `git describe` / tag +> comparisons lie; the authoritative fork-point is +> `git merge-base local/daily-driver `. Upstream's release tags are also **parallel +> snapshots, not a linear chain** (`v3.64.0` is not an ancestor of `v3.66.0`), so **merge the single +> target tag directly** — don't chain per-release merges. Always `git fetch upstream --tags` first so +> the target tag resolves (local tags are never clobbered by fetch, which is why the shadowing +> persists). ## Divergence at a glance -| # | Local feature | Origin commit(s) | Nature | -|---|---|---|---| -| 1 | Effort-based Anthropic reasoning (Opus 4.6/4.7/4.8) | `fd93c5bde`, `64fc5fc98` | modifies shared provider logic | -| 2 | OpenRouter effort-array mirroring + gpt-5.5 defs | `062657a7d`, `64fc5fc98` | modifies shared fetcher/registry | -| 3 | Claude Fable 5 + safety-refusal handling | `811b5ca55` | modifies shared provider logic | -| 4 | `"max"` reasoningEffort i18n label | `dd675fd3b` | mechanical i18n | -| 5 | Workspace-scoped code-index config (`.roo/codebase-index.json`) | `3efa0728e`→`8f54e2274` (phases 1–5) | mostly new files + isolated wiring | +| # | Local feature | Origin commit(s) | Nature | +| --- | --------------------------------------------------------------- | ------------------------------------ | ---------------------------------- | +| 1 | Effort-based Anthropic reasoning (Opus 4.6/4.7/4.8) | `fd93c5bde`, `64fc5fc98` | modifies shared provider logic | +| 2 | OpenRouter effort-array mirroring + gpt-5.5 defs | `062657a7d`, `64fc5fc98` | modifies shared fetcher/registry | +| 3 | Claude Fable 5 + safety-refusal handling | `811b5ca55` | modifies shared provider logic | +| 4 | `"max"` reasoningEffort i18n label | `dd675fd3b` | mechanical i18n | +| 5 | Workspace-scoped code-index config (`.roo/codebase-index.json`) | `3efa0728e`→`8f54e2274` (phases 1–5) | mostly new files + isolated wiring | ## Conflict-prone code paths (shared files we modified) Risk = **upstream churn** (commits touching the file on `upstream/main` in the last 6 months) × -**change nature** (isolated additive wiring merges cleanly; changes to shared *logic* conflict +**change nature** (isolated additive wiring merges cleanly; changes to shared _logic_ conflict hard). High churn with an isolated add is usually a clean 3-way merge; low churn on keystone logic can still be the ugliest conflict. ### Feature 1+3 — Anthropic reasoning + Fable -| File | Upstream churn (6mo) | Our change | Change nature | Risk | -|---|---:|---|---|---| -| `packages/types/src/providers/anthropic.ts` | 10 | Effort-shape model entries (Opus 4.6/4.7/4.8, Fable 5): `supportsReasoningEffort`, `requiredReasoningEffort`, `supportsTemperature:false`, **no** `supportsReasoningBudget` | modifies contract registry | **HIGH** (this shape drives the request payload) | -| `src/api/transform/reasoning.ts` | 5 | `getAnthropicReasoning` branch on `supportsReasoningEffort && !supportsReasoningBudget` → `{ thinking:{type:"adaptive"}, output_config:{effort} }` | keystone logic | **HIGH** (low churn but ugliest if upstream refactors reasoning extraction) | -| `src/api/providers/anthropic.ts` | 23 | Spread `reasoning.thinking` + `reasoning.output_config` into request; Fable `stop_reason:"refusal"` → category-aware text chunk | modifies shared logic | **HIGH** | -| `src/api/providers/anthropic-vertex.ts` | 17 | Destructure + spread `reasoning.thinking`/`output_config`; no provider-side adaptive guard (registry shape alone decides payload) | modifies shared logic | **MED** | -| `packages/types/src/provider-settings.ts` | 17 | Effort field/enum plumbing | modifies shared types | **MED** | -| `src/shared/api.ts` | 12 | Effort plumbing | modifies shared types | **MED** | -| `packages/types/src/model.ts` | 4 | Effort type support | modifies shared types | **LOW–MED** | +| File | Upstream churn (6mo) | Our change | Change nature | Risk | +| ------------------------------------------- | -------------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------- | +| `packages/types/src/providers/anthropic.ts` | 10 | Effort-shape model entries (Opus 4.6/4.7/4.8, Fable 5, **Sonnet 5**): `supportsReasoningEffort`, `requiredReasoningEffort`, `supportsTemperature:false`, **no** `supportsReasoningBudget` | modifies contract registry | **HIGH** (this shape drives the request payload) | +| `packages/types/src/providers/vertex.ts` | — | Effort-shape Vertex entries (Opus 4.8, **Sonnet 5**); Vertex has no provider-side guard so the registry shape alone must steer `getAnthropicReasoning` | modifies contract registry | **MED** | +| `src/api/transform/reasoning.ts` | 5 | `getAnthropicReasoning` branch on `supportsReasoningEffort && !supportsReasoningBudget` → `{ thinking:{type:"adaptive"}, output_config:{effort} }` | keystone logic | **HIGH** (low churn but ugliest if upstream refactors reasoning extraction) | +| `src/api/providers/anthropic.ts` | 23 | Spread `reasoning.thinking` + `reasoning.output_config` into request; Fable `stop_reason:"refusal"` → category-aware text chunk | modifies shared logic | **HIGH** | +| `src/api/providers/anthropic-vertex.ts` | 17 | Destructure + spread `reasoning.thinking`/`output_config`; no provider-side adaptive guard (registry shape alone decides payload) | modifies shared logic | **MED** | +| `packages/types/src/provider-settings.ts` | 17 | Effort field/enum plumbing | modifies shared types | **MED** | +| `src/shared/api.ts` | 12 | Effort plumbing | modifies shared types | **MED** | +| `packages/types/src/model.ts` | 4 | Effort type support | modifies shared types | **LOW–MED** | **Provider guards to preserve (semantic, easy to break on merge):** -- **Vertex** has *no* provider-side adaptive-thinking guard — the registry shape alone decides the + +- **Vertex** has _no_ provider-side adaptive-thinking guard — the registry shape alone decides the payload. Opus 4.8 is on the effort shape; **Opus 4.7/4.6 still declare `supportsReasoningBudget` and will 400 on the live API for 4.7 — known follow-up from `fd93c5bde`.** Do not "fix" this by reintroducing the budget shape on Vertex 4.8. - **Bedrock** has its own `isAdaptiveThinkingModel(modelId)` guard in - `src/api/providers/bedrock.ts` (matches `opus-4-7`, `opus-4-8`, `sonnet-4-7`, `sonnet-4-8` after - `parseBaseModelId`) that overrides the payload regardless of registry shape. Bedrock registry - entries keep the upstream-style `supportsReasoningBudget` shape — intentional and correct there. + `src/api/providers/bedrock.ts` (matches `opus-4-7`, `opus-4-8`, `sonnet-4-7`, `sonnet-4-8`, + `sonnet-5` after `parseBaseModelId`) that overrides the payload regardless of registry shape. + This guard is **upstream/shared code** — the fork does _not_ modify `bedrock.ts` (upstream added + `sonnet-5` itself in #778), so it merges clean. Bedrock registry entries keep the upstream-style + `supportsReasoningBudget` shape — intentional and correct there. - Anything that re-declares `supportsReasoningBudget` on the effort models drops them onto the - legacy budget path, which Opus 4.7+ rejects with a 400. + legacy budget path, which Opus 4.7+ / Fable 5 / Sonnet 5 reject with a 400. -When adding a new effort-capable model, mirror the existing 4.7/4.8 entries and add a parametrized -test in `src/api/providers/__tests__/anthropic.spec.ts` (effort assertion, `requiredReasoningEffort` -always-on, user-chosen effort) rather than upstream-style budget/binary assertions. +When adopting a new effort-capable model, mirror the existing 4.7/4.8/Fable-5 entries across **five** +sites: the `packages/types` Anthropic **and** Vertex registries, plus the `openrouter.ts` **and** +`requesty.ts` fetcher override blocks (set `supportsReasoningBudget:false`, not +`supportsReasoningBinary`). Bedrock needs no change (shared guard). Then update effort assertions — +not upstream-style budget/binary — in **all** of: `anthropic.spec.ts`, `anthropic-vertex.spec.ts`, +the `requesty.spec.ts` provider spec (**including its `getModels` mock entry**), and the +`openrouter.spec.ts` / `requesty.spec.ts` fetcher specs. This is exactly what the v3.68.0 sync did +for **Claude Sonnet 5** — upstream #778 shipped it budget/binary; the fork converts it to effort-shape. -### Feature 2 — OpenRouter / OpenAI effort +### Feature 2 — OpenRouter / OpenAI / Requesty effort -| File | Upstream churn (6mo) | Our change | Change nature | Risk | -|---|---:|---|---|---| -| `packages/types/src/providers/openai.ts` | 8 | gpt-5.5 defs + static effort arrays | modifies registry | **MED** | -| `src/api/providers/fetchers/openrouter.ts` | 6 | Dynamic fetcher patches known IDs (`anthropic/claude-opus-4.7`, `anthropic/claude-opus-4.8`, gpt-5.5 family) to mirror the static effort arrays so `xhigh`/`max` stay reachable from the UI | modifies shared fetcher | **MED** | +| File | Upstream churn (6mo) | Our change | Change nature | Risk | +| ------------------------------------------ | -------------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------- | +| `packages/types/src/providers/openai.ts` | 8 | gpt-5.5 defs + static effort arrays | modifies registry | **MED** | +| `src/api/providers/fetchers/openrouter.ts` | 6 | Dynamic fetcher patches known IDs (`anthropic/claude-opus-4.7`, `4.8`, `claude-sonnet-5`, `claude-fable-5`, gpt-5.5 family) to mirror the static effort arrays so `xhigh`/`max` stay reachable from the UI | modifies shared fetcher | **MED** | +| `src/api/providers/fetchers/requesty.ts` | low | Same effort-array mirroring as openrouter for `anthropic/claude-fable-5` + `claude-sonnet-5` (sets `supportsReasoningBudget:false`). Upstream's Sonnet 5 (#778) patches the same block → conflict-prone. **Was missing from this map before the v3.68.0 sync.** | modifies shared fetcher | **MED** | ### Feature 5 — Workspace-scoped code-index (modified shared files) -| File | Upstream churn (6mo) | Our change | Change nature | Risk | -|---|---:|---|---|---| -| `src/core/webview/ClineProvider.ts` | 81 | Wire code-index scope | isolated additive | **MED** (high churn, but additive — usually clean 3-way) | -| `src/core/webview/webviewMessageHandler.ts` | 59 | Code-index scope message handlers | isolated additive | **MED** | -| `packages/types/src/vscode-extension-host.ts` | 52 | Code-index host type | isolated additive | **MED** | -| `webview-ui/src/components/chat/CodeIndexPopover.tsx` | 2 | Scope switcher + pinned-by-dotfile badge | UI additive | **LOW–MED** | -| `src/services/code-index/manager.ts` | 2 | Two-scope wiring | our changes dominate | **LOW–MED** | -| `src/services/code-index/config-manager.ts` | 1 | Two-scope config model | our changes dominate | **LOW** | -| `packages/types/src/codebase-index.ts` | 1 | Config shape/types | our changes dominate | **LOW** | +| File | Upstream churn (6mo) | Our change | Change nature | Risk | +| ----------------------------------------------------- | -------------------: | ---------------------------------------- | -------------------- | -------------------------------------------------------- | +| `src/core/webview/ClineProvider.ts` | 81 | Wire code-index scope | isolated additive | **MED** (high churn, but additive — usually clean 3-way) | +| `src/core/webview/webviewMessageHandler.ts` | 59 | Code-index scope message handlers | isolated additive | **MED** | +| `packages/types/src/vscode-extension-host.ts` | 52 | Code-index host type | isolated additive | **MED** | +| `webview-ui/src/components/chat/CodeIndexPopover.tsx` | 2 | Scope switcher + pinned-by-dotfile badge | UI additive | **LOW–MED** | +| `src/services/code-index/manager.ts` | 2 | Two-scope wiring | our changes dominate | **LOW–MED** | +| `src/services/code-index/config-manager.ts` | 1 | Two-scope config model | our changes dominate | **LOW** | +| `packages/types/src/codebase-index.ts` | 1 | Config shape/types | our changes dominate | **LOW** | ### i18n -| Files | Our change | Risk | -|---|---|---| +| Files | Our change | Risk | +| ---------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ | | `webview-ui/src/i18n/locales/*/settings.json` (18 locales) | `"max"` reasoningEffort label + code-index scope strings | **MED** — frequent but mechanical; resolve by taking both sides / regenerating | ## Fork-only files (added — no textual conflict, watch for semantic drift) -These don't exist upstream, so they never produce merge markers. The risk is *drift*: if upstream +These don't exist upstream, so they never produce merge markers. The risk is _drift_: if upstream restructures the code-index service or the types package, these need re-wiring, not merging. - `packages/types/schemas/codebase-index.schema.json` — published JSON Schema for the dotfile @@ -112,8 +132,10 @@ These conflict on essentially **every** upstream merge and are expected — reso don't investigate them as regressions. - `src/package.json` — version string → re-bump (see checklist re: VSIX) -- `CHANGELOG.md` — take the union; keep our fork entries -- `README.md`, `locales/*/README.md`, `webview-ui/src/i18n/locales/*/chat.json` — release/marketing churn +- `src/core/webview/ClineProvider.ts` — the `latestAnnouncementId` string collides every release → take upstream's +- `CHANGELOG.md` — take the union; keep our fork entries (the HEAD side is usually empty — upstream just prepends the new `## [X.Y.Z]` sections, so "take theirs" on the hunk preserves our older entries below) +- `README.md`, `locales/*/README.md`, `webview-ui/src/i18n/locales/*/chat.json` — release/marketing churn; the conflict is the "What's New" / announcement `highlightN` block → take upstream's (in v3.68.0 our v3.62.0 highlights were superseded). Fork branding (Zoo Code, migration guide) sits _outside_ the conflict and auto-merges — do **not** `git checkout --theirs` the whole file +- `webview-ui/src/i18n/locales/*/settings.json` usually **auto-merges** (both sides add different keys) — our `"max"` label + code-index strings and upstream's new keys coexist - `AGENTS.md` — now just a pointer paragraph, so the footprint is small ## Post-merge checklist @@ -127,7 +149,22 @@ don't investigate them as regressions. every recent merge). A clean run = those 22 and nothing else; a 23rd is the regression to investigate. - [ ] Verify the Anthropic effort payload still emits `output_config.effort` (not `budget_tokens`) - for Opus 4.7/4.8 — see the provider guards above. + for Opus 4.7/4.8, Fable 5, and Sonnet 5 — see the provider guards above. +- [ ] **The full build needs a networked, non-sandboxed shell:** `pnpm install` runs postinstalls + that download the ripgrep binary and the tree-sitter WASM grammars. If those are absent the + `services/tree-sitter/**` and `__tests__/dist_assets.spec.ts` suites fail on missing _assets_ + only (not code) — a false alarm. Run `pnpm install && pnpm build && pnpm vsix --force` before + trusting the full `pnpm -w test` count. + +## Sync log / decisions + +- **v3.68.0** (from v3.62.0-era, merge-base `8c3ae1e8b`) — 63 upstream commits / 337 files, but the + real conflict surface was ~14 code files, essentially all from upstream's **Claude Sonnet 5** + (#778). **Decision:** adopt Sonnet 5 on the fork's **effort-shape** (not upstream's budget/binary) + across the Anthropic + Vertex registries and the OpenRouter + Requesty fetchers — Sonnet 5 is + adaptive-only and 400s on `budget_tokens`, so the budget shape would break it. Also surfaced: + `fetchers/requesty.ts` was an undocumented fork divergence (now mapped in Feature 2), and upstream + removed the `openai-error-handler` shim (#767 — all callers moved to `error-handler`, merged clean). ## Regenerate this map diff --git a/README.md b/README.md index 1a6c25c273..c47798a394 100644 --- a/README.md +++ b/README.md @@ -53,17 +53,15 @@ You can find a quick guide for migrating from Roo Code to Zoo Code in the [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration). We plan to try and help users as they transition over, we have our [Reddit](https://www.reddit.com/r/ZooCode) and [Discord](https://discord.gg/VxfP4Vx3gX) for this exact support, so if you are having problems or if you have question, jump on and ask. -## What's New in v3.62.0 - -- **GLM-5.2 support** — the latest GLM model is now available in your provider settings -- **OpenCode-Go improvements** — native model parameters, Anthropic-format routing, and a context-token fix for more reliable responses -- **Tool-writer mode** — a new specialized mode for writing and maintaining tool definitions, now available in the Marketplace -- **LiteLLM session header** — forward taskId as X-Zoo-Session-ID request header for better request tracing -- Fix apiRequestTimeout applied consistently across all providers -- Fix diff view scroll position and tab handling on save/deny -- Fix terminal completion signal delivery when end event wins the race -- Refactor RateLimitClock out of Task static state for cleaner rate-limit handling -- Security updates: vitest v4, shell-quote v1.8.4, esbuild v0.28.1, vite v8.0.16 +## What's New in v3.68.0 + +- **Friendli provider with GLM-5.2 support** — use the latest GLM model through Friendli. +- **Native Ollama thinking/reasoning support** — preserve reasoning output end-to-end when you use Ollama models. +- **Anthropic custom `apiModelId` fix** — custom Anthropic model IDs now stay selected instead of silently falling back to `claude-sonnet-4-5`. +- Fix: Ollama provider tool result handling and premature context condensing. +- Fix: preserve the parent-child task link when a delegated subtask is interrupted. +- Improve Anthropic Vertex Claude content block handling for more reliable responses. +- CI, nightly publishing, and dependency/tooling updates.
🌐 Available languages @@ -87,7 +85,7 @@ for this exact support, so if you are having problems or if you have question, j - [简体中文](locales/zh-CN/README.md) - [繁體中文](locales/zh-TW/README.md) - ... -
+ --- diff --git a/apps/cli/package.json b/apps/cli/package.json index c350bc736c..15b932aeb7 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -29,7 +29,6 @@ "@trpc/client": "^11.8.1", "@vscode/ripgrep": "^1.15.9", "commander": "^12.1.0", - "cross-spawn": "^7.0.6", "execa": "^9.5.2", "fuzzysort": "^3.1.0", "ink": "^6.6.0", @@ -41,13 +40,13 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "24.2.1", - "@types/react": "18.3.23", - "@vitest/coverage-v8": "4.1.0", + "@types/node": "20.19.43", + "@types/react": "18.3.31", + "@vitest/coverage-v8": "4.1.9", "ink-testing-library": "4.0.0", "rimraf": "6.0.1", "tsup": "8.5.0", - "tsx": "4.19.4", - "vitest": "4.1.0" + "tsx": "4.22.4", + "vitest": "4.1.9" } } diff --git a/apps/cli/src/ui/components/autocomplete/triggers/HistoryTrigger.tsx b/apps/cli/src/ui/components/autocomplete/triggers/HistoryTrigger.tsx index 443fdfa979..f1be762244 100644 --- a/apps/cli/src/ui/components/autocomplete/triggers/HistoryTrigger.tsx +++ b/apps/cli/src/ui/components/autocomplete/triggers/HistoryTrigger.tsx @@ -21,7 +21,7 @@ export interface HistoryResult extends AutocompleteItem { /** Mode the task was run in */ mode?: string /** Task status */ - status?: "active" | "completed" | "delegated" + status?: "active" | "completed" | "delegated" | "interrupted" } /** @@ -133,8 +133,22 @@ export function createHistoryTrigger(config: HistoryTriggerConfig): Autocomplete renderItem: (item: HistoryResult, isSelected: boolean) => { // Status indicator - const statusIcon = item.status === "completed" ? "✓" : item.status === "active" ? "●" : "○" - const statusColor = item.status === "completed" ? "green" : item.status === "active" ? "yellow" : "gray" + const statusIcon = + item.status === "completed" + ? "✓" + : item.status === "active" + ? "●" + : item.status === "interrupted" + ? "⏸" + : "○" + const statusColor = + item.status === "completed" + ? "green" + : item.status === "active" + ? "yellow" + : item.status === "interrupted" + ? "cyan" + : "gray" // Mode indicator (if available) const modeText = item.mode ? ` [${item.mode}]` : "" @@ -178,7 +192,7 @@ export function toHistoryResult(item: { totalCost?: number workspace?: string mode?: string - status?: "active" | "completed" | "delegated" + status?: "active" | "completed" | "delegated" | "interrupted" }): HistoryResult { return { key: item.id, // Use task ID as the unique key diff --git a/apps/cli/src/ui/components/autocomplete/triggers/__tests__/HistoryTrigger.test.tsx b/apps/cli/src/ui/components/autocomplete/triggers/__tests__/HistoryTrigger.test.tsx index 8e5906ac7c..1d1a891425 100644 --- a/apps/cli/src/ui/components/autocomplete/triggers/__tests__/HistoryTrigger.test.tsx +++ b/apps/cli/src/ui/components/autocomplete/triggers/__tests__/HistoryTrigger.test.tsx @@ -188,6 +188,24 @@ describe("HistoryTrigger", () => { expect(output).toContain("○") }) + it("should render interrupted status with correct indicator", () => { + const trigger = createHistoryTrigger({ getHistory: () => mockHistoryItems }) + + const interruptedItem: HistoryResult = { + key: "task-interrupted", + id: "task-interrupted", + task: "Interrupted subtask waiting to resume", + ts: Date.now() - 1000 * 60 * 5, + mode: "ask", + status: "interrupted", + } + const { lastFrame } = render(trigger.renderItem(interruptedItem, false) as React.ReactElement) + + const output = lastFrame() + // Should contain the interrupted status indicator (⏸) + expect(output).toContain("⏸") + }) + it("should render selected items with different styling", () => { const trigger = createHistoryTrigger({ getHistory: () => mockHistoryItems }) diff --git a/apps/cli/src/ui/types.ts b/apps/cli/src/ui/types.ts index 3c45377c67..b3c8944c22 100644 --- a/apps/cli/src/ui/types.ts +++ b/apps/cli/src/ui/types.ts @@ -109,7 +109,7 @@ export interface TaskHistoryItem { totalCost?: number workspace?: string mode?: string - status?: "active" | "completed" | "delegated" + status?: "active" | "completed" | "delegated" | "interrupted" tokensIn?: number tokensOut?: number } diff --git a/apps/vscode-e2e/.vscode-test.mjs b/apps/vscode-e2e/.vscode-test.mjs deleted file mode 100644 index 71773e561d..0000000000 --- a/apps/vscode-e2e/.vscode-test.mjs +++ /dev/null @@ -1,16 +0,0 @@ -/** - * See: https://code.visualstudio.com/api/working-with-extensions/testing-extension - */ - -import { defineConfig } from "@vscode/test-cli" - -export default defineConfig({ - label: "integrationTest", - files: "out/suite/**/*.test.js", - workspaceFolder: ".", - mocha: { - ui: "tdd", - timeout: 60000, - }, - launchArgs: ["--enable-proposed-api=ZooCodeOrganization.zoo-code", "--disable-extensions"], -}) diff --git a/apps/vscode-e2e/fixtures/deepseek-v4.json b/apps/vscode-e2e/fixtures/deepseek-v4.json index 995dd0951b..a4710207e2 100644 --- a/apps/vscode-e2e/fixtures/deepseek-v4.json +++ b/apps/vscode-e2e/fixtures/deepseek-v4.json @@ -7,6 +7,8 @@ "sequenceIndex": 0 }, "response": { + "content": "", + "reasoning": "I should read the file to find the marker.", "toolCalls": [ { "name": "read_file", @@ -69,6 +71,8 @@ "sequenceIndex": 0 }, "response": { + "content": "", + "reasoning": "I should read the file to find the marker.", "toolCalls": [ { "name": "read_file", diff --git a/apps/vscode-e2e/fixtures/openrouter.json b/apps/vscode-e2e/fixtures/openrouter.json index 1eef046277..eb1e63cfe3 100644 --- a/apps/vscode-e2e/fixtures/openrouter.json +++ b/apps/vscode-e2e/fixtures/openrouter.json @@ -1,5 +1,19 @@ { "fixtures": [ + { + "match": { + "userMessage": "openrouter-image-e2e" + }, + "response": { + "toolCalls": [ + { + "name": "attempt_completion", + "arguments": "{\"result\":\"Red\"}", + "id": "call_openrouter_image_001" + } + ] + } + }, { "match": { "userMessage": "openrouter-identity-smoke" diff --git a/apps/vscode-e2e/package.json b/apps/vscode-e2e/package.json index 71fadd5e00..71a25f22ed 100644 --- a/apps/vscode-e2e/package.json +++ b/apps/vscode-e2e/package.json @@ -17,9 +17,8 @@ "@roo-code/types": "workspace:^", "@copilotkit/aimock": "1.15.1", "@types/mocha": "10.0.10", - "@types/node": "20.19.41", + "@types/node": "20.19.43", "@types/vscode": "1.100.0", - "@vscode/test-cli": "0.0.11", "@vscode/test-electron": "2.5.2", "glob": "11.1.0", "mocha": "11.2.2", diff --git a/apps/vscode-e2e/src/fixtures/subtasks.ts b/apps/vscode-e2e/src/fixtures/subtasks.ts index bb639dde9c..772eb3306a 100644 --- a/apps/vscode-e2e/src/fixtures/subtasks.ts +++ b/apps/vscode-e2e/src/fixtures/subtasks.ts @@ -5,6 +5,8 @@ import { toolResultContains } from "./tool-result" const SUBTASK_PARENT_MARKER = "SUBTASK_PARENT_CANCELLATION_SMOKE" const SUBTASK_CHILD_MARKER = "SUBTASK_CHILD_CALCULATOR_SMOKE" +const SUBTASK_INTERRUPT_PARENT_MARKER = "SUBTASK_PARENT_INTERRUPT_RESUME" +const SUBTASK_INTERRUPT_CHILD_MARKER = "SUBTASK_CHILD_INTERRUPT_RESUME" const SUBTASK_FAST_PARENT_MARKER = "SUBTASK_PARENT_IMMEDIATE_COMPLETION" const SUBTASK_FAST_CHILD_MARKER = "SUBTASK_CHILD_IMMEDIATE_COMPLETION" const SUBTASK_XPROFILE_PARENT_MARKER = "SUBTASK_PARENT_CROSS_PROFILE" @@ -17,6 +19,11 @@ export const SUBTASK_CHILD_FOLLOWUP_ANSWER = "9" const SUBTASK_FAST_CHILD_PROMPT = `${SUBTASK_FAST_CHILD_MARKER}: Complete immediately with the exact result "Fast child completed".` export const SUBTASK_FAST_PARENT_PROMPT = `${SUBTASK_FAST_PARENT_MARKER}: Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${SUBTASK_FAST_CHILD_PROMPT}" Do not answer directly.` +const SUBTASK_INTERRUPT_CHILD_PROMPT = `${SUBTASK_INTERRUPT_CHILD_MARKER}: Ask the user exactly this follow-up question: What is the square root of 81? After the user answers, complete with only the answer.` +export const SUBTASK_INTERRUPT_PARENT_PROMPT = `${SUBTASK_INTERRUPT_PARENT_MARKER}: Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${SUBTASK_INTERRUPT_CHILD_PROMPT}" Do not answer directly. When the subtask returns, complete with the exact result "Interrupted parent resumed".` +export const SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER = "9" +export const SUBTASK_INTERRUPT_PARENT_RESULT = "Interrupted parent resumed" + const SUBTASK_XPROFILE_SAME_CHILD_PROMPT = `${SUBTASK_XPROFILE_SAME_CHILD_MARKER}: Complete immediately with the exact result "Same-profile child completed".` const SUBTASK_XPROFILE_DIFFERENT_CHILD_PROMPT = `${SUBTASK_XPROFILE_DIFFERENT_CHILD_MARKER}: Complete immediately with the exact result "Different-profile child completed".` export const SUBTASK_XPROFILE_PARENT_PROMPT = `${SUBTASK_XPROFILE_PARENT_MARKER}: First use new_task to create a code-mode subtask with this exact message: "${SUBTASK_XPROFILE_SAME_CHILD_PROMPT}" After it returns, create an ask-mode subtask with the next instructions you receive.` @@ -32,15 +39,17 @@ const requestContains = (req: ChatCompletionRequest, expected: string[]) => { const completionAfterAnswer = (followupId: string, completionId: string) => ({ match: { predicate: (req: ChatCompletionRequest) => + !requestContains(req, [SUBTASK_INTERRUPT_CHILD_MARKER]) && + !requestContains(req, [SUBTASK_INTERRUPT_PARENT_MARKER]) && // Preferred: structured tool-result message carries the followup answer. - toolResultContains(req, followupId, [SUBTASK_CHILD_FOLLOWUP_ANSWER]) || - // Fallback 1: answer present alongside the tool-call ID but not in a role:tool message. - requestContains(req, [followupId, SUBTASK_CHILD_FOLLOWUP_ANSWER]) || - // Fallback 2: answer arrives as a bare user message after task resume (no tool-call ID context). - requestContains(req, [ - SUBTASK_CHILD_MARKER, - `\\n${SUBTASK_CHILD_FOLLOWUP_ANSWER}\\n`, - ]), + (toolResultContains(req, followupId, [SUBTASK_CHILD_FOLLOWUP_ANSWER]) || + // Fallback 1: answer present alongside the tool-call ID but not in a role:tool message. + requestContains(req, [followupId, SUBTASK_CHILD_FOLLOWUP_ANSWER]) || + // Fallback 2: answer arrives as a bare user message after task resume (no tool-call ID context). + requestContains(req, [ + SUBTASK_CHILD_MARKER, + `\\n${SUBTASK_CHILD_FOLLOWUP_ANSWER}\\n`, + ])), }, response: { toolCalls: [ @@ -90,7 +99,8 @@ export function addSubtaskFixtures(mock: InstanceType) { mock.addFixture({ match: { - toolCallId: "call_subtasks_fast_parent_new_task_001", + predicate: (req: ChatCompletionRequest) => + requestContains(req, [SUBTASK_FAST_PARENT_MARKER, "call_subtasks_fast_parent_new_task_001"]), }, response: { toolCalls: [ @@ -143,7 +153,8 @@ export function addSubtaskFixtures(mock: InstanceType) { mock.addFixture({ match: { - toolCallId: "call_subtasks_parent_new_task_001", + predicate: (req: ChatCompletionRequest) => + requestContains(req, [SUBTASK_PARENT_MARKER, "call_subtasks_parent_new_task_001"]), }, response: { toolCalls: [ @@ -252,4 +263,95 @@ export function addSubtaskFixtures(mock: InstanceType) { ], }, }) + + // Interrupted-child-resumes-and-reports-back scenario (#560) + mock.addFixture({ + match: { + userMessage: new RegExp(SUBTASK_INTERRUPT_PARENT_MARKER), + sequenceIndex: 0, + }, + response: { + toolCalls: [ + { + name: "new_task", + arguments: JSON.stringify({ + mode: "ask", + message: SUBTASK_INTERRUPT_CHILD_PROMPT, + }), + id: "call_interrupt_parent_new_task_001", + }, + ], + }, + }) + + // The parent prompt embeds SUBTASK_INTERRUPT_CHILD_MARKER verbatim, so parent-resume turns + // also match a bare substring check. Exclude the parent marker so they fall through. + // The answer exclusion must use the wrapping: the bare answer is a single + // digit that can appear anywhere in the serialized request (timestamps in environment + // details, token counts), which would make this fixture unmatchable. + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => + requestContains(req, [SUBTASK_INTERRUPT_CHILD_MARKER]) && + !requestContains(req, [SUBTASK_INTERRUPT_PARENT_MARKER]) && + !requestContains(req, ["call_interrupt_child_followup_001"]) && + !requestContains(req, [ + `\\n${SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER}\\n`, + ]), + }, + response: { + toolCalls: [ + { + name: "ask_followup_question", + arguments: JSON.stringify({ + question: "What is the square root of 81?", + follow_up: [{ text: SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER }], + }), + id: "call_interrupt_child_followup_001", + }, + ], + }, + }) + + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => + // Preferred: structured tool-result message carries the followup answer. + toolResultContains(req, "call_interrupt_child_followup_001", [ + SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER, + ]) || + // Fallback 1: answer present alongside the tool-call ID. + requestContains(req, ["call_interrupt_child_followup_001", SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER]) || + // Fallback 2: answer arrives as a bare user message after task resume. + requestContains(req, [ + SUBTASK_INTERRUPT_CHILD_MARKER, + `\\n${SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER}\\n`, + ]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER }), + id: "call_interrupt_child_completion_002", + }, + ], + }, + }) + + mock.addFixture({ + match: { + predicate: (req: ChatCompletionRequest) => + requestContains(req, [SUBTASK_INTERRUPT_PARENT_MARKER, "call_interrupt_parent_new_task_001"]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: SUBTASK_INTERRUPT_PARENT_RESULT }), + id: "call_interrupt_parent_completion_003", + }, + ], + }, + }) } diff --git a/apps/vscode-e2e/src/runTest.ts b/apps/vscode-e2e/src/runTest.ts index 101a00c53d..23c7f317fe 100644 --- a/apps/vscode-e2e/src/runTest.ts +++ b/apps/vscode-e2e/src/runTest.ts @@ -1,6 +1,7 @@ import * as path from "path" import * as os from "os" import * as fs from "fs/promises" +import { readFileSync } from "fs" import { runTests } from "@vscode/test-electron" import { LLMock } from "@copilotkit/aimock" @@ -156,12 +157,16 @@ async function main() { } // Download VS Code, unzip it and run the integration test + // Read VS Code version from package.json to keep in sync with @types/vscode + const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "../package.json"), "utf-8")) + const vscodeVersion = process.env.VSCODE_VERSION || pkg.devDependencies["@types/vscode"] + await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs: [testWorkspace], extensionTestsEnv, - version: process.env.VSCODE_VERSION || "1.100.0", + version: vscodeVersion, }) } catch (error) { console.error("Failed to run tests", error) diff --git a/apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts b/apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts index c8704882e0..6391c79873 100644 --- a/apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts +++ b/apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts @@ -19,6 +19,8 @@ type CapturedDeepSeekRequest = { maxCompletionTokens?: number probeTag?: string lastUserMessage: string + /** True if any assistant message in the conversation history has a non-empty reasoning_content field. */ + hasReasoningContentInHistory: boolean } type DeepSeekProbeResult = { @@ -57,7 +59,7 @@ function getRequestBody(init?: RequestInit): thinking?: { type?: "enabled" | "disabled" } reasoning_effort?: string max_completion_tokens?: number - messages?: Array<{ role?: string; content?: unknown }> + messages?: Array<{ role?: string; content?: unknown; reasoning_content?: string }> } | undefined { if (!init?.body || typeof init.body !== "string") { @@ -83,6 +85,13 @@ function installDeepSeekRequestCapture(capture: CapturedDeepSeekRequest[], baseU const allMessagesText = JSON.stringify(body.messages ?? []) const probeTag = allMessagesText.match(/deepseek-v4-e2e:[^"\s]+/)?.[0] + const hasReasoningContentInHistory = (body.messages ?? []).some( + (message) => + message.role === "assistant" && + typeof message.reasoning_content === "string" && + message.reasoning_content.length > 0, + ) + const request = { model: body.model, thinkingType: body.thinking?.type, @@ -90,6 +99,7 @@ function installDeepSeekRequestCapture(capture: CapturedDeepSeekRequest[], baseU maxCompletionTokens: body.max_completion_tokens, probeTag, lastUserMessage, + hasReasoningContentInHistory, } satisfies CapturedDeepSeekRequest capture.push(request) @@ -123,6 +133,7 @@ function formatDiagnostics(result: DeepSeekProbeResult) { thinkingType: request.thinkingType, reasoningEffort: request.reasoningEffort, maxCompletionTokens: request.maxCompletionTokens, + hasReasoningContentInHistory: request.hasReasoningContentInHistory, probeTag: request.probeTag, lastUserMessage: request.lastUserMessage.slice(0, 160), } @@ -368,6 +379,20 @@ suite("DeepSeek V4 provider", function () { firstRequest.reasoningEffort === "high" || firstRequest.reasoningEffort === "max", `Reasoning-enabled probe should send a DeepSeek reasoning_effort.\n${diagnostics}`, ) + + // Verify that reasoning_content from turn 1 is round-tripped in the turn 2 request. + // DeepSeek's API spec requires reasoning_content to be passed back when thinking mode + // is active — omitting it may cause a 400 error depending on model version (issue #201). + const secondRequest = result.requests[1] + assert.ok( + secondRequest, + `Reasoning-enabled probe should issue a second request (after tool call).\n${diagnostics}`, + ) + assert.ok( + secondRequest.hasReasoningContentInHistory, + `Turn 2 request must include reasoning_content on the assistant message from turn 1 ` + + `(required by DeepSeek API spec when thinking mode is active — issue #201).\n${diagnostics}`, + ) } else { assert.strictEqual( firstRequest.thinkingType, @@ -379,6 +404,17 @@ suite("DeepSeek V4 provider", function () { undefined, `Reasoning-disabled probe should omit reasoning_effort.\n${diagnostics}`, ) + + // Negative guard: reasoning-off requests must never carry reasoning_content, + // which would indicate the capture flag itself is broken. + const secondRequestOff = result.requests[1] + if (secondRequestOff) { + assert.strictEqual( + secondRequestOff.hasReasoningContentInHistory, + false, + `Turn 2 request must NOT include reasoning_content when thinking is disabled.\n${diagnostics}`, + ) + } } assert.ok(result.completed, `Task should complete cleanly.\n${diagnostics}`) diff --git a/apps/vscode-e2e/src/suite/providers/openrouter.test.ts b/apps/vscode-e2e/src/suite/providers/openrouter.test.ts index 140175168c..8b7e3d788f 100644 --- a/apps/vscode-e2e/src/suite/providers/openrouter.test.ts +++ b/apps/vscode-e2e/src/suite/providers/openrouter.test.ts @@ -9,6 +9,7 @@ type CapturedOpenRouterRequest = { xTitle: string | undefined httpReferer: string | undefined userAgent: string | undefined + userMessageContent?: unknown } function getRequestUrl(input: RequestInfo | URL): string { @@ -40,10 +41,22 @@ function installOpenRouterRequestCapture(capture: CapturedOpenRouterRequest[], b if (new URL(url).origin === targetOrigin) { const xTitle = getHeaderValue(init, "X-Title") ?? getHeaderValue(init, "x-title") if (xTitle !== undefined) { + let userMessageContent: unknown + if (init?.body && typeof init.body === "string") { + try { + const body = JSON.parse(init.body) + const messages: Array<{ role?: string; content?: unknown }> = body.messages ?? [] + const lastUser = [...messages].reverse().find((m) => m.role === "user") + userMessageContent = lastUser?.content + } catch { + // ignore parse errors + } + } capture.push({ xTitle, httpReferer: getHeaderValue(init, "HTTP-Referer") ?? getHeaderValue(init, "http-referer"), userAgent: getHeaderValue(init, "User-Agent") ?? getHeaderValue(init, "user-agent"), + userMessageContent, }) } } @@ -82,7 +95,7 @@ suite("OpenRouter provider", function () { await globalThis.api.setConfiguration({ apiProvider: "openrouter" as const, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : OPENROUTER_API_KEY!, - openRouterModelId: "openai/gpt-4.1", + openRouterModelId: "anthropic/claude-haiku-4-5", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), }) }) @@ -92,6 +105,41 @@ suite("OpenRouter provider", function () { restoreFetch = undefined }) + test("Should forward base64 images as image_url content parts in outbound request", async () => { + requests.length = 0 + + // 8x8 red PNG (1x1 is too small and rejected by Anthropic's vision API) + const base64Png = + "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEklEQVR4nGP4z8CAFWEXHbQSACj/P8Fu7N9hAAAAAElFTkSuQmCC" + const dataUri = `data:image/png;base64,${base64Png}` + + const api = globalThis.api + const taskId = await api.startNewTask({ + configuration: { mode: "ask", autoApprovalEnabled: true }, + text: "openrouter-image-e2e: describe the image in one word.", + images: [dataUri], + }) + + await waitUntilCompleted({ api, taskId }) + + // Find the first request that contains the probe tag + const probeRequest = requests.find((r) => { + const content = r.userMessageContent + return Array.isArray(content) && JSON.stringify(content).includes("openrouter-image-e2e") + }) + + assert.ok(probeRequest, "Should have captured an outbound request containing the probe tag") + + const content = probeRequest.userMessageContent as Array<{ type: string; image_url?: { url: string } }> + const imagePart = content.find((p) => p.type === "image_url") + assert.ok(imagePart, "User message should contain an image_url content part") + assert.strictEqual( + imagePart?.image_url?.url, + dataUri, + "image_url.url should be the original data URI passed via startNewTask", + ) + }) + test("Should identify as Zoo Code in outbound DEFAULT_HEADERS", async () => { requests.length = 0 diff --git a/apps/vscode-e2e/src/suite/providers/zai.test.ts b/apps/vscode-e2e/src/suite/providers/zai.test.ts index 69e938d582..d3603c370c 100644 --- a/apps/vscode-e2e/src/suite/providers/zai.test.ts +++ b/apps/vscode-e2e/src/suite/providers/zai.test.ts @@ -47,11 +47,10 @@ function installZAiFetchInterceptor( }) : {} - if (capture) { - capture.maxTokens = body.max_tokens - } - if (passthrough) { + if (capture) { + capture.maxTokens = body.max_tokens + } return original.call(globalThis, input, init as RequestInit) } @@ -65,6 +64,10 @@ function installZAiFetchInterceptor( throw new Error(`Z.ai fetch interceptor: no fixture matched. Last user message: ${text.slice(0, 200)}`) } + if (capture) { + capture.maxTokens = body.max_tokens + } + return makeZAiSSEResponse(fixture.result) } @@ -220,6 +223,10 @@ suite("Z.ai GLM provider", function () { await waitUntilCompleted({ api, taskId }) const capturedMaxTokens = requestCapture.maxTokens + assert.ok( + capturedMaxTokens !== undefined, + "max_tokens should have been captured by the fetch interceptor before task completion", + ) const completionMessage = messages.find( ({ say, text }) => (say === "completion_result" || say === "text") && text?.trim() === "4", @@ -229,8 +236,6 @@ suite("Z.ai GLM provider", function () { // Verify max_tokens uses the restored default clamp (20% of context window) // unless the user explicitly overrides it via modelMaxTokens. - // Snapshot immediately after waitUntilCompleted to avoid straggling async calls - // from this task overwriting requestCapture before the assertion runs. assert.strictEqual( capturedMaxTokens, 40_000, @@ -264,6 +269,10 @@ suite("Z.ai GLM provider", function () { await waitUntilCompleted({ api, taskId }) const capturedMaxTokens = requestCapture.maxTokens + assert.ok( + capturedMaxTokens !== undefined, + "max_tokens should have been captured by the fetch interceptor before task completion", + ) const completionMessage = messages.find( ({ say, text }) => (say === "completion_result" || say === "text") && text?.trim() === "4", @@ -273,11 +282,10 @@ suite("Z.ai GLM provider", function () { // Verify max_tokens uses the restored default clamp (20% of context window) // unless the user explicitly overrides it via modelMaxTokens. - // Snapshot immediately after waitUntilCompleted to avoid straggling async calls - // from the prior test overwriting requestCapture before this assertion runs. + const expectedMaxTokens = 40_551 // Math.ceil(202_752 * 0.2) for glm-5-turbo assert.strictEqual( capturedMaxTokens, - 40_551, + expectedMaxTokens, `max_tokens should default to the glm-5-turbo clamp (40_551) but was ${capturedMaxTokens}`, ) }) diff --git a/apps/vscode-e2e/src/suite/subtasks.test.ts b/apps/vscode-e2e/src/suite/subtasks.test.ts index d8f6d23299..95d74970b9 100644 --- a/apps/vscode-e2e/src/suite/subtasks.test.ts +++ b/apps/vscode-e2e/src/suite/subtasks.test.ts @@ -3,10 +3,13 @@ import * as assert from "assert" import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { setDefaultSuiteTimeout } from "./test-utils" -import { waitFor, waitUntilCompleted } from "./utils" +import { sleep, waitFor, waitUntilCompleted } from "./utils" import { SUBTASK_CHILD_FOLLOWUP_ANSWER, SUBTASK_FAST_PARENT_PROMPT, + SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER, + SUBTASK_INTERRUPT_PARENT_PROMPT, + SUBTASK_INTERRUPT_PARENT_RESULT, SUBTASK_PARENT_PROMPT, SUBTASK_XPROFILE_DIFFERENT_CHILD_RESULT, SUBTASK_XPROFILE_PARENT_PROMPT, @@ -69,6 +72,7 @@ suite("Roo Code Subtasks", function () { while (api.getCurrentTaskStack().length > 0) { await api.clearCurrentTask() } + await sleep(1_500) } }) @@ -150,6 +154,103 @@ suite("Roo Code Subtasks", function () { } }) + test("delegated child completion persists parent and child history state", async () => { + const api = globalThis.api + const asks: Record = {} + const says: Record = {} + + let delegationCompletedParentId: string | undefined + let delegationCompletedChildId: string | undefined + let delegationCompletedSummary: string | undefined + + const messageHandler = ({ taskId, message }: { taskId: string; message: ClineMessage }) => { + if (message.type === "ask") { + asks[taskId] = asks[taskId] || [] + asks[taskId].push(message) + } + if (message.type === "say" && message.partial === false) { + says[taskId] = says[taskId] || [] + says[taskId].push(message) + } + } + + const delegationCompletedHandler = (parentId: string, childId: string, summary: string) => { + delegationCompletedParentId = parentId + delegationCompletedChildId = childId + delegationCompletedSummary = summary + } + + api.on(RooCodeEventName.Message, messageHandler) + api.on(RooCodeEventName.TaskDelegationCompleted, delegationCompletedHandler) + + try { + const parentTaskId = await api.startNewTask({ + configuration: { + mode: "ask", + alwaysAllowModeSwitch: true, + alwaysAllowSubtasks: true, + autoApprovalEnabled: true, + enableCheckpoints: false, + }, + text: SUBTASK_PARENT_PROMPT, + }) + + let childTaskId: string | undefined + await waitFor(() => { + const stack = api.getCurrentTaskStack() + const current = stack[stack.length - 1] + if (current && current !== parentTaskId) { + childTaskId = current + return true + } + return false + }) + + await waitFor(() => asks[childTaskId!]?.some(({ ask }) => ask === "followup") ?? false) + + // Send the answer, then wait for TaskDelegationCompleted. That event fires after + // atomicUpdatePair writes the persisted history but before the parent is re-created, + // so it is the right gate for history assertions. waitUntilCompleted alone is not + // sufficient because the resumed parent runs into a mock 404 and never emits + // TaskCompleted in the test environment. + await api.sendMessage(SUBTASK_CHILD_FOLLOWUP_ANSWER) + await waitFor(() => delegationCompletedParentId !== undefined) + + assert.strictEqual( + delegationCompletedParentId, + parentTaskId, + "TaskDelegationCompleted should fire for parent", + ) + assert.strictEqual(delegationCompletedChildId, childTaskId, "TaskDelegationCompleted should fire for child") + assert.strictEqual(delegationCompletedSummary, "9", "TaskDelegationCompleted summary should be '9'") + + const parent = await api.getTaskHistoryItem(parentTaskId) + assert.ok(parent, "Parent history item should exist") + assert.strictEqual(parent.status, "active", "Parent status should be 'active' after child completes") + assert.strictEqual(parent.awaitingChildId, undefined, "Parent awaitingChildId should be cleared") + assert.strictEqual(parent.delegatedToId, undefined, "Parent delegatedToId should be cleared") + assert.strictEqual(parent.completedByChildId, childTaskId, "Parent completedByChildId should be the child") + assert.strictEqual(parent.completionResultSummary, "9", "Parent completionResultSummary should be '9'") + assert.ok(parent.childIds?.includes(childTaskId!), "Parent childIds should include the child") + + const child = await api.getTaskHistoryItem(childTaskId!) + assert.ok(child, "Child history item should exist") + assert.strictEqual(child.status, "completed", "Child status should be 'completed'") + assert.strictEqual(child.parentTaskId, parentTaskId, "Child parentTaskId should point to parent") + assert.strictEqual(child.completionResultSummary, "9", "Child completionResultSummary should be '9'") + } finally { + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskDelegationCompleted, delegationCompletedHandler) + if (api.getCurrentTaskStack().length > 0) { + await api.clearCurrentTask() + } + if (api.getCurrentTaskStack().length > 0) { + await api.clearCurrentTask() + } + await waitFor(() => api.getCurrentTaskStack().length === 0).catch(() => {}) + } + }) + // Race mitigation: skipDelegationRepair prevents removeClineFromStack from // auto-resuming the parent when the child is cancelled (Race 2). test("parent stays paused after subtask cancellation", async () => { @@ -221,10 +322,13 @@ suite("Roo Code Subtasks", function () { // Race mitigation: runDelegationTransition lock + cancelledDelegationChildIds guard // ensures cancelTask() wins over a concurrent reopenParentFromDelegation() (Race 3). - test("cancelled child completes in-place and does not reopen parent", async () => { + // Before issue #560 was fixed, a cancelled child would have its parent link severed on cancel, so + // it would complete in-place without reopening the parent. The correct behavior (post-fix) is that + // the cancelled child is marked "interrupted", and when it resumes and completes it reopens the parent. + test("cancelled child completes and reopens parent", async () => { const api = globalThis.api const asks: Record = {} - const messages: Record = {} + const says: Record = {} const messageHandler = ({ taskId, message }: { taskId: string; message: ClineMessage }) => { if (message.type === "ask") { @@ -232,26 +336,11 @@ suite("Roo Code Subtasks", function () { asks[taskId].push(message) } if (message.type === "say" && message.partial === false) { - messages[taskId] = messages[taskId] || [] - messages[taskId].push(message) + says[taskId] = says[taskId] || [] + says[taskId].push(message) } } - const findCompletionText = (taskId: string) => - messages[taskId] - ?.filter( - (message) => - message.type === "say" && (message.say === "completion_result" || message.say === "text"), - ) - .map((message) => message.text?.trim()) - .find((text): text is string => !!text) - - const findErrorText = (taskId: string) => - messages[taskId] - ?.filter((message) => message.type === "say" && message.say === "error") - .map((message) => message.text?.trim()) - .find((text): text is string => !!text) - api.on(RooCodeEventName.Message, messageHandler) try { @@ -280,6 +369,7 @@ suite("Roo Code Subtasks", function () { await waitFor( () => asks[spawnedTaskId!]?.some(({ type, ask }) => type === "ask" && ask === "followup") ?? false, ) + await waitFor(async () => (await api.getTaskApiConversationHistoryLength(spawnedTaskId!)) > 0) const cancelledChildTaskId = spawnedTaskId! await api.cancelCurrentTask() @@ -291,41 +381,25 @@ suite("Roo Code Subtasks", function () { false, ) - const resumedChildTaskId = await waitUntilCompleted({ + // Resume the child — it should complete and reopen the parent (fix for #560) + const completedTaskId = await waitUntilCompleted({ api, start: async () => { await api.sendMessage(SUBTASK_CHILD_FOLLOWUP_ANSWER) - return cancelledChildTaskId + return parentTaskId }, }) assert.strictEqual( - resumedChildTaskId, - cancelledChildTaskId, - "Cancelled child task should be resumed in place", + completedTaskId, + parentTaskId, + "Parent task should complete after interrupted child reports back", ) assert.strictEqual( - findErrorText(resumedChildTaskId), - undefined, - "Resumed child task should not emit an error", - ) - assert.strictEqual( - findCompletionText(resumedChildTaskId), - "9", - "Resumed child task should complete with `9`", - ) - assert.strictEqual( - api.getCurrentTaskStack().at(-1), - cancelledChildTaskId, - "Cancelled child task should remain the active completed task", - ) - assert.ok( - messages[parentTaskId]?.find(({ type, text }) => type === "say" && text === "Parent task resumed") === - undefined, - "Parent task should not have resumed after the cancelled child completed", + says[parentTaskId]?.find(({ say }) => say === "completion_result")?.text?.trim(), + "Parent task resumed", + "Parent task should complete with its expected result", ) - - await api.clearCurrentTask() } finally { api.off(RooCodeEventName.Message, messageHandler) } @@ -439,4 +513,101 @@ suite("Roo Code Subtasks", function () { } } }) + + // Issue #560: interrupted child resumes and reports back to parent. + // Before the fix, cancelTask() severed the parent link, so the resumed child + // fell through to "Start New Task" instead of delegating back to the parent. + test("interrupted child resumes and reports back to parent", async () => { + const api = globalThis.api + const asks: Record = {} + const says: Record = {} + + const messageHandler = ({ taskId, message }: { taskId: string; message: ClineMessage }) => { + if (message.type === "ask") { + asks[taskId] = asks[taskId] || [] + asks[taskId].push(message) + } + if (message.type === "say" && message.partial === false) { + says[taskId] = says[taskId] || [] + says[taskId].push(message) + } + } + + api.on(RooCodeEventName.Message, messageHandler) + + try { + const parentTaskId = await api.startNewTask({ + configuration: { + mode: "ask", + alwaysAllowModeSwitch: true, + alwaysAllowSubtasks: true, + autoApprovalEnabled: true, + enableCheckpoints: false, + }, + text: SUBTASK_INTERRUPT_PARENT_PROMPT, + }) + + // Wait for child to spawn + let childTaskId: string | undefined + await waitFor(() => { + const stack = api.getCurrentTaskStack() + const current = stack[stack.length - 1] + if (current && current !== parentTaskId) { + childTaskId = current + return true + } + return false + }) + + // Wait for the child's followup question + await waitFor(() => asks[childTaskId!]?.some(({ ask }) => ask === "followup") ?? false) + await waitFor(async () => (await api.getTaskApiConversationHistoryLength(childTaskId!)) > 0) + + // Cancel the child — it should be marked "interrupted", parent stays "delegated" + await api.cancelCurrentTask() + + // Child should be back on the stack (rehydrated as interrupted) + await waitFor(() => api.getCurrentTaskStack().at(-1) === childTaskId) + await waitFor( + () => asks[childTaskId!]?.some(({ type, ask }) => type === "ask" && ask === "resume_task") ?? false, + ) + + // Parent must not have resumed yet + assert.strictEqual( + says[parentTaskId]?.find(({ say }) => say === "completion_result"), + undefined, + "Parent must not have resumed while child is interrupted", + ) + + // Resume the child and answer the followup — child should complete and reopen parent. + const completedParentTaskId = await waitUntilCompleted({ + api, + start: async () => { + await api.sendMessage(SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER) + return parentTaskId + }, + }) + + assert.strictEqual( + completedParentTaskId, + parentTaskId, + "Parent task should be the one that completes after interrupted child reports back", + ) + + assert.strictEqual( + says[parentTaskId] + ?.filter(({ say }) => say === "completion_result") + .map(({ text }) => text?.trim()) + .find((text) => text === SUBTASK_INTERRUPT_PARENT_RESULT), + SUBTASK_INTERRUPT_PARENT_RESULT, + "Parent should complete with expected result after interrupted child reports back", + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + while (api.getCurrentTaskStack().length > 0) { + await api.clearCurrentTask() + } + await waitFor(() => api.getCurrentTaskStack().length === 0).catch(() => {}) + } + }) }) diff --git a/apps/vscode-e2e/src/types/global.d.ts b/apps/vscode-e2e/src/types/global.d.ts index e25959bf19..5c7fb164ad 100644 --- a/apps/vscode-e2e/src/types/global.d.ts +++ b/apps/vscode-e2e/src/types/global.d.ts @@ -1,7 +1,6 @@ import type { RooCodeAPI } from "@roo-code/types" declare global { - // eslint-disable-next-line no-var -- var is required in declare global var api: RooCodeAPI } diff --git a/knip.json b/knip.json index 9037fa042d..1e0771ef9b 100644 --- a/knip.json +++ b/knip.json @@ -1,32 +1,20 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "ignore": ["**/__tests__/**", "apps/vscode-e2e/**", "scripts/**", "apps/cli/scripts/**"], - "ignoreDependencies": ["lint-staged", "only-allow", "ovsx"], + "ignoreDependencies": ["lint-staged", "ovsx"], "ignoreExportsUsedInFile": true, "workspaces": { "src": { "entry": ["extension.ts", "extension/api.ts", "workers/countTokens.ts"], "project": ["**/*.ts", "!**/__tests__/**"], "ignoreDependencies": [ - "@ai-sdk/amazon-bedrock", - "@ai-sdk/baseten", - "@ai-sdk/deepseek", - "@ai-sdk/fireworks", - "@ai-sdk/google", - "@ai-sdk/google-vertex", - "@ai-sdk/mistral", - "@ai-sdk/xai", "@roo-code/config-typescript", - "@types/mocha", "@types/node-cache", "@types/vscode", - "@vscode/codicons", "@vscode/ripgrep", "esbuild-wasm", - "sambanova-ai-provider", "tree-sitter-wasms", - "vscode-material-icons", - "zhipu-ai-provider" + "vscode-material-icons" ] }, "webview-ui": { @@ -39,7 +27,6 @@ "babel-plugin-react-compiler", "katex", "react-compiler-runtime", - "rehype-highlight", "source-map", "tailwindcss", "tailwindcss-animate", @@ -48,7 +35,7 @@ }, "apps/cli": { "project": ["src/**/*.ts", "src/**/*.tsx"], - "ignoreDependencies": ["@vscode/ripgrep", "cross-spawn"] + "ignoreDependencies": ["@vscode/ripgrep"] }, "packages/config-typescript": { "ignoreUnresolved": ["vitest/globals"] @@ -62,7 +49,7 @@ }, "packages/cloud": { "project": ["src/**/*.ts"], - "ignoreDependencies": ["@types/vscode", "ioredis", "p-wait-for"] + "ignoreDependencies": ["@types/vscode"] }, "packages/telemetry": { "project": ["src/**/*.ts"], diff --git a/locales/ca/README.md b/locales/ca/README.md index 68ef28a029..c58553fb68 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -30,11 +30,15 @@ Pots trobar una guia ràpida per passar de Roo Code a Zoo Code a la [guia de migració Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Volem ajudar tant com puguem durant la transició, i per això tens el nostre [Reddit](https://www.reddit.com/r/ZooCode) i [Discord](https://discord.gg/VxfP4Vx3gX) per a aquest suport. Si tens problemes o alguna pregunta, entra i pregunta. -## Novetats a la v3.62.0 - -- **Suport per a GLM-5.2** — l'últim model GLM ja està disponible a la configuració del proveïdor -- **Millores d'OpenCode-Go** — paràmetres de model natius, encaminament en format Anthropic i correcció de context-token per a respostes més fiables -- **Mode tool-writer** — un nou mode especialitzat per escriure i mantenir definicions d'eines, ja disponible al Marketplace +## Novetats a la v3.68.0 + +- **Proveïdor Friendli amb suport per a GLM-5.2** — utilitza el model GLM més recent a través de Friendli. +- **Suport natiu de thinking/reasoning per a Ollama** — conserva la sortida de raonament d'extrem a extrem quan fas servir models d'Ollama. +- **Correcció del `apiModelId` personalitzat d'Anthropic** — els identificadors de model personalitzats d'Anthropic ara es mantenen seleccionats en lloc de tornar en silenci a `claude-sonnet-4-5`. +- Correcció: gestió dels resultats d'eines del proveïdor Ollama i condensació prematura del context. +- Correcció: conserva l'enllaç pare-fill de la tasca quan s'interromp una subtasca delegada. +- Millora la gestió dels blocs de contingut de Claude a Anthropic Vertex per obtenir respostes més fiables. +- Actualitzacions de CI, publicació nightly i dependències/eines. ## Què pot fer Zoo Code per TU? diff --git a/locales/de/README.md b/locales/de/README.md index ac187fc3e5..808b0faf3f 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -30,11 +30,15 @@ Eine kurze Anleitung für den Wechsel von Roo Code zu Zoo Code findest du im [Roo→Zoo-Migrationsleitfaden](https://docs.zoocode.dev/roo-to-zoo-migration). Wir wollen Nutzer beim Umstieg so gut wie möglich unterstützen, und genau dafür sind unser [Reddit](https://www.reddit.com/r/ZooCode) und [Discord](https://discord.gg/VxfP4Vx3gX) da. Wenn du Probleme hast oder Fragen auftauchen, komm vorbei und frag nach. -## Neu in v3.62.0 - -- **GLM-5.2-Unterstützung** — das neueste GLM-Modell ist jetzt in deinen Anbieter-Einstellungen verfügbar -- **OpenCode-Go-Verbesserungen** — native Modellparameter, Anthropic-Format-Routing und ein Context-Token-Fix für zuverlässigere Antworten -- **Tool-Writer-Modus** — ein neuer spezieller Modus zum Schreiben und Pflegen von Tool-Definitionen, jetzt im Marketplace verfügbar +## Neu in v3.68.0 + +- **Friendli-Anbieter mit GLM-5.2-Unterstützung** — nutze das neueste GLM-Modell über Friendli. +- **Native Ollama-Unterstützung für thinking/reasoning** — behalte die Reasoning-Ausgabe durchgängig bei, wenn du Ollama-Modelle verwendest. +- **Fix für benutzerdefinierte `apiModelId` bei Anthropic** — benutzerdefinierte Anthropic-Modell-IDs bleiben jetzt ausgewählt, statt stillschweigend auf `claude-sonnet-4-5` zurückzufallen. +- Fix: Behandlung von Tool-Ergebnissen im Ollama-Anbieter und verfrühte Kontextkondensierung. +- Fix: Behalte die Eltern-Kind-Verknüpfung bei, wenn ein delegierter Untertask unterbrochen wird. +- Verbessere die Verarbeitung von Claude-Content-Blöcken in Anthropic Vertex für zuverlässigere Antworten. +- Updates für CI, Nightly-Publishing und Abhängigkeiten/Tooling. ## Was kann Zoo Code für DICH tun? diff --git a/locales/es/README.md b/locales/es/README.md index 2bc21b70df..a63742ccaf 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -30,11 +30,15 @@ Puedes encontrar una guía rápida para pasar de Roo Code a Zoo Code en la [guía de migración Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ayudar a los usuarios durante la transición, y para eso tenemos nuestro [Reddit](https://www.reddit.com/r/ZooCode) y [Discord](https://discord.gg/VxfP4Vx3gX). Si tienes problemas o alguna pregunta, entra y pregúntanos. -## Novedades de la v3.62.0 - -- **Soporte para GLM-5.2** — el último modelo GLM ya está disponible en la configuración del proveedor -- **Mejoras de OpenCode-Go** — parámetros de modelo nativos, enrutamiento en formato Anthropic y corrección de context-token para respuestas más fiables -- **Modo tool-writer** — un nuevo modo especializado para escribir y mantener definiciones de herramientas, ya disponible en el Marketplace +## Novedades de la v3.68.0 + +- **Proveedor Friendli con soporte para GLM-5.2** — usa el modelo GLM más reciente a través de Friendli. +- **Compatibilidad nativa de thinking/reasoning para Ollama** — conserva la salida de razonamiento de extremo a extremo cuando uses modelos de Ollama. +- **Corrección de `apiModelId` personalizado de Anthropic** — los IDs de modelo personalizados de Anthropic ahora permanecen seleccionados en lugar de volver silenciosamente a `claude-sonnet-4-5`. +- Corrección: manejo de resultados de herramientas del proveedor Ollama y condensación prematura del contexto. +- Corrección: conserva el vínculo padre-hijo de la tarea cuando se interrumpe una subtarea delegada. +- Mejora el manejo de bloques de contenido de Claude en Anthropic Vertex para obtener respuestas más fiables. +- Actualizaciones de CI, publicación nightly y dependencias/herramientas. ## ¿Qué puede hacer Zoo Code por TI? diff --git a/locales/fr/README.md b/locales/fr/README.md index 3162a1fbc2..a164e50050 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -30,11 +30,15 @@ Tu peux trouver un guide rapide pour passer de Roo Code à Zoo Code dans le [guide de migration Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). On veut aider au maximum pendant la transition, et notre [Reddit](https://www.reddit.com/r/ZooCode) et notre [Discord](https://discord.gg/VxfP4Vx3gX) sont là pour ça. Si tu rencontres un problème ou si tu as une question, viens demander. -## Nouveautés de la v3.62.0 - -- **Prise en charge de GLM-5.2** — le dernier modèle GLM est désormais disponible dans les paramètres du fournisseur -- **Améliorations d'OpenCode-Go** — paramètres de modèle natifs, routage au format Anthropic et correction de context-token pour des réponses plus fiables -- **Mode tool-writer** — un nouveau mode spécialisé pour écrire et maintenir des définitions d'outils, désormais disponible sur le Marketplace +## Nouveautés de la v3.68.0 + +- **Provider Friendli avec prise en charge de GLM-5.2** — utilise le dernier modèle GLM via Friendli. +- **Prise en charge native du thinking/reasoning pour Ollama** — préserve la sortie de raisonnement de bout en bout quand tu utilises des modèles Ollama. +- **Correction du `apiModelId` personnalisé d'Anthropic** — les identifiants de modèle Anthropic personnalisés restent désormais sélectionnés au lieu de revenir silencieusement à `claude-sonnet-4-5`. +- Correctif : gestion des résultats d'outils du fournisseur Ollama et condensation prématurée du contexte. +- Correctif : préserve le lien parent-enfant de la tâche lorsqu'une sous-tâche déléguée est interrompue. +- Améliore la gestion des blocs de contenu Claude dans Anthropic Vertex pour des réponses plus fiables. +- Mises à jour de la CI, de la publication nightly et des dépendances/outils. ## Que peut faire Zoo Code pour VOUS ? diff --git a/locales/hi/README.md b/locales/hi/README.md index ba9b3f8a62..a9ccd13cc3 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -30,11 +30,15 @@ Roo Code से Zoo Code में आने के लिए एक quick guide तुम्हें [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration) में मिल जाएगी। We plan to help users as much as possible during the transition, और उसी support के लिए हमारा [Reddit](https://www.reddit.com/r/ZooCode) और [Discord](https://discord.gg/VxfP4Vx3gX) है। अगर तुम्हें कोई problem हो या कोई question हो, आकर पूछो। -## v3.62.0 में नया क्या है - -- **GLM-5.2 समर्थन** — नवीनतम GLM मॉडल अब आपके provider settings में उपलब्ध है -- **OpenCode-Go सुधार** — native model parameters, Anthropic-format routing, और अधिक विश्वसनीय प्रतिक्रियाओं के लिए context-token fix -- **Tool-writer mode** — tool definitions को लिखने और बनाए रखने के लिए एक नया विशेष मोड, अब Marketplace में उपलब्ध है +## v3.68.0 में नया क्या है + +- **GLM-5.2 समर्थन के साथ Friendli प्रदाता** — Friendli के जरिए नवीनतम GLM मॉडल का उपयोग करें। +- **Ollama के लिए नेटिव thinking/reasoning समर्थन** — Ollama मॉडल इस्तेमाल करते समय reasoning आउटपुट को शुरू से अंत तक सुरक्षित रखें। +- **Anthropic कस्टम `apiModelId` फिक्स** — कस्टम Anthropic मॉडल ID अब चुपचाप `claude-sonnet-4-5` पर वापस जाने के बजाय चुनी हुई रहती हैं। +- फिक्स: Ollama प्रदाता में tool result हैंडलिंग और समय से पहले context condensing। +- फिक्स: delegated subtask रुकने पर parent-child task link को बनाए रखें। +- अधिक विश्वसनीय responses के लिए Anthropic Vertex Claude content block handling में सुधार। +- CI, nightly publishing, और dependency/tooling अपडेट्स। ## Zoo Code आपके लिए क्या कर सकता है? diff --git a/locales/id/README.md b/locales/id/README.md index 55fed23cfc..0e2d652969 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -30,11 +30,15 @@ Kamu bisa menemukan panduan singkat untuk berpindah dari Roo Code ke Zoo Code di [panduan migrasi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Kami ingin membantu pengguna semaksimal mungkin selama masa transisi, dan itulah gunanya [Reddit](https://www.reddit.com/r/ZooCode) dan [Discord](https://discord.gg/VxfP4Vx3gX) kami. Kalau kamu mengalami masalah atau punya pertanyaan, langsung mampir dan tanya. -## Yang Baru di v3.62.0 - -- **Dukungan GLM-5.2** — model GLM terbaru kini tersedia di pengaturan penyedia kamu -- **Peningkatan OpenCode-Go** — parameter model native, routing format Anthropic, dan perbaikan context-token untuk respons yang lebih andal -- **Mode tool-writer** — mode khusus baru untuk menulis dan memelihara definisi tool, kini tersedia di Marketplace +## Yang Baru di v3.68.0 + +- **Provider Friendli dengan dukungan GLM-5.2** — gunakan model GLM terbaru lewat Friendli. +- **Dukungan thinking/reasoning native untuk Ollama** — pertahankan keluaran penalaran dari awal hingga akhir saat kamu memakai model Ollama. +- **Perbaikan `apiModelId` kustom Anthropic** — ID model Anthropic kustom kini tetap terpilih alih-alih diam-diam kembali ke `claude-sonnet-4-5`. +- Perbaikan: penanganan hasil tool penyedia Ollama dan pemadatan konteks yang terlalu dini. +- Perbaikan: pertahankan tautan induk-anak tugas saat subtugas yang didelegasikan terputus. +- Peningkatan penanganan blok konten Claude Anthropic Vertex untuk respons yang lebih andal. +- Pembaruan CI, publikasi nightly, dan dependensi/tooling. ## Apa yang Bisa Zoo Code Lakukan Untuk ANDA? diff --git a/locales/it/README.md b/locales/it/README.md index 5272e846a2..6f47b7d0d6 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -30,11 +30,15 @@ Puoi trovare una guida rapida per passare da Roo Code a Zoo Code nella [guida alla migrazione Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Vogliamo aiutare gli utenti il più possibile durante la transizione, e per questo abbiamo il nostro [Reddit](https://www.reddit.com/r/ZooCode) e il nostro [Discord](https://discord.gg/VxfP4Vx3gX). Se hai problemi o domande, passa pure e chiedi. -## Novità in v3.62.0 - -- **Supporto GLM-5.2** — l'ultimo modello GLM è ora disponibile nelle impostazioni del provider -- **Miglioramenti di OpenCode-Go** — parametri del modello nativi, routing in formato Anthropic e correzione del context-token per risposte più affidabili -- **Modalità tool-writer** — una nuova modalità specializzata per scrivere e mantenere le definizioni degli strumenti, ora disponibile nel Marketplace +## Novità in v3.68.0 + +- **Provider Friendli con supporto GLM-5.2** — usa il modello GLM più recente tramite Friendli. +- **Supporto nativo thinking/reasoning per Ollama** — conserva l'output di ragionamento end-to-end quando usi i modelli Ollama. +- **Correzione del `apiModelId` personalizzato di Anthropic** — gli ID modello Anthropic personalizzati ora restano selezionati invece di tornare silenziosamente a `claude-sonnet-4-5`. +- Correzione: gestione dei risultati degli strumenti del provider Ollama e condensazione prematura del contesto. +- Correzione: mantieni il collegamento padre-figlio dell'attività quando una sottoattività delegata viene interrotta. +- Migliora la gestione dei blocchi di contenuto Claude in Anthropic Vertex per risposte più affidabili. +- Aggiornamenti a CI, pubblicazione nightly e dipendenze/tooling. ## Cosa può fare Zoo Code per TE? diff --git a/locales/ja/README.md b/locales/ja/README.md index 7f3f50c32c..0dfe4ecbd5 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -30,11 +30,15 @@ Roo Code から Zoo Code へ移行するためのクイックガイドは、[Roo→Zoo 移行ガイド](https://docs.zoocode.dev/roo-to-zoo-migration) で確認できます。移行中のユーザーをできるだけ支援したいと考えていて、そのために [Reddit](https://www.reddit.com/r/ZooCode) と [Discord](https://discord.gg/VxfP4Vx3gX) を用意しています。困ったことや質問があれば、気軽に参加して聞いてください。 -## v3.62.0 の新機能 - -- **GLM-5.2 サポート** — 最新の GLM モデルがプロバイダー設定で利用可能になりました -- **OpenCode-Go の改善** — ネイティブモデルパラメータ、Anthropic フォーマットルーティング、より信頼性の高い応答のための context-token 修正 -- **Tool-writer モード** — ツール定義の作成と保守のための新しい専門モードが Marketplace で利用可能になりました +## v3.68.0 の新機能 + +- **GLM-5.2 をサポートした Friendli プロバイダー** — Friendli 経由で最新の GLM モデルを使えます。 +- **Ollama のネイティブ thinking/reasoning サポート** — Ollama モデル使用時に reasoning 出力をエンドツーエンドで保持します。 +- **Anthropic のカスタム `apiModelId` 修正** — カスタム Anthropic モデル ID が `claude-sonnet-4-5` に黙ってフォールバックせず、そのまま選択された状態を維持します。 +- 修正: Ollama プロバイダーのツール結果処理と早すぎるコンテキスト圧縮。 +- 修正: 委譲されたサブタスクが中断されたときに親子タスクのリンクを維持。 +- Anthropic Vertex での Claude コンテンツブロック処理を改善し、応答の信頼性を向上。 +- CI、nightly 公開、依存関係/ツール更新。 ## Zoo Codeがあなたのためにできること diff --git a/locales/ko/README.md b/locales/ko/README.md index 6ecd6a5e7f..33b19fcfc8 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -30,11 +30,15 @@ Roo Code에서 Zoo Code로 옮겨오는 빠른 가이드는 [Roo→Zoo 마이그레이션 가이드](https://docs.zoocode.dev/roo-to-zoo-migration)에서 확인할 수 있어. 전환하는 동안 사용자들을 최대한 돕고 싶고, 바로 그 지원을 위해 [Reddit](https://www.reddit.com/r/ZooCode)와 [Discord](https://discord.gg/VxfP4Vx3gX)를 운영하고 있어. 문제가 있거나 궁금한 점이 있으면 들어와서 편하게 물어봐. -## v3.62.0의 새로운 기능 - -- **GLM-5.2 지원** — 최신 GLM 모델이 이제 공급자 설정에서 사용 가능합니다 -- **OpenCode-Go 개선** — 네이티브 모델 매개변수, Anthropic 형식 라우팅, 더 안정적인 응답을 위한 context-token 수정 -- **Tool-writer 모드** — 도구 정의를 작성하고 유지 관리하는 새로운 전문 모드가 Marketplace에서 사용 가능합니다 +## v3.68.0의 새로운 기능 + +- **GLM-5.2를 지원하는 Friendli 프로바이더** — Friendli를 통해 최신 GLM 모델을 사용할 수 있습니다. +- **Ollama용 네이티브 thinking/reasoning 지원** — Ollama 모델을 사용할 때 reasoning 출력을 처음부터 끝까지 유지합니다. +- **Anthropic 사용자 지정 `apiModelId` 수정** — 사용자 지정 Anthropic 모델 ID가 이제 조용히 `claude-sonnet-4-5`로 되돌아가지 않고 선택된 상태로 유지됩니다. +- 수정: Ollama 프로바이더의 도구 결과 처리 및 너무 이른 컨텍스트 압축. +- 수정: 위임된 하위 작업이 중단될 때 부모-자식 작업 연결을 유지합니다. +- Anthropic Vertex Claude 콘텐츠 블록 처리를 개선해 더 안정적인 응답을 제공합니다. +- CI, nightly 배포, 의존성/툴링 업데이트. ## Zoo Code가 당신을 위해 무엇을 할 수 있을까요? diff --git a/locales/nl/README.md b/locales/nl/README.md index 06470a4687..de7cdd352b 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -30,11 +30,15 @@ Je vindt een korte handleiding voor de overstap van Roo Code naar Zoo Code in de [Roo→Zoo-migratiegids](https://docs.zoocode.dev/roo-to-zoo-migration). We willen gebruikers zo goed mogelijk helpen tijdens de overgang, en precies daarvoor zijn onze [Reddit](https://www.reddit.com/r/ZooCode) en [Discord](https://discord.gg/VxfP4Vx3gX) er. Als je ergens tegenaan loopt of vragen hebt, kom langs en vraag het. -## Nieuw in v3.62.0 - -- **GLM-5.2-ondersteuning** — het nieuwste GLM-model is nu beschikbaar in je providerinstellingen -- **OpenCode-Go-verbeteringen** — native modelparameters, Anthropic-formaat routing en een context-token-fix voor betrouwbaardere antwoorden -- **Tool-writer-modus** — een nieuwe gespecialiseerde modus voor het schrijven en onderhouden van tool-definities, nu beschikbaar in de Marketplace +## Nieuw in v3.68.0 + +- **Friendli-provider met GLM-5.2-ondersteuning** — gebruik het nieuwste GLM-model via Friendli. +- **Native thinking/reasoning-ondersteuning voor Ollama** — behoud reasoning-uitvoer van begin tot eind wanneer je Ollama-modellen gebruikt. +- **Fix voor aangepaste Anthropic-`apiModelId`** — aangepaste Anthropic-model-ID's blijven nu geselecteerd in plaats van stilletjes terug te vallen op `claude-sonnet-4-5`. +- Fix: afhandeling van toolresultaten in de Ollama-provider en voortijdige contextcompressie. +- Fix: behoud de ouder-kind-koppeling van taken wanneer een gedelegeerde subtaak wordt onderbroken. +- Verbeterde verwerking van Claude-contentblokken in Anthropic Vertex voor betrouwbaardere antwoorden. +- Updates voor CI, nightly-publicatie en dependencies/tooling. ## Wat kan Zoo Code voor JOU doen? diff --git a/locales/pl/README.md b/locales/pl/README.md index 5fa2b3a4d2..deba8b5bca 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -30,11 +30,15 @@ Szybki przewodnik po przejściu z Roo Code do Zoo Code znajdziesz w [przewodniku migracji Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chcemy jak najlepiej pomagać użytkownikom w czasie przejścia i właśnie do tego służą nasze [Reddit](https://www.reddit.com/r/ZooCode) oraz [Discord](https://discord.gg/VxfP4Vx3gX). Jeśli masz problem albo pytanie, wpadaj i pytaj. -## Nowości w v3.62.0 - -- **Obsługa GLM-5.2** — najnowszy model GLM jest teraz dostępny w ustawieniach dostawcy -- **Ulepszenia OpenCode-Go** — natywne parametry modelu, routing w formacie Anthropic i poprawka context-token dla bardziej niezawodnych odpowiedzi -- **Tryb tool-writer** — nowy specjalistyczny tryb do pisania i utrzymania definicji narzędzi, teraz dostępny w Marketplace +## Nowości w v3.68.0 + +- **Provider Friendli z obsługą GLM-5.2** — używaj najnowszego modelu GLM przez Friendli. +- **Natywna obsługa thinking/reasoning dla Ollamy** — zachowuj wynik rozumowania od początku do końca podczas korzystania z modeli Ollama. +- **Poprawka niestandardowego `apiModelId` Anthropic** — niestandardowe identyfikatory modeli Anthropic pozostają teraz wybrane zamiast po cichu wracać do `claude-sonnet-4-5`. +- Poprawka: obsługa wyników narzędzi w providerze Ollama i przedwczesne kondensowanie kontekstu. +- Poprawka: zachowuj powiązanie zadania nadrzędnego i podrzędnego, gdy delegowane podzadanie zostanie przerwane. +- Ulepszona obsługa bloków treści Claude w Anthropic Vertex dla bardziej niezawodnych odpowiedzi. +- Aktualizacje CI, publikacji nightly oraz zależności/narzędzi. ## Co Zoo Code może zrobić dla CIEBIE? diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index e045c232b6..5f6c10e81e 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -30,11 +30,15 @@ Você encontra um guia rápido para migrar do Roo Code para o Zoo Code no [guia de migração Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Queremos ajudar os usuários durante essa transição da melhor forma possível, e é exatamente para isso que temos nosso [Reddit](https://www.reddit.com/r/ZooCode) e nosso [Discord](https://discord.gg/VxfP4Vx3gX). Se você tiver algum problema ou dúvida, apareça por lá e pergunte. -## Novidades na v3.62.0 - -- **Suporte a GLM-5.2** — o modelo GLM mais recente já está disponível nas configurações do provedor -- **Melhorias no OpenCode-Go** — parâmetros de modelo nativos, roteamento em formato Anthropic e correção de context-token para respostas mais confiáveis -- **Modo tool-writer** — um novo modo especializado para escrever e manter definições de ferramentas, agora disponível no Marketplace +## Novidades na v3.68.0 + +- **Provedor Friendli com suporte a GLM-5.2** — use o modelo GLM mais recente pelo Friendli. +- **Suporte nativo de thinking/reasoning para Ollama** — preserve a saída de raciocínio de ponta a ponta ao usar modelos do Ollama. +- **Correção de `apiModelId` personalizado da Anthropic** — os IDs de modelo personalizados da Anthropic agora permanecem selecionados em vez de voltar silenciosamente para `claude-sonnet-4-5`. +- Correção: tratamento de resultados de ferramentas do provedor Ollama e condensação prematura de contexto. +- Correção: preserve o vínculo pai-filho da tarefa quando uma subtarefa delegada é interrompida. +- Melhora o tratamento de blocos de conteúdo Claude no Anthropic Vertex para respostas mais confiáveis. +- Atualizações de CI, publicação nightly e dependências/tooling. ## O que o Zoo Code pode fazer por VOCÊ? diff --git a/locales/ru/README.md b/locales/ru/README.md index b051ef0f13..850b25b3e0 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -30,11 +30,15 @@ Короткое руководство по переходу с Roo Code на Zoo Code можно найти в [гайде по миграции Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Мы хотим как можно лучше помочь пользователям во время перехода, и именно для этого у нас есть [Reddit](https://www.reddit.com/r/ZooCode) и [Discord](https://discord.gg/VxfP4Vx3gX). Если у тебя возникнут проблемы или вопросы, заходи и спрашивай. -## Что нового в v3.62.0 - -- **Поддержка GLM-5.2** — последняя модель GLM теперь доступна в настройках провайдера -- **Улучшения OpenCode-Go** — нативные параметры модели, маршрутизация в формате Anthropic и исправление context-token для более надёжных ответов -- **Режим tool-writer** — новый специализированный режим для написания и поддержки определений инструментов, теперь доступен в Marketplace +## Что нового в v3.68.0 + +- **Провайдер Friendli с поддержкой GLM-5.2** — используй новейшую модель GLM через Friendli. +- **Нативная поддержка thinking/reasoning для Ollama** — сохраняй вывод рассуждений от начала до конца при использовании моделей Ollama. +- **Исправление пользовательского `apiModelId` Anthropic** — пользовательские идентификаторы моделей Anthropic теперь остаются выбранными вместо тихого возврата к `claude-sonnet-4-5`. +- Исправление: обработка результатов инструментов провайдера Ollama и преждевременное сжатие контекста. +- Исправление: сохраняй связь родительской и дочерней задачи, когда делегированная подзадача прерывается. +- Улучшена обработка блоков контента Claude в Anthropic Vertex для более надёжных ответов. +- Обновления CI, nightly-публикации и зависимостей/инструментов. ## Что Zoo Code может сделать для ВАС? diff --git a/locales/tr/README.md b/locales/tr/README.md index 4770b69ccf..d95e0e0c5d 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -30,11 +30,15 @@ Roo Code'dan Zoo Code'a geçmek için hızlı bir rehberi [Roo→Zoo geçiş rehberinde](https://docs.zoocode.dev/roo-to-zoo-migration) bulabilirsin. Geçiş sürecinde kullanıcılara elimizden geldiğince yardımcı olmak istiyoruz ve bunun için [Reddit](https://www.reddit.com/r/ZooCode) ile [Discord](https://discord.gg/VxfP4Vx3gX) topluluklarımız var. Bir sorun yaşarsan ya da sorunun olursa gel ve sor. -## v3.62.0'daki Yenilikler - -- **GLM-5.2 desteği** — en yeni GLM modeli artık sağlayıcı ayarlarında mevcut -- **OpenCode-Go iyileştirmeleri** — yerel model parametreleri, Anthropic formatı yönlendirme ve daha güvenilir yanıtlar için context-token düzeltmesi -- **Tool-writer modu** — araç tanımlarını yazmak ve sürdürmek için yeni bir uzmanlaşmış mod, artık Marketplace'te mevcut +## v3.68.0'daki Yenilikler + +- **GLM-5.2 destekli Friendli sağlayıcısı** — en yeni GLM modelini Friendli üzerinden kullan. +- **Ollama için yerel thinking/reasoning desteği** — Ollama modellerini kullanırken reasoning çıktısını uçtan uca koru. +- **Anthropic özel `apiModelId` düzeltmesi** — özel Anthropic model kimlikleri artık sessizce `claude-sonnet-4-5` modeline geri dönmek yerine seçili kalıyor. +- Düzeltme: Ollama sağlayıcısında araç sonucu işleme ve erken bağlam yoğunlaştırma. +- Düzeltme: yetkilendirilen bir alt görev kesintiye uğradığında görev üst-alt bağlantısını koru. +- Daha güvenilir yanıtlar için Anthropic Vertex Claude içerik bloğu işleme iyileştirildi. +- CI, nightly yayınlama ve bağımlılık/araç güncellemeleri. ## Zoo Code SİZİN İçin Ne Yapabilir? diff --git a/locales/vi/README.md b/locales/vi/README.md index 3534ab91e8..27217bc3bc 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -30,11 +30,15 @@ Bạn có thể xem hướng dẫn nhanh để chuyển từ Roo Code sang Zoo Code trong [hướng dẫn chuyển đổi Roo→Zoo](https://docs.zoocode.dev/roo-to-zoo-migration). Chúng tôi muốn hỗ trợ người dùng nhiều nhất có thể trong quá trình chuyển đổi, và đó chính là lý do chúng tôi có [Reddit](https://www.reddit.com/r/ZooCode) và [Discord](https://discord.gg/VxfP4Vx3gX). Nếu bạn gặp vấn đề hoặc có câu hỏi, cứ vào hỏi nhé. -## Điểm mới trong v3.62.0 - -- **Hỗ trợ GLM-5.2** — mô hình GLM mới nhất hiện có trong cài đặt nhà cung cấp của bạn -- **Cải tiến OpenCode-Go** — tham số mô hình gốc, định tuyến định dạng Anthropic và sửa lỗi context-token để phản hồi đáng tin cậy hơn -- **Chế độ tool-writer** — chế độ chuyên biệt mới để viết và duy trì các định nghĩa công cụ, hiện có trên Marketplace +## Điểm mới trong v3.68.0 + +- **Nhà cung cấp Friendli với hỗ trợ GLM-5.2** — dùng mẫu GLM mới nhất qua Friendli. +- **Hỗ trợ thinking/reasoning gốc cho Ollama** — giữ đầu ra suy luận xuyên suốt khi bạn dùng các mẫu Ollama. +- **Sửa lỗi `apiModelId` tùy chỉnh của Anthropic** — ID mẫu Anthropic tùy chỉnh giờ sẽ tiếp tục được chọn thay vì âm thầm rơi về `claude-sonnet-4-5`. +- Sửa lỗi: xử lý kết quả công cụ của nhà cung cấp Ollama và cô đọng ngữ cảnh quá sớm. +- Sửa lỗi: giữ liên kết cha-con của tác vụ khi một tác vụ con được ủy quyền bị gián đoạn. +- Cải thiện xử lý khối nội dung Claude trên Anthropic Vertex để có phản hồi đáng tin cậy hơn. +- Cập nhật CI, phát hành nightly và phụ thuộc/công cụ. ## Zoo Code có thể làm gì cho BẠN? diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index 9659f4d603..7b6b88a5de 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -30,11 +30,15 @@ 你可以在 [Roo→Zoo 迁移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到从 Roo Code 迁移到 Zoo Code 的快速说明。我们希望在大家迁移过程中尽可能提供帮助,这也是我们设立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社区的原因。如果你遇到问题或有任何疑问,欢迎加入后直接提问。 -## v3.62.0 新增内容 - -- **GLM-5.2 支持** — 最新 GLM 模型现已在提供商设置中可用 -- **OpenCode-Go 改进** — 原生模型参数、Anthropic 格式路由和 context-token 修复,提供更可靠的响应 -- **Tool-writer 模式** — 用于编写和维护工具定义的新专用模式,现已在 Marketplace 中可用 +## v3.68.0 新增内容 + +- **支持 GLM-5.2 的 Friendli 提供商** — 通过 Friendli 使用最新的 GLM 模型。 +- **Ollama 原生 thinking/reasoning 支持** — 使用 Ollama 模型时可端到端保留 reasoning 输出。 +- **Anthropic 自定义 `apiModelId` 修复** — 自定义 Anthropic 模型 ID 现在会保持选中,不再静默回退到 `claude-sonnet-4-5`。 +- 修复:Ollama 提供商的工具结果处理和过早的上下文压缩。 +- 修复:委派子任务被中断时保留父子任务链接。 +- 改进 Anthropic Vertex 中 Claude 内容块的处理,以获得更可靠的响应。 +- CI、nightly 发布以及依赖/工具更新。 ## Zoo Code 能为您做什么? diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 10f4d54ee1..988c42cbbe 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -30,11 +30,15 @@ 你可以在 [Roo→Zoo 遷移指南](https://docs.zoocode.dev/roo-to-zoo-migration) 中找到從 Roo Code 遷移到 Zoo Code 的快速說明。我們希望在大家轉移過程中盡可能提供協助,這也是我們設立 [Reddit](https://www.reddit.com/r/ZooCode) 和 [Discord](https://discord.gg/VxfP4Vx3gX) 社群的原因。如果你遇到問題或有任何疑問,歡迎加入後直接提問。 -## v3.62.0 新功能 - -- **GLM-5.2 支援** — 最新 GLM 模型現已在提供商設定中可用 -- **OpenCode-Go 改進** — 原生模型參數、Anthropic 格式路由和 context-token 修復,提供更可靠的回應 -- **Tool-writer 模式** — 用於編寫和維護工具定義的新專用模式,現已在 Marketplace 中可用 +## v3.68.0 新功能 + +- **支援 GLM-5.2 的 Friendli 供應商** — 透過 Friendli 使用最新的 GLM 模型。 +- **Ollama 原生 thinking/reasoning 支援** — 使用 Ollama 模型時可端對端保留 reasoning 輸出。 +- **Anthropic 自訂 `apiModelId` 修正** — 自訂 Anthropic 模型 ID 現在會保持選取,不再靜默回退到 `claude-sonnet-4-5`。 +- 修正:Ollama 供應商的工具結果處理與過早的內容壓縮。 +- 修正:委派子任務被中斷時保留父子任務連結。 +- 改進 Anthropic Vertex 中 Claude 內容區塊的處理,以獲得更可靠的回應。 +- CI、nightly 發布以及相依套件/工具更新。 ## Zoo Code 能為您做什麼? diff --git a/package.json b/package.json index 18db64c5fa..707521afc8 100644 --- a/package.json +++ b/package.json @@ -31,21 +31,20 @@ "@changesets/cli": "2.29.7", "@dotenvx/dotenvx": "1.66.0", "@roo-code/config-typescript": "workspace:^", - "@types/node": "24.2.1", - "@vscode/vsce": "3.3.2", + "@types/node": "20.19.43", + "@vscode/vsce": "3.9.2", "esbuild": "0.28.1", - "eslint": "9.28.0", + "eslint": "9.39.4", "husky": "9.1.7", "knip": "5.60.2", "lint-staged": "16.4.0", "mkdirp": "3.0.1", - "only-allow": "1.2.1", - "ovsx": "0.10.4", - "prettier": "3.5.3", + "ovsx": "0.10.12", + "prettier": "3.8.4", "rimraf": "6.0.1", - "tsx": "4.19.4", - "turbo": "2.9.14", - "typescript": "5.8.3" + "tsx": "4.22.4", + "turbo": "2.10.0", + "typescript": "5.9.3" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,css,md}": [ @@ -60,13 +59,14 @@ "tar-fs": ">=3.1.1", "esbuild": "0.28.1", "rollup": "4.60.4", - "vite": "8.0.16", - "undici": ">=5.29.0", + "vite": "8.1.0", + "undici": "6.27.0", "form-data": ">=4.0.4", "bluebird": ">=3.7.2", "glob": "11.1.0", - "@types/react": "18.3.23", + "@types/react": "18.3.31", "@types/react-dom": "18.3.7", + "csstype": "3.1.3", "zod": "3.25.76" } } diff --git a/packages/build/package.json b/packages/build/package.json index c74b7aa114..e0175a38d3 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.41", - "vitest": "4.1.0" + "@types/node": "20.19.43", + "vitest": "4.1.9" } } diff --git a/packages/cloud/package.json b/packages/cloud/package.json index 9a1ccc39b7..c6cb23c2c0 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -13,18 +13,16 @@ }, "dependencies": { "@roo-code/types": "workspace:^", - "ioredis": "^5.6.1", "jwt-decode": "^4.0.0", - "p-wait-for": "^5.0.2", "zod": "^3.25.76" }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "24.2.1", + "@types/node": "20.19.43", "@types/vscode": "1.100.0", "globals": "16.3.0", - "@vitest/coverage-v8": "4.1.0", - "vitest": "4.1.0" + "@vitest/coverage-v8": "4.1.9", + "vitest": "4.1.9" } } diff --git a/packages/config-eslint/base.js b/packages/config-eslint/base.js index abb708016c..6a15f09c03 100644 --- a/packages/config-eslint/base.js +++ b/packages/config-eslint/base.js @@ -39,6 +39,15 @@ export const config = [ caughtErrorsIgnorePattern: "^_", }, ], + // Reject irregular whitespace (incl. zero-width space U+200B and + // BOM U+FEFF) in identifiers and between tokens. This rule does NOT + // catch bidi-override, ZWJ/ZWNJ, or word-joiner characters; the CI + // invisible-chars job in code-qa.yml is the authoritative defense + // for the full Trojan Source character set across all files. + "no-irregular-whitespace": [ + "error", + { skipStrings: true, skipComments: false, skipRegExps: true, skipTemplates: false }, + ], }, }, ] diff --git a/packages/config-eslint/package.json b/packages/config-eslint/package.json index 554fc2a32b..b5be1b4b57 100644 --- a/packages/config-eslint/package.json +++ b/packages/config-eslint/package.json @@ -10,12 +10,12 @@ "devDependencies": { "@eslint/js": "9.27.0", "@next/eslint-plugin-next": "15.3.2", - "eslint": "9.27.0", + "eslint": "9.39.4", "eslint-config-prettier": "10.1.8", - "eslint-plugin-only-warn": "1.1.0", + "eslint-plugin-only-warn": "1.2.1", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.2.0", - "eslint-plugin-turbo": "2.5.6", + "eslint-plugin-turbo": "2.10.0", "globals": "16.1.0", "typescript-eslint": "8.32.1" } diff --git a/packages/core/package.json b/packages/core/package.json index 8b2e84968d..16883a6430 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,8 +30,8 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "24.2.1", - "@vitest/coverage-v8": "4.1.0", - "vitest": "4.1.0" + "@types/node": "20.19.43", + "@vitest/coverage-v8": "4.1.9", + "vitest": "4.1.9" } } diff --git a/packages/core/src/task-history/index.ts b/packages/core/src/task-history/index.ts index 384bdfc254..f2439ee973 100644 --- a/packages/core/src/task-history/index.ts +++ b/packages/core/src/task-history/index.ts @@ -41,7 +41,10 @@ function extractSessionEntry(value: unknown): TaskSessionEntry | undefined { ts, workspace: typeof workspace === "string" ? workspace : undefined, mode: typeof mode === "string" ? mode : undefined, - status: status === "active" || status === "completed" || status === "delegated" ? status : undefined, + status: + status === "active" || status === "completed" || status === "delegated" || status === "interrupted" + ? status + : undefined, } } diff --git a/packages/ipc/package.json b/packages/ipc/package.json index abe90f52fa..d79e8b5900 100644 --- a/packages/ipc/package.json +++ b/packages/ipc/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.41", + "@types/node": "20.19.43", "@types/node-ipc": "9.2.3" } } diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index e3b78ae018..c7be0214fe 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -19,9 +19,9 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.19.41", + "@types/node": "20.19.43", "@types/vscode": "1.100.0", - "@vitest/coverage-v8": "4.1.0", - "vitest": "4.1.0" + "@vitest/coverage-v8": "4.1.9", + "vitest": "4.1.9" } } diff --git a/packages/types/package.json b/packages/types/package.json index 5ecc8bf422..77a8415f86 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -30,11 +30,11 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "24.2.1", + "@types/node": "20.19.43", "globals": "16.3.0", "tsup": "8.5.0", - "ajv": "8.18.0", - "vitest": "4.1.0", + "ajv": "8.20.0", + "vitest": "4.1.9", "zod-to-json-schema": "3.25.1" } } diff --git a/packages/types/src/__tests__/message.test.ts b/packages/types/src/__tests__/message.test.ts index da057732b3..b9b5fbdaea 100644 --- a/packages/types/src/__tests__/message.test.ts +++ b/packages/types/src/__tests__/message.test.ts @@ -1,6 +1,14 @@ // pnpm --filter @roo-code/types test src/__tests__/message.test.ts -import { clineAsks, isIdleAsk, isInteractiveAsk, isResumableAsk, isNonBlockingAsk } from "../message.js" +import { + clineAsks, + getCompletionCheckpoint, + isIdleAsk, + isInteractiveAsk, + isResumableAsk, + isNonBlockingAsk, + type ClineMessage, +} from "../message.js" describe("ask messages", () => { test("all ask messages are classified", () => { @@ -12,3 +20,46 @@ describe("ask messages", () => { } }) }) + +describe("getCompletionCheckpoint", () => { + it("returns the first checkpoint after the latest user prompt before completion", () => { + const messages: ClineMessage[] = [ + { type: "say", say: "text", ts: 1, text: "Initial task" }, + { type: "say", say: "checkpoint_saved", ts: 2, text: "initial-checkpoint" }, + { type: "say", say: "completion_result", ts: 3, text: "First completion" }, + { type: "say", say: "user_feedback", ts: 4, text: "Change it" }, + { type: "say", say: "checkpoint_saved", ts: 5, text: "latest-prompt-checkpoint" }, + { type: "say", say: "checkpoint_saved", ts: 6, text: "later-edit-checkpoint" }, + { type: "ask", ask: "completion_result", ts: 7, text: "", partial: false }, + ] + + expect(getCompletionCheckpoint(messages)).toEqual({ + ts: 5, + commitHash: "latest-prompt-checkpoint", + }) + }) + + it("returns the first checkpoint after an initial task row before completion", () => { + const messages: ClineMessage[] = [ + { type: "say", say: "task", ts: 1, text: "Initial task" }, + { type: "say", say: "checkpoint_saved", ts: 2, text: "checkpoint-after-initial-task" }, + { type: "ask", ask: "completion_result", ts: 3, text: "Task complete", partial: false }, + ] + + expect(getCompletionCheckpoint(messages)).toEqual({ + ts: 2, + commitHash: "checkpoint-after-initial-task", + }) + }) + + it("returns undefined when completion has no checkpoint after the latest user prompt", () => { + const messages: ClineMessage[] = [ + { type: "say", say: "text", ts: 1, text: "Initial task" }, + { type: "say", say: "checkpoint_saved", ts: 2, text: "initial-checkpoint" }, + { type: "say", say: "user_feedback", ts: 3, text: "Change it" }, + { type: "ask", ask: "completion_result", ts: 4, text: "", partial: false }, + ] + + expect(getCompletionCheckpoint(messages)).toBeUndefined() + }) +}) diff --git a/packages/types/src/__tests__/vscode-llm.spec.ts b/packages/types/src/__tests__/vscode-llm.spec.ts new file mode 100644 index 0000000000..041bc3c8b4 --- /dev/null +++ b/packages/types/src/__tests__/vscode-llm.spec.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest" +import { vscodeLlmModels, vscodeLlmDefaultModelId } from "../providers/vscode-llm.js" + +describe("vscodeLlmModels", () => { + it("exposes the opus-4.8 row with its measured maxInputTokens and contextWindow", () => { + // claude-opus-4.8 intentionally diverges: maxInputTokens (197897) is the enforced ceiling the + // UI reads, contextWindow (679560) the advertised window. Assert the on-disk literals as a tripwire. + expect(vscodeLlmModels).toHaveProperty("claude-opus-4.8") + expect(vscodeLlmModels["claude-opus-4.8"].contextWindow).toBe(679560) + expect(vscodeLlmModels["claude-opus-4.8"].maxInputTokens).toBe(197897) + }) + it("preserves the real window for models captured with a smaller maxInputTokens", () => { + expect(vscodeLlmModels["gpt-4o-mini"].maxInputTokens).toBe(12078) + expect(vscodeLlmModels["gpt-4o-mini"].contextWindow).toBe(12078) + expect(vscodeLlmModels["gemini-2.5-pro"].contextWindow).toBe(108594) + expect(vscodeLlmModels["gemini-2.5-pro"].maxInputTokens).toBe(108594) + }) + it("keeps both window fields populated and positive for every row", () => { + for (const [family, model] of Object.entries(vscodeLlmModels)) { + expect(model.contextWindow, `${family}: contextWindow must be a positive integer`).toBeGreaterThan(0) + expect(model.maxInputTokens, `${family}: maxInputTokens must be a positive integer`).toBeGreaterThan(0) + } + }) + it("excludes fabricated/internal/alias families and the dropped legacy rows", () => { + expect(vscodeLlmModels).not.toHaveProperty("claude-opus-4.7-high") + expect(vscodeLlmModels).not.toHaveProperty("claude-3.5-sonnet") + expect(vscodeLlmModels).not.toHaveProperty("claude-4-sonnet") + }) + it("defaults to a model id that exists in the table", () => { + expect(vscodeLlmDefaultModelId).toBe("claude-sonnet-4.5") + expect(vscodeLlmModels).toHaveProperty(vscodeLlmDefaultModelId) + }) +}) diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index c9b89b3b00..2dbaae7920 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -3,6 +3,7 @@ import type { Socket } from "net" import type { RooCodeEvents } from "./events.js" import type { RooCodeSettings } from "./global-settings.js" +import type { HistoryItem } from "./history.js" import type { ProviderSettingsEntry, ProviderSettings } from "./provider-settings.js" import type { IpcMessage, IpcServerEvents } from "./ipc.js" @@ -38,6 +39,18 @@ export interface RooCodeAPI extends EventEmitter { * @returns True if the task is in the task history, false otherwise. */ isTaskInHistory(taskId: string): Promise + /** + * Returns the HistoryItem for a task by ID. Intended for use in tests only. + * @param taskId The ID of the task. + * @returns The HistoryItem, or undefined if not found. + */ + getTaskHistoryItem(taskId: string): Promise + /** + * Returns the persisted API conversation history length for a task. Intended for use in tests only. + * @param taskId The ID of the task. + * @returns The number of persisted API conversation history entries, or 0 if unavailable. + */ + getTaskApiConversationHistoryLength(taskId: string): Promise /** * Returns the current task stack. * @returns An array of task IDs. diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index d7eb0b03d6..5d511859b1 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -6,7 +6,13 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js" * ExperimentId */ -export const experimentIds = ["preventFocusDisruption", "imageGeneration", "runSlashCommand", "customTools"] as const +export const experimentIds = [ + "preventFocusDisruption", + "imageGeneration", + "runSlashCommand", + "customTools", + "parallelToolExecution", +] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -21,6 +27,7 @@ export const experimentsSchema = z.object({ imageGeneration: z.boolean().optional(), runSlashCommand: z.boolean().optional(), customTools: z.boolean().optional(), + parallelToolExecution: z.boolean().optional(), }) export type Experiments = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index bac3548ccb..802210626c 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -1,19 +1,18 @@ import { z } from "zod" -import { type Keys } from "./type-fu.js" +import { codebaseIndexConfigSchema, codebaseIndexModelsSchema } from "./codebase-index.js" +import { experimentsSchema } from "./experiment.js" +import { historyItemSchema } from "./history.js" +import { customModePromptsSchema, customSupportPromptsSchema, modeConfigSchema } from "./mode.js" import { type ProviderSettings, PROVIDER_SETTINGS_KEYS, providerSettingsEntrySchema, providerSettingsSchema, } from "./provider-settings.js" -import { historyItemSchema } from "./history.js" -import { codebaseIndexModelsSchema, codebaseIndexConfigSchema } from "./codebase-index.js" -import { experimentsSchema } from "./experiment.js" import { telemetrySettingsSchema } from "./telemetry.js" -import { modeConfigSchema } from "./mode.js" -import { customModePromptsSchema, customSupportPromptsSchema } from "./mode.js" import { toolNamesSchema } from "./tool.js" +import { type Keys } from "./type-fu.js" import { languagesSchema } from "./vscode.js" /** @@ -23,6 +22,30 @@ import { languagesSchema } from "./vscode.js" */ export const DEFAULT_WRITE_DELAY_MS = 1000 +/** + * Default values for the "auto-close files Zoo opened" settings. + * + * These are defined once here and consumed by every site that reads the setting + * (DiffViewProvider save/revert, ClineProvider state serialization, and the + * UISettings checkboxes) so there is a single source of truth for the default + * behavior. Auto-closing edited tabs is opt-in: by default, files Zoo edits stay + * open in the editor (the long-standing behavior). Users who want to save + * context tokens by closing the edited tab after each edit can enable it. + */ +export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES = false +export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED = false +export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES = false + +/** + * Default fuzzy matching threshold for the multi-search-replace diff strategy. + * A value of 1.0 (exact match) is used by default for safety, especially when + * auto-approval for writes is enabled. This prevents unintended changes from + * being applied due to minor mismatches. Users can lower this threshold manually + * in settings to reduce "Edit Unsuccessful" errors caused by minor whitespace + * or formatting differences, accepting a higher risk of unintended edits. + */ +export const DEFAULT_DIFF_FUZZY_THRESHOLD = 1.0 + /** * Terminal output preview size options for persisted command output. * @@ -102,6 +125,12 @@ export const globalSettingsSchema = z.object({ alwaysAllowWriteOutsideWorkspace: z.boolean().optional(), alwaysAllowWriteProtected: z.boolean().optional(), writeDelayMs: z.number().min(0).optional(), + /** + * Fuzzy matching threshold for the multi-search-replace diff strategy. + * Range: 0.5 (50% minimum similarity) to 1.0 (exact match only). + * `@default` 1.0 + */ + diffFuzzyThreshold: z.number().min(0.5).max(1).optional(), requestDelaySeconds: z.number().optional(), alwaysAllowMcp: z.boolean().optional(), alwaysAllowModeSwitch: z.boolean().optional(), @@ -287,6 +316,7 @@ export const SECRET_STATE_KEYS = [ "sambaNovaApiKey", "zaiApiKey", "fireworksApiKey", + "friendliApiKey", "vercelAiGatewayApiKey", "opencodeGoApiKey", "basetenApiKey", diff --git a/packages/types/src/history.ts b/packages/types/src/history.ts index a60d1a75b6..5b173c6a6b 100644 --- a/packages/types/src/history.ts +++ b/packages/types/src/history.ts @@ -20,7 +20,7 @@ export const historyItemSchema = z.object({ workspace: z.string().optional(), mode: z.string().optional(), apiConfigName: z.string().optional(), // Provider profile name for sticky profile feature - status: z.enum(["active", "completed", "delegated"]).optional(), + status: z.enum(["active", "completed", "delegated", "interrupted"]).optional(), delegatedToId: z.string().optional(), // Last child this parent delegated to childIds: z.array(z.string()).optional(), // All children spawned by this task awaitingChildId: z.string().optional(), // Child currently awaited (set when delegated) diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ee4d485720..eaa419b1af 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -22,6 +22,7 @@ export * from "./provider-settings.js" export * from "./task.js" export * from "./todo.js" export * from "./skills.js" +export * from "./rules.js" export * from "./marketplace.js" export * from "./telemetry.js" export * from "./terminal.js" diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index e518972a1c..dc0e3dfff5 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -150,6 +150,7 @@ export const clineSays = [ "api_req_rate_limit_wait", "api_req_deleted", "text", + "task", "image", "reasoning", "completion_result", @@ -275,6 +276,76 @@ export const clineMessageSchema = z.object({ export type ClineMessage = z.infer +export interface CompletionCheckpoint { + ts: number + commitHash: string +} + +const isInitialTaskMessage = (message: ClineMessage | undefined): boolean => { + return message?.type === "say" && (message.say === "text" || message.say === "task") +} + +const isUserFeedbackMessage = (message: ClineMessage): boolean => { + return message.type === "say" && message.say === "user_feedback" +} + +const isCompletionMessage = (message: ClineMessage): boolean => { + return ( + (message.type === "ask" && message.ask === "completion_result") || + (message.type === "say" && message.say === "completion_result") + ) +} + +const isCheckpointMessage = (message: ClineMessage): boolean => { + return message.type === "say" && message.say === "checkpoint_saved" && typeof message.text === "string" +} + +function findLastIndexBefore( + messages: ClineMessage[], + beforeIndex: number, + predicate: (message: ClineMessage) => boolean, +): number { + for (let i = beforeIndex - 1; i >= 0; i--) { + const message = messages[i] + + if (message && predicate(message)) { + return i + } + } + + return -1 +} + +/** + * Finds the checkpoint that should anchor completion-result actions. + * + * The baseline is the first checkpoint created after the latest user prompt in + * the turn that produced the completion. Restoring to that checkpoint reverts + * changes made for the latest prompt, and diffing from it shows the same scoped + * changes. + */ +export function getCompletionCheckpoint(messages: ClineMessage[]): CompletionCheckpoint | undefined { + const completionIndex = findLastIndexBefore(messages, messages.length, isCompletionMessage) + const searchEnd = completionIndex === -1 ? messages.length : completionIndex + const latestUserFeedbackIndex = findLastIndexBefore(messages, searchEnd, isUserFeedbackMessage) + const latestUserPromptIndex = + latestUserFeedbackIndex !== -1 ? latestUserFeedbackIndex : isInitialTaskMessage(messages[0]) ? 0 : -1 + + if (latestUserPromptIndex === -1) { + return undefined + } + + for (let i = latestUserPromptIndex + 1; i < searchEnd; i++) { + const message = messages[i] + + if (message && isCheckpointMessage(message)) { + return { ts: message.ts, commitHash: message.text! } + } + } + + return undefined +} + /** * TokenUsage */ diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index c66b9f6d82..dbf6b869d7 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -8,6 +8,7 @@ import { bedrockModels, deepSeekModels, fireworksModels, + friendliModels, geminiModels, mistralModels, moonshotModels, @@ -119,6 +120,7 @@ export const providerNames = [ "baseten", "deepseek", "fireworks", + "friendli", "gemini", "gemini-cli", "mistral", @@ -396,6 +398,10 @@ const fireworksSchema = apiModelIdProviderModelSchema.extend({ fireworksApiKey: z.string().optional(), }) +const friendliSchema = apiModelIdProviderModelSchema.extend({ + friendliApiKey: z.string().optional(), +}) + const qwenCodeSchema = apiModelIdProviderModelSchema.extend({ qwenCodeOauthPath: z.string().optional(), }) @@ -452,6 +458,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })), zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })), fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })), + friendliSchema.merge(z.object({ apiProvider: z.literal("friendli") })), qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })), opencodeGoSchema.merge(z.object({ apiProvider: z.literal("opencode-go") })), @@ -488,6 +495,7 @@ export const providerSettingsSchema = z.object({ ...sambaNovaSchema.shape, ...zaiSchema.shape, ...fireworksSchema.shape, + ...friendliSchema.shape, ...qwenCodeSchema.shape, ...vercelAiGatewaySchema.shape, ...opencodeGoSchema.shape, @@ -568,6 +576,7 @@ export const modelIdKeysByProvider: Record = { sambanova: "apiModelId", zai: "apiModelId", fireworks: "apiModelId", + friendli: "apiModelId", "vercel-ai-gateway": "vercelAiGatewayModelId", "opencode-go": "opencodeGoModelId", "zoo-gateway": "zooGatewayModelId", @@ -641,6 +650,11 @@ export const MODELS_BY_PROVIDER: Record< label: "Fireworks", models: Object.keys(fireworksModels), }, + friendli: { + id: "friendli", + label: "Friendli", + models: Object.keys(friendliModels), + }, gemini: { id: "gemini", label: "Google Gemini", diff --git a/packages/types/src/providers/anthropic.ts b/packages/types/src/providers/anthropic.ts index 6da1399c8d..0290e4568c 100644 --- a/packages/types/src/providers/anthropic.ts +++ b/packages/types/src/providers/anthropic.ts @@ -53,6 +53,30 @@ export const anthropicModels = { }, ], }, + "claude-sonnet-5": { + maxTokens: 128_000, // Overridden to 8k if `enableReasoningEffort` is false. + contextWindow: 1_000_000, // 1M context window native (no beta header required) + supportsImages: true, + supportsPromptCache: true, + inputPrice: 2.0, // $2 per million input tokens (introductory pricing through Aug 31, 2026) + outputPrice: 10.0, // $10 per million output tokens (introductory pricing through Aug 31, 2026) + cacheWritesPrice: 2.5, // $2.50 per million tokens (introductory pricing through Aug 31, 2026) + cacheReadsPrice: 0.2, // $0.20 per million tokens (introductory pricing through Aug 31, 2026) + // Sonnet 5 is adaptive-thinking only, like Opus 4.7+ and Fable 5 on the + // direct Anthropic provider path: manual extended thinking (budget_tokens) + // is removed and returns a 400, and sampling parameters + // (temperature/top_p/top_k) return a 400. Fork convention: drive it through + // the effort path (thinking:{type:"adaptive"} + output_config.effort); + // declaring supportsReasoningBudget would route it to the legacy budget + // branch in getAnthropicReasoning and 400. Mirrors Opus 4.7/4.8/Fable 5. + supportsReasoningEffort: ["low", "medium", "high", "xhigh", "max"], + requiredReasoningEffort: true, + reasoningEffort: "high", + supportsTemperature: false, + supportsReasoningDisplay: true, + description: + "Claude Sonnet 5 is the best combination of speed and intelligence, optimized for coding, tool use, and agentic workflows.", + }, "claude-sonnet-4-5": { maxTokens: 64_000, // Overridden to 8k if `enableReasoningEffort` is false. contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07' diff --git a/packages/types/src/providers/bedrock.ts b/packages/types/src/providers/bedrock.ts index eb03f0c686..70fbe16057 100644 --- a/packages/types/src/providers/bedrock.ts +++ b/packages/types/src/providers/bedrock.ts @@ -51,6 +51,24 @@ export const bedrockModels = { }, ], }, + "anthropic.claude-sonnet-5": { + maxTokens: 8192, + contextWindow: 1_000_000, // 1M context window native (no beta header required) + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + inputPrice: 2.0, // $2 per million input tokens (introductory pricing through Aug 31, 2026) + outputPrice: 10.0, // $10 per million output tokens (introductory pricing through Aug 31, 2026) + cacheWritesPrice: 2.5, // $2.50 per million tokens (introductory pricing through Aug 31, 2026) + cacheReadsPrice: 0.2, // $0.20 per million tokens (introductory pricing through Aug 31, 2026) + minTokensPerCachePoint: 1024, + maxCachePoints: 4, + cachableFields: ["system", "messages", "tools"], + description: + "Claude Sonnet 5 is the best combination of speed and intelligence, optimized for coding, tool use, and agentic workflows.", + }, "amazon.nova-pro-v1:0": { maxTokens: 5000, contextWindow: 300_000, @@ -602,6 +620,7 @@ export const BEDROCK_1M_CONTEXT_MODEL_IDS = [ // - Claude Sonnet 4 // - Claude Sonnet 4.5 // - Claude Sonnet 4.6 +// - Claude Sonnet 5 // - Claude Haiku 4.5 // - Claude Opus 4.5 // - Claude Opus 4.6 @@ -611,6 +630,7 @@ export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [ "anthropic.claude-sonnet-4-20250514-v1:0", "anthropic.claude-sonnet-4-5-20250929-v1:0", "anthropic.claude-sonnet-4-6", + "anthropic.claude-sonnet-5", "anthropic.claude-haiku-4-5-20251001-v1:0", "anthropic.claude-opus-4-5-20251101-v1:0", "anthropic.claude-opus-4-6-v1", diff --git a/packages/types/src/providers/fireworks.ts b/packages/types/src/providers/fireworks.ts index 12e24c161e..3b18ad50b8 100644 --- a/packages/types/src/providers/fireworks.ts +++ b/packages/types/src/providers/fireworks.ts @@ -6,6 +6,7 @@ export type FireworksModelId = | "accounts/fireworks/models/kimi-k2-thinking" | "accounts/fireworks/models/kimi-k2p5" | "accounts/fireworks/models/kimi-k2p6" + | "accounts/fireworks/models/kimi-k2p7-code" | "accounts/fireworks/models/minimax-m2" | "accounts/fireworks/models/minimax-m2p1" | "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507" @@ -265,6 +266,20 @@ export const fireworksModels = { description: "DeepSeek V4 Pro is the latest iteration of the DeepSeek model family, with improved reasoning, code generation, and instruction following over the V3 series.", }, + "accounts/fireworks/models/kimi-k2p7-code": { + maxTokens: 16384, + contextWindow: 262144, + supportsImages: true, + supportsPromptCache: true, + preserveReasoning: true, + supportsTemperature: true, + defaultTemperature: 1.0, + inputPrice: 0.95, + outputPrice: 4.0, + cacheReadsPrice: 0.19, + description: + "Kimi K2.7 Code is Moonshot AI's latest coding-focused model, building on K2.6 with enhanced code generation, reasoning, and unified vision/text understanding for software development workflows.", + }, "accounts/fireworks/models/glm-5p1": { maxTokens: 25344, contextWindow: 202752, diff --git a/packages/types/src/providers/friendli.ts b/packages/types/src/providers/friendli.ts new file mode 100644 index 0000000000..71591d8f40 --- /dev/null +++ b/packages/types/src/providers/friendli.ts @@ -0,0 +1,63 @@ +import type { ModelInfo } from "../model.js" + +export type FriendliModelId = + | "zai-org/GLM-5.2" + | "zai-org/GLM-5.1" + | "deepseek-ai/DeepSeek-V3.2" + | "MiniMaxAI/MiniMax-M2.5" + +export const friendliDefaultModelId: FriendliModelId = "zai-org/GLM-5.2" + +// Pricing sourced from https://friendli.ai/api/public/model-apis (per 1M tokens). +export const friendliModels = { + "zai-org/GLM-5.2": { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + inputPrice: 1.4, + outputPrice: 4.4, + cacheWritesPrice: 0, + cacheReadsPrice: 0.26, + description: + "GLM-5.2 is Zhipu's flagship model with a 1M context window and 128k max output, served via Friendli Model APIs. It delivers top-tier long-context reasoning, coding, and agentic performance for extended engineering sessions.", + }, + "zai-org/GLM-5.1": { + maxTokens: 131_072, + contextWindow: 200_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + inputPrice: 1.4, + outputPrice: 4.4, + cacheWritesPrice: 0, + cacheReadsPrice: 0.26, + description: + "GLM-5.1 is Zhipu's most capable model with a 200k context window and 128k max output, served via Friendli Model APIs. It delivers top-tier reasoning, coding, and agentic performance.", + }, + "deepseek-ai/DeepSeek-V3.2": { + maxTokens: 16384, + contextWindow: 163_840, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.5, + outputPrice: 1.5, + cacheWritesPrice: 0, + cacheReadsPrice: 0.25, + description: + "DeepSeek V3.2 is the latest iteration of the V3 model family with enhanced reasoning capabilities, improved code generation, and better instruction following, served via Friendli Model APIs.", + }, + "MiniMaxAI/MiniMax-M2.5": { + maxTokens: 4096, + contextWindow: 204_800, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.3, + outputPrice: 1.2, + cacheWritesPrice: 0, + cacheReadsPrice: 0.06, + description: + "MiniMax M2.5 is a high-performance language model with a 204.8K context window, optimized for long-context understanding and generation tasks, served via Friendli Model APIs.", + }, +} as const satisfies Record diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index f283cb474c..bcb4a76d5d 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -3,6 +3,7 @@ export * from "./baseten.js" export * from "./bedrock.js" export * from "./deepseek.js" export * from "./fireworks.js" +export * from "./friendli.js" export * from "./gemini.js" export * from "./lite-llm.js" export * from "./lm-studio.js" @@ -33,6 +34,7 @@ import { basetenDefaultModelId } from "./baseten.js" import { bedrockDefaultModelId } from "./bedrock.js" import { deepSeekDefaultModelId } from "./deepseek.js" import { fireworksDefaultModelId } from "./fireworks.js" +import { friendliDefaultModelId } from "./friendli.js" import { geminiDefaultModelId } from "./gemini.js" import { litellmDefaultModelId } from "./lite-llm.js" import { mistralDefaultModelId } from "./mistral.js" @@ -111,6 +113,8 @@ export function getProviderDefaultModelId( return sambaNovaDefaultModelId case "fireworks": return fireworksDefaultModelId + case "friendli": + return friendliDefaultModelId case "qwen-code": return qwenCodeDefaultModelId case "poe": diff --git a/packages/types/src/providers/openrouter.ts b/packages/types/src/providers/openrouter.ts index 9898ace232..9b904bd6a9 100644 --- a/packages/types/src/providers/openrouter.ts +++ b/packages/types/src/providers/openrouter.ts @@ -39,6 +39,7 @@ export const OPEN_ROUTER_PROMPT_CACHING_MODELS = new Set([ "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.5", "anthropic/claude-sonnet-4.6", + "anthropic/claude-sonnet-5", "anthropic/claude-opus-4", "anthropic/claude-opus-4.1", "anthropic/claude-opus-4.5", @@ -79,6 +80,7 @@ export const OPEN_ROUTER_REASONING_BUDGET_MODELS = new Set([ "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.5", "anthropic/claude-sonnet-4.6", + "anthropic/claude-sonnet-5", "anthropic/claude-haiku-4.5", "google/gemini-2.5-pro-preview", "google/gemini-2.5-pro", diff --git a/packages/types/src/providers/vercel-ai-gateway.ts b/packages/types/src/providers/vercel-ai-gateway.ts index d23cd33cf1..38e959649e 100644 --- a/packages/types/src/providers/vercel-ai-gateway.ts +++ b/packages/types/src/providers/vercel-ai-gateway.ts @@ -16,6 +16,7 @@ export const VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS = new Set([ "anthropic/claude-fable-5", "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.6", + "anthropic/claude-sonnet-5", "openai/gpt-4.1", "openai/gpt-4.1-mini", "openai/gpt-4.1-nano", @@ -59,6 +60,7 @@ export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([ "anthropic/claude-fable-5", "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.6", + "anthropic/claude-sonnet-5", "google/gemini-1.5-flash", "google/gemini-1.5-pro", "google/gemini-2.0-flash", diff --git a/packages/types/src/providers/vertex.ts b/packages/types/src/providers/vertex.ts index bfc01f67d6..410233a458 100644 --- a/packages/types/src/providers/vertex.ts +++ b/packages/types/src/providers/vertex.ts @@ -355,6 +355,26 @@ export const vertexModels = { }, ], }, + "claude-sonnet-5": { + maxTokens: 8192, + contextWindow: 1_000_000, // 1M context window native (no beta header required) + supportsImages: true, + supportsPromptCache: true, + inputPrice: 2.0, // $2 per million input tokens (introductory pricing through Aug 31, 2026) + outputPrice: 10.0, // $10 per million output tokens (introductory pricing through Aug 31, 2026) + cacheWritesPrice: 2.5, // $2.50 per million tokens (introductory pricing through Aug 31, 2026) + cacheReadsPrice: 0.2, // $0.20 per million tokens (introductory pricing through Aug 31, 2026) + // Vertex routes through getAnthropicReasoning with no provider-side adaptive + // guard, so the registry shape must steer the helper into the effort/adaptive + // branch (Sonnet 5 rejects budget_tokens). Mirrors the Vertex Opus 4.8 entry. + supportsReasoningEffort: ["low", "medium", "high", "xhigh", "max"], + requiredReasoningEffort: true, + reasoningEffort: "high", + supportsTemperature: false, + supportsReasoningDisplay: true, + description: + "Claude Sonnet 5 is the best combination of speed and intelligence, optimized for coding, tool use, and agentic workflows.", + }, "claude-haiku-4-5@20251001": { maxTokens: 8192, contextWindow: 200_000, diff --git a/packages/types/src/providers/vscode-llm.ts b/packages/types/src/providers/vscode-llm.ts index efe0691913..5286b0ed28 100644 --- a/packages/types/src/providers/vscode-llm.ts +++ b/packages/types/src/providers/vscode-llm.ts @@ -2,189 +2,215 @@ import type { ModelInfo } from "../model.js" export type VscodeLlmModelId = keyof typeof vscodeLlmModels -export const vscodeLlmDefaultModelId: VscodeLlmModelId = "claude-3.5-sonnet" +export const vscodeLlmDefaultModelId: VscodeLlmModelId = "claude-sonnet-4.5" -// https://docs.cline.bot/provider-config/vscode-language-model-api +// Curated VS Code LM (GitHub Copilot) model catalog. +// The API exposes only `maxInputTokens`; the UI and condense gate read that. `contextWindow` is +// the advertised window, kept for rows where it diverges from the ceiling (e.g. claude-opus-4.8). export const vscodeLlmModels = { - "gpt-3.5-turbo": { - contextWindow: 12114, - supportsImages: false, + "claude-opus-4.8": { + contextWindow: 679560, + supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gpt-3.5-turbo", - version: "gpt-3.5-turbo-0613", - name: "GPT 3.5 Turbo", + family: "claude-opus-4.8", + version: "claude-opus-4.8", + name: "Claude Opus 4.8", supportsToolCalling: true, - maxInputTokens: 12114, + maxInputTokens: 197897, }, - "gpt-4o-mini": { - contextWindow: 12115, - supportsImages: false, + "claude-opus-4.7": { + contextWindow: 197897, + supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gpt-4o-mini", - version: "gpt-4o-mini-2024-07-18", - name: "GPT-4o mini", + family: "claude-opus-4.7", + version: "claude-opus-4.7", + name: "Claude Opus 4.7", supportsToolCalling: true, - maxInputTokens: 12115, + maxInputTokens: 197897, }, - "gpt-4": { - contextWindow: 28501, - supportsImages: false, + "claude-opus-4.6": { + contextWindow: 197897, + supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gpt-4", - version: "gpt-4-0613", - name: "GPT 4", + family: "claude-opus-4.6", + version: "claude-opus-4.6", + name: "Claude Opus 4.6", supportsToolCalling: true, - maxInputTokens: 28501, + maxInputTokens: 197897, }, - "gpt-4-0125-preview": { - contextWindow: 63826, - supportsImages: false, + "claude-opus-4.5": { + contextWindow: 167790, + supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gpt-4-turbo", - version: "gpt-4-0125-preview", - name: "GPT 4 Turbo", + family: "claude-opus-4.5", + version: "claude-opus-4.5", + name: "Claude Opus 4.5", supportsToolCalling: true, - maxInputTokens: 63826, + maxInputTokens: 167790, }, - "gpt-4o": { - contextWindow: 63827, + "claude-sonnet-4.6": { + contextWindow: 197896, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gpt-4o", - version: "gpt-4o-2024-11-20", - name: "GPT-4o", + family: "claude-sonnet-4.6", + version: "claude-sonnet-4.6", + name: "Claude Sonnet 4.6", supportsToolCalling: true, - maxInputTokens: 63827, + maxInputTokens: 197896, }, - o1: { - contextWindow: 19827, - supportsImages: false, + "claude-sonnet-4.5": { + contextWindow: 167790, + supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "o1-ga", - version: "o1-2024-12-17", - name: "o1 (Preview)", + family: "claude-sonnet-4.5", + version: "claude-sonnet-4.5", + name: "Claude Sonnet 4.5", supportsToolCalling: true, - maxInputTokens: 19827, + maxInputTokens: 167790, }, - "o3-mini": { - contextWindow: 63827, - supportsImages: false, + "claude-haiku-4.5": { + contextWindow: 135790, + supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "o3-mini", - version: "o3-mini-2025-01-31", - name: "o3-mini", + family: "claude-haiku-4.5", + version: "claude-haiku-4.5", + name: "Claude Haiku 4.5", supportsToolCalling: true, - maxInputTokens: 63827, + maxInputTokens: 135790, }, - "claude-3.5-sonnet": { - contextWindow: 81638, + "gpt-5.5": { + contextWindow: 268426, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "claude-3.5-sonnet", - version: "claude-3.5-sonnet", - name: "Claude 3.5 Sonnet", + family: "gpt-5.5", + version: "gpt-5.5", + name: "GPT-5.5", supportsToolCalling: true, - maxInputTokens: 81638, + maxInputTokens: 268426, }, - "claude-4-sonnet": { - contextWindow: 128000, + "gpt-5.4": { + contextWindow: 268424, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "claude-sonnet-4", - version: "claude-sonnet-4", - name: "Claude Sonnet 4", + family: "gpt-5.4", + version: "gpt-5.4", + name: "GPT-5.4", supportsToolCalling: true, - maxInputTokens: 111836, + maxInputTokens: 268424, }, - "gemini-2.0-flash-001": { - contextWindow: 127827, + "gpt-5.4-mini": { + contextWindow: 271790, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gemini-2.0-flash", - version: "gemini-2.0-flash-001", - name: "Gemini 2.0 Flash", - supportsToolCalling: false, - maxInputTokens: 127827, + family: "gpt-5.4-mini", + version: "gpt-5.4-mini", + name: "GPT-5.4 mini", + supportsToolCalling: true, + maxInputTokens: 271790, }, - "gemini-2.5-pro": { - contextWindow: 128000, + "gpt-5.3-codex": { + contextWindow: 271790, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gemini-2.5-pro", - version: "gemini-2.5-pro-preview-03-25", - name: "Gemini 2.5 Pro (Preview)", + family: "gpt-5.3-codex", + version: "gpt-5.3-codex", + name: "GPT-5.3-Codex", supportsToolCalling: true, - maxInputTokens: 108637, + maxInputTokens: 271790, }, - "o4-mini": { - contextWindow: 128000, + "gpt-5-mini": { + contextWindow: 127790, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + family: "gpt-5-mini", + version: "gpt-5-mini", + name: "GPT-5 mini", + supportsToolCalling: true, + maxInputTokens: 127790, + }, + "gpt-4o-mini": { + contextWindow: 12078, supportsImages: false, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "o4-mini", - version: "o4-mini-2025-04-16", - name: "o4-mini (Preview)", + family: "gpt-4o-mini", + version: "gpt-4o-mini-2024-07-18", + name: "GPT-4o mini", supportsToolCalling: true, - maxInputTokens: 111452, + maxInputTokens: 12078, }, - "gpt-4.1": { - contextWindow: 128000, + "gemini-3.1-pro-preview": { + contextWindow: 197897, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gpt-4.1", - version: "gpt-4.1-2025-04-14", - name: "GPT-4.1 (Preview)", + family: "gemini-3.1-pro-preview", + version: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro (Preview)", supportsToolCalling: true, - maxInputTokens: 111452, + maxInputTokens: 197897, }, - "gpt-5-mini": { - contextWindow: 128000, + "gemini-3.5-flash": { + contextWindow: 197895, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gpt-5-mini", - version: "gpt-5-mini", - name: "GPT-5 mini (Preview)", + family: "gemini-3.5-flash", + version: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + supportsToolCalling: true, + maxInputTokens: 197895, + }, + "gemini-3-flash": { + contextWindow: 108594, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + family: "gemini-3-flash", + version: "gemini-3-flash-preview", + name: "Gemini 3 Flash (Preview)", supportsToolCalling: true, - maxInputTokens: 108637, + maxInputTokens: 108594, }, - "gpt-5": { - contextWindow: 128000, + "gemini-2.5-pro": { + contextWindow: 108594, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - family: "gpt-5", - version: "gpt-5", - name: "GPT-5 (Preview)", + family: "gemini-2.5-pro", + version: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", supportsToolCalling: true, - maxInputTokens: 108637, + maxInputTokens: 108594, }, } as const satisfies Record< string, diff --git a/packages/types/src/rules.ts b/packages/types/src/rules.ts new file mode 100644 index 0000000000..0774f8f325 --- /dev/null +++ b/packages/types/src/rules.ts @@ -0,0 +1,34 @@ +export type RuleScope = "global" | "project" + +export type RuleKind = "generic" | "mode" + +export interface RuleMetadata { + id: string + name: string + scope: RuleScope + kind: RuleKind + modeSlug?: string + modeName?: string + filePath: string + relativePath: string + directoryPath: string + description?: string + isSymlink?: boolean +} + +export interface CreateRuleInput { + scope: RuleScope + kind: RuleKind + modeSlug?: string + fileName: string +} + +export interface DeleteRuleInput { + id?: string + scope: RuleScope + kind: RuleKind + modeSlug?: string + relativePath: string +} + +export type RuleLookupInput = DeleteRuleInput diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts index 7447dc772e..572302861b 100644 --- a/packages/types/src/task.ts +++ b/packages/types/src/task.ts @@ -90,7 +90,7 @@ export interface CreateTaskOptions { experiments?: Record initialTodos?: TodoItem[] /** Initial status for the task's history item (e.g., "active" for child tasks) */ - initialStatus?: "active" | "delegated" | "completed" + initialStatus?: "active" | "delegated" | "completed" | "interrupted" /** Whether to start the task loop immediately (default: true). * When false, the caller must invoke `task.start()` manually. */ startTask?: boolean diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 396fefd98a..0df94bdb6e 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -15,6 +15,7 @@ import type { McpServer } from "./mcp.js" import type { ModelRecord, RouterModels } from "./model.js" import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js" import type { SkillMetadata } from "./skills.js" +import type { RuleMetadata } from "./rules.js" import type { TelemetrySetting } from "./telemetry.js" import type { WorktreeIncludeStatus } from "./worktree.js" @@ -99,7 +100,9 @@ export interface ExtensionMessage { | "branchWorktreeIncludeResult" | "folderSelected" | "skills" + | "rules" | "fileContent" + | "rooHistoryImportProgress" text?: string /** For fileContent: { path, content, error? } */ fileContent?: { path: string; content: string | null; error?: string } @@ -177,7 +180,17 @@ export interface ExtensionMessage { list?: string[] // For dismissedUpsells tools?: SerializedCustomToolDefinition[] // For customToolsResult skills?: SkillMetadata[] // For skills response + rules?: RuleMetadata[] // For rules response modes?: { slug: string; name: string }[] // For modes response + rooHistoryImportProgress?: { + status: "starting" | "copying" | "finished" | "failed" + copiedFileCount: number + totalFileCount: number + importedTaskCount: number + totalTaskCount: number + currentTaskId?: string + currentFileName?: string + } aggregatedCosts?: { // For taskWithAggregatedCosts response totalCost: number @@ -322,6 +335,7 @@ export type ExtensionState = Pick< taskHistory: HistoryItem[] writeDelayMs: number + diffFuzzyThreshold: number enableCheckpoints: boolean checkpointTimeout: number // Timeout for checkpoint initialization in seconds (default: 15) @@ -539,6 +553,8 @@ export interface WebviewMessage { | "openCustomModesSettings" | "checkpointDiff" | "checkpointRestore" + | "completionCheckpointDiff" + | "completionCheckpointRestore" | "deleteMcpServer" | "codebaseIndexEnabled" | "telemetrySetting" @@ -622,6 +638,7 @@ export interface WebviewMessage { | "removeInstalledMarketplaceItem" | "marketplaceInstallResult" | "shareTaskSuccess" + | "importRooHistory" // Skills messages | "requestSkills" | "createSkill" @@ -629,6 +646,12 @@ export interface WebviewMessage { | "moveSkill" | "updateSkillModes" | "openSkillFile" + // Rules messages + | "requestRules" + | "createRule" + | "deleteRule" + | "openRuleFile" + | "openRulesDirectory" text?: string taskId?: string editedMessageContent?: string diff --git a/packages/vscode-shim/package.json b/packages/vscode-shim/package.json index e439ed240e..c366384104 100644 --- a/packages/vscode-shim/package.json +++ b/packages/vscode-shim/package.json @@ -13,8 +13,8 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@types/node": "24.2.1", - "vitest": "4.1.0" + "@types/node": "20.19.43", + "vitest": "4.1.9" }, "dependencies": {} } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4caf0e862a..b951c5c107 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,13 +8,14 @@ overrides: tar-fs: '>=3.1.1' esbuild: 0.28.1 rollup: 4.60.4 - vite: 8.0.16 - undici: '>=5.29.0' + vite: 8.1.0 + undici: 6.27.0 form-data: '>=4.0.4' bluebird: '>=3.7.2' glob: 11.1.0 - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 + csstype: 3.1.3 zod: 3.25.76 importers: @@ -23,7 +24,7 @@ importers: devDependencies: '@changesets/cli': specifier: 2.29.7 - version: 2.29.7(@types/node@24.2.1) + version: 2.29.7(@types/node@20.19.43) '@dotenvx/dotenvx': specifier: 1.66.0 version: 1.66.0 @@ -31,56 +32,53 @@ importers: specifier: workspace:^ version: link:packages/config-typescript '@types/node': - specifier: 24.2.1 - version: 24.2.1 + specifier: 20.19.43 + version: 20.19.43 '@vscode/vsce': - specifier: 3.3.2 - version: 3.3.2 + specifier: 3.9.2 + version: 3.9.2 esbuild: specifier: 0.28.1 version: 0.28.1 eslint: - specifier: 9.28.0 - version: 9.28.0(jiti@2.4.2) + specifier: 9.39.4 + version: 9.39.4(jiti@2.7.0) husky: specifier: 9.1.7 version: 9.1.7 knip: specifier: 5.60.2 - version: 5.60.2(@types/node@24.2.1)(typescript@5.8.3) + version: 5.60.2(@types/node@20.19.43)(typescript@5.9.3) lint-staged: specifier: 16.4.0 version: 16.4.0 mkdirp: specifier: 3.0.1 version: 3.0.1 - only-allow: - specifier: 1.2.1 - version: 1.2.1 ovsx: - specifier: 0.10.4 - version: 0.10.4 + specifier: 0.10.12 + version: 0.10.12 prettier: - specifier: 3.5.3 - version: 3.5.3 + specifier: 3.8.4 + version: 3.8.4 rimraf: specifier: 6.0.1 version: 6.0.1 tsx: - specifier: 4.19.4 - version: 4.19.4 + specifier: 4.22.4 + version: 4.22.4 turbo: - specifier: 2.9.14 - version: 2.9.14 + specifier: 2.10.0 + version: 2.10.0 typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 apps/cli: dependencies: '@inkjs/ui': specifier: ^2.0.0 - version: 2.0.0(ink@6.6.0(@types/react@18.3.23)(react@19.2.3)) + version: 2.0.0(ink@6.6.0(@types/react@18.3.31)(react@19.2.3)) '@roo-code/core': specifier: workspace:^ version: link:../../packages/core @@ -92,25 +90,22 @@ importers: version: link:../../packages/vscode-shim '@trpc/client': specifier: ^11.8.1 - version: 11.8.1(@trpc/server@11.8.1(typescript@5.8.3))(typescript@5.8.3) + version: 11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3) '@vscode/ripgrep': specifier: ^1.15.9 version: 1.17.0 commander: specifier: ^12.1.0 version: 12.1.0 - cross-spawn: - specifier: ^7.0.6 - version: 7.0.6 execa: specifier: ^9.5.2 - version: 9.6.0 + version: 9.6.1 fuzzysort: specifier: ^3.1.0 version: 3.1.0 ink: specifier: ^6.6.0 - version: 6.6.0(@types/react@18.3.23)(react@19.2.3) + version: 6.6.0(@types/react@18.3.31)(react@19.2.3) p-wait-for: specifier: ^5.0.2 version: 5.0.2 @@ -122,7 +117,7 @@ importers: version: 2.2.6 zustand: specifier: ^5.0.0 - version: 5.0.9(@types/react@18.3.23)(react@19.2.3) + version: 5.0.9(@types/react@18.3.31)(react@19.2.3) devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -131,35 +126,35 @@ importers: specifier: workspace:^ version: link:../../packages/config-typescript '@types/node': - specifier: 24.2.1 - version: 24.2.1 + specifier: 20.19.43 + version: 20.19.43 '@types/react': - specifier: 18.3.23 - version: 18.3.23 + specifier: 18.3.31 + version: 18.3.31 '@vitest/coverage-v8': - specifier: 4.1.0 - version: 4.1.0(vitest@4.1.0) + specifier: 4.1.9 + version: 4.1.9(vitest@4.1.9) ink-testing-library: specifier: 4.0.0 - version: 4.0.0(@types/react@18.3.23) + version: 4.0.0(@types/react@18.3.31) rimraf: specifier: 6.0.1 version: 6.0.1 tsup: specifier: 8.5.0 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.15)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.9.0) + version: 8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) tsx: - specifier: 4.19.4 - version: 4.19.4 + specifier: 4.22.4 + version: 4.22.4 vitest: - specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.2.1)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + specifier: 4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) apps/vscode-e2e: devDependencies: '@copilotkit/aimock': specifier: 1.15.1 - version: 1.15.1(vitest@4.1.0) + version: 1.15.1(vitest@4.1.9) '@roo-code/config-eslint': specifier: workspace:^ version: link:../../packages/config-eslint @@ -173,14 +168,11 @@ importers: specifier: 10.0.10 version: 10.0.10 '@types/node': - specifier: 20.19.41 - version: 20.19.41 + specifier: 20.19.43 + version: 20.19.43 '@types/vscode': specifier: 1.100.0 version: 1.100.0 - '@vscode/test-cli': - specifier: 0.0.11 - version: 0.0.11 '@vscode/test-electron': specifier: 2.5.2 version: 2.5.2 @@ -213,26 +205,20 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.41 - version: 20.19.41 + specifier: 20.19.43 + version: 20.19.43 vitest: - specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + specifier: 4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/cloud: dependencies: '@roo-code/types': specifier: workspace:^ version: link:../types - ioredis: - specifier: ^5.6.1 - version: 5.6.1 jwt-decode: specifier: ^4.0.0 version: 4.0.0 - p-wait-for: - specifier: ^5.0.2 - version: 5.0.2 zod: specifier: 3.25.76 version: 3.25.76 @@ -244,20 +230,20 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 24.2.1 - version: 24.2.1 + specifier: 20.19.43 + version: 20.19.43 '@types/vscode': specifier: 1.100.0 version: 1.100.0 '@vitest/coverage-v8': - specifier: 4.1.0 - version: 4.1.0(vitest@4.1.0) + specifier: 4.1.9 + version: 4.1.9(vitest@4.1.9) globals: specifier: 16.3.0 version: 16.3.0 vitest: - specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.2.1)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + specifier: 4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/config-eslint: devDependencies: @@ -268,29 +254,29 @@ importers: specifier: 15.3.2 version: 15.3.2 eslint: - specifier: 9.27.0 - version: 9.27.0(jiti@2.4.2) + specifier: 9.39.4 + version: 9.39.4(jiti@2.7.0) eslint-config-prettier: specifier: 10.1.8 - version: 10.1.8(eslint@9.27.0(jiti@2.4.2)) + version: 10.1.8(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-only-warn: - specifier: 1.1.0 - version: 1.1.0 + specifier: 1.2.1 + version: 1.2.1 eslint-plugin-react: specifier: 7.37.5 - version: 7.37.5(eslint@9.27.0(jiti@2.4.2)) + version: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: specifier: 5.2.0 - version: 5.2.0(eslint@9.27.0(jiti@2.4.2)) + version: 5.2.0(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-turbo: - specifier: 2.5.6 - version: 2.5.6(eslint@9.27.0(jiti@2.4.2))(turbo@2.9.14) + specifier: 2.10.0 + version: 2.10.0(eslint@9.39.4(jiti@2.7.0))(turbo@2.10.0) globals: specifier: 16.1.0 version: 16.1.0 typescript-eslint: specifier: 8.32.1 - version: 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) packages/config-typescript: {} @@ -304,13 +290,13 @@ importers: version: 0.28.1 execa: specifier: ^9.5.2 - version: 9.6.0 + version: 9.6.1 ignore: specifier: ^7.0.3 version: 7.0.5 openai: specifier: ^5.12.2 - version: 5.12.2(ws@8.20.1)(zod@3.25.76) + version: 5.23.2(ws@8.21.0)(zod@3.25.76) zod: specifier: 3.25.76 version: 3.25.76 @@ -322,14 +308,14 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 24.2.1 - version: 24.2.1 + specifier: 20.19.43 + version: 20.19.43 '@vitest/coverage-v8': - specifier: 4.1.0 - version: 4.1.0(vitest@4.1.0) + specifier: 4.1.9 + version: 4.1.9(vitest@4.1.9) vitest: - specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.2.1)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + specifier: 4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/ipc: dependencies: @@ -347,8 +333,8 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.41 - version: 20.19.41 + specifier: 20.19.43 + version: 20.19.43 '@types/node-ipc': specifier: 9.2.3 version: 9.2.3 @@ -372,23 +358,23 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 20.19.41 - version: 20.19.41 + specifier: 20.19.43 + version: 20.19.43 '@types/vscode': specifier: 1.100.0 version: 1.100.0 '@vitest/coverage-v8': - specifier: 4.1.0 - version: 4.1.0(vitest@4.1.0) + specifier: 4.1.9 + version: 4.1.9(vitest@4.1.9) vitest: - specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + specifier: 4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/types: dependencies: ai-sdk-provider-poe: specifier: 2.0.18 - version: 2.0.18(ai@6.0.77(zod@3.25.76))(zod@3.25.76) + version: 2.0.18(ai@6.0.209(zod@3.25.76))(zod@3.25.76) zod: specifier: 3.25.76 version: 3.25.76 @@ -400,20 +386,20 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 24.2.1 - version: 24.2.1 + specifier: 20.19.43 + version: 20.19.43 ajv: - specifier: 8.18.0 - version: 8.18.0 + specifier: 8.20.0 + version: 8.20.0 globals: specifier: 16.3.0 version: 16.3.0 tsup: specifier: 8.5.0 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.15)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.9.0) + version: 8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) vitest: - specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.2.1)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + specifier: 4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) zod-to-json-schema: specifier: 3.25.1 version: 3.25.1(zod@3.25.76) @@ -427,65 +413,41 @@ importers: specifier: workspace:^ version: link:../config-typescript '@types/node': - specifier: 24.2.1 - version: 24.2.1 + specifier: 20.19.43 + version: 20.19.43 vitest: - specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.2.1)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + specifier: 4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) src: dependencies: - '@ai-sdk/amazon-bedrock': - specifier: ^4.0.51 - version: 4.0.107(zod@3.25.76) - '@ai-sdk/baseten': - specifier: ^1.0.31 - version: 1.0.50(zod@3.25.76) - '@ai-sdk/deepseek': - specifier: ^2.0.18 - version: 2.0.18(zod@3.25.76) - '@ai-sdk/fireworks': - specifier: ^2.0.32 - version: 2.0.32(zod@3.25.76) - '@ai-sdk/google': - specifier: ^3.0.22 - version: 3.0.22(zod@3.25.76) - '@ai-sdk/google-vertex': - specifier: ^4.0.45 - version: 4.0.45(zod@3.25.76) - '@ai-sdk/mistral': - specifier: ^3.0.19 - version: 3.0.19(zod@3.25.76) - '@ai-sdk/xai': - specifier: ^3.0.48 - version: 3.0.48(zod@3.25.76) '@anthropic-ai/sdk': - specifier: ^0.37.0 - version: 0.37.0 + specifier: ^0.106.0 + version: 0.106.0(zod@3.25.76) '@anthropic-ai/vertex-sdk': - specifier: ^0.7.0 - version: 0.7.0 + specifier: ^0.18.0 + version: 0.18.0(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': specifier: ^3.922.0 - version: 3.922.0 + version: 3.1075.0 '@aws-sdk/credential-providers': specifier: ^3.922.0 - version: 3.922.0 + version: 3.1075.0 '@google/genai': specifier: ^1.29.1 - version: 1.29.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)) + version: 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)) '@lmstudio/sdk': specifier: ^1.1.1 - version: 1.2.0 + version: 1.5.0 '@mistralai/mistralai': specifier: ^1.9.18 - version: 1.9.18(zod@3.25.76) + version: 1.15.1 '@modelcontextprotocol/sdk': - specifier: 1.26.0 - version: 1.26.0(zod@3.25.76) + specifier: 1.29.0 + version: 1.29.0(zod@3.25.76) '@qdrant/js-client-rest': specifier: ^1.14.0 - version: 1.14.0(typescript@5.8.3) + version: 1.18.0(typescript@5.9.3) '@roo-code/cloud': specifier: workspace:^ version: link:../packages/cloud @@ -501,18 +463,15 @@ importers: '@roo-code/types': specifier: workspace:^ version: link:../packages/types - '@vscode/codicons': - specifier: ^0.0.36 - version: 0.0.36 ai-sdk-provider-poe: specifier: 2.0.18 - version: 2.0.18(ai@6.0.77(zod@3.25.76))(zod@3.25.76) + version: 2.0.18(ai@6.0.209(zod@3.25.76))(zod@3.25.76) async-mutex: specifier: ^0.5.0 version: 0.5.0 axios: specifier: ^1.12.0 - version: 1.16.0 + version: 1.18.1 chokidar: specifier: ^4.0.1 version: 4.0.3 @@ -551,10 +510,10 @@ importers: version: 4.0.3 i18next: specifier: ^25.0.0 - version: 25.2.1(typescript@5.8.3) + version: 25.2.1(typescript@5.9.3) ignore: specifier: ^7.0.3 - version: 7.0.4 + version: 7.0.5 isbinaryfile: specifier: ^5.0.7 version: 5.0.7 @@ -574,11 +533,11 @@ importers: specifier: ^5.1.2 version: 5.1.2 ollama: - specifier: ^0.5.17 - version: 0.5.17 + specifier: ^0.6.0 + version: 0.6.3 openai: specifier: ^5.12.2 - version: 5.12.2(ws@8.20.1)(zod@3.25.76) + version: 5.23.2(ws@8.21.0)(zod@3.25.76) os-name: specifier: ^6.0.0 version: 6.1.0 @@ -593,7 +552,7 @@ importers: version: 0.1.7 pdf-parse: specifier: ^1.1.1 - version: 1.1.1 + version: 1.1.4 pretty-bytes: specifier: ^7.0.0 version: 7.0.0 @@ -609,9 +568,6 @@ importers: safe-stable-stringify: specifier: ^2.5.0 version: 2.5.0 - sambanova-ai-provider: - specifier: ^1.2.2 - version: 1.2.2(zod@3.25.76) sanitize-filename: specifier: ^1.6.3 version: 1.6.3 @@ -635,13 +591,13 @@ importers: version: 5.0.0 tiktoken: specifier: ^1.0.21 - version: 1.0.21 + version: 1.0.22 tree-sitter-wasms: specifier: ^0.1.12 version: 0.1.12 undici: - specifier: '>=5.29.0' - version: 6.24.0 + specifier: 6.27.0 + version: 6.27.0 uuid: specifier: ^11.1.0 version: 11.1.1 @@ -656,17 +612,14 @@ importers: version: 9.2.0 yaml: specifier: ^2.8.0 - version: 2.8.3 - zhipu-ai-provider: - specifier: ^0.2.2 - version: 0.2.2(zod@3.25.76) + version: 2.9.0 zod: specifier: 3.25.76 version: 3.25.76 devDependencies: '@ai-sdk/openai-compatible': - specifier: 2.0.28 - version: 2.0.28(zod@3.25.76) + specifier: 2.0.51 + version: 2.0.51(zod@3.25.76) '@roo-code/build': specifier: workspace:^ version: link:../packages/build @@ -685,12 +638,9 @@ importers: '@types/lodash.debounce': specifier: 4.0.9 version: 4.0.9 - '@types/mocha': - specifier: 10.0.10 - version: 10.0.10 '@types/node': - specifier: 20.19.41 - version: 20.19.41 + specifier: 20.19.43 + version: 20.19.43 '@types/node-cache': specifier: 4.2.5 version: 4.2.5 @@ -710,89 +660,89 @@ importers: specifier: 1.100.0 version: 1.100.0 '@vitest/coverage-v8': - specifier: 4.1.0 - version: 4.1.0(vitest@4.1.0) + specifier: 4.1.9 + version: 4.1.9(vitest@4.1.9) '@vscode/vsce': - specifier: 3.3.2 - version: 3.3.2 + specifier: 3.9.2 + version: 3.9.2 ai: - specifier: 6.0.77 - version: 6.0.77(zod@3.25.76) + specifier: 6.0.209 + version: 6.0.209(zod@3.25.76) esbuild-wasm: specifier: 0.25.12 version: 0.25.12 execa: - specifier: 9.5.3 - version: 9.5.3 + specifier: 9.6.1 + version: 9.6.1 mkdirp: specifier: 3.0.1 version: 3.0.1 nock: - specifier: 14.0.10 - version: 14.0.10 + specifier: 14.0.15 + version: 14.0.15 ovsx: - specifier: 0.10.4 - version: 0.10.4 + specifier: 0.10.12 + version: 0.10.12 rimraf: specifier: 6.0.1 version: 6.0.1 vitest: - specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.3)) + specifier: 4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) webview-ui: dependencies: '@radix-ui/react-alert-dialog': specifier: ^1.1.6 - version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-checkbox': specifier: ^1.1.5 - version: 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-collapsible': specifier: ^1.1.3 - version: 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.1.6 - version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': specifier: ^2.1.5 - version: 2.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.2 version: 1.3.2(react@18.3.1) '@radix-ui/react-popover': specifier: ^1.1.6 - version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-portal': specifier: ^1.1.5 - version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-progress': specifier: ^1.1.2 - version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-radio-group': specifier: ^1.3.8 - version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': specifier: ^2.1.6 - version: 2.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.2.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-separator': specifier: ^1.1.2 - version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slider': specifier: ^1.2.3 - version: 1.3.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.3.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.2 - version: 1.2.2(@types/react@18.3.23)(react@18.3.1) + version: 1.2.3(@types/react@18.3.31)(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.1.8 - version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@roo-code/types': specifier: workspace:^ version: link:../packages/types '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.1.6(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + version: 4.1.6(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@tanstack/react-query': specifier: ^5.68.0 version: 5.76.1(react@18.3.1) @@ -807,7 +757,7 @@ importers: version: 0.7.2 axios: specifier: ^1.12.0 - version: 1.16.0 + version: 1.18.1 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -816,7 +766,7 @@ importers: version: 2.1.1 cmdk: specifier: ^1.0.0 - version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -837,22 +787,22 @@ importers: version: 2.3.6 i18next: specifier: ^25.0.0 - version: 25.2.1(typescript@5.8.3) + version: 25.2.1(typescript@5.9.3) katex: specifier: ^0.16.11 - version: 0.16.22 + version: 0.16.47 lru-cache: specifier: ^11.1.0 - version: 11.1.0 + version: 11.2.2 lucide-react: specifier: ^0.518.0 version: 0.518.0(react@18.3.1) mermaid: specifier: ^11.4.1 - version: 11.15.0 + version: 11.16.0 posthog-js: specifier: ^1.227.2 - version: 1.242.1 + version: 1.393.4 pretty-bytes: specifier: ^7.0.0 version: 7.0.0 @@ -867,28 +817,25 @@ importers: version: 18.3.1(react@18.3.1) react-i18next: specifier: ^15.4.1 - version: 15.5.1(i18next@25.2.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + version: 15.5.1(i18next@25.2.1(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) react-icons: specifier: ^5.5.0 version: 5.5.0(react@18.3.1) react-markdown: specifier: ^9.0.3 - version: 9.1.0(@types/react@18.3.23)(react@18.3.1) + version: 9.1.0(@types/react@18.3.31)(react@18.3.1) react-remark: specifier: ^2.1.0 version: 2.1.0(react@18.3.1) react-textarea-autosize: specifier: ^8.5.3 - version: 8.5.9(@types/react@18.3.23)(react@18.3.1) + version: 8.5.9(@types/react@18.3.31)(react@18.3.1) react-use: specifier: ^17.5.1 - version: 17.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 17.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-virtuoso: specifier: ^4.7.13 version: 4.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - rehype-highlight: - specifier: ^7.0.0 - version: 7.0.2 rehype-katex: specifier: ^7.0.1 version: 7.0.1 @@ -936,7 +883,7 @@ importers: version: 0.1.1 vscrui: specifier: ^0.2.2 - version: 0.2.2(@types/react@18.3.23)(react@18.3.1) + version: 0.2.2(@types/react@18.3.31)(react@18.3.1) zod: specifier: 3.25.76 version: 3.25.76 @@ -948,11 +895,11 @@ importers: specifier: workspace:^ version: link:../packages/config-typescript '@testing-library/jest-dom': - specifier: 6.6.3 - version: 6.6.3 + specifier: 6.9.1 + version: 6.9.1 '@testing-library/react': - specifier: 16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 16.3.2 + version: 16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/user-event': specifier: 14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) @@ -966,14 +913,14 @@ importers: specifier: 0.16.7 version: 0.16.7 '@types/node': - specifier: 20.19.41 - version: 20.19.41 + specifier: 20.19.43 + version: 20.19.43 '@types/react': - specifier: 18.3.23 - version: 18.3.23 + specifier: 18.3.31 + version: 18.3.31 '@types/react-dom': specifier: 18.3.7 - version: 18.3.7(@types/react@18.3.23) + version: 18.3.7(@types/react@18.3.31) '@types/shell-quote': specifier: 1.7.5 version: 1.7.5 @@ -985,13 +932,13 @@ importers: version: 1.57.5 '@vitejs/plugin-react': specifier: 5.2.0 - version: 5.2.0(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + version: 5.2.0(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/coverage-v8': - specifier: 4.1.0 - version: 4.1.0(vitest@4.1.0) + specifier: 4.1.9 + version: 4.1.9(vitest@4.1.9) '@vitest/ui': - specifier: 4.1.0 - version: 4.1.0(vitest@4.1.0) + specifier: 4.1.9 + version: 4.1.9(vitest@4.1.9) babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 @@ -999,103 +946,31 @@ importers: specifier: 26.1.0 version: 26.1.0 vite: - specifier: 8.0.16 - version: 8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0) + specifier: 8.1.0 + version: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) vitest: - specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + specifier: 4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} - '@ai-sdk/amazon-bedrock@4.0.107': - resolution: {integrity: sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/anthropic@3.0.38': - resolution: {integrity: sha512-9MchyPRPni0WzrFeIGNevZpQVfWxaS+MQFupIXYQo9VgHnuO1Vyrp9SBmjkkuoAdBs7GomsWqLZCcNMJAVbdFA==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - '@ai-sdk/anthropic@3.0.42': resolution: {integrity: sha512-snoLXB9DmvAmmngbPN/Io8IGzZ9zWpC208EgIIztYf1e1JhwuMkgKCYkL30vGhSen4PrBafu2+sO4G/17wu45A==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/anthropic@3.0.78': - resolution: {integrity: sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/baseten@1.0.50': - resolution: {integrity: sha512-/D7fa7wQeiOomj0q+Mqu0iJJ97YyMCMYUJQ5diGDlSG2wkWEKHEW/ivjXNN/FV5CifrybLk8bLMIdv+QNy425A==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/deepseek@2.0.18': - resolution: {integrity: sha512-AwtmFm7acnCsz3z82Yu5QKklSZz+cBwtxrc2hbw47tPF/38xr1zX3Vf/pP627EHwWkLV18UWivIxg0SHPP2w3A==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/fireworks@2.0.32': - resolution: {integrity: sha512-2qOEvocoRxUND086pjgliSBFKTyy6LUKbHZvXr++zlHm8ZbMT4dES78f5MHbOP9UVvRCPfTKmlPsUFUP/EVhJQ==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/gateway@3.0.39': - resolution: {integrity: sha512-SeCZBAdDNbWpVUXiYgOAqis22p5MEYfrjRw0hiBa5hM+7sDGYQpMinUjkM8kbPXMkY+AhKLrHleBl+SuqpzlgA==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/google-vertex@4.0.45': - resolution: {integrity: sha512-KkOsYd9DiyNatqxr/dSKzC6qrxwxOXZ63vu6Yfz2A7bPCsrwKzcN9SQRuhbVkBa1j0C78YiSDKuQvclfOk/0Kw==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/google@3.0.22': - resolution: {integrity: sha512-g1N5P/jfTiH4qwdv4WT3hkKzzAbITFz457NomtBfjP8Q3SCzdbU9oPK5ACBMG8RN5mc2QPL6DLtM3Hf5T8KPmw==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/mistral@3.0.19': - resolution: {integrity: sha512-yd0OJ3fm2YKdwxh1pd9m720sENVVcylAD+Bki8C80QqVpUxGNL1/C4N4JJGb56eCCWr6VU/3gHFe9PKui9n/Hg==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/openai-compatible@1.0.11': - resolution: {integrity: sha512-eRD6dZviy31KYz4YvxAR/c6UEYx3p4pCiWZeDdYdAHj0rn8xZlGVxtQRs1qynhz6IYGOo4aLBf9zVW5w0tI/Uw==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/openai-compatible@2.0.28': - resolution: {integrity: sha512-WzDnU0B13FMSSupDtm2lksFZvWGXnOfhG5S0HoPI0pkX5uVkr6N1UTATMyVaxLCG0MRkMhXCjkg4NXgEbb330Q==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/openai-compatible@2.0.37': - resolution: {integrity: sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw==} + '@ai-sdk/gateway@3.0.134': + resolution: {integrity: sha512-PfUkQp01EihEyNM6e+nexXmVANiY5KcHMui/MmaHsC4KVVQYh/MwPeOsD3bTPu63e581BKO+AwaPPNmD4x943A==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/openai-compatible@2.0.47': - resolution: {integrity: sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg==} + '@ai-sdk/openai-compatible@2.0.51': + resolution: {integrity: sha512-A6qfyaVs4lxmRxRux6U3ViOa8mMbsSd0OaHghpei2MpiBT6791J4zFH5MN7kaW1tLfQ246rSC2DVTMOavsycjQ==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 @@ -1106,34 +981,18 @@ packages: peerDependencies: zod: 3.25.76 - '@ai-sdk/provider-utils@3.0.5': - resolution: {integrity: sha512-HliwB/yzufw3iwczbFVE2Fiwf1XqROB/I6ng8EKUsPM5+2wnIa8f4VbljZcDx+grhFrPV+PnRZH7zBqi8WZM7Q==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - '@ai-sdk/provider-utils@4.0.14': resolution: {integrity: sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/provider-utils@4.0.21': - resolution: {integrity: sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - - '@ai-sdk/provider-utils@4.0.27': - resolution: {integrity: sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==} + '@ai-sdk/provider-utils@4.0.30': + resolution: {integrity: sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/provider@2.0.0': - resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} - engines: {node: '>=18'} - '@ai-sdk/provider@3.0.10': resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} engines: {node: '>=18'} @@ -1142,12 +1001,6 @@ packages: resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} engines: {node: '>=18'} - '@ai-sdk/xai@3.0.48': - resolution: {integrity: sha512-fUefjg7TwngHUtv0s+8j+GSPBiQRSETOPpICpaubz0CDNj0inBw/bZ6DKskQol7O20BIcoz0eKweedtC+F5iyQ==} - engines: {node: '>=18'} - peerDependencies: - zod: 3.25.76 - '@alcalzone/ansi-tokenize@0.2.3': resolution: {integrity: sha512-jsElTJ0sQ4wHRz+C45tfect76BwbTbgkgKByOzpCN9xG61N5V6u/glvg1CsNJhq2xJIFpKHSwG3D2wPPuEYOrQ==} engines: {node: '>=18'} @@ -1159,11 +1012,17 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/sdk@0.37.0': - resolution: {integrity: sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==} + '@anthropic-ai/sdk@0.106.0': + resolution: {integrity: sha512-ufwVvYNDBj2dzOGupBCTaNzBLxqcTnGOzI4z8Wouxlt+mT3J3HuOmatgCy1VmwCHOUueqZ41ERhm0O99OUcbWA==} + hasBin: true + peerDependencies: + zod: 3.25.76 + peerDependenciesMeta: + zod: + optional: true - '@anthropic-ai/vertex-sdk@0.7.0': - resolution: {integrity: sha512-zNm3hUXgYmYDTyveIxOyxbcnh5VXFkrLo4bSnG6LAfGzW7k3k2iCNDSVKtR9qZrK2BCid7JtVu7jsEKaZ/9dSw==} + '@anthropic-ai/vertex-sdk@0.18.0': + resolution: {integrity: sha512-o79bEmNwfRyq8Pv1X4P7zXfvOPmNBnzzg3TtchyWqy0mpxtTYQoEMJUHCJXQYEYdHyZKnt3bIGxbPCNiBcDY7Q==} '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -1185,137 +1044,107 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-bedrock-runtime@3.922.0': - resolution: {integrity: sha512-Hxud+cikuvVNa9Kb+cee3rX0svnY+kjLvGaJtnWUC6gFxYiW4dxeNh5MK8qfy7OUpyT/YTEe6vR78cvXV85BNw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/client-cognito-identity@3.922.0': - resolution: {integrity: sha512-C8JR4ZlVYuP0rMWnPkhmCtfLzfLgVu6vlRU9jTSoNeXgEdWzgKhACwrNIJxgHwnLuJGHzfe27OjfSiTwB0szcQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/client-sso@3.922.0': - resolution: {integrity: sha512-jdHs7uy7cSpiMvrxhYmqHyJxgK7hyqw4plG8OQ4YTBpq0SbfAxdoOuOkwJ1IVUUQho4otR1xYYjiX/8e8J8qwQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/core@3.922.0': - resolution: {integrity: sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-cognito-identity@3.922.0': - resolution: {integrity: sha512-heamj3qvnFLPHnCdD0Z5DF9lqpnTkCffmaeBULyVPOBacGelmtythu8tRZ7a4ktMskr9igPcv1qcxSYMXWSKaQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-bedrock-runtime@3.1075.0': + resolution: {integrity: sha512-LDGtNMOxnMz0dw9q+8z0f/X+Soj8OyiYg5zPcqToLh6H9/HHlazogFj7PXqFLOhnvhCqyAvKVAC1ZrL0RX418g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.922.0': - resolution: {integrity: sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-cognito-identity@3.1075.0': + resolution: {integrity: sha512-typFcdyFwIPt86QPKsO7xwcsYoEmNcDpoNqn0tqa4+Lss7niNKQzPe/765XNgAHO3I1ixiT1OKv8KD6M+CKw2w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.922.0': - resolution: {integrity: sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.974.23': + resolution: {integrity: sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.922.0': - resolution: {integrity: sha512-bVF+pI5UCLNkvbiZr/t2fgTtv84s8FCdOGAPxQiQcw5qOZywNuuCCY3wIIchmQr6GJr8YFkEp5LgDCac5EC5aQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-cognito-identity@3.972.48': + resolution: {integrity: sha512-dTSY4wCPx87Gd1peDcTop1li61f6KLAs3LSlq/omnCxpIOvMDpAQjylJ7ZDaZk6KA88+oZ0k/XuVSou1YuWI/w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.922.0': - resolution: {integrity: sha512-agCwaD6mBihToHkjycL8ObIS2XOnWypWZZWhJSoWyHwFrhEKz1zGvgylK9Dc711oUfU+zU6J8e0JPKNJMNb3BQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-env@3.972.49': + resolution: {integrity: sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.922.0': - resolution: {integrity: sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.972.51': + resolution: {integrity: sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.922.0': - resolution: {integrity: sha512-nbD3G3hShTYxLCkKMqLkLPtKwAAfxdY/k9jHtZmVBFXek2T6tQrqZHKxlAu+fd23Ga4/Aik7DLQQx1RA1a5ipg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.972.56': + resolution: {integrity: sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.922.0': - resolution: {integrity: sha512-wjGIhgMHGGQfQTdFaJphNOKyAL8wZs6znJdHADPVURmgR+EWLyN/0fDO1u7wx8xaLMZpbHIFWBEvf9TritR/cQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-login@3.972.55': + resolution: {integrity: sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-providers@3.922.0': - resolution: {integrity: sha512-+njl9vzuxj+wvogVFoSrFCJ4QOFVSUIVbL3V4fI7voRio+quZdBOzFqrMxeQ+GSedTLqjyRZT1O7ii7Ah8T4kQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.972.58': + resolution: {integrity: sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/eventstream-handler-node@3.922.0': - resolution: {integrity: sha512-DTKHeH1Bk17zSdoa5qXPGwCmZXuhQReqXOVW2/jIVX8NGVvnraH7WppGPlQxBjFtwSSwVTgzH2NVPgediQphNA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.972.49': + resolution: {integrity: sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-eventstream@3.922.0': - resolution: {integrity: sha512-qDHi3NxIZCOh10aKcDPz58qlt7xtTXTMHGv7N2uVWeb7gAhk/KGerHLukY6SFAID5FJ246Le14h2blQOHi9U2Q==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.972.55': + resolution: {integrity: sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.922.0': - resolution: {integrity: sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.55': + resolution: {integrity: sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.922.0': - resolution: {integrity: sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-providers@3.1075.0': + resolution: {integrity: sha512-2HoJ1IxwdzEryyUmZvl6dVKfgWBnS6NzSDl5hOqmbHns5Cy+wCQLDFU+HGVi+kTGZgSyMZIa0rH7fQvNf7v9jQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.922.0': - resolution: {integrity: sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/eventstream-handler-node@3.972.22': + resolution: {integrity: sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.922.0': - resolution: {integrity: sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-eventstream@3.972.18': + resolution: {integrity: sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-websocket@3.922.0': - resolution: {integrity: sha512-cBGDpMORc2lkpsSWJJkXes1lduPeUo58TIjMuC66TK134o8Wc+EsSutInxZXAT031BVWoyddhW9dBZJ1ybQQ2Q==} + '@aws-sdk/middleware-websocket@3.972.31': + resolution: {integrity: sha512-ps1rumU1LybSFHaW9dTDgkhCMJLVaedEY78kKSzUDDY+b9974/g6aiaYYA0U9WV0oL4CJCJrVWG+EZ/qr4or7g==} engines: {node: '>= 14.0.0'} - '@aws-sdk/nested-clients@3.922.0': - resolution: {integrity: sha512-uYvKCF1TGh/MuJ4TMqmUM0Csuao02HawcseG4LUDyxdUsd/EFuxalWq1Cx4fKZQ2K8F504efZBjctMAMNY+l7A==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/region-config-resolver@3.922.0': - resolution: {integrity: sha512-44Y/rNNwhngR2KHp6gkx//TOr56/hx6s4l+XLjOqH7EBCHL7XhnrT1y92L+DLiroVr1tCSmO8eHQwBv0Y2+mvw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/token-providers@3.922.0': - resolution: {integrity: sha512-/inmPnjZE0ZBE16zaCowAvouSx05FJ7p6BQYuzlJ8vxEU0sS0Hf8fvhuiRnN9V9eDUPIBY+/5EjbMWygXL4wlQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.997.23': + resolution: {integrity: sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==} + engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.922.0': - resolution: {integrity: sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==} - engines: {node: '>=18.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.35': + resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.973.8': - resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} + '@aws-sdk/token-providers@3.1074.0': + resolution: {integrity: sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.922.0': - resolution: {integrity: sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.1075.0': + resolution: {integrity: sha512-SsunyegDXq68TaN5Iut8ElErGIAA6DeuKPKd5/v0lpSmZBI7ZKOC5OALyi1MRHsl/cuO/zHkJL3vKnNHdGaI+Q==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-format-url@3.922.0': - resolution: {integrity: sha512-UYLWPvZEd6TYilNkrQrIeXh2bXZsY3ighYErSEjD24f3JQhg0XdXoR/QHIE8licHu2qFrTRM6yi9LH1GY6X0cg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-locate-window@3.804.0': - resolution: {integrity: sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-user-agent-browser@3.922.0': - resolution: {integrity: sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==} + '@aws-sdk/xml-builder@3.972.31': + resolution: {integrity: sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-user-agent-node@3.922.0': - resolution: {integrity: sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==} + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} engines: {node: '>=18.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - '@aws-sdk/xml-builder@3.921.0': - resolution: {integrity: sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==} - engines: {node: '>=18.0.0'} + '@azu/format-text@1.0.2': + resolution: {integrity: sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==} - '@aws/lambda-invoke-store@0.1.1': - resolution: {integrity: sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==} - engines: {node: '>=18.0.0'} + '@azu/style-format@1.0.1': + resolution: {integrity: sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==} '@azure/abort-controller@2.1.2': resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} @@ -1432,14 +1261,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.27.1': - resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.27.4': - resolution: {integrity: sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} @@ -1456,96 +1277,6 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@basetenlabs/performance-client-android-arm-eabi@0.0.10': - resolution: {integrity: sha512-gwDZ6GDJA0AAmQAHxt2vaCz0tYTaLjxJKZnoYt+0Eji4gy231JZZFAwvbAqNdQCrGEQ9lXnk7SNM1Apet4NlYg==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - - '@basetenlabs/performance-client-android-arm64@0.0.10': - resolution: {integrity: sha512-oGRB/6hH89majhsmoVmj1IAZv4C7F2aLeTSebevBelmdYO4CFkn5qewxLzU1pDkkmxVVk2k+TRpYa1Dt4B96qQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@basetenlabs/performance-client-darwin-arm64@0.0.10': - resolution: {integrity: sha512-QpBOUjeO05tWgFWkDw2RUQZa3BMplX5jNiBBTi5mH1lIL/m1sm2vkxoc0iorEESp1mMPstYFS/fr4ssBuO7wyA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@basetenlabs/performance-client-darwin-universal@0.0.10': - resolution: {integrity: sha512-CBM38GAhekjylrlf7jW/0WNyFAGnAMBCNHZxaPnAjjhDNzJh1tcrwhvtOs66XbAqCOjO/tkt5Pdu6mg2Ui2Pjw==} - engines: {node: '>= 10'} - os: [darwin] - - '@basetenlabs/performance-client-darwin-x64@0.0.10': - resolution: {integrity: sha512-R+NsA72Axclh1CUpmaWOCLTWCqXn5/tFMj2z9BnHVSRTelx/pYFlx6ZngVTB1HYp1n21m3upPXGo8CHF8R7Itw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@basetenlabs/performance-client-linux-arm-gnueabihf@0.0.10': - resolution: {integrity: sha512-96kEo0Eas4GVQdFkxIB1aAv6dy5Ga57j+RIg5l0Yiawv+AYIEmgk9BsGkqcwayp8Iiu6LN22Z+AUsGY2gstNrg==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@basetenlabs/performance-client-linux-arm-musleabihf@0.0.10': - resolution: {integrity: sha512-lzEHeu+/BWDl2q+QZcqCkg1rDGF4MeyM3HgYwX+07t+vGZoqtM2we9vEV68wXMpl6ToEHQr7ML2KHA1Gb6ogxg==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@basetenlabs/performance-client-linux-arm64-gnu@0.0.10': - resolution: {integrity: sha512-MnY2cIRY/cQOYERWIHhh5CoaS2wgmmXtGDVGSLYyZvjwizrXZvjkEz7Whv2jaQ21T5S56VER67RABjz2TItrHQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@basetenlabs/performance-client-linux-riscv64-gnu@0.0.10': - resolution: {integrity: sha512-2KUvdK4wuoZdIqNnJhx7cu6ybXCwtiwGAtlrEvhai3FOkUQ3wE2Xa+TQ33mNGSyFbw6wAvLawYtKVFmmw27gJw==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - - '@basetenlabs/performance-client-linux-x64-gnu@0.0.10': - resolution: {integrity: sha512-9jjQPjHLiVOGwUPlmhnBl7OmmO7hQ8WMt+v3mJuxkS5JTNDmVOngfmgGlbN9NjBhQMENjdcMUVOquVo7HeybGQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@basetenlabs/performance-client-linux-x64-musl@0.0.10': - resolution: {integrity: sha512-bjYB8FKcPvEa251Ep2Gm3tvywADL9eavVjZsikdf0AvJ1K5pT+vLLvJBU9ihBsTPWnbF4pJgxVjwS6UjVObsQA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@basetenlabs/performance-client-win32-arm64-msvc@0.0.10': - resolution: {integrity: sha512-Vxq5UXEmfh3C3hpwXdp3Daaf0dnLR9zFH2x8MJ1Hf/TcilmOP1clneewNpIv0e7MrnT56Z4pM6P3d8VFMZqBKg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@basetenlabs/performance-client-win32-ia32-msvc@0.0.10': - resolution: {integrity: sha512-KJrm7CgZdP/UDC5+tHtqE6w9XMfY5YUfMOxJfBZGSsLMqS2OGsakQsaF0a55k+58l29X5w/nAkjHrI1BcQO03w==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@basetenlabs/performance-client-win32-x64-msvc@0.0.10': - resolution: {integrity: sha512-M/mhvfTItUcUX+aeXRb5g5MbRlndfg6yelV7tSYfLU4YixMIe5yoGaAP3iDilpFJjcC99f+EU4l4+yLbPtpXig==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@basetenlabs/performance-client@0.0.10': - resolution: {integrity: sha512-H6bpd1JcDbuJsOS2dNft+CCGLzBqHJO/ST/4mMKhLAW641J6PpVJUw1szYsk/dTetdedbWxHpMkvFObOKeP8nw==} - engines: {node: '>= 10'} - - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -1662,14 +1393,20 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.0': + resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.0': + resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} '@emotion/is-prop-valid@1.2.2': resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} @@ -1836,8 +1573,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -1846,36 +1583,36 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.2': - resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.14.0': - resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.27.0': resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.28.0': - resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.1': - resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@fast-csv/format@4.3.5': @@ -1899,11 +1636,11 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} - '@google/genai@1.29.1': - resolution: {integrity: sha512-Buywpq0A6xf9cOdhiWCi5KUiDBbZkjCH5xbl+xxNQRItoYQgd31p0OKyn5cUnT0YNzC/pAmszqXoOc7kncqfFQ==} + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} engines: {node: '>=20.0.0'} peerDependencies: - '@modelcontextprotocol/sdk': ^1.20.1 + '@modelcontextprotocol/sdk': ^1.25.2 peerDependenciesMeta: '@modelcontextprotocol/sdk': optional: true @@ -1955,9 +1692,6 @@ packages: '@types/node': optional: true - '@ioredis/commands@1.3.0': - resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1966,10 +1700,6 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - '@jest/expect-utils@29.7.0': resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1992,9 +1722,6 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -2007,11 +1734,11 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} - '@lmstudio/lms-isomorphic@0.4.5': - resolution: {integrity: sha512-Or9KS1Iz3LC7D7WMe4zbqAqKOlDsVcrvMoQFBhmydzzxOg+eYBM5gtfgMMjcwjM0BuUVPhYOjTWEyfXpqfVJzg==} + '@lmstudio/lms-isomorphic@0.4.6': + resolution: {integrity: sha512-v0LIjXKnDe3Ff3XZO5eQjlVxTjleUHXaom14MV7QU9bvwaoo3l5p71+xJ3mmSaqZq370CQ6pTKCn1Bb7Jf+VwQ==} - '@lmstudio/sdk@1.2.0': - resolution: {integrity: sha512-Eoolmi1cSuGXmLYwtn6pD9eOwjMTb+bQ4iv+i/EYz/hCc+HtbfJamoKfyyw4FogRc03RHsXHe1X18voR40D+2g==} + '@lmstudio/sdk@1.5.0': + resolution: {integrity: sha512-fdY12x4hb14PEjYijh7YeCqT1ZDY5Ok6VR4l4+E/dI+F6NW8oB+P83Sxed5vqE4XgTzbgyPuSR2ZbMNxxF+6jA==} '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -2023,8 +1750,8 @@ packages: resolution: {integrity: sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==} engines: {node: '>=12'} - '@mermaid-js/parser@1.1.1': - resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} + '@mermaid-js/parser@1.2.0': + resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} '@microsoft/fast-element@1.14.0': resolution: {integrity: sha512-zXvuSOzvsu8zDTy9eby8ix8VqLop2rwKRgp++ZN2kTCsoB3+QJVoaGD2T/Cyso2ViZQFXNpiNCVKfnmxBvmWkQ==} @@ -2040,13 +1767,11 @@ packages: '@microsoft/fast-web-utilities@5.4.1': resolution: {integrity: sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==} - '@mistralai/mistralai@1.9.18': - resolution: {integrity: sha512-D/vNAGEvWMsg95tzgLTg7pPnW9leOPyH+nh1Os05NwxVPbUykoYgMAwOEX7J46msahWdvZ4NQQuxUXIUV2P6dg==} - peerDependencies: - zod: 3.25.76 + '@mistralai/mistralai@1.15.1': + resolution: {integrity: sha512-fb995eiz3r0KsBGtRjFV+/iLbX+UpfalxpF+YitT3R6ukrPD4PN+FGwwmYcRFhNAzVzDUtTVxQYnjQWEnwV5nw==} - '@modelcontextprotocol/sdk@1.26.0': - resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -2055,15 +1780,15 @@ packages: '@cfworker/json-schema': optional: true - '@mswjs/interceptors@0.39.6': - resolution: {integrity: sha512-bndDP83naYYkfayr/qhBHMhk0YGwS1iv6vaEGcr0SQbO0IZtbOPqjKjds/WcG+bJA+1T5vCx6kprKOzn5Bg+Vw==} + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -2195,80 +1920,143 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': + resolution: {integrity: sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.21.3': + resolution: {integrity: sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==} + cpu: [arm64] + os: [android] - '@oxc-resolver/binding-darwin-arm64@11.2.0': - resolution: {integrity: sha512-ruKLkS+Dm/YIJaUhzEB7zPI+jh3EXxu0QnNV8I7t9jf0lpD2VnltuyRbhrbJEkksklZj//xCMyFFsILGjiU2Mg==} + '@oxc-resolver/binding-darwin-arm64@11.21.3': + resolution: {integrity: sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==} cpu: [arm64] os: [darwin] - '@oxc-resolver/binding-darwin-x64@11.2.0': - resolution: {integrity: sha512-0zhgNUm5bYezdSFOg3FYhtVP83bAq7FTV/3suGQDl/43MixfQG7+bl+hlrP4mz6WlD2SUb2u9BomnJWl1uey9w==} + '@oxc-resolver/binding-darwin-x64@11.21.3': + resolution: {integrity: sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==} cpu: [x64] os: [darwin] - '@oxc-resolver/binding-freebsd-x64@11.2.0': - resolution: {integrity: sha512-SHOxfCcZV1axeIGfyeD1BkdLvfQgjmPy18tO0OUXGElcdScxD6MqU5rj/AVtiuBT+51GtFfOKlwl1+BdVwhD1A==} + '@oxc-resolver/binding-freebsd-x64@11.21.3': + resolution: {integrity: sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==} cpu: [x64] os: [freebsd] - '@oxc-resolver/binding-linux-arm-gnueabihf@11.2.0': - resolution: {integrity: sha512-mgEkYrJ+N90sgEDqEZ07zH+4I1D28WjqAhdzfW3aS2x2vynVpoY9jWfHuH8S62vZt3uATJrTKTRa8CjPWEsrdw==} + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': + resolution: {integrity: sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm64-gnu@11.2.0': - resolution: {integrity: sha512-BhEzNLjn4HjP8+Q18D3/jeIDBxW7OgoJYIjw2CaaysnYneoTlij8hPTKxHfyqq4IGM3fFs9TLR/k338M3zkQ7g==} + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': + resolution: {integrity: sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': + resolution: {integrity: sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-arm64-musl@11.2.0': - resolution: {integrity: sha512-yxbMYUgRmN2V8x8XoxmD/Qq6aG7YIW3ToMDILfmcfeeRRVieEJ3DOWBT0JSE+YgrOy79OyFDH/1lO8VnqLmDQQ==} + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': + resolution: {integrity: sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-riscv64-gnu@11.2.0': - resolution: {integrity: sha512-QG1UfgC2N2qhW1tOnDCgB/26vn1RCshR5sYPhMeaxO1gMQ3kEKbZ3QyBXxrG1IX5qsXYj5hPDJLDYNYUjRcOpg==} + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': + resolution: {integrity: sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': + resolution: {integrity: sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': + resolution: {integrity: sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==} cpu: [riscv64] os: [linux] - '@oxc-resolver/binding-linux-s390x-gnu@11.2.0': - resolution: {integrity: sha512-uqTDsQdi6mrkSV1gvwbuT8jf/WFl6qVDVjNlx7IPSaAByrNiJfPrhTmH8b+Do58Dylz7QIRZgxQ8CHIZSyBUdg==} + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': + resolution: {integrity: sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==} cpu: [s390x] os: [linux] - '@oxc-resolver/binding-linux-x64-gnu@11.2.0': - resolution: {integrity: sha512-GZdHXhJ7p6GaQg9MjRqLebwBf8BLvGIagccI6z5yMj4fV3LU4QuDfwSEERG+R6oQ/Su9672MBqWwncvKcKT68w==} + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': + resolution: {integrity: sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==} cpu: [x64] os: [linux] - '@oxc-resolver/binding-linux-x64-musl@11.2.0': - resolution: {integrity: sha512-YBAC3GOicYznReG2twE7oFPSeK9Z1f507z1EYWKg6HpGYRYRlJyszViu7PrhMT85r/MumDTs429zm+CNqpFWOA==} + '@oxc-resolver/binding-linux-x64-musl@11.21.3': + resolution: {integrity: sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==} cpu: [x64] os: [linux] - '@oxc-resolver/binding-wasm32-wasi@11.2.0': - resolution: {integrity: sha512-+qlIg45CPVPy+Jn3vqU1zkxA/AAv6e/2Ax/ImX8usZa8Tr2JmQn/93bmSOOOnr9fXRV9d0n4JyqYzSWxWPYDEw==} + '@oxc-resolver/binding-openharmony-arm64@11.21.3': + resolution: {integrity: sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.21.3': + resolution: {integrity: sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-resolver/binding-win32-arm64-msvc@11.2.0': - resolution: {integrity: sha512-AI4KIpS8Zf6vwfOPk0uQPSC0pQ1m5HU4hCbtrgL21JgJSlnJaeEu3/aoOBB45AXKiExBU9R+CDR7aSnW7uhc5A==} + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': + resolution: {integrity: sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==} cpu: [arm64] os: [win32] - '@oxc-resolver/binding-win32-x64-msvc@11.2.0': - resolution: {integrity: sha512-r19cQc7HaEJ76HFsMsbiKMTIV2YqFGSof8H5hB7e5Jkb/23Y8Isv1YrSzkDaGhcw02I/COsrPo+eEmjy35eFuA==} + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': + resolution: {integrity: sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==} cpu: [x64] os: [win32] '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@qdrant/js-client-rest@1.14.0': - resolution: {integrity: sha512-2sM2g17FSkN2sNCSeAfqxHRr+SPEVnUQLXBjVv/whm4YQ4JjZ53Jiy1iShk95G+xBf3hKBhJdj8itRnor03IYw==} - engines: {node: '>=18.0.0', pnpm: '>=8'} + '@posthog/core@1.38.0': + resolution: {integrity: sha512-QLrJh0hMVpEJXHNyiJR9YhvIO5tzLDedw4UHdvB+ub4fXHMtHCA8H44qgl11HWR3ajpjxtJ8hs3HyOGVF3Zc1w==} + + '@posthog/types@1.391.1': + resolution: {integrity: sha512-ASwd7Nf4pViqdYRYaNRyPYRVKWa1CcHUAUWR0XeQJLGdNnsWACBwe0sSieb/cHnKsRXjRwO/23KIY83lm/Ccpw==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + + '@qdrant/js-client-rest@1.18.0': + resolution: {integrity: sha512-/0dqX5uV9chC1DnYSnU4gNMrDqse/pt6hHg3Rqqpl5isH7xl1xSNvffjzBoxycDD79luWn7Ho6Rh/61sOs5DNw==} + engines: {node: '>=18.17.0', pnpm: '>=8'} peerDependencies: typescript: '>=4.7' @@ -2288,7 +2076,7 @@ packages: '@radix-ui/react-alert-dialog@1.1.13': resolution: {integrity: sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2301,7 +2089,7 @@ packages: '@radix-ui/react-arrow@1.1.6': resolution: {integrity: sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2314,7 +2102,7 @@ packages: '@radix-ui/react-checkbox@1.3.1': resolution: {integrity: sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2327,7 +2115,7 @@ packages: '@radix-ui/react-collapsible@1.1.10': resolution: {integrity: sha512-O2mcG3gZNkJ/Ena34HurA3llPOEA/M4dJtIRMa6y/cknRDC8XY5UZBInKTsUwW5cUue9A4k0wi1XU5fKBzKe1w==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2340,7 +2128,7 @@ packages: '@radix-ui/react-collection@1.1.6': resolution: {integrity: sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2353,7 +2141,7 @@ packages: '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2366,7 +2154,7 @@ packages: '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2375,7 +2163,7 @@ packages: '@radix-ui/react-context@1.1.2': resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2384,7 +2172,7 @@ packages: '@radix-ui/react-dialog@1.1.13': resolution: {integrity: sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2397,7 +2185,7 @@ packages: '@radix-ui/react-direction@1.1.1': resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2406,7 +2194,7 @@ packages: '@radix-ui/react-dismissable-layer@1.1.9': resolution: {integrity: sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2419,7 +2207,7 @@ packages: '@radix-ui/react-dropdown-menu@2.1.14': resolution: {integrity: sha512-lzuyNjoWOoaMFE/VC5FnAAYM16JmQA8ZmucOXtlhm2kKR5TSU95YLAueQ4JYuRmUJmBvSqXaVFGIfuukybwZJQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2432,7 +2220,7 @@ packages: '@radix-ui/react-focus-guards@1.1.2': resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2441,7 +2229,7 @@ packages: '@radix-ui/react-focus-scope@1.1.6': resolution: {integrity: sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2459,7 +2247,7 @@ packages: '@radix-ui/react-id@1.1.1': resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2468,7 +2256,7 @@ packages: '@radix-ui/react-menu@2.1.14': resolution: {integrity: sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2481,7 +2269,7 @@ packages: '@radix-ui/react-popover@1.1.13': resolution: {integrity: sha512-84uqQV3omKDR076izYgcha6gdpN8m3z6w/AeJ83MSBJYVG/AbOHdLjAgsPZkeC/kt+k64moXFCnio8BbqXszlw==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2494,7 +2282,7 @@ packages: '@radix-ui/react-popper@1.2.6': resolution: {integrity: sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2507,7 +2295,7 @@ packages: '@radix-ui/react-portal@1.1.8': resolution: {integrity: sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2520,7 +2308,7 @@ packages: '@radix-ui/react-presence@1.1.4': resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2533,7 +2321,7 @@ packages: '@radix-ui/react-presence@1.1.5': resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2546,7 +2334,7 @@ packages: '@radix-ui/react-primitive@2.1.2': resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2559,7 +2347,7 @@ packages: '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2572,7 +2360,7 @@ packages: '@radix-ui/react-progress@1.1.6': resolution: {integrity: sha512-QzN9a36nKk2eZKMf9EBCia35x3TT+SOgZuzQBVIHyRrmYYi73VYBRK3zKwdJ6az/F5IZ6QlacGJBg7zfB85liA==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2585,7 +2373,7 @@ packages: '@radix-ui/react-radio-group@1.3.8': resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2598,7 +2386,7 @@ packages: '@radix-ui/react-roving-focus@1.1.11': resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2611,7 +2399,7 @@ packages: '@radix-ui/react-roving-focus@1.1.9': resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2624,7 +2412,7 @@ packages: '@radix-ui/react-select@2.2.4': resolution: {integrity: sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2637,7 +2425,7 @@ packages: '@radix-ui/react-separator@1.1.6': resolution: {integrity: sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2650,7 +2438,7 @@ packages: '@radix-ui/react-slider@1.3.4': resolution: {integrity: sha512-Cp6hEmQtRJFci285vkdIJ+HCDLTRDk+25VhFwa1fcubywjMUE3PynBgtN5RLudOgSCYMlT4jizCXdmV+8J7Y2w==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2663,7 +2451,7 @@ packages: '@radix-ui/react-slot@1.2.2': resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2672,7 +2460,7 @@ packages: '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2681,7 +2469,7 @@ packages: '@radix-ui/react-tooltip@1.2.6': resolution: {integrity: sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2694,7 +2482,7 @@ packages: '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2703,7 +2491,7 @@ packages: '@radix-ui/react-use-controllable-state@1.2.2': resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2712,7 +2500,7 @@ packages: '@radix-ui/react-use-effect-event@0.0.2': resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2721,7 +2509,7 @@ packages: '@radix-ui/react-use-escape-keydown@1.1.1': resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2730,7 +2518,7 @@ packages: '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2739,7 +2527,7 @@ packages: '@radix-ui/react-use-previous@1.1.1': resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2748,7 +2536,7 @@ packages: '@radix-ui/react-use-rect@1.1.1': resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2757,7 +2545,7 @@ packages: '@radix-ui/react-use-size@1.1.1': resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2766,7 +2554,7 @@ packages: '@radix-ui/react-visually-hidden@1.2.2': resolution: {integrity: sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -2779,91 +2567,91 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3002,16 +2790,58 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@sevinf/maybe@0.5.0': - resolution: {integrity: sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg==} + '@secretlint/config-creator@10.2.2': + resolution: {integrity: sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==} + engines: {node: '>=20.0.0'} - '@shikijs/core@3.4.1': - resolution: {integrity: sha512-GCqSd3KXRTKX1sViP7fIyyyf6do2QVg+fTd4IT00ucYCVSKiSN8HbFbfyjGsoZePNKWcQqXe4U4rrz2IVldG5A==} + '@secretlint/config-loader@10.2.2': + resolution: {integrity: sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==} + engines: {node: '>=20.0.0'} - '@shikijs/engine-javascript@3.4.1': - resolution: {integrity: sha512-oGvRqN3Bsk+cGzmCb/5Kt/LfD7uyA8vCUUawyqmLti/AYNV7++zIZFEW8JwW5PrpPNWWx9RcZ/chnYLedzlVIQ==} + '@secretlint/core@10.2.2': + resolution: {integrity: sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==} + engines: {node: '>=20.0.0'} - '@shikijs/engine-oniguruma@3.4.1': + '@secretlint/formatter@10.2.2': + resolution: {integrity: sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==} + engines: {node: '>=20.0.0'} + + '@secretlint/node@10.2.2': + resolution: {integrity: sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==} + engines: {node: '>=20.0.0'} + + '@secretlint/profiler@10.2.2': + resolution: {integrity: sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==} + + '@secretlint/resolver@10.2.2': + resolution: {integrity: sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==} + + '@secretlint/secretlint-formatter-sarif@10.2.2': + resolution: {integrity: sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==} + + '@secretlint/secretlint-rule-no-dotenv@10.2.2': + resolution: {integrity: sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==} + engines: {node: '>=20.0.0'} + + '@secretlint/secretlint-rule-preset-recommend@10.2.2': + resolution: {integrity: sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==} + engines: {node: '>=20.0.0'} + + '@secretlint/source-creator@10.2.2': + resolution: {integrity: sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==} + engines: {node: '>=20.0.0'} + + '@secretlint/types@10.2.2': + resolution: {integrity: sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==} + engines: {node: '>=20.0.0'} + + '@shikijs/core@3.4.1': + resolution: {integrity: sha512-GCqSd3KXRTKX1sViP7fIyyyf6do2QVg+fTd4IT00ucYCVSKiSN8HbFbfyjGsoZePNKWcQqXe4U4rrz2IVldG5A==} + + '@shikijs/engine-javascript@3.4.1': + resolution: {integrity: sha512-oGvRqN3Bsk+cGzmCb/5Kt/LfD7uyA8vCUUawyqmLti/AYNV7++zIZFEW8JwW5PrpPNWWx9RcZ/chnYLedzlVIQ==} + + '@shikijs/engine-oniguruma@3.4.1': resolution: {integrity: sha512-p8I5KWgEDUcXRif9JjJUZtNeqCyxZ8xcslecDJMigsqSZfokwqQIsH4aGpdjzmDf8LIWvT+C3TCxnJQVaPmCbQ==} '@shikijs/langs@3.4.1': @@ -3035,213 +2865,52 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@smithy/abort-controller@4.2.4': - resolution: {integrity: sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ==} - engines: {node: '>=18.0.0'} - - '@smithy/config-resolver@4.4.2': - resolution: {integrity: sha512-4Jys0ni2tB2VZzgslbEgszZyMdTkPOFGA8g+So/NjR8oy6Qwaq4eSwsrRI+NMtb0Dq4kqCzGUu/nGUx7OM/xfw==} - engines: {node: '>=18.0.0'} - - '@smithy/core@3.17.2': - resolution: {integrity: sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ==} - engines: {node: '>=18.0.0'} - - '@smithy/core@3.24.3': - resolution: {integrity: sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.2.4': - resolution: {integrity: sha512-YVNMjhdz2pVto5bRdux7GMs0x1m0Afz3OcQy/4Yf9DH4fWOtroGH7uLvs7ZmDyoBJzLdegtIPpXrpJOZWvUXdw==} + '@smithy/core@3.26.0': + resolution: {integrity: sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-codec@4.3.3': - resolution: {integrity: sha512-sXa/zJrKDY2XtuS6bcxUpnSasItoawGi+ru54fNUzvQtJH51+gF0fJ+3Q0fm5e8zQWgibFhkPzRh1K4vxmI78A==} + '@smithy/credential-provider-imds@4.4.2': + resolution: {integrity: sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-browser@4.2.4': - resolution: {integrity: sha512-d5T7ZS3J/r8P/PDjgmCcutmNxnSRvPH1U6iHeXjzI50sMr78GLmFcrczLw33Ap92oEKqa4CLrkAPeSSOqvGdUA==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-config-resolver@4.3.4': - resolution: {integrity: sha512-lxfDT0UuSc1HqltOGsTEAlZ6H29gpfDSdEPTapD5G63RbnYToZ+ezjzdonCCH90j5tRRCw3aLXVbiZaBW3VRVg==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-node@4.2.4': - resolution: {integrity: sha512-TPhiGByWnYyzcpU/K3pO5V7QgtXYpE0NaJPEZBCa1Y5jlw5SjqzMSbFiLb+ZkJhqoQc0ImGyVINqnq1ze0ZRcQ==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-universal@4.2.4': - resolution: {integrity: sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.3.5': - resolution: {integrity: sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-node@4.2.4': - resolution: {integrity: sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw==} - engines: {node: '>=18.0.0'} - - '@smithy/invalid-dependency@4.2.4': - resolution: {integrity: sha512-z6aDLGiHzsMhbS2MjetlIWopWz//K+mCoPXjW6aLr0mypF+Y7qdEh5TyJ20Onf9FbWHiWl4eC+rITdizpnXqOw==} + '@smithy/fetch-http-handler@5.5.2': + resolution: {integrity: sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} - '@smithy/is-array-buffer@4.2.0': - resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-content-length@4.2.4': - resolution: {integrity: sha512-hJRZuFS9UsElX4DJSJfoX4M1qXRH+VFiLMUnhsWvtOOUWRNvvOfDaUSdlNbjwv1IkpVjj/Rd/O59Jl3nhAcxow==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-endpoint@4.3.6': - resolution: {integrity: sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-retry@4.4.6': - resolution: {integrity: sha512-OhLx131znrEDxZPAvH/OYufR9d1nB2CQADyYFN4C3V/NQS7Mg4V6uvxHC/Dr96ZQW8IlHJTJ+vAhKt6oxWRndA==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-serde@4.2.4': - resolution: {integrity: sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-stack@4.2.4': - resolution: {integrity: sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA==} - engines: {node: '>=18.0.0'} - - '@smithy/node-config-provider@4.3.4': - resolution: {integrity: sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.4.4': - resolution: {integrity: sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA==} - engines: {node: '>=18.0.0'} - - '@smithy/property-provider@4.2.4': - resolution: {integrity: sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w==} - engines: {node: '>=18.0.0'} - - '@smithy/protocol-http@5.3.4': - resolution: {integrity: sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-builder@4.2.4': - resolution: {integrity: sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig==} + '@smithy/node-http-handler@4.8.2': + resolution: {integrity: sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==} engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.2.4': - resolution: {integrity: sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ==} + '@smithy/signature-v4@5.5.2': + resolution: {integrity: sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==} engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.2.4': - resolution: {integrity: sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng==} - engines: {node: '>=18.0.0'} - - '@smithy/shared-ini-file-loader@4.3.4': - resolution: {integrity: sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.3.4': - resolution: {integrity: sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A==} - engines: {node: '>=18.0.0'} - - '@smithy/smithy-client@4.9.2': - resolution: {integrity: sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.14.2': - resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.8.1': - resolution: {integrity: sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA==} - engines: {node: '>=18.0.0'} - - '@smithy/url-parser@4.2.4': - resolution: {integrity: sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-base64@4.3.0': - resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-browser@4.2.0': - resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-node@4.2.1': - resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} engines: {node: '>=18.0.0'} '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} - '@smithy/util-buffer-from@4.2.0': - resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} - engines: {node: '>=18.0.0'} - - '@smithy/util-config-provider@4.2.0': - resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-browser@4.3.5': - resolution: {integrity: sha512-GwaGjv/QLuL/QHQaqhf/maM7+MnRFQQs7Bsl6FlaeK6lm6U7mV5AAnVabw68cIoMl5FQFyKK62u7RWRzWL25OQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-node@4.2.8': - resolution: {integrity: sha512-gIoTf9V/nFSIZ0TtgDNLd+Ws59AJvijmMDYrOozoMHPJaG9cMRdqNO50jZTlbM6ydzQYY8L/mQ4tKSw/TB+s6g==} - engines: {node: '>=18.0.0'} - - '@smithy/util-endpoints@3.2.4': - resolution: {integrity: sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-hex-encoding@4.2.0': - resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-middleware@4.2.4': - resolution: {integrity: sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-retry@4.2.4': - resolution: {integrity: sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-stream@4.5.5': - resolution: {integrity: sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w==} - engines: {node: '>=18.0.0'} - - '@smithy/util-uri-escape@4.2.0': - resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} - engines: {node: '>=18.0.0'} - '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@smithy/util-utf8@4.2.0': - resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-utf8@4.3.3': - resolution: {integrity: sha512-c1QpRBn3aMsoqE64dd4Imgjy8Pynfw+eR7GkjElquxUFSnezwYVaOFm8JcYa+Bo/5ssbEyPKcT3+4bmrWYh6eQ==} - engines: {node: '>=18.0.0'} - - '@smithy/uuid@1.1.0': - resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} - engines: {node: '>=18.0.0'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -3334,7 +3003,7 @@ packages: '@tailwindcss/vite@4.1.6': resolution: {integrity: sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==} peerDependencies: - vite: 8.0.16 + vite: 8.1.0 '@tanstack/query-core@5.76.0': resolution: {integrity: sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg==} @@ -3348,16 +3017,16 @@ packages: resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.6.3': - resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.0': - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 - '@types/react': 18.3.23 + '@types/react': 18.3.31 '@types/react-dom': 18.3.7 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -3373,6 +3042,21 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@textlint/ast-node-types@15.7.1': + resolution: {integrity: sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==} + + '@textlint/linter-formatter@15.7.1': + resolution: {integrity: sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==} + + '@textlint/module-interop@15.7.1': + resolution: {integrity: sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==} + + '@textlint/resolver@15.7.1': + resolution: {integrity: sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==} + + '@textlint/types@15.7.1': + resolution: {integrity: sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==} + '@trpc/client@11.8.1': resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==} peerDependencies: @@ -3384,38 +3068,38 @@ packages: peerDependencies: typescript: '>=5.7.2' - '@turbo/darwin-64@2.9.14': - resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==} + '@turbo/darwin-64@2.10.0': + resolution: {integrity: sha512-EwvHThXzpY0KGd1/NAmuewI5D+aVa3Rl/OlxE36yfjUKb/+ySrfJrSlEFt8aD1OXwnnaHnQnPKHFndor0Zxlsg==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.14': - resolution: {integrity: sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw==} + '@turbo/darwin-arm64@2.10.0': + resolution: {integrity: sha512-9d2fTyyG0lf5Wq1bwJA9qUaeecViMkLcdctWaMMmCkxZ/JqypmqOwK3W6vmejeKVgkr06gSoiX8bD+xN5Jpxcg==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.9.14': - resolution: {integrity: sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA==} + '@turbo/linux-64@2.10.0': + resolution: {integrity: sha512-sZBtjMuufitanjzi6UssoUpJMnnPlLMcdcJj3m3ptNsSq31Xh7MnjhwA5nWvLDTfEFg8GPcbYFXMo8vSdKRfqQ==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.14': - resolution: {integrity: sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g==} + '@turbo/linux-arm64@2.10.0': + resolution: {integrity: sha512-vkq/Z8R+1DQ+kifWFa810IjRy2NNBVvha3cg9sWA3nFh6nnGrHSMnnJKrzH7c/No9kq4Jb55Ru44YKsCSBgrKg==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.14': - resolution: {integrity: sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A==} + '@turbo/windows-64@2.10.0': + resolution: {integrity: sha512-CRUEguLWxFQHptYZS7HjPhNhAFawfea07iR+xAQ5e4klgLrPCMdexBkXwSCwOxqTFknJ7RZFN3gOaADsw+Gttg==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.14': - resolution: {integrity: sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g==} + '@turbo/windows-arm64@2.10.0': + resolution: {integrity: sha512-dVHGaf9F8twzgibcBqKoADT/LLqf9++jDb+hq/LPWWaOmRpp4M+/pVOm7vy4z9D++xg8eaxWLT0+wQxFwhYu9A==} cpu: [arm64] os: [win32] - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -3567,8 +3251,8 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} - '@types/js-cookie@2.2.7': - resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3598,9 +3282,6 @@ packages: resolution: {integrity: sha512-faK2Owokboz53g8ooq2dw3iDJ6/HMTCIa2RvMte5WMTiABy+wA558K+iuyRtlR67Un5q9gEKysSDtqZYbSa0Pg==} deprecated: This is a stub types definition. node-cache provides its own type definitions, so you do not need this installed. - '@types/node-fetch@2.6.12': - resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} - '@types/node-ipc@9.2.3': resolution: {integrity: sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==} @@ -3610,14 +3291,11 @@ packages: '@types/node@14.18.63': resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} - '@types/node@18.19.100': - resolution: {integrity: sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA==} - - '@types/node@20.19.41': - resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} - '@types/node@24.2.1': - resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==} + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} @@ -3631,14 +3309,20 @@ packages: '@types/react-dom@18.3.7': resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@types/react@18.3.23': - resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} '@types/retry@0.12.5': resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} + '@types/sarif@2.1.7': + resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} + '@types/semver-compare@1.0.3': resolution: {integrity: sha512-mVZkB2QjXmZhh+MrtwMlJ8BqUnmbiSkpd88uOWskfwB8yitBT0tBRAKt+41VRgZD9zr9Sc+Xs02qGgvzd1Rq/Q==} @@ -3734,58 +3418,58 @@ packages: '@upsetjs/venn.js@2.0.0': resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} - '@vercel/oidc@3.1.0': - resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} '@vitejs/plugin-react@5.2.0': resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: 8.0.16 + vite: 8.1.0 - '@vitest/coverage-v8@4.1.0': - resolution: {integrity: sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==} + '@vitest/coverage-v8@4.1.9': + resolution: {integrity: sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==} peerDependencies: - '@vitest/browser': 4.1.0 - vitest: 4.1.0 + '@vitest/browser': 4.1.9 + vitest: 4.1.9 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.0': - resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} - '@vitest/mocker@4.1.0': - resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} peerDependencies: msw: ^2.4.9 - vite: 8.0.16 + vite: 8.1.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.1.0': - resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} - '@vitest/runner@4.1.0': - resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} - '@vitest/snapshot@4.1.0': - resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} - '@vitest/spy@4.1.0': - resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} - '@vitest/ui@4.1.0': - resolution: {integrity: sha512-sTSDtVM1GOevRGsCNhp1mBUHKo9Qlc55+HCreFT4fe99AHxl1QQNXSL3uj4Pkjh5yEuWZIx8E2tVC94nnBZECQ==} + '@vitest/ui@4.1.9': + resolution: {integrity: sha512-U/cRvtqfEPj27FI1n9cyUvi4vXXdcLhjJiI+InYKdk8hP4VrS6RXOjGL7rfFaeBc37iRKANsR6eEzIoC7lmgBQ==} peerDependencies: - vitest: 4.1.0 + vitest: 4.1.9 - '@vitest/utils@4.1.0': - resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} '@vscode/codicons@0.0.36': resolution: {integrity: sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ==} @@ -3793,11 +3477,6 @@ packages: '@vscode/ripgrep@1.17.0': resolution: {integrity: sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g==} - '@vscode/test-cli@0.0.11': - resolution: {integrity: sha512-qO332yvzFqGhBMJrp6TdwbIydiHgCtxXc2Nl6M58mbH/Z+0CyLR76Jzv4YWPEthhrARprzCRJUqzFvTHFhTj7Q==} - engines: {node: '>=18'} - hasBin: true - '@vscode/test-electron@2.5.2': resolution: {integrity: sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==} engines: {node: '>=16'} @@ -3850,8 +3529,8 @@ packages: '@vscode/vsce-sign@2.0.5': resolution: {integrity: sha512-GfYWrsT/vypTMDMgWDm75iDmAOMe7F71sZECJ+Ws6/xyIfmB3ELVnVN+LwMFAvmXY+e6eWhR2EzNGF/zAhWY3Q==} - '@vscode/vsce@3.3.2': - resolution: {integrity: sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==} + '@vscode/vsce@3.9.2': + resolution: {integrity: sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==} engines: {node: '>= 20'} hasBin: true @@ -3868,10 +3547,6 @@ packages: '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -3886,21 +3561,21 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} - agentkeepalive@4.6.0: - resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} - engines: {node: '>= 8.0.0'} - ai-sdk-provider-poe@2.0.18: resolution: {integrity: sha512-uzFR5Zq+PD6LfrqpYAr4dt2KmfOfS9a0Wh8QJsOaGI/vOByhW02P/0mj0NtOnOWF1RIUfBkQJo647XjrgnFjjg==} peerDependencies: ai: '>=6.0.0' - ai@6.0.77: - resolution: {integrity: sha512-tyyhrRpCRFVlivdNIFLK8cexSBB2jwTqO0z1qJQagk+UxZ+MW8h5V8xsvvb+xdKDY482Y8KAm0mr7TDnPKvvlw==} + ai@6.0.209: + resolution: {integrity: sha512-NE7uErmxOe+i77BPT1+AhW4wr7xzilbQLsKVmvGEyil5AjaF6C/m92hlPI4Xwtnv5n77D/29bizwcR27/S/gSQ==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 @@ -3913,20 +3588,16 @@ packages: ajv: optional: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - ansi-escapes@7.2.0: - resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} - engines: {node: '>=18'} - ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -3939,10 +3610,6 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -3963,10 +3630,6 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} engines: {node: '>= 6'} @@ -4035,6 +3698,10 @@ packages: ast-v8-to-istanbul@1.0.4: resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -4056,11 +3723,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - aws4fetch@1.0.20: - resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} - - axios@1.16.0: - resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} azure-devops-node-api@12.5.0: resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} @@ -4128,13 +3792,13 @@ packages: bignumber.js@9.3.0: resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==} - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - binary@0.3.0: resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + binaryextensions@6.11.0: + resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==} + engines: {node: '>=4'} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -4152,8 +3816,11 @@ packages: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - bowser@2.11.0: - resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + boundary@2.0.0: + resolution: {integrity: sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} brace-expansion@1.1.14: resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} @@ -4208,11 +3875,6 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - c8@9.1.0: - resolution: {integrity: sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==} - engines: {node: '>=14.14.0'} - hasBin: true - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -4253,22 +3915,10 @@ packages: chainsaw@0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@3.0.0: - resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} - engines: {node: '>=8'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -4304,10 +3954,6 @@ packages: resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} engines: {node: '>=18.17'} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -4346,10 +3992,6 @@ packages: resolution: {integrity: sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==} engines: {node: '>=18.20'} - cli-truncate@5.1.1: - resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} - engines: {node: '>=20'} - cli-truncate@5.2.0: resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} @@ -4373,10 +4015,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - cmdk@1.1.1: resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} peerDependencies: @@ -4391,16 +4029,10 @@ packages: resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -4493,8 +4125,8 @@ packages: resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} hasBin: true - core-js@3.42.0: - resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -4563,8 +4195,8 @@ packages: peerDependencies: cytoscape: ^3.2.0 - cytoscape@3.33.4: - resolution: {integrity: sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==} + cytoscape@3.34.0: + resolution: {integrity: sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==} engines: {node: '>=0.10'} d3-array@2.12.1: @@ -4732,9 +4364,6 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - dayjs@1.11.21: resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} @@ -4742,23 +4371,6 @@ packages: resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} engines: {node: '>=18'} - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -4824,10 +4436,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -4840,10 +4448,6 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} - engines: {node: '>=8'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -4892,8 +4496,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.7: - resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==} + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -4902,10 +4506,6 @@ packages: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} - dotenv@16.4.5: - resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} - engines: {node: '>=12'} - dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -4937,6 +4537,10 @@ packages: resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} + editions@6.22.0: + resolution: {integrity: sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==} + engines: {ecmascript: '>= es5', node: '>=4'} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -5023,11 +4627,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es-toolkit@1.43.0: - resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} - - es-toolkit@1.47.0: - resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} + es-toolkit@1.49.0: + resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -5049,10 +4650,6 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -5071,9 +4668,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-only-warn@1.1.0: - resolution: {integrity: sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==} - engines: {node: '>=6'} + eslint-plugin-only-warn@1.2.1: + resolution: {integrity: sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w==} eslint-plugin-react-hooks@5.2.0: resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} @@ -5087,8 +4683,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-turbo@2.5.6: - resolution: {integrity: sha512-KUDE23aP2JV8zbfZ4TeM1HpAXzMM/AYG/bJam7P4AalUxas8Pd/lS/6R3p4uX91qJcH1LwL4h0ED48nDe8KorQ==} + eslint-plugin-turbo@2.10.0: + resolution: {integrity: sha512-0HoDr8jdJ5oN9n2ip1DMDtFwmkXYc1ry/JsqGLA1fv6w3kdpvUinPm1FiJPcNnXLxO38N6It//YhW3tfk5j8TQ==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' @@ -5105,18 +4701,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.27.0: - resolution: {integrity: sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - eslint@9.28.0: - resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -5167,19 +4753,11 @@ packages: event-stream@3.3.4: resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} - engines: {node: '>=18.0.0'} - - eventsource-parser@3.0.8: - resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} eventsource@3.0.7: @@ -5198,12 +4776,8 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} - execa@9.5.3: - resolution: {integrity: sha512-QFNnTvU3UjgWFy8Ef9iDHvIdcgZ344ebkwYx4/KLbR+CKQA4xBaHzv+iRpp86QfMHP8faFQLh8iOc57215y4Rg==} - engines: {node: ^18.19.0 || >=20.5.0} - - execa@9.6.0: - resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} exenv-es6@1.1.1: @@ -5266,16 +4840,15 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-parser@5.2.5: - resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} - hasBin: true - fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} @@ -5346,20 +4919,8 @@ packages: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - flatted@3.4.0: - resolution: {integrity: sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==} - - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} @@ -5378,15 +4939,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data-encoder@1.7.2: - resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} - engines: {node: '>= 6'} - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} formatly@0.2.4: @@ -5394,10 +4948,6 @@ packages: engines: {node: '>=18.3.0'} hasBin: true - formdata-node@4.4.1: - resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} - engines: {node: '>= 12.20'} - formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -5416,6 +4966,10 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.3.5: + resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + engines: {node: '>=14.14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -5454,8 +5008,8 @@ packages: resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} engines: {node: '>=14'} - gaxios@7.1.3: - resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} + gaxios@7.1.5: + resolution: {integrity: sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==} engines: {node: '>=18'} gcp-metadata@6.1.1: @@ -5474,10 +5028,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.3.0: - resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} - engines: {node: '>=18'} - get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -5515,9 +5065,6 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.10.0: - resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} - github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -5559,8 +5106,12 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - google-auth-library@10.5.0: - resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==} + globby@14.1.0: + resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} + engines: {node: '>=18'} + + google-auth-library@10.9.0: + resolution: {integrity: sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==} engines: {node: '>=18'} google-auth-library@9.15.1: @@ -5593,10 +5144,6 @@ packages: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} - gtoken@8.0.0: - resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} - engines: {node: '>=18'} - hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -5604,10 +5151,6 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -5627,10 +5170,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -5675,10 +5214,6 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - highlight.js@11.11.1: - resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} - engines: {node: '>=12.0.0'} - hono@4.12.23: resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} engines: {node: '>=16.9.0'} @@ -5687,6 +5222,10 @@ packages: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + howler@2.2.4: resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==} @@ -5709,10 +5248,6 @@ packages: htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -5721,6 +5256,10 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -5741,9 +5280,6 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -5775,10 +5311,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.4: - resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} - engines: {node: '>= 4'} - ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} @@ -5805,6 +5337,10 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -5815,7 +5351,7 @@ packages: resolution: {integrity: sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==} engines: {node: '>=18'} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 peerDependenciesMeta: '@types/react': optional: true @@ -5824,7 +5360,7 @@ packages: resolution: {integrity: sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ==} engines: {node: '>=20'} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: '>=19.0.0' react-devtools-core: ^6.1.2 peerDependenciesMeta: @@ -5853,10 +5389,6 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - ioredis@5.6.1: - resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} - engines: {node: '>=12.22.0'} - ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -5889,10 +5421,6 @@ packages: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -5948,10 +5476,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-fullwidth-code-point@5.0.0: - resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} - engines: {node: '>=18'} - is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} @@ -6129,14 +5653,14 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} - engines: {node: '>=8'} - istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + istextorbinary@9.5.0: + resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==} + engines: {node: '>=4'} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -6165,8 +5689,8 @@ packages: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true jose@6.2.3: @@ -6176,8 +5700,8 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-cookie@2.2.1: - resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} js-message@1.0.7: resolution: {integrity: sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==} @@ -6197,8 +5721,8 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true jsdom@26.1.0: @@ -6221,6 +5745,10 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -6254,6 +5782,9 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jsonschema@1.5.0: resolution: {integrity: sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==} @@ -6274,20 +5805,16 @@ packages: jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + jws@3.2.3: + resolution: {integrity: sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==} - jws@4.0.0: - resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} jwt-decode@4.0.0: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} - katex@0.16.22: - resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} - hasBin: true - katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true @@ -6526,9 +6053,6 @@ packages: lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} - lodash.isarguments@3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} @@ -6569,14 +6093,17 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + lodash.union@4.6.0: resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -6590,6 +6117,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -6600,16 +6130,9 @@ packages: lop@0.4.2: resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==} - lowlight@3.3.0: - resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.1.0: - resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} - engines: {node: 20 || >=22} - lru-cache@11.2.2: resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} engines: {node: 20 || >=22} @@ -6756,8 +6279,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.15.0: - resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} + mermaid@11.16.0: + resolution: {integrity: sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -6898,8 +6421,8 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} @@ -6974,11 +6497,6 @@ packages: react: '*' react-dom: '*' - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -6994,8 +6512,8 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - nock@14.0.10: - resolution: {integrity: sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==} + nock@14.0.15: + resolution: {integrity: sha512-S0a47C9pLvcYx/Ugf0H30BVBEcUgMMBDk9VJIDlJ8XGrfH2QDUD4Tgdp45qDIiHttokBG+IbsOtsvIjGR/j3bg==} engines: {node: '>=18.20.0 <20 || >=20.12.1'} node-abi@3.75.0: @@ -7037,9 +6555,17 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-sarif-builder@3.4.0: + resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==} + engines: {node: '>=20'} + noms@0.0.0: resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} + normalize-package-data@6.0.2: + resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} + engines: {node: ^16.14.0 || >=18.0.0} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -7098,8 +6624,8 @@ packages: resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} engines: {node: '>=12.20.0'} - ollama@0.5.17: - resolution: {integrity: sha512-q5LmPtk6GLFouS+3aURIVl+qcAOPC4+Msmx7uBb3pd+fxI55WnGjmLZ0yijI/CYy79x0QPGx3BwC3u5zv9fBvQ==} + ollama@0.6.3: + resolution: {integrity: sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -7129,16 +6655,12 @@ packages: oniguruma-to-es@4.3.3: resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} - only-allow@1.2.1: - resolution: {integrity: sha512-M7CJbmv7UCopc0neRKdzfoGWaVZC+xC1925GitKH9EAqYFzX9//25Q7oX4+jw0tiCCj+t5l6VZh8UPH23NZkMA==} - hasBin: true - open@10.1.2: resolution: {integrity: sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==} engines: {node: '>=18'} - openai@5.12.2: - resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==} + openai@5.23.2: + resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==} hasBin: true peerDependencies: ws: ^8.18.0 @@ -7170,8 +6692,8 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - ovsx@0.10.4: - resolution: {integrity: sha512-9HY9TsFpL31EqhPZSdal7k3sTOfrFvj2GEnXKF2PYKukQpiAilOaPhW8K0NSGCiVh9MQYr2IyhX74PKMVlcJ5g==} + ovsx@0.10.12: + resolution: {integrity: sha512-WwMj1iQDvCk02029oxPnkFXsPrHZ+WzmoNW5pJ8JGepHtL30i2JE4s3C3wqzQqj6a35vx2hp0gV3TdfefGmvMg==} engines: {node: '>= 20'} hasBin: true @@ -7179,8 +6701,8 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - oxc-resolver@11.2.0: - resolution: {integrity: sha512-3iJYyIdDZMDoj0ZSVBrI1gUvPBMkDC4gxonBG+7uqUyK5EslG0mCwnf6qhxK8oEU7jLHjbRBNyzflPSd3uvH7Q==} + oxc-resolver@11.21.3: + resolution: {integrity: sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==} p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} @@ -7210,6 +6732,14 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + engines: {node: '>=18'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + p-timeout@6.1.4: resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} engines: {node: '>=14.16'} @@ -7244,6 +6774,10 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -7305,14 +6839,18 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + path-type@6.0.0: + resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} + engines: {node: '>=18'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} pause-stream@0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} - pdf-parse@1.1.1: - resolution: {integrity: sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==} + pdf-parse@1.1.4: + resolution: {integrity: sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==} engines: {node: '>=6.8.1'} pend@1.2.0: @@ -7344,6 +6882,13 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pluralize@2.0.0: + resolution: {integrity: sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} @@ -7383,23 +6928,15 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - posthog-js@1.242.1: - resolution: {integrity: sha512-j2mzw0eukyuw/Qm3tNZ6pfaXmc7eglWj6ftmvR1Lz9GtMr85ndGNXJvIGO+6PBrQW2o0D1G0k/KV93ehta0hFA==} - peerDependencies: - '@rrweb/types': 2.0.0-alpha.17 - rrweb-snapshot: 2.0.0-alpha.17 - peerDependenciesMeta: - '@rrweb/types': - optional: true - rrweb-snapshot: - optional: true + posthog-js@1.393.4: + resolution: {integrity: sha512-mOBSVQczEfyLnW3aFRbLXZEuE0Rfbmhqm9UCnuLeMFUUz0uYTkbnyhl5+Uc+BLSGfyiXHLf3nm2BqxYC+G/PXA==} posthog-node@5.1.1: resolution: {integrity: sha512-6VISkNdxO24ehXiDA4dugyCSIV7lpGVaEu5kn/dlAj+SJ1lgcDru9PQ8p/+GSXsXVxohd1t7kHL2JKc9NoGb0w==} engines: {node: '>=20'} - preact@10.26.6: - resolution: {integrity: sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g==} + preact@10.29.3: + resolution: {integrity: sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==} prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} @@ -7416,8 +6953,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + prettier@3.8.4: + resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} engines: {node: '>=14'} hasBin: true @@ -7433,8 +6970,8 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-ms@9.2.0: - resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} process-nextick-args@2.0.1: @@ -7456,6 +6993,10 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + protobufjs@7.6.4: + resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -7483,10 +7024,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - qs@6.15.2: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} @@ -7494,6 +7031,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -7504,14 +7044,13 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} - raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + rc-config-loader@4.1.4: + resolution: {integrity: sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -7559,7 +7098,7 @@ packages: react-markdown@9.1.0: resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: '>=18' react-reconciler@0.33.0: @@ -7582,7 +7121,7 @@ packages: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': @@ -7592,7 +7131,7 @@ packages: resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -7602,7 +7141,7 @@ packages: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -7620,8 +7159,8 @@ packages: react: '*' tslib: '*' - react-use@17.6.0: - resolution: {integrity: sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==} + react-use@17.6.1: + resolution: {integrity: sha512-uibb3pgzV4LFsYPHyXYGu7dD2+pyk/ZJlPH+AizBR3zolqPWyCleKcWWbUQaKSKfydwgnQ3ymGm1Ab3/saGHCA==} peerDependencies: react: '*' react-dom: '*' @@ -7640,6 +7179,10 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + read-pkg@9.0.1: + resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} + engines: {node: '>=18'} + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -7661,10 +7204,6 @@ packages: readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -7677,14 +7216,6 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - redis-errors@1.2.0: - resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} - engines: {node: '>=4'} - - redis-parser@3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} - reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -7702,9 +7233,6 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - rehype-highlight@7.0.2: - resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==} - rehype-katex@7.0.1: resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} @@ -7754,9 +7282,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@2.0.0-next.5: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true @@ -7773,6 +7298,10 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -7785,10 +7314,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@5.0.10: - resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} - hasBin: true - rimraf@6.0.1: resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} engines: {node: 20 || >=22} @@ -7801,8 +7326,8 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -7859,9 +7384,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sambanova-ai-provider@1.2.2: - resolution: {integrity: sha512-MU/D+9GCg6me0guDRPw/x0N8cnpkOkv03FR7QXdrcinX0hprS7bsZXXTYEz81Svc+oVwXDZwh0v+Sd5pUxV3mg==} - sanitize-filename@1.6.3: resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} @@ -7890,6 +7412,11 @@ packages: resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} engines: {node: '>=0.10.0'} + secretlint@10.2.2: + resolution: {integrity: sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==} + engines: {node: '>=20.0.0'} + hasBin: true + section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -7905,11 +7432,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -8025,9 +7547,13 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slice-ansi@7.1.0: - resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} - engines: {node: '>=18'} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} @@ -8037,8 +7563,8 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} - smol-toml@1.3.4: - resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==} + smol-toml@1.7.0: + resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} engines: {node: '>= 18'} source-map-js@1.2.1: @@ -8071,6 +7597,18 @@ packages: spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + split@0.3.3: resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} @@ -8099,12 +7637,8 @@ packages: stacktrace-js@2.0.2: resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} - standard-as-callback@2.1.0: - resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} @@ -8142,10 +7676,6 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string-width@8.1.0: - resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} - engines: {node: '>=20'} - string-width@8.2.1: resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} engines: {node: '>=20'} @@ -8182,10 +7712,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} @@ -8230,9 +7756,6 @@ packages: resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} engines: {node: '>=14.16'} - strnum@2.1.1: - resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} - strong-type@0.1.6: resolution: {integrity: sha512-eJe5caH6Pi5oMMeQtIoBPpvNu/s4jiyb63u5tkHNnQXomK+puyQ5i+Z5iTLBr/xUz/pIcps0NSfzzFI34+gAXg==} engines: {node: '>=12.0.0'} @@ -8241,6 +7764,9 @@ packages: resolution: {integrity: sha512-X5Z6riticuH5GnhUyzijfDi1SoXas8ODDyN7K8lJeQK+Jfi4dKdoJGL4CXTskY/ATBcN+rz5lROGn1tAUkOX7g==} engines: {node: '>=12.21.0'} + structured-source@4.0.0: + resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==} + style-to-js@1.1.16: resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} @@ -8272,10 +7798,6 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -8284,9 +7806,9 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - supports-color@9.4.0: - resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} - engines: {node: '>=12'} + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -8298,6 +7820,10 @@ packages: tabbable@5.3.3: resolution: {integrity: sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==} + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + tailwind-merge@3.3.0: resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} @@ -8332,13 +7858,20 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + terminal-link@4.0.0: + resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==} + engines: {node: '>=18'} text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + textextensions@6.11.0: + resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==} + engines: {node: '>=4'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -8356,8 +7889,8 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tiktoken@1.0.21: - resolution: {integrity: sha512-/kqtlepLMptX0OgbYD9aMYbM7EFrMZCL7EoHM8Psmg2FuhXoo/bH64KqOiZGGwa6oS9TPdSEDKBnV2LuB8+5vQ==} + tiktoken@1.0.22: + resolution: {integrity: sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA==} tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -8365,18 +7898,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.2.3: - resolution: {integrity: sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ==} - engines: {node: '>=18'} - tinyexec@1.2.4: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -8392,10 +7917,6 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true - tmp@0.2.3: - resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} - engines: {node: '>=14.14'} - tmp@0.2.4: resolution: {integrity: sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==} engines: {node: '>=14.14'} @@ -8451,14 +7972,17 @@ packages: truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' - ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} engines: {node: '>=6.10'} ts-easing@0.2.0: @@ -8495,8 +8019,8 @@ packages: typescript: optional: true - tsx@4.19.4: - resolution: {integrity: sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==} + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} engines: {node: '>=18.0.0'} hasBin: true @@ -8507,8 +8031,8 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - turbo@2.9.14: - resolution: {integrity: sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw==} + turbo@2.10.0: + resolution: {integrity: sha512-o016H9PPtuH2deb3mh3Vci3Avfi9UYgM/RONQisY7HnloupP0IFSbFS3gFYJgFJP8nwBrByHWFQIDa8T2zIXPw==} hasBin: true type-check@0.4.0: @@ -8553,8 +8077,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -8568,25 +8092,20 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - underscore@1.13.7: - resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} - underscore@1.13.8: resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.10.0: - resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} - - undici@6.24.0: - resolution: {integrity: sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==} + undici@6.27.0: + resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} engines: {node: '>=18.17'} + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -8652,6 +8171,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -8679,7 +8202,7 @@ packages: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -8716,7 +8239,7 @@ packages: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -8737,10 +8260,6 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} - hasBin: true - uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -8751,14 +8270,17 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + version-range@4.15.0: + resolution: {integrity: sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==} + engines: {node: '>=4'} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -8774,13 +8296,13 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: 0.28.1 jiti: '>=1.21.0' less: ^4.0.0 @@ -8817,21 +8339,23 @@ packages: yaml: optional: true - vitest@4.1.0: - resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.0 - '@vitest/browser-preview': 4.1.0 - '@vitest/browser-webdriverio': 4.1.0 - '@vitest/ui': 4.1.0 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 happy-dom: '*' jsdom: '*' - vite: 8.0.16 + vite: 8.1.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -8845,6 +8369,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -8862,7 +8390,7 @@ packages: vscrui@0.2.2: resolution: {integrity: sha512-buw2OipqUl7GCBq1mxcAjUwoUsslGzVhdaxDPmEx27xzc3QAJJZHtT30QbakgZVJ1Jb3E6kcsguUIFEGxrgkyQ==} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: ^17 || ^18 || ^19 w3c-xmlserializer@5.0.0: @@ -8883,15 +8411,11 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - web-streams-polyfill@4.0.0-beta.3: - resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} - engines: {node: '>= 14'} - web-tree-sitter@0.25.6: resolution: {integrity: sha512-WG+/YGbxw8r+rLlzzhV+OvgiOJCWdIpOucG3qBf3RCBFMkGDb1CanUi2BxCxjnkpzU3/hLWPT8VO5EKsMk9Fxg==} - web-vitals@4.2.4: - resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + web-vitals@5.3.0: + resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==} webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -8937,10 +8461,6 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-pm-runs@1.1.0: - resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} - engines: {node: '>=4'} - which-typed-array@1.1.19: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} @@ -8986,10 +8506,6 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrap-ansi@9.0.0: - resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} - engines: {node: '>=18'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -8997,32 +8513,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.2: - resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -9070,11 +8562,6 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} - engines: {node: '>= 14.6'} - hasBin: true - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -9107,6 +8594,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yauzl@3.4.0: + resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} + engines: {node: '>=12'} + yazl@2.5.1: resolution: {integrity: sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==} @@ -9122,10 +8613,6 @@ packages: resolution: {integrity: sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==} engines: {node: '>=18.19'} - yoctocolors@2.1.1: - resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} - engines: {node: '>=18'} - yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -9133,10 +8620,6 @@ packages: yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} - zhipu-ai-provider@0.2.2: - resolution: {integrity: sha512-UjX1ho4DI9ICUv/mrpAnzmrRe5/LXrGkS5hF6h4WDY2aup5GketWWopFzWYCqsbArXAM5wbzzdH9QzZusgGiBg==} - engines: {node: '>=18'} - zip-stream@4.1.1: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} @@ -9146,8 +8629,8 @@ packages: peerDependencies: zod: 3.25.76 - zod-validation-error@3.4.1: - resolution: {integrity: sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw==} + zod-validation-error@3.5.4: + resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} engines: {node: '>=18.0.0'} peerDependencies: zod: 3.25.76 @@ -9159,7 +8642,7 @@ packages: resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 immer: '>=9.0.6' react: '>=18.0.0' use-sync-external-store: '>=1.2.0' @@ -9180,107 +8663,23 @@ snapshots: '@adobe/css-tools@4.4.2': {} - '@ai-sdk/amazon-bedrock@4.0.107(zod@3.25.76)': - dependencies: - '@ai-sdk/anthropic': 3.0.78(zod@3.25.76) - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@3.25.76) - '@smithy/eventstream-codec': 4.3.3 - '@smithy/util-utf8': 4.3.3 - aws4fetch: 1.0.20 - zod: 3.25.76 - - '@ai-sdk/anthropic@3.0.38(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - zod: 3.25.76 - '@ai-sdk/anthropic@3.0.42(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.8 '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/anthropic@3.0.78(zod@3.25.76)': + '@ai-sdk/gateway@3.0.134(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + '@vercel/oidc': 3.2.0 zod: 3.25.76 - '@ai-sdk/baseten@1.0.50(zod@3.25.76)': + '@ai-sdk/openai-compatible@2.0.51(zod@3.25.76)': dependencies: - '@ai-sdk/openai-compatible': 2.0.47(zod@3.25.76) '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@3.25.76) - '@basetenlabs/performance-client': 0.0.10 - zod: 3.25.76 - - '@ai-sdk/deepseek@2.0.18(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/fireworks@2.0.32(zod@3.25.76)': - dependencies: - '@ai-sdk/openai-compatible': 2.0.28(zod@3.25.76) - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/gateway@3.0.39(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - '@vercel/oidc': 3.1.0 - zod: 3.25.76 - - '@ai-sdk/google-vertex@4.0.45(zod@3.25.76)': - dependencies: - '@ai-sdk/anthropic': 3.0.38(zod@3.25.76) - '@ai-sdk/google': 3.0.22(zod@3.25.76) - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - google-auth-library: 10.5.0 - zod: 3.25.76 - transitivePeerDependencies: - - supports-color - - '@ai-sdk/google@3.0.22(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/mistral@3.0.19(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/openai-compatible@1.0.11(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.5(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/openai-compatible@2.0.28(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/openai-compatible@2.0.37(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.21(zod@3.25.76) - zod: 3.25.76 - - '@ai-sdk/openai-compatible@2.0.47(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) zod: 3.25.76 '@ai-sdk/openai@3.0.27(zod@3.25.76)': @@ -9289,39 +8688,20 @@ snapshots: '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/provider-utils@3.0.5(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.8 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) - '@ai-sdk/provider-utils@4.0.14(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.8 '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.6 - zod: 3.25.76 - - '@ai-sdk/provider-utils@4.0.21(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.8 + eventsource-parser: 3.1.0 zod: 3.25.76 - '@ai-sdk/provider-utils@4.0.27(zod@3.25.76)': + '@ai-sdk/provider-utils@4.0.30(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.10 '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.8 + eventsource-parser: 3.1.0 zod: 3.25.76 - '@ai-sdk/provider@2.0.0': - dependencies: - json-schema: 0.4.0 - '@ai-sdk/provider@3.0.10': dependencies: json-schema: 0.4.0 @@ -9330,17 +8710,10 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/xai@3.0.48(zod@3.25.76)': - dependencies: - '@ai-sdk/openai-compatible': 2.0.28(zod@3.25.76) - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - zod: 3.25.76 - '@alcalzone/ansi-tokenize@0.2.3': dependencies: ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.0.0 + is-fullwidth-code-point: 5.1.0 '@ampproject/remapping@2.3.0': dependencies: @@ -9352,25 +8725,21 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.2.4 - '@anthropic-ai/sdk@0.37.0': + '@anthropic-ai/sdk@0.106.0(zod@3.25.76)': dependencies: - '@types/node': 18.19.100 - '@types/node-fetch': 2.6.12 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding + json-schema-to-ts: 3.1.1 + standardwebhooks: 1.0.0 + optionalDependencies: + zod: 3.25.76 - '@anthropic-ai/vertex-sdk@0.7.0': + '@anthropic-ai/vertex-sdk@0.18.0(zod@3.25.76)': dependencies: - '@anthropic-ai/sdk': 0.37.0 + '@anthropic-ai/sdk': 0.106.0(zod@3.25.76) google-auth-library: 9.15.1 transitivePeerDependencies: - encoding - supports-color + - zod '@asamuzakjp/css-color@3.2.0': dependencies: @@ -9383,7 +8752,7 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.8 + '@aws-sdk/types': 3.973.13 tslib: 2.8.1 '@aws-crypto/sha256-browser@5.2.0': @@ -9391,15 +8760,15 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.922.0 - '@aws-sdk/util-locate-window': 3.804.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.922.0 + '@aws-sdk/types': 3.973.13 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -9408,462 +8777,246 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.922.0 + '@aws-sdk/types': 3.973.13 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-bedrock-runtime@3.922.0': + '@aws-sdk/client-bedrock-runtime@3.1075.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.922.0 - '@aws-sdk/credential-provider-node': 3.922.0 - '@aws-sdk/eventstream-handler-node': 3.922.0 - '@aws-sdk/middleware-eventstream': 3.922.0 - '@aws-sdk/middleware-host-header': 3.922.0 - '@aws-sdk/middleware-logger': 3.922.0 - '@aws-sdk/middleware-recursion-detection': 3.922.0 - '@aws-sdk/middleware-user-agent': 3.922.0 - '@aws-sdk/middleware-websocket': 3.922.0 - '@aws-sdk/region-config-resolver': 3.922.0 - '@aws-sdk/token-providers': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@aws-sdk/util-endpoints': 3.922.0 - '@aws-sdk/util-user-agent-browser': 3.922.0 - '@aws-sdk/util-user-agent-node': 3.922.0 - '@smithy/config-resolver': 4.4.2 - '@smithy/core': 3.17.2 - '@smithy/eventstream-serde-browser': 4.2.4 - '@smithy/eventstream-serde-config-resolver': 4.3.4 - '@smithy/eventstream-serde-node': 4.2.4 - '@smithy/fetch-http-handler': 5.3.5 - '@smithy/hash-node': 4.2.4 - '@smithy/invalid-dependency': 4.2.4 - '@smithy/middleware-content-length': 4.2.4 - '@smithy/middleware-endpoint': 4.3.6 - '@smithy/middleware-retry': 4.4.6 - '@smithy/middleware-serde': 4.2.4 - '@smithy/middleware-stack': 4.2.4 - '@smithy/node-config-provider': 4.3.4 - '@smithy/node-http-handler': 4.4.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 - '@smithy/url-parser': 4.2.4 - '@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.5 - '@smithy/util-defaults-mode-node': 4.2.8 - '@smithy/util-endpoints': 3.2.4 - '@smithy/util-middleware': 4.2.4 - '@smithy/util-retry': 4.2.4 - '@smithy/util-stream': 4.5.5 - '@smithy/util-utf8': 4.2.0 - '@smithy/uuid': 1.1.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-node': 3.972.58 + '@aws-sdk/eventstream-handler-node': 3.972.22 + '@aws-sdk/middleware-eventstream': 3.972.18 + '@aws-sdk/middleware-websocket': 3.972.31 + '@aws-sdk/token-providers': 3.1075.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/client-cognito-identity@3.922.0': + '@aws-sdk/client-cognito-identity@3.1075.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.922.0 - '@aws-sdk/credential-provider-node': 3.922.0 - '@aws-sdk/middleware-host-header': 3.922.0 - '@aws-sdk/middleware-logger': 3.922.0 - '@aws-sdk/middleware-recursion-detection': 3.922.0 - '@aws-sdk/middleware-user-agent': 3.922.0 - '@aws-sdk/region-config-resolver': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@aws-sdk/util-endpoints': 3.922.0 - '@aws-sdk/util-user-agent-browser': 3.922.0 - '@aws-sdk/util-user-agent-node': 3.922.0 - '@smithy/config-resolver': 4.4.2 - '@smithy/core': 3.17.2 - '@smithy/fetch-http-handler': 5.3.5 - '@smithy/hash-node': 4.2.4 - '@smithy/invalid-dependency': 4.2.4 - '@smithy/middleware-content-length': 4.2.4 - '@smithy/middleware-endpoint': 4.3.6 - '@smithy/middleware-retry': 4.4.6 - '@smithy/middleware-serde': 4.2.4 - '@smithy/middleware-stack': 4.2.4 - '@smithy/node-config-provider': 4.3.4 - '@smithy/node-http-handler': 4.4.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 - '@smithy/url-parser': 4.2.4 - '@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.5 - '@smithy/util-defaults-mode-node': 4.2.8 - '@smithy/util-endpoints': 3.2.4 - '@smithy/util-middleware': 4.2.4 - '@smithy/util-retry': 4.2.4 - '@smithy/util-utf8': 4.3.3 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-node': 3.972.58 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/client-sso@3.922.0': + '@aws-sdk/core@3.974.23': dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.922.0 - '@aws-sdk/middleware-host-header': 3.922.0 - '@aws-sdk/middleware-logger': 3.922.0 - '@aws-sdk/middleware-recursion-detection': 3.922.0 - '@aws-sdk/middleware-user-agent': 3.922.0 - '@aws-sdk/region-config-resolver': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@aws-sdk/util-endpoints': 3.922.0 - '@aws-sdk/util-user-agent-browser': 3.922.0 - '@aws-sdk/util-user-agent-node': 3.922.0 - '@smithy/config-resolver': 4.4.2 - '@smithy/core': 3.17.2 - '@smithy/fetch-http-handler': 5.3.5 - '@smithy/hash-node': 4.2.4 - '@smithy/invalid-dependency': 4.2.4 - '@smithy/middleware-content-length': 4.2.4 - '@smithy/middleware-endpoint': 4.3.6 - '@smithy/middleware-retry': 4.4.6 - '@smithy/middleware-serde': 4.2.4 - '@smithy/middleware-stack': 4.2.4 - '@smithy/node-config-provider': 4.3.4 - '@smithy/node-http-handler': 4.4.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 - '@smithy/url-parser': 4.2.4 - '@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.5 - '@smithy/util-defaults-mode-node': 4.2.8 - '@smithy/util-endpoints': 3.2.4 - '@smithy/util-middleware': 4.2.4 - '@smithy/util-retry': 4.2.4 - '@smithy/util-utf8': 4.3.3 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.922.0': - dependencies: - '@aws-sdk/types': 3.922.0 - '@aws-sdk/xml-builder': 3.921.0 - '@smithy/core': 3.17.2 - '@smithy/node-config-provider': 4.3.4 - '@smithy/property-provider': 4.2.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/signature-v4': 5.3.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 - '@smithy/util-base64': 4.3.0 - '@smithy/util-middleware': 4.2.4 - '@smithy/util-utf8': 4.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/xml-builder': 3.972.31 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.26.0 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-cognito-identity@3.922.0': + '@aws-sdk/credential-provider-cognito-identity@3.972.48': dependencies: - '@aws-sdk/client-cognito-identity': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/property-provider': 4.2.4 - '@smithy/types': 4.8.1 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/credential-provider-env@3.922.0': + '@aws-sdk/credential-provider-env@3.972.49': dependencies: - '@aws-sdk/core': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/property-provider': 4.2.4 - '@smithy/types': 4.8.1 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.922.0': - dependencies: - '@aws-sdk/core': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/fetch-http-handler': 5.3.5 - '@smithy/node-http-handler': 4.4.4 - '@smithy/property-provider': 4.2.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 - '@smithy/util-stream': 4.5.5 + '@aws-sdk/credential-provider-http@3.972.51': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.922.0': - dependencies: - '@aws-sdk/core': 3.922.0 - '@aws-sdk/credential-provider-env': 3.922.0 - '@aws-sdk/credential-provider-http': 3.922.0 - '@aws-sdk/credential-provider-process': 3.922.0 - '@aws-sdk/credential-provider-sso': 3.922.0 - '@aws-sdk/credential-provider-web-identity': 3.922.0 - '@aws-sdk/nested-clients': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/credential-provider-imds': 4.2.4 - '@smithy/property-provider': 4.2.4 - '@smithy/shared-ini-file-loader': 4.3.4 - '@smithy/types': 4.8.1 + '@aws-sdk/credential-provider-ini@3.972.56': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-login': 3.972.55 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-node@3.922.0': - dependencies: - '@aws-sdk/credential-provider-env': 3.922.0 - '@aws-sdk/credential-provider-http': 3.922.0 - '@aws-sdk/credential-provider-ini': 3.922.0 - '@aws-sdk/credential-provider-process': 3.922.0 - '@aws-sdk/credential-provider-sso': 3.922.0 - '@aws-sdk/credential-provider-web-identity': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/credential-provider-imds': 4.2.4 - '@smithy/property-provider': 4.2.4 - '@smithy/shared-ini-file-loader': 4.3.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/credential-provider-process@3.922.0': + '@aws-sdk/credential-provider-login@3.972.55': dependencies: - '@aws-sdk/core': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/property-provider': 4.2.4 - '@smithy/shared-ini-file-loader': 4.3.4 - '@smithy/types': 4.8.1 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.922.0': - dependencies: - '@aws-sdk/client-sso': 3.922.0 - '@aws-sdk/core': 3.922.0 - '@aws-sdk/token-providers': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/property-provider': 4.2.4 - '@smithy/shared-ini-file-loader': 4.3.4 - '@smithy/types': 4.8.1 + '@aws-sdk/credential-provider-node@3.972.58': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-ini': 3.972.56 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/credential-provider-web-identity@3.922.0': + '@aws-sdk/credential-provider-process@3.972.49': dependencies: - '@aws-sdk/core': 3.922.0 - '@aws-sdk/nested-clients': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/property-provider': 4.2.4 - '@smithy/shared-ini-file-loader': 4.3.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-providers@3.922.0': - dependencies: - '@aws-sdk/client-cognito-identity': 3.922.0 - '@aws-sdk/core': 3.922.0 - '@aws-sdk/credential-provider-cognito-identity': 3.922.0 - '@aws-sdk/credential-provider-env': 3.922.0 - '@aws-sdk/credential-provider-http': 3.922.0 - '@aws-sdk/credential-provider-ini': 3.922.0 - '@aws-sdk/credential-provider-node': 3.922.0 - '@aws-sdk/credential-provider-process': 3.922.0 - '@aws-sdk/credential-provider-sso': 3.922.0 - '@aws-sdk/credential-provider-web-identity': 3.922.0 - '@aws-sdk/nested-clients': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/config-resolver': 4.4.2 - '@smithy/core': 3.17.2 - '@smithy/credential-provider-imds': 4.2.4 - '@smithy/node-config-provider': 4.3.4 - '@smithy/property-provider': 4.2.4 - '@smithy/types': 4.8.1 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/eventstream-handler-node@3.922.0': + '@aws-sdk/credential-provider-sso@3.972.55': dependencies: - '@aws-sdk/types': 3.922.0 - '@smithy/eventstream-codec': 4.3.3 - '@smithy/types': 4.8.1 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/token-providers': 3.1074.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/middleware-eventstream@3.922.0': + '@aws-sdk/credential-provider-web-identity@3.972.55': dependencies: - '@aws-sdk/types': 3.922.0 - '@smithy/protocol-http': 5.3.4 - '@smithy/types': 4.8.1 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.922.0': - dependencies: - '@aws-sdk/types': 3.922.0 - '@smithy/protocol-http': 5.3.4 - '@smithy/types': 4.8.1 + '@aws-sdk/credential-providers@3.1075.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.1075.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-cognito-identity': 3.972.48 + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-ini': 3.972.56 + '@aws-sdk/credential-provider-login': 3.972.55 + '@aws-sdk/credential-provider-node': 3.972.58 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.922.0': + '@aws-sdk/eventstream-handler-node@3.972.22': dependencies: - '@aws-sdk/types': 3.922.0 - '@smithy/types': 4.8.1 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.922.0': + '@aws-sdk/middleware-eventstream@3.972.18': dependencies: - '@aws-sdk/types': 3.922.0 - '@aws/lambda-invoke-store': 0.1.1 - '@smithy/protocol-http': 5.3.4 - '@smithy/types': 4.8.1 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.922.0': + '@aws-sdk/middleware-websocket@3.972.31': dependencies: - '@aws-sdk/core': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@aws-sdk/util-endpoints': 3.922.0 - '@smithy/core': 3.17.2 - '@smithy/protocol-http': 5.3.4 - '@smithy/types': 4.8.1 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/middleware-websocket@3.922.0': - dependencies: - '@aws-sdk/types': 3.922.0 - '@aws-sdk/util-format-url': 3.922.0 - '@smithy/eventstream-codec': 4.3.3 - '@smithy/eventstream-serde-browser': 4.2.4 - '@smithy/fetch-http-handler': 5.3.5 - '@smithy/protocol-http': 5.3.4 - '@smithy/signature-v4': 5.3.4 - '@smithy/types': 4.8.1 - '@smithy/util-hex-encoding': 4.2.0 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.922.0': + '@aws-sdk/nested-clients@3.997.23': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.922.0 - '@aws-sdk/middleware-host-header': 3.922.0 - '@aws-sdk/middleware-logger': 3.922.0 - '@aws-sdk/middleware-recursion-detection': 3.922.0 - '@aws-sdk/middleware-user-agent': 3.922.0 - '@aws-sdk/region-config-resolver': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@aws-sdk/util-endpoints': 3.922.0 - '@aws-sdk/util-user-agent-browser': 3.922.0 - '@aws-sdk/util-user-agent-node': 3.922.0 - '@smithy/config-resolver': 4.4.2 - '@smithy/core': 3.17.2 - '@smithy/fetch-http-handler': 5.3.5 - '@smithy/hash-node': 4.2.4 - '@smithy/invalid-dependency': 4.2.4 - '@smithy/middleware-content-length': 4.2.4 - '@smithy/middleware-endpoint': 4.3.6 - '@smithy/middleware-retry': 4.4.6 - '@smithy/middleware-serde': 4.2.4 - '@smithy/middleware-stack': 4.2.4 - '@smithy/node-config-provider': 4.3.4 - '@smithy/node-http-handler': 4.4.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 - '@smithy/url-parser': 4.2.4 - '@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.5 - '@smithy/util-defaults-mode-node': 4.2.8 - '@smithy/util-endpoints': 3.2.4 - '@smithy/util-middleware': 4.2.4 - '@smithy/util-retry': 4.2.4 - '@smithy/util-utf8': 4.3.3 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/region-config-resolver@3.922.0': + '@aws-sdk/signature-v4-multi-region@3.996.35': dependencies: - '@aws-sdk/types': 3.922.0 - '@smithy/config-resolver': 4.4.2 - '@smithy/node-config-provider': 4.3.4 - '@smithy/types': 4.8.1 + '@aws-sdk/types': 3.973.13 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.922.0': + '@aws-sdk/token-providers@3.1074.0': dependencies: - '@aws-sdk/core': 3.922.0 - '@aws-sdk/nested-clients': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/property-provider': 4.2.4 - '@smithy/shared-ini-file-loader': 4.3.4 - '@smithy/types': 4.8.1 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/types@3.922.0': + '@aws-sdk/token-providers@3.1075.0': dependencies: - '@smithy/types': 4.8.1 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/types@3.973.8': + '@aws-sdk/types@3.973.13': dependencies: - '@smithy/types': 4.14.2 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.922.0': + '@aws-sdk/util-locate-window@3.965.8': dependencies: - '@aws-sdk/types': 3.922.0 - '@smithy/types': 4.8.1 - '@smithy/url-parser': 4.2.4 - '@smithy/util-endpoints': 3.2.4 tslib: 2.8.1 - '@aws-sdk/util-format-url@3.922.0': + '@aws-sdk/xml-builder@3.972.31': dependencies: - '@aws-sdk/types': 3.922.0 - '@smithy/querystring-builder': 4.2.4 - '@smithy/types': 4.8.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.804.0': - dependencies: - tslib: 2.8.1 + '@aws/lambda-invoke-store@0.2.4': {} - '@aws-sdk/util-user-agent-browser@3.922.0': - dependencies: - '@aws-sdk/types': 3.922.0 - '@smithy/types': 4.8.1 - bowser: 2.11.0 - tslib: 2.8.1 + '@azu/format-text@1.0.2': {} - '@aws-sdk/util-user-agent-node@3.922.0': + '@azu/style-format@1.0.1': dependencies: - '@aws-sdk/middleware-user-agent': 3.922.0 - '@aws-sdk/types': 3.922.0 - '@smithy/node-config-provider': 4.3.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.921.0': - dependencies: - '@smithy/types': 4.8.1 - fast-xml-parser: 5.2.5 - tslib: 2.8.1 - - '@aws/lambda-invoke-store@0.1.1': {} + '@azu/format-text': 1.0.2 '@azure/abort-controller@2.1.2': dependencies: @@ -10037,10 +9190,6 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/runtime@7.27.1': {} - - '@babel/runtime@7.27.4': {} - '@babel/runtime@7.29.7': {} '@babel/template@7.28.6': @@ -10066,67 +9215,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.29.7 - '@basetenlabs/performance-client-android-arm-eabi@0.0.10': - optional: true - - '@basetenlabs/performance-client-android-arm64@0.0.10': - optional: true - - '@basetenlabs/performance-client-darwin-arm64@0.0.10': - optional: true - - '@basetenlabs/performance-client-darwin-universal@0.0.10': - optional: true - - '@basetenlabs/performance-client-darwin-x64@0.0.10': - optional: true - - '@basetenlabs/performance-client-linux-arm-gnueabihf@0.0.10': - optional: true - - '@basetenlabs/performance-client-linux-arm-musleabihf@0.0.10': - optional: true - - '@basetenlabs/performance-client-linux-arm64-gnu@0.0.10': - optional: true - - '@basetenlabs/performance-client-linux-riscv64-gnu@0.0.10': - optional: true - - '@basetenlabs/performance-client-linux-x64-gnu@0.0.10': - optional: true - - '@basetenlabs/performance-client-linux-x64-musl@0.0.10': - optional: true - - '@basetenlabs/performance-client-win32-arm64-msvc@0.0.10': - optional: true - - '@basetenlabs/performance-client-win32-ia32-msvc@0.0.10': - optional: true - - '@basetenlabs/performance-client-win32-x64-msvc@0.0.10': - optional: true - - '@basetenlabs/performance-client@0.0.10': - optionalDependencies: - '@basetenlabs/performance-client-android-arm-eabi': 0.0.10 - '@basetenlabs/performance-client-android-arm64': 0.0.10 - '@basetenlabs/performance-client-darwin-arm64': 0.0.10 - '@basetenlabs/performance-client-darwin-universal': 0.0.10 - '@basetenlabs/performance-client-darwin-x64': 0.0.10 - '@basetenlabs/performance-client-linux-arm-gnueabihf': 0.0.10 - '@basetenlabs/performance-client-linux-arm-musleabihf': 0.0.10 - '@basetenlabs/performance-client-linux-arm64-gnu': 0.0.10 - '@basetenlabs/performance-client-linux-riscv64-gnu': 0.0.10 - '@basetenlabs/performance-client-linux-x64-gnu': 0.0.10 - '@basetenlabs/performance-client-linux-x64-musl': 0.0.10 - '@basetenlabs/performance-client-win32-arm64-msvc': 0.0.10 - '@basetenlabs/performance-client-win32-ia32-msvc': 0.0.10 - '@basetenlabs/performance-client-win32-x64-msvc': 0.0.10 - - '@bcoe/v8-coverage@0.2.3': {} - '@bcoe/v8-coverage@1.0.2': {} '@braintree/sanitize-url@7.1.2': {} @@ -10160,7 +9248,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.7(@types/node@24.2.1)': + '@changesets/cli@2.29.7(@types/node@20.19.43)': dependencies: '@changesets/apply-release-plan': 7.0.13 '@changesets/assemble-release-plan': 6.0.9 @@ -10176,7 +9264,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.2(@types/node@24.2.1) + '@inquirer/external-editor': 1.0.2(@types/node@20.19.43) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 @@ -10277,9 +9365,9 @@ snapshots: '@chevrotain/types@11.1.2': {} - '@copilotkit/aimock@1.15.1(vitest@4.1.0)': + '@copilotkit/aimock@1.15.1(vitest@4.1.9)': optionalDependencies: - vitest: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@csstools/color-helpers@5.0.2': {} @@ -10318,18 +9406,29 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 - '@emnapi/core@1.10.0': + '@emnapi/core@1.11.0': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.0': dependencies: - '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true @@ -10420,55 +9519,52 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': dependencies: - eslint: 9.27.0(jiti@2.4.2) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.7.0(eslint@9.28.0(jiti@2.4.2))': - dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.39.4(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.21.2': dependencies: - '@eslint/object-schema': 2.1.6 + '@eslint/object-schema': 2.1.7 debug: 4.4.3(supports-color@8.1.1) - minimatch: 3.1.2 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.2': {} + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 - '@eslint/core@0.14.0': + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.5': dependencies: - ajv: 6.12.6 + ajv: 6.15.0 debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 + js-yaml: 4.2.0 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color '@eslint/js@9.27.0': {} - '@eslint/js@9.28.0': {} + '@eslint/js@9.39.4': {} - '@eslint/object-schema@2.1.6': {} + '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.3.1': + '@eslint/plugin-kit@0.4.1': dependencies: - '@eslint/core': 0.14.0 + '@eslint/core': 0.17.0 levn: 0.4.1 '@fast-csv/format@4.3.5': @@ -10507,12 +9603,14 @@ snapshots: '@floating-ui/utils@0.2.9': {} - '@google/genai@1.29.1(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))': + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))': dependencies: - google-auth-library: 10.5.0 - ws: 8.18.3 + google-auth-library: 10.9.0 + p-retry: 4.6.2 + protobufjs: 7.6.4 + ws: 8.21.0 optionalDependencies: - '@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) transitivePeerDependencies: - bufferutil - supports-color @@ -10543,22 +9641,20 @@ snapshots: '@iconify/types': 2.0.0 import-meta-resolve: 4.2.0 - '@inkjs/ui@2.0.0(ink@6.6.0(@types/react@18.3.23)(react@19.2.3))': + '@inkjs/ui@2.0.0(ink@6.6.0(@types/react@18.3.31)(react@19.2.3))': dependencies: - chalk: 5.4.1 + chalk: 5.6.2 cli-spinners: 3.3.0 deepmerge: 4.3.1 figures: 6.1.0 - ink: 6.6.0(@types/react@18.3.23)(react@19.2.3) + ink: 6.6.0(@types/react@18.3.31)(react@19.2.3) - '@inquirer/external-editor@1.0.2(@types/node@24.2.1)': + '@inquirer/external-editor@1.0.2(@types/node@20.19.43)': dependencies: chardet: 2.1.0 iconv-lite: 0.7.0 optionalDependencies: - '@types/node': 24.2.1 - - '@ioredis/commands@1.3.0': {} + '@types/node': 20.19.43 '@isaacs/cliui@8.0.2': dependencies: @@ -10573,8 +9669,6 @@ snapshots: dependencies: minipass: 7.1.2 - '@istanbuljs/schema@0.1.3': {} - '@jest/expect-utils@29.7.0': dependencies: jest-get-type: 29.6.3 @@ -10588,7 +9682,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.2.1 + '@types/node': 20.19.43 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -10604,8 +9698,6 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -10621,16 +9713,16 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} - '@lmstudio/lms-isomorphic@0.4.5': + '@lmstudio/lms-isomorphic@0.4.6': dependencies: - ws: 8.18.2 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@lmstudio/sdk@1.2.0': + '@lmstudio/sdk@1.5.0': dependencies: - '@lmstudio/lms-isomorphic': 0.4.5 + '@lmstudio/lms-isomorphic': 0.4.6 chalk: 4.1.2 jsonschema: 1.5.0 zod: 3.25.76 @@ -10659,7 +9751,7 @@ snapshots: dependencies: unist-util-visit: 1.4.1 - '@mermaid-js/parser@1.1.1': + '@mermaid-js/parser@1.2.0': dependencies: '@chevrotain/types': 11.1.2 @@ -10682,34 +9774,38 @@ snapshots: dependencies: exenv-es6: 1.1.1 - '@mistralai/mistralai@1.9.18(zod@3.25.76)': + '@mistralai/mistralai@1.15.1': dependencies: + ws: 8.21.0 zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - utf-8-validate - '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.23) - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 eventsource: 3.0.7 - eventsource-parser: 3.0.8 + eventsource-parser: 3.1.0 express: 5.2.1 express-rate-limit: 8.5.2(express@5.2.1) hono: 4.12.23 jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 - raw-body: 3.0.0 + raw-body: 3.0.2 zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - supports-color - '@mswjs/interceptors@0.39.6': + '@mswjs/interceptors@0.41.9': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -10720,16 +9816,23 @@ snapshots: '@napi-rs/wasm-runtime@0.2.11': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 '@tybys/wasm-util': 0.9.0 optional: true - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true '@next/eslint-plugin-next@15.3.2': @@ -10828,57 +9931,102 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.137.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': + optional: true + + '@oxc-resolver/binding-android-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.21.3': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': + optional: true - '@oxc-resolver/binding-darwin-arm64@11.2.0': + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': optional: true - '@oxc-resolver/binding-darwin-x64@11.2.0': + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': optional: true - '@oxc-resolver/binding-freebsd-x64@11.2.0': + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': optional: true - '@oxc-resolver/binding-linux-arm-gnueabihf@11.2.0': + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': optional: true - '@oxc-resolver/binding-linux-arm64-gnu@11.2.0': + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': optional: true - '@oxc-resolver/binding-linux-arm64-musl@11.2.0': + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': optional: true - '@oxc-resolver/binding-linux-riscv64-gnu@11.2.0': + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': optional: true - '@oxc-resolver/binding-linux-s390x-gnu@11.2.0': + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': optional: true - '@oxc-resolver/binding-linux-x64-gnu@11.2.0': + '@oxc-resolver/binding-linux-x64-musl@11.21.3': optional: true - '@oxc-resolver/binding-linux-x64-musl@11.2.0': + '@oxc-resolver/binding-openharmony-arm64@11.21.3': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.2.0': + '@oxc-resolver/binding-wasm32-wasi@11.21.3': dependencies: - '@napi-rs/wasm-runtime': 0.2.11 + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) optional: true - '@oxc-resolver/binding-win32-arm64-msvc@11.2.0': + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': optional: true - '@oxc-resolver/binding-win32-x64-msvc@11.2.0': + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': optional: true '@polka/url@1.0.0-next.29': {} - '@qdrant/js-client-rest@1.14.0(typescript@5.8.3)': + '@posthog/core@1.38.0': + dependencies: + '@posthog/types': 1.391.1 + + '@posthog/types@1.391.1': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + + '@qdrant/js-client-rest@1.18.0(typescript@5.9.3)': dependencies: '@qdrant/openapi-typescript-fetch': 1.2.6 - '@sevinf/maybe': 0.5.0 - typescript: 5.8.3 - undici: 6.24.0 + typescript: 5.9.3 + undici: 6.27.0 '@qdrant/openapi-typescript-fetch@1.2.6': {} @@ -10888,561 +10036,561 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-alert-dialog@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-alert-dialog@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-arrow@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-arrow@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-checkbox@1.3.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-checkbox@1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-collapsible@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collapsible@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-collection@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-context@1.1.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-context@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-dialog@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dialog@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll: 2.6.3(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-direction@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-direction@1.1.1(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-dropdown-menu@2.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dropdown-menu@2.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-menu': 2.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-menu': 2.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-focus-scope@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) '@radix-ui/react-icons@1.3.2(react@18.3.1)': dependencies: react: 18.3.1 - '@radix-ui/react-id@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-id@1.1.1(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-menu@2.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-menu@2.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll: 2.6.3(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-popover@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popover@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll: 2.6.3(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-popper@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popper@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-arrow': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.31)(react@18.3.1) '@radix-ui/rect': 1.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-portal@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-primitive@2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-progress@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-progress@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-roving-focus@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-select@2.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-select@2.2.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll: 2.6.3(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-separator@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-separator@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-slider@1.3.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-slider@1.3.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-collection': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-slot@1.2.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-slot@1.2.2(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-slot@1.2.3(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-tooltip@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tooltip@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.31)(react@18.3.1)': dependencies: '@radix-ui/rect': 1.1.1 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-use-size@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@radix-ui/react-visually-hidden@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) '@radix-ui/rect@1.1.1': {} - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.3': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.3': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.3': optional: true '@rolldown/pluginutils@1.0.0-rc.3': {} @@ -11526,347 +10674,166 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@sevinf/maybe@0.5.0': {} - - '@shikijs/core@3.4.1': - dependencies: - '@shikijs/types': 3.4.1 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@3.4.1': - dependencies: - '@shikijs/types': 3.4.1 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.3 - - '@shikijs/engine-oniguruma@3.4.1': - dependencies: - '@shikijs/types': 3.4.1 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@3.4.1': - dependencies: - '@shikijs/types': 3.4.1 - - '@shikijs/themes@3.4.1': + '@secretlint/config-creator@10.2.2': dependencies: - '@shikijs/types': 3.4.1 + '@secretlint/types': 10.2.2 - '@shikijs/types@3.4.1': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - - '@simple-git/args-pathspec@1.0.3': {} - - '@simple-git/argv-parser@1.1.1': + '@secretlint/config-loader@10.2.2': dependencies: - '@simple-git/args-pathspec': 1.0.3 - - '@sinclair/typebox@0.27.8': {} - - '@sindresorhus/merge-streams@4.0.0': {} - - '@smithy/abort-controller@4.2.4': - dependencies: - '@smithy/types': 4.8.1 - tslib: 2.8.1 - - '@smithy/config-resolver@4.4.2': - dependencies: - '@smithy/node-config-provider': 4.3.4 - '@smithy/types': 4.8.1 - '@smithy/util-config-provider': 4.2.0 - '@smithy/util-endpoints': 3.2.4 - '@smithy/util-middleware': 4.2.4 - tslib: 2.8.1 - - '@smithy/core@3.17.2': - dependencies: - '@smithy/middleware-serde': 4.2.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/types': 4.8.1 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-middleware': 4.2.4 - '@smithy/util-stream': 4.5.5 - '@smithy/util-utf8': 4.2.0 - '@smithy/uuid': 1.1.0 - tslib: 2.8.1 - - '@smithy/core@3.24.3': - dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.14.2 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.2.4': - dependencies: - '@smithy/node-config-provider': 4.3.4 - '@smithy/property-provider': 4.2.4 - '@smithy/types': 4.8.1 - '@smithy/url-parser': 4.2.4 - tslib: 2.8.1 - - '@smithy/eventstream-codec@4.3.3': - dependencies: - '@smithy/core': 3.24.3 - tslib: 2.8.1 - - '@smithy/eventstream-serde-browser@4.2.4': - dependencies: - '@smithy/eventstream-serde-universal': 4.2.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-config-resolver@4.3.4': - dependencies: - '@smithy/types': 4.8.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-node@4.2.4': - dependencies: - '@smithy/eventstream-serde-universal': 4.2.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-universal@4.2.4': - dependencies: - '@smithy/eventstream-codec': 4.3.3 - '@smithy/types': 4.8.1 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.3.5': - dependencies: - '@smithy/protocol-http': 5.3.4 - '@smithy/querystring-builder': 4.2.4 - '@smithy/types': 4.8.1 - '@smithy/util-base64': 4.3.0 - tslib: 2.8.1 - - '@smithy/hash-node@4.2.4': - dependencies: - '@smithy/types': 4.8.1 - '@smithy/util-buffer-from': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@smithy/invalid-dependency@4.2.4': - dependencies: - '@smithy/types': 4.8.1 - tslib: 2.8.1 - - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/is-array-buffer@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/middleware-content-length@4.2.4': - dependencies: - '@smithy/protocol-http': 5.3.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 - - '@smithy/middleware-endpoint@4.3.6': - dependencies: - '@smithy/core': 3.17.2 - '@smithy/middleware-serde': 4.2.4 - '@smithy/node-config-provider': 4.3.4 - '@smithy/shared-ini-file-loader': 4.3.4 - '@smithy/types': 4.8.1 - '@smithy/url-parser': 4.2.4 - '@smithy/util-middleware': 4.2.4 - tslib: 2.8.1 - - '@smithy/middleware-retry@4.4.6': - dependencies: - '@smithy/node-config-provider': 4.3.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/service-error-classification': 4.2.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 - '@smithy/util-middleware': 4.2.4 - '@smithy/util-retry': 4.2.4 - '@smithy/uuid': 1.1.0 - tslib: 2.8.1 + '@secretlint/profiler': 10.2.2 + '@secretlint/resolver': 10.2.2 + '@secretlint/types': 10.2.2 + ajv: 8.20.0 + debug: 4.4.3(supports-color@8.1.1) + rc-config-loader: 4.1.4 + transitivePeerDependencies: + - supports-color - '@smithy/middleware-serde@4.2.4': + '@secretlint/core@10.2.2': dependencies: - '@smithy/protocol-http': 5.3.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 + '@secretlint/profiler': 10.2.2 + '@secretlint/types': 10.2.2 + debug: 4.4.3(supports-color@8.1.1) + structured-source: 4.0.0 + transitivePeerDependencies: + - supports-color - '@smithy/middleware-stack@4.2.4': + '@secretlint/formatter@10.2.2': dependencies: - '@smithy/types': 4.8.1 - tslib: 2.8.1 + '@secretlint/resolver': 10.2.2 + '@secretlint/types': 10.2.2 + '@textlint/linter-formatter': 15.7.1 + '@textlint/module-interop': 15.7.1 + '@textlint/types': 15.7.1 + chalk: 5.6.2 + debug: 4.4.3(supports-color@8.1.1) + pluralize: 8.0.0 + strip-ansi: 7.2.0 + table: 6.9.0 + terminal-link: 4.0.0 + transitivePeerDependencies: + - supports-color - '@smithy/node-config-provider@4.3.4': + '@secretlint/node@10.2.2': dependencies: - '@smithy/property-provider': 4.2.4 - '@smithy/shared-ini-file-loader': 4.3.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 + '@secretlint/config-loader': 10.2.2 + '@secretlint/core': 10.2.2 + '@secretlint/formatter': 10.2.2 + '@secretlint/profiler': 10.2.2 + '@secretlint/source-creator': 10.2.2 + '@secretlint/types': 10.2.2 + debug: 4.4.3(supports-color@8.1.1) + p-map: 7.0.4 + transitivePeerDependencies: + - supports-color - '@smithy/node-http-handler@4.4.4': - dependencies: - '@smithy/abort-controller': 4.2.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/querystring-builder': 4.2.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 + '@secretlint/profiler@10.2.2': {} - '@smithy/property-provider@4.2.4': - dependencies: - '@smithy/types': 4.8.1 - tslib: 2.8.1 + '@secretlint/resolver@10.2.2': {} - '@smithy/protocol-http@5.3.4': + '@secretlint/secretlint-formatter-sarif@10.2.2': dependencies: - '@smithy/types': 4.8.1 - tslib: 2.8.1 + node-sarif-builder: 3.4.0 - '@smithy/querystring-builder@4.2.4': + '@secretlint/secretlint-rule-no-dotenv@10.2.2': dependencies: - '@smithy/types': 4.8.1 - '@smithy/util-uri-escape': 4.2.0 - tslib: 2.8.1 + '@secretlint/types': 10.2.2 - '@smithy/querystring-parser@4.2.4': - dependencies: - '@smithy/types': 4.8.1 - tslib: 2.8.1 + '@secretlint/secretlint-rule-preset-recommend@10.2.2': {} - '@smithy/service-error-classification@4.2.4': + '@secretlint/source-creator@10.2.2': dependencies: - '@smithy/types': 4.8.1 + '@secretlint/types': 10.2.2 + istextorbinary: 9.5.0 - '@smithy/shared-ini-file-loader@4.3.4': - dependencies: - '@smithy/types': 4.8.1 - tslib: 2.8.1 + '@secretlint/types@10.2.2': {} - '@smithy/signature-v4@5.3.4': + '@shikijs/core@3.4.1': dependencies: - '@smithy/is-array-buffer': 4.2.0 - '@smithy/protocol-http': 5.3.4 - '@smithy/types': 4.8.1 - '@smithy/util-hex-encoding': 4.2.0 - '@smithy/util-middleware': 4.2.4 - '@smithy/util-uri-escape': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 + '@shikijs/types': 3.4.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 - '@smithy/smithy-client@4.9.2': + '@shikijs/engine-javascript@3.4.1': dependencies: - '@smithy/core': 3.17.2 - '@smithy/middleware-endpoint': 4.3.6 - '@smithy/middleware-stack': 4.2.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/types': 4.8.1 - '@smithy/util-stream': 4.5.5 - tslib: 2.8.1 + '@shikijs/types': 3.4.1 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 - '@smithy/types@4.14.2': + '@shikijs/engine-oniguruma@3.4.1': dependencies: - tslib: 2.8.1 + '@shikijs/types': 3.4.1 + '@shikijs/vscode-textmate': 10.0.2 - '@smithy/types@4.8.1': + '@shikijs/langs@3.4.1': dependencies: - tslib: 2.8.1 + '@shikijs/types': 3.4.1 - '@smithy/url-parser@4.2.4': + '@shikijs/themes@3.4.1': dependencies: - '@smithy/querystring-parser': 4.2.4 - '@smithy/types': 4.8.1 - tslib: 2.8.1 + '@shikijs/types': 3.4.1 - '@smithy/util-base64@4.3.0': + '@shikijs/types@3.4.1': dependencies: - '@smithy/util-buffer-from': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 - '@smithy/util-body-length-browser@4.2.0': - dependencies: - tslib: 2.8.1 + '@shikijs/vscode-textmate@10.0.2': {} - '@smithy/util-body-length-node@4.2.1': - dependencies: - tslib: 2.8.1 + '@simple-git/args-pathspec@1.0.3': {} - '@smithy/util-buffer-from@2.2.0': + '@simple-git/argv-parser@1.1.1': dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 + '@simple-git/args-pathspec': 1.0.3 - '@smithy/util-buffer-from@4.2.0': - dependencies: - '@smithy/is-array-buffer': 4.2.0 - tslib: 2.8.1 + '@sinclair/typebox@0.27.8': {} - '@smithy/util-config-provider@4.2.0': - dependencies: - tslib: 2.8.1 + '@sindresorhus/merge-streams@2.3.0': {} + + '@sindresorhus/merge-streams@4.0.0': {} - '@smithy/util-defaults-mode-browser@4.3.5': + '@smithy/core@3.26.0': dependencies: - '@smithy/property-provider': 4.2.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.8': + '@smithy/credential-provider-imds@4.4.2': dependencies: - '@smithy/config-resolver': 4.4.2 - '@smithy/credential-provider-imds': 4.2.4 - '@smithy/node-config-provider': 4.3.4 - '@smithy/property-provider': 4.2.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@smithy/util-endpoints@3.2.4': + '@smithy/fetch-http-handler@5.5.2': dependencies: - '@smithy/node-config-provider': 4.3.4 - '@smithy/types': 4.8.1 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@smithy/util-hex-encoding@4.2.0': + '@smithy/is-array-buffer@2.2.0': dependencies: tslib: 2.8.1 - '@smithy/util-middleware@4.2.4': + '@smithy/node-http-handler@4.8.2': dependencies: - '@smithy/types': 4.8.1 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@smithy/util-retry@4.2.4': + '@smithy/signature-v4@5.5.2': dependencies: - '@smithy/service-error-classification': 4.2.4 - '@smithy/types': 4.8.1 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@smithy/util-stream@4.5.5': + '@smithy/types@4.15.0': dependencies: - '@smithy/fetch-http-handler': 5.3.5 - '@smithy/node-http-handler': 4.4.4 - '@smithy/types': 4.8.1 - '@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.8.1 - '@smithy/util-uri-escape@4.2.0': + '@smithy/util-buffer-from@2.2.0': dependencies: + '@smithy/is-array-buffer': 2.2.0 tslib: 2.8.1 '@smithy/util-utf8@2.3.0': @@ -11874,19 +10841,7 @@ snapshots: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@smithy/util-utf8@4.2.0': - dependencies: - '@smithy/util-buffer-from': 4.2.0 - tslib: 2.8.1 - - '@smithy/util-utf8@4.3.3': - dependencies: - '@smithy/core': 3.24.3 - tslib: 2.8.1 - - '@smithy/uuid@1.1.0': - dependencies: - tslib: 2.8.1 + '@stablelib/base64@1.0.1': {} '@standard-schema/spec@1.1.0': {} @@ -11894,7 +10849,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 enhanced-resolve: 5.18.1 - jiti: 2.4.2 + jiti: 2.7.0 lightningcss: 1.29.2 magic-string: 0.30.21 source-map-js: 1.2.1 @@ -11938,7 +10893,7 @@ snapshots: '@tailwindcss/oxide@4.1.6': dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 tar: 7.4.3 optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.6 @@ -11954,12 +10909,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.6 '@tailwindcss/oxide-win32-x64-msvc': 4.1.6 - '@tailwindcss/vite@4.1.6(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0))': + '@tailwindcss/vite@4.1.6(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.1.6 '@tailwindcss/oxide': 4.1.6 tailwindcss: 4.1.6 - vite: 8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0) + vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) '@tanstack/query-core@5.76.0': {} @@ -11979,58 +10934,86 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.6.3': + '@testing-library/jest-dom@6.9.1': dependencies: '@adobe/css-tools': 4.4.2 aria-query: 5.3.2 - chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 - lodash: 4.17.21 + picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.29.7 '@testing-library/dom': 10.4.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': dependencies: '@testing-library/dom': 10.4.0 - '@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.8.3))(typescript@5.8.3)': + '@textlint/ast-node-types@15.7.1': {} + + '@textlint/linter-formatter@15.7.1': + dependencies: + '@azu/format-text': 1.0.2 + '@azu/style-format': 1.0.1 + '@textlint/module-interop': 15.7.1 + '@textlint/resolver': 15.7.1 + '@textlint/types': 15.7.1 + chalk: 4.1.2 + debug: 4.4.3(supports-color@8.1.1) + js-yaml: 4.2.0 + lodash: 4.18.1 + pluralize: 2.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + table: 6.9.0 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + '@textlint/module-interop@15.7.1': {} + + '@textlint/resolver@15.7.1': {} + + '@textlint/types@15.7.1': + dependencies: + '@textlint/ast-node-types': 15.7.1 + + '@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)': dependencies: - '@trpc/server': 11.8.1(typescript@5.8.3) - typescript: 5.8.3 + '@trpc/server': 11.8.1(typescript@5.9.3) + typescript: 5.9.3 - '@trpc/server@11.8.1(typescript@5.8.3)': + '@trpc/server@11.8.1(typescript@5.9.3)': dependencies: - typescript: 5.8.3 + typescript: 5.9.3 - '@turbo/darwin-64@2.9.14': + '@turbo/darwin-64@2.10.0': optional: true - '@turbo/darwin-arm64@2.9.14': + '@turbo/darwin-arm64@2.10.0': optional: true - '@turbo/linux-64@2.9.14': + '@turbo/linux-64@2.10.0': optional: true - '@turbo/linux-arm64@2.9.14': + '@turbo/linux-arm64@2.10.0': optional: true - '@turbo/windows-64@2.9.14': + '@turbo/windows-64@2.10.0': optional: true - '@turbo/windows-arm64@2.9.14': + '@turbo/windows-arm64@2.10.0': optional: true - '@tybys/wasm-util@0.10.2': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -12222,7 +11205,7 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 - '@types/js-cookie@2.2.7': {} + '@types/js-cookie@3.0.6': {} '@types/json-schema@7.0.15': {} @@ -12250,30 +11233,19 @@ snapshots: dependencies: node-cache: 5.1.2 - '@types/node-fetch@2.6.12': - dependencies: - '@types/node': 24.2.1 - form-data: 4.0.5 - '@types/node-ipc@9.2.3': dependencies: - '@types/node': 24.2.1 + '@types/node': 20.19.43 '@types/node@12.20.55': {} '@types/node@14.18.63': {} - '@types/node@18.19.100': - dependencies: - undici-types: 5.26.5 - - '@types/node@20.19.41': + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 - '@types/node@24.2.1': - dependencies: - undici-types: 7.10.0 + '@types/normalize-package-data@2.4.4': {} '@types/prop-types@15.7.14': {} @@ -12283,17 +11255,21 @@ snapshots: '@types/ps-tree@1.1.6': {} - '@types/react-dom@18.3.7(@types/react@18.3.23)': + '@types/react-dom@18.3.7(@types/react@18.3.31)': dependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - '@types/react@18.3.23': + '@types/react@18.3.31': dependencies: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/retry@0.12.0': {} + '@types/retry@0.12.5': {} + '@types/sarif@2.1.7': {} + '@types/semver-compare@1.0.3': {} '@types/shell-quote@1.7.5': {} @@ -12323,32 +11299,32 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.32.1 - '@typescript-eslint/type-utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.32.1 - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.39.4(jiti@2.7.0) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.32.1 '@typescript-eslint/types': 8.32.1 - '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.32.1 debug: 4.4.3(supports-color@8.1.1) - eslint: 9.27.0(jiti@2.4.2) - typescript: 5.8.3 + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -12357,20 +11333,20 @@ snapshots: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/visitor-keys': 8.32.1 - '@typescript-eslint/type-utils@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 9.27.0(jiti@2.4.2) - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + eslint: 9.39.4(jiti@2.7.0) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.32.1': {} - '@typescript-eslint/typescript-estree@8.32.1(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.32.1(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/visitor-keys': 8.32.1 @@ -12379,19 +11355,19 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.3 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.32.1 '@typescript-eslint/types': 8.32.1 - '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) - eslint: 9.27.0(jiti@2.4.2) - typescript: 5.8.3 + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -12415,9 +11391,9 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vercel/oidc@3.1.0': {} + '@vercel/oidc@3.2.0': {} - '@vitejs/plugin-react@5.2.0(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -12425,14 +11401,14 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0) + vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.1.0(vitest@4.1.0)': + '@vitest/coverage-v8@4.1.9(vitest@4.1.9)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.0 + '@vitest/utils': 4.1.9 ast-v8-to-istanbul: 1.0.4 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -12441,73 +11417,57 @@ snapshots: obug: 2.1.2 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.2.1)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) - '@vitest/expect@4.1.0': + '@vitest/expect@4.1.9': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.0(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 4.1.0 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.3) - - '@vitest/mocker@4.1.0(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.0 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0) - - '@vitest/mocker@4.1.0(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0))': + '@vitest/mocker@4.1.9(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.0 + '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0) + vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) - '@vitest/pretty-format@4.1.0': + '@vitest/pretty-format@4.1.9': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.0': + '@vitest/runner@4.1.9': dependencies: - '@vitest/utils': 4.1.0 + '@vitest/utils': 4.1.9 pathe: 2.0.3 - '@vitest/snapshot@4.1.0': + '@vitest/snapshot@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.0 - '@vitest/utils': 4.1.0 + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.0': {} + '@vitest/spy@4.1.9': {} - '@vitest/ui@4.1.0(vitest@4.1.0)': + '@vitest/ui@4.1.9(vitest@4.1.9)': dependencies: - '@vitest/utils': 4.1.0 + '@vitest/utils': 4.1.9 fflate: 0.8.2 - flatted: 3.4.0 + flatted: 3.4.2 pathe: 2.0.3 sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.2.1)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) - '@vitest/utils@4.1.0': + '@vitest/utils@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.0 + '@vitest/pretty-format': 4.1.9 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -12521,18 +11481,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vscode/test-cli@0.0.11': - dependencies: - '@types/mocha': 10.0.10 - c8: 9.1.0 - chokidar: 3.6.0 - enhanced-resolve: 5.18.1 - glob: 11.1.0 - minimatch: 9.0.5 - mocha: 11.2.2 - supports-color: 9.4.0 - yargs: 17.7.2 - '@vscode/test-electron@2.5.2': dependencies: http-proxy-agent: 7.0.2 @@ -12582,31 +11530,36 @@ snapshots: '@vscode/vsce-sign-win32-arm64': 2.0.2 '@vscode/vsce-sign-win32-x64': 2.0.2 - '@vscode/vsce@3.3.2': + '@vscode/vsce@3.9.2': dependencies: '@azure/identity': 4.9.1 + '@secretlint/node': 10.2.2 + '@secretlint/secretlint-formatter-sarif': 10.2.2 + '@secretlint/secretlint-rule-no-dotenv': 10.2.2 + '@secretlint/secretlint-rule-preset-recommend': 10.2.2 '@vscode/vsce-sign': 2.0.5 azure-devops-node-api: 12.5.0 - chalk: 2.4.2 + chalk: 4.1.2 cheerio: 1.0.0 cockatiel: 3.2.1 commander: 12.1.0 - form-data: 4.0.4 + form-data: 4.0.6 glob: 11.1.0 hosted-git-info: 4.1.0 jsonc-parser: 3.3.1 leven: 3.1.0 markdown-it: 14.1.0 mime: 1.6.0 - minimatch: 3.1.2 + minimatch: 10.2.5 parse-semver: 1.1.1 read: 1.0.7 - semver: 7.7.2 - tmp: 0.2.3 + secretlint: 10.2.2 + semver: 7.7.3 + tmp: 0.2.4 typed-rest-client: 1.8.11 url-join: 4.0.1 xml2js: 0.5.0 - yauzl: 2.10.0 + yauzl: 3.4.0 yazl: 2.5.1 optionalDependencies: keytar: 7.9.0 @@ -12626,10 +11579,6 @@ snapshots: '@xobotyi/scrollbar-width@1.9.5': {} - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - accepts@2.0.0: dependencies: mime-types: 3.0.1 @@ -12641,43 +11590,45 @@ snapshots: acorn@8.15.0: {} - agent-base@7.1.3: {} - - agentkeepalive@4.6.0: + agent-base@6.0.2: dependencies: - humanize-ms: 1.2.1 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.3: {} - ai-sdk-provider-poe@2.0.18(ai@6.0.77(zod@3.25.76))(zod@3.25.76): + ai-sdk-provider-poe@2.0.18(ai@6.0.209(zod@3.25.76))(zod@3.25.76): dependencies: '@ai-sdk/anthropic': 3.0.42(zod@3.25.76) '@ai-sdk/openai': 3.0.27(zod@3.25.76) - '@ai-sdk/openai-compatible': 2.0.37(zod@3.25.76) + '@ai-sdk/openai-compatible': 2.0.51(zod@3.25.76) '@ai-sdk/provider': 3.0.8 '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) - ai: 6.0.77(zod@3.25.76) + ai: 6.0.209(zod@3.25.76) transitivePeerDependencies: - zod - ai@6.0.77(zod@3.25.76): + ai@6.0.209(zod@3.25.76): dependencies: - '@ai-sdk/gateway': 3.0.39(zod@3.25.76) - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) + '@ai-sdk/gateway': 3.0.134(zod@3.25.76) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) '@opentelemetry/api': 1.9.0 zod: 3.25.76 - ajv-formats@3.0.1(ajv@8.18.0): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: - ajv: 8.18.0 + ajv: 8.20.0 - ajv@6.12.6: + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.18.0: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 @@ -12686,10 +11637,6 @@ snapshots: ansi-colors@4.1.3: {} - ansi-escapes@7.2.0: - dependencies: - environment: 1.1.0 - ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -12698,10 +11645,6 @@ snapshots: ansi-regex@6.2.2: {} - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -12716,11 +11659,6 @@ snapshots: any-promise@1.3.0: {} - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - archiver-utils@2.1.0: dependencies: glob: 11.1.0 @@ -12838,6 +11776,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + astral-regex@2.0.0: {} + async-function@1.0.0: {} async-mutex@0.5.0: @@ -12854,15 +11794,15 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - aws4fetch@1.0.20: {} - - axios@1.16.0: + axios@1.18.1: dependencies: follow-redirects: 1.16.0 - form-data: 4.0.5 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: - debug + - supports-color azure-devops-node-api@12.5.0: dependencies: @@ -12919,13 +11859,15 @@ snapshots: bignumber.js@9.3.0: {} - binary-extensions@2.3.0: {} - binary@0.3.0: dependencies: buffers: 0.1.1 chainsaw: 0.1.0 + binaryextensions@6.11.0: + dependencies: + editions: 6.22.0 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -12939,7 +11881,7 @@ snapshots: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3(supports-color@8.1.1) - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.7.0 on-finished: 2.4.1 qs: 6.15.2 @@ -12952,7 +11894,9 @@ snapshots: boolean@3.2.0: {} - bowser@2.11.0: {} + boundary@2.0.0: {} + + bowser@2.14.1: {} brace-expansion@1.1.14: dependencies: @@ -13004,20 +11948,6 @@ snapshots: bytes@3.1.2: {} - c8@9.1.0: - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@istanbuljs/schema': 0.1.3 - find-up: 5.0.0 - foreground-child: 3.3.1 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.1.7 - test-exclude: 6.0.0 - v8-to-istanbul: 9.3.0 - yargs: 17.7.2 - yargs-parser: 21.1.1 - cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -13053,24 +11983,11 @@ snapshots: dependencies: traverse: 0.3.9 - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@3.0.0: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.4.1: {} - chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -13109,21 +12026,9 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 6.24.0 + undici: 6.27.0 whatwg-mimetype: 4.0.0 - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -13152,11 +12057,6 @@ snapshots: cli-spinners@3.3.0: {} - cli-truncate@5.1.1: - dependencies: - slice-ansi: 7.1.0 - string-width: 8.1.0 - cli-truncate@5.2.0: dependencies: slice-ansi: 8.0.0 @@ -13184,14 +12084,12 @@ snapshots: clsx@2.1.1: {} - cluster-key-slot@1.1.2: {} - - cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -13204,16 +12102,10 @@ snapshots: dependencies: convert-to-spaces: 2.0.1 - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} colorette@2.0.20: {} @@ -13278,14 +12170,14 @@ snapshots: copyfiles@2.4.1: dependencies: glob: 11.1.0 - minimatch: 3.1.2 + minimatch: 3.1.5 mkdirp: 1.0.4 noms: 0.0.0 through2: 2.0.5 untildify: 4.0.0 yargs: 16.2.0 - core-js@3.42.0: {} + core-js@3.49.0: {} core-util-is@1.0.3: {} @@ -13351,17 +12243,17 @@ snapshots: csstype@3.1.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.4): + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0): dependencies: cose-base: 1.0.3 - cytoscape: 3.33.4 + cytoscape: 3.34.0 - cytoscape-fcose@2.2.0(cytoscape@3.33.4): + cytoscape-fcose@2.2.0(cytoscape@3.34.0): dependencies: cose-base: 2.2.0 - cytoscape: 3.33.4 + cytoscape: 3.34.0 - cytoscape@3.33.4: {} + cytoscape@3.34.0: {} d3-array@2.12.1: dependencies: @@ -13562,20 +12454,10 @@ snapshots: date-fns@4.1.0: {} - dayjs@1.11.13: {} - dayjs@1.11.21: {} debounce@2.2.0: {} - debug@3.2.7: - dependencies: - ms: 2.1.3 - - debug@4.4.1: - dependencies: - ms: 2.1.3 - debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -13631,16 +12513,12 @@ snapshots: delayed-stream@1.0.0: {} - denque@2.1.0: {} - depd@2.0.0: {} dequal@2.0.3: {} detect-indent@6.1.0: {} - detect-libc@2.0.4: {} - detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -13681,7 +12559,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.7: + dompurify@3.4.11: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -13693,8 +12571,6 @@ snapshots: dotenv@16.0.3: {} - dotenv@16.4.5: {} - dotenv@17.4.2: {} duck@0.1.12: @@ -13728,6 +12604,10 @@ snapshots: '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 + editions@6.22.0: + dependencies: + version-range: 4.15.0 + ee-first@1.1.1: {} electron-to-chromium@1.5.152: {} @@ -13871,9 +12751,7 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.43.0: {} - - es-toolkit@1.47.0: {} + es-toolkit@1.49.0: {} es6-error@4.1.1: {} @@ -13912,25 +12790,23 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@1.0.5: {} - escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.27.0(jiti@2.4.2)): + eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)): dependencies: - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-only-warn@1.1.0: {} + eslint-plugin-only-warn@1.2.1: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.27.0(jiti@2.4.2)): + eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.7.0)): dependencies: - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-react@7.37.5(eslint@9.27.0(jiti@2.4.2)): + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.7.0)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -13938,11 +12814,11 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.39.4(jiti@2.7.0) estraverse: 5.3.0 hasown: 2.0.4 jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 + minimatch: 3.1.5 object.entries: 1.1.9 object.fromentries: 2.0.8 object.values: 1.2.1 @@ -13952,11 +12828,11 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-turbo@2.5.6(eslint@9.27.0(jiti@2.4.2))(turbo@2.9.14): + eslint-plugin-turbo@2.10.0(eslint@9.39.4(jiti@2.7.0))(turbo@2.10.0): dependencies: dotenv: 16.0.3 - eslint: 9.27.0(jiti@2.4.2) - turbo: 2.9.14 + eslint: 9.39.4(jiti@2.7.0) + turbo: 2.10.0 eslint-scope@8.4.0: dependencies: @@ -13967,64 +12843,21 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.27.0(jiti@2.4.2): - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.2 - '@eslint/core': 0.14.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.27.0 - '@eslint/plugin-kit': 0.3.1 - '@humanfs/node': 0.16.6 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@8.1.1) - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.4.2 - transitivePeerDependencies: - - supports-color - - eslint@9.28.0(jiti@2.4.2): + eslint@9.39.4(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.2 - '@eslint/core': 0.14.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.28.0 - '@eslint/plugin-kit': 0.3.1 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 + ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3(supports-color@8.1.1) @@ -14043,11 +12876,11 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.4.2 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -14094,22 +12927,18 @@ snapshots: stream-combiner: 0.0.4 through: 2.3.8 - event-target-shim@5.0.1: {} - eventemitter3@5.0.4: {} - eventsource-parser@3.0.6: {} - - eventsource-parser@3.0.8: {} + eventsource-parser@3.1.0: {} eventsource@3.0.7: dependencies: - eventsource-parser: 3.0.8 + eventsource-parser: 3.1.0 exceljs@4.4.0: dependencies: archiver: 5.3.2 - dayjs: 1.11.13 + dayjs: 1.11.21 fast-csv: 4.3.6 jszip: 3.10.1 readable-stream: 3.6.2 @@ -14142,7 +12971,7 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 - execa@9.5.3: + execa@9.6.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 cross-spawn: 7.0.6 @@ -14152,26 +12981,11 @@ snapshots: is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 6.0.0 - pretty-ms: 9.2.0 + pretty-ms: 9.3.0 signal-exit: 4.1.0 strip-final-newline: 4.0.0 yoctocolors: 2.1.2 - execa@9.6.0: - dependencies: - '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.6 - figures: 6.1.0 - get-stream: 9.0.1 - human-signals: 8.0.1 - is-plain-obj: 4.1.0 - is-stream: 4.0.1 - npm-run-path: 6.0.0 - pretty-ms: 9.2.0 - signal-exit: 4.1.0 - strip-final-newline: 4.0.0 - yoctocolors: 2.1.1 - exenv-es6@1.1.1: {} expand-template@2.0.3: @@ -14207,19 +13021,19 @@ snapshots: etag: 1.8.1 finalhandler: 2.1.0 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 mime-types: 3.0.1 on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.15.2 range-parser: 1.2.1 router: 2.2.0 send: 1.2.0 serve-static: 2.2.0 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: @@ -14263,14 +13077,12 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} + fast-shallow-equal@1.0.0: {} fast-uri@3.1.0: {} - fast-xml-parser@5.2.5: - dependencies: - strnum: 2.1.1 - fastest-levenshtein@1.0.16: {} fastest-stable-stringify@2.0.2: {} @@ -14319,7 +13131,7 @@ snapshots: escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -14341,16 +13153,12 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.3 + flatted: 3.4.2 keyv: 4.5.4 flat@5.0.2: {} - flatted@3.3.3: {} - - flatted@3.4.0: {} - - follow-redirects@1.15.9: {} + flatted@3.4.2: {} follow-redirects@1.16.0: {} @@ -14363,17 +13171,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data-encoder@1.7.2: {} - - form-data@4.0.4: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - form-data@4.0.5: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -14385,11 +13183,6 @@ snapshots: dependencies: fd-package-json: 2.0.0 - formdata-node@4.4.1: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 4.0.0-beta.3 - formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -14402,6 +13195,12 @@ snapshots: fs-constants@1.0.0: {} + fs-extra@11.3.5: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -14452,12 +13251,11 @@ snapshots: - encoding - supports-color - gaxios@7.1.3: + gaxios@7.1.5: dependencies: extend: 3.0.2 https-proxy-agent: 7.0.6 node-fetch: 3.3.2 - rimraf: 5.0.10 transitivePeerDependencies: - supports-color @@ -14472,7 +13270,7 @@ snapshots: gcp-metadata@8.1.2: dependencies: - gaxios: 7.1.3 + gaxios: 7.1.5 google-logging-utils: 1.1.3 json-bigint: 1.0.0 transitivePeerDependencies: @@ -14482,8 +13280,6 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.3.0: {} - get-east-asian-width@1.6.0: {} get-folder-size@5.0.0: {} @@ -14523,10 +13319,6 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.10.0: - dependencies: - resolve-pkg-maps: 1.0.0 - github-from-package@0.0.0: optional: true @@ -14576,15 +13368,23 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - google-auth-library@10.5.0: + globby@14.1.0: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + path-type: 6.0.0 + slash: 5.1.0 + unicorn-magic: 0.3.0 + + google-auth-library@10.9.0: dependencies: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 - gaxios: 7.1.3 + gaxios: 7.1.5 gcp-metadata: 8.1.2 google-logging-utils: 1.1.3 - gtoken: 8.0.0 - jws: 4.0.0 + jws: 4.0.1 transitivePeerDependencies: - supports-color @@ -14595,7 +13395,7 @@ snapshots: gaxios: 6.7.1 gcp-metadata: 6.1.1 gtoken: 7.1.0 - jws: 4.0.0 + jws: 4.0.1 transitivePeerDependencies: - encoding - supports-color @@ -14620,24 +13420,15 @@ snapshots: gtoken@7.1.0: dependencies: gaxios: 6.7.1 - jws: 4.0.0 + jws: 4.0.1 transitivePeerDependencies: - encoding - supports-color - gtoken@8.0.0: - dependencies: - gaxios: 7.1.3 - jws: 4.0.0 - transitivePeerDependencies: - - supports-color - hachure-fill@0.5.2: {} has-bigints@1.1.0: {} - has-flag@3.0.0: {} - has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -14654,10 +13445,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -14768,14 +13555,16 @@ snapshots: he@1.2.0: {} - highlight.js@11.11.1: {} - hono@4.12.23: {} hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + howler@2.2.4: {} html-encoding-sniffer@4.0.0: @@ -14796,16 +13585,8 @@ snapshots: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.2.2 - entities: 4.5.0 - - 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 + domutils: 3.2.2 + entities: 4.5.0 http-errors@2.0.1: dependencies: @@ -14822,6 +13603,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 @@ -14837,19 +13625,15 @@ snapshots: human-signals@8.0.1: {} - humanize-ms@1.2.1: - dependencies: - ms: 2.1.3 - husky@9.1.7: {} hyphenate-style-name@1.1.0: {} - i18next@25.2.1(typescript@5.8.3): + i18next@25.2.1(typescript@5.9.3): dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.29.7 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 iconv-lite@0.6.3: dependencies: @@ -14863,8 +13647,6 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.4: {} - ignore@7.0.5: {} immediate@3.0.6: {} @@ -14882,43 +13664,45 @@ snapshots: indent-string@5.0.0: {} + index-to-position@1.2.0: {} + inherits@2.0.4: {} ini@1.3.8: optional: true - ink-testing-library@4.0.0(@types/react@18.3.23): + ink-testing-library@4.0.0(@types/react@18.3.31): optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - ink@6.6.0(@types/react@18.3.23)(react@19.2.3): + ink@6.6.0(@types/react@18.3.31)(react@19.2.3): dependencies: '@alcalzone/ansi-tokenize': 0.2.3 - ansi-escapes: 7.2.0 + ansi-escapes: 7.3.0 ansi-styles: 6.2.3 auto-bind: 5.0.1 chalk: 5.6.2 cli-boxes: 3.0.0 cli-cursor: 4.0.0 - cli-truncate: 5.1.1 + cli-truncate: 5.2.0 code-excerpt: 4.0.0 - es-toolkit: 1.43.0 + es-toolkit: 1.49.0 indent-string: 5.0.0 is-in-ci: 2.0.0 patch-console: 2.0.0 react: 19.2.3 react-reconciler: 0.33.0(react@19.2.3) signal-exit: 3.0.7 - slice-ansi: 7.1.0 + slice-ansi: 7.1.2 stack-utils: 2.0.6 - string-width: 8.1.0 + string-width: 8.2.1 type-fest: 4.41.0 widest-line: 5.0.0 - wrap-ansi: 9.0.0 - ws: 8.18.3 + wrap-ansi: 9.0.2 + ws: 8.21.0 yoga-layout: 3.2.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -14941,20 +13725,6 @@ snapshots: internmap@2.0.3: {} - ioredis@5.6.1: - dependencies: - '@ioredis/commands': 1.3.0 - cluster-key-slot: 1.1.2 - debug: 4.4.1 - 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 - transitivePeerDependencies: - - supports-color - ip-address@10.2.0: {} ipaddr.js@1.9.1: {} @@ -14991,10 +13761,6 @@ snapshots: dependencies: has-bigints: 1.1.0 - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -15039,10 +13805,6 @@ snapshots: is-fullwidth-code-point@3.0.0: {} - is-fullwidth-code-point@5.0.0: - dependencies: - get-east-asian-width: 1.3.0 - is-fullwidth-code-point@5.1.0: dependencies: get-east-asian-width: 1.6.0 @@ -15183,15 +13945,16 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-reports@3.1.7: + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - istanbul-reports@3.2.0: + istextorbinary@9.5.0: dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 + binaryextensions: 6.11.0 + editions: 6.22.0 + textextensions: 6.11.0 iterator.prototype@1.1.5: dependencies: @@ -15237,19 +14000,19 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.2.1 + '@types/node': 20.19.43 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 picomatch: 2.3.2 - jiti@2.4.2: {} + jiti@2.7.0: {} jose@6.2.3: {} joycon@3.1.1: {} - js-cookie@2.2.1: {} + js-cookie@3.0.8: {} js-message@1.0.7: {} @@ -15266,7 +14029,7 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: + js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -15290,7 +14053,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.20.1 + ws: 8.21.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -15305,6 +14068,11 @@ snapshots: json-buffer@3.0.1: {} + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -15327,11 +14095,17 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jsonschema@1.5.0: {} jsonwebtoken@9.0.2: dependencies: - jws: 3.2.2 + jws: 3.2.3 lodash.includes: 4.3.0 lodash.isboolean: 3.0.3 lodash.isinteger: 4.0.4 @@ -15368,22 +14142,18 @@ snapshots: ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jws@3.2.2: + jws@3.2.3: dependencies: jwa: 1.4.2 safe-buffer: 5.2.1 - jws@4.0.0: + jws@4.0.1: dependencies: jwa: 2.0.1 safe-buffer: 5.2.1 jwt-decode@4.0.0: {} - katex@0.16.22: - dependencies: - commander: 8.3.0 - katex@0.16.47: dependencies: commander: 8.3.0 @@ -15404,23 +14174,23 @@ snapshots: kind-of@6.0.3: {} - knip@5.60.2(@types/node@24.2.1)(typescript@5.8.3): + knip@5.60.2(@types/node@20.19.43)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 24.2.1 + '@types/node': 20.19.43 fast-glob: 3.3.3 formatly: 0.2.4 - jiti: 2.4.2 - js-yaml: 4.1.0 + jiti: 2.7.0 + js-yaml: 4.2.0 minimist: 1.2.8 - oxc-resolver: 11.2.0 + oxc-resolver: 11.21.3 picocolors: 1.1.1 picomatch: 4.0.4 - smol-toml: 1.3.4 + smol-toml: 1.7.0 strip-json-comments: 5.0.2 - typescript: 5.8.3 + typescript: 5.9.3 zod: 3.25.76 - zod-validation-error: 3.4.1(zod@3.25.76) + zod-validation-error: 3.5.4(zod@3.25.76) layout-base@1.0.2: {} @@ -15549,7 +14319,7 @@ snapshots: listr2: 9.0.5 picomatch: 4.0.4 string-argv: 0.3.2 - tinyexec: 1.2.3 + tinyexec: 1.2.4 yaml: 2.9.0 listenercount@1.0.1: {} @@ -15589,8 +14359,6 @@ snapshots: lodash.includes@4.3.0: {} - lodash.isarguments@3.1.0: {} - lodash.isboolean@3.0.3: {} lodash.isequal@4.5.0: {} @@ -15617,11 +14385,13 @@ snapshots: lodash.startcase@4.4.0: {} + lodash.truncate@4.4.2: {} + lodash.union@4.6.0: {} lodash.uniq@4.5.0: {} - lodash@4.17.21: {} + lodash@4.18.1: {} log-symbols@4.1.0: dependencies: @@ -15641,6 +14411,8 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -15653,16 +14425,8 @@ snapshots: option: 0.2.4 underscore: 1.13.8 - lowlight@3.3.0: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - highlight.js: 11.11.1 - lru-cache@10.4.3: {} - lru-cache@11.1.0: {} - lru-cache@11.2.2: {} lru-cache@5.1.1: @@ -15935,29 +14699,29 @@ snapshots: merge2@1.4.1: {} - mermaid@11.15.0: + mermaid@11.16.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.3 - '@mermaid-js/parser': 1.1.1 + '@mermaid-js/parser': 1.2.0 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 - cytoscape: 3.33.4 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.4) - cytoscape-fcose: 2.2.0(cytoscape@3.33.4) + cytoscape: 3.34.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.0) + cytoscape-fcose: 2.2.0(cytoscape@3.34.0) d3: 7.9.0 d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.21 - dompurify: 3.4.7 - es-toolkit: 1.47.0 + dompurify: 3.4.11 + es-toolkit: 1.49.0 katex: 0.16.47 khroma: 2.1.0 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.4.0 - ts-dedent: 2.2.0 - uuid: 14.0.0 + ts-dedent: 2.3.0 + uuid: 11.1.1 micromark-core-commonmark@2.0.3: dependencies: @@ -16040,7 +14804,7 @@ snapshots: dependencies: '@types/katex': 0.16.7 devlop: 1.1.0 - katex: 0.16.22 + katex: 0.16.47 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 @@ -16201,7 +14965,7 @@ snapshots: dependencies: brace-expansion: 5.0.5 - minimatch@3.1.2: + minimatch@3.1.5: dependencies: brace-expansion: 1.1.14 @@ -16249,7 +15013,7 @@ snapshots: find-up: 5.0.0 glob: 11.1.0 he: 1.2.0 - js-yaml: 4.1.0 + js-yaml: 4.2.0 log-symbols: 4.1.0 minimatch: 5.1.6 ms: 2.1.3 @@ -16284,7 +15048,7 @@ snapshots: nano-css@5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 css-tree: 1.1.3 csstype: 3.1.3 fastest-stable-stringify: 2.0.2 @@ -16295,8 +15059,6 @@ snapshots: stacktrace-js: 2.0.2 stylis: 4.4.0 - nanoid@3.3.11: {} - nanoid@3.3.12: {} napi-build-utils@2.0.0: @@ -16306,9 +15068,9 @@ snapshots: negotiator@1.0.0: {} - nock@14.0.10: + nock@14.0.15: dependencies: - '@mswjs/interceptors': 0.39.6 + '@mswjs/interceptors': 0.41.9 json-stringify-safe: 5.0.1 propagate: 2.0.1 @@ -16347,11 +15109,22 @@ snapshots: node-releases@2.0.19: {} + node-sarif-builder@3.4.0: + dependencies: + '@types/sarif': 2.1.7 + fs-extra: 11.3.5 + noms@0.0.0: dependencies: inherits: 2.0.4 readable-stream: 1.0.34 + normalize-package-data@6.0.2: + dependencies: + hosted-git-info: 7.0.2 + semver: 7.7.3 + validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} npm-run-path@4.0.1: @@ -16413,7 +15186,7 @@ snapshots: obug@2.1.2: {} - ollama@0.5.17: + ollama@0.6.3: dependencies: whatwg-fetch: 3.6.20 @@ -16447,10 +15220,6 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 - only-allow@1.2.1: - dependencies: - which-pm-runs: 1.1.0 - open@10.1.2: dependencies: default-browser: 5.2.1 @@ -16458,9 +15227,9 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - openai@5.12.2(ws@8.20.1)(zod@3.25.76): + openai@5.23.2(ws@8.21.0)(zod@3.25.76): optionalDependencies: - ws: 8.20.1 + ws: 8.21.0 zod: 3.25.76 option@0.2.4: {} @@ -16495,15 +15264,15 @@ snapshots: outvariant@1.4.3: {} - ovsx@0.10.4: + ovsx@0.10.12: dependencies: - '@vscode/vsce': 3.3.2 + '@vscode/vsce': 3.9.2 commander: 6.2.1 - follow-redirects: 1.15.9 + follow-redirects: 1.16.0 is-ci: 2.0.0 leven: 3.1.0 - semver: 7.7.2 - tmp: 0.2.3 + semver: 7.7.3 + tmp: 0.2.4 yauzl-promise: 4.0.0 transitivePeerDependencies: - bare-buffer @@ -16516,21 +15285,27 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - oxc-resolver@11.2.0: + oxc-resolver@11.21.3: optionalDependencies: - '@oxc-resolver/binding-darwin-arm64': 11.2.0 - '@oxc-resolver/binding-darwin-x64': 11.2.0 - '@oxc-resolver/binding-freebsd-x64': 11.2.0 - '@oxc-resolver/binding-linux-arm-gnueabihf': 11.2.0 - '@oxc-resolver/binding-linux-arm64-gnu': 11.2.0 - '@oxc-resolver/binding-linux-arm64-musl': 11.2.0 - '@oxc-resolver/binding-linux-riscv64-gnu': 11.2.0 - '@oxc-resolver/binding-linux-s390x-gnu': 11.2.0 - '@oxc-resolver/binding-linux-x64-gnu': 11.2.0 - '@oxc-resolver/binding-linux-x64-musl': 11.2.0 - '@oxc-resolver/binding-wasm32-wasi': 11.2.0 - '@oxc-resolver/binding-win32-arm64-msvc': 11.2.0 - '@oxc-resolver/binding-win32-x64-msvc': 11.2.0 + '@oxc-resolver/binding-android-arm-eabi': 11.21.3 + '@oxc-resolver/binding-android-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-x64': 11.21.3 + '@oxc-resolver/binding-freebsd-x64': 11.21.3 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-arm64-musl': 11.21.3 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-musl': 11.21.3 + '@oxc-resolver/binding-linux-s390x-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-musl': 11.21.3 + '@oxc-resolver/binding-openharmony-arm64': 11.21.3 + '@oxc-resolver/binding-wasm32-wasi': 11.21.3 + '@oxc-resolver/binding-win32-arm64-msvc': 11.21.3 + '@oxc-resolver/binding-win32-x64-msvc': 11.21.3 p-filter@2.1.0: dependencies: @@ -16558,6 +15333,13 @@ snapshots: p-map@2.1.0: {} + p-map@7.0.4: {} + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + p-timeout@6.1.4: {} p-try@2.2.0: {} @@ -16599,6 +15381,12 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.7 + index-to-position: 1.2.0 + type-fest: 4.41.0 + parse-ms@4.0.0: {} parse-semver@1.1.1: @@ -16645,18 +15433,17 @@ snapshots: path-type@4.0.0: {} + path-type@6.0.0: {} + pathe@2.0.3: {} pause-stream@0.0.11: dependencies: through: 2.3.8 - pdf-parse@1.1.1: + pdf-parse@1.1.4: dependencies: - debug: 3.2.7 node-ensure: 0.0.0 - transitivePeerDependencies: - - supports-color pend@1.2.0: {} @@ -16678,6 +15465,10 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 + pluralize@2.0.0: {} + + pluralize@8.0.0: {} + points-on-curve@0.2.0: {} points-on-path@0.2.1: @@ -16687,20 +15478,20 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.15)(tsx@4.19.4)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: - jiti: 2.4.2 + jiti: 2.7.0 postcss: 8.5.15 - tsx: 4.19.4 + tsx: 4.22.4 yaml: 2.9.0 postcss-value-parser@4.2.0: {} postcss@8.4.49: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -16710,20 +15501,24 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - posthog-js@1.242.1: + posthog-js@1.393.4: dependencies: - core-js: 3.42.0 + '@posthog/core': 1.38.0 + '@posthog/types': 1.391.1 + core-js: 3.49.0 + dompurify: 3.4.11 fflate: 0.4.8 - preact: 10.26.6 - web-vitals: 4.2.4 + preact: 10.29.3 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.3.0 posthog-node@5.1.1: {} - preact@10.26.6: {} + preact@10.29.3: {} prebuild-install@7.1.3: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 expand-template: 2.0.3 github-from-package: 0.0.0 minimist: 1.2.8 @@ -16743,7 +15538,7 @@ snapshots: prettier@2.8.8: {} - prettier@3.5.3: {} + prettier@3.8.4: {} pretty-bytes@7.0.0: {} @@ -16759,7 +15554,7 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - pretty-ms@9.2.0: + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -16785,6 +15580,20 @@ snapshots: property-information@7.1.0: {} + protobufjs@7.6.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 20.19.43 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -16808,16 +15617,14 @@ snapshots: punycode@2.3.1: {} - qs@6.14.0: - dependencies: - side-channel: 1.1.0 - qs@6.15.2: dependencies: side-channel: 1.1.0 quansync@0.2.11: {} + query-selector-shadow-dom@1.0.1: {} + queue-microtask@1.2.3: {} randombytes@2.1.0: @@ -16826,13 +15633,6 @@ snapshots: range-parser@1.2.1: {} - raw-body@3.0.0: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 - unpipe: 1.0.0 - raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -16840,6 +15640,15 @@ snapshots: iconv-lite: 0.7.0 unpipe: 1.0.0 + rc-config-loader@4.1.4: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + js-yaml: 4.2.0 + json5: 2.2.3 + require-from-string: 2.0.2 + transitivePeerDependencies: + - supports-color + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -16858,15 +15667,15 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-i18next@15.5.1(i18next@25.2.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): + react-i18next@15.5.1(i18next@25.2.1(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3): dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.29.7 html-parse-stringify: 3.0.1 - i18next: 25.2.1(typescript@5.8.3) + i18next: 25.2.1(typescript@5.9.3) react: 18.3.1 optionalDependencies: react-dom: 18.3.1(react@18.3.1) - typescript: 5.8.3 + typescript: 5.9.3 react-icons@5.5.0(react@18.3.1): dependencies: @@ -16878,11 +15687,11 @@ snapshots: react-is@18.3.1: {} - react-markdown@9.1.0(@types/react@18.3.23)(react@18.3.1): + react-markdown@9.1.0(@types/react@18.3.31)(react@18.3.1): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 18.3.23 + '@types/react': 18.3.31 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -16913,39 +15722,39 @@ snapshots: transitivePeerDependencies: - supports-color - react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1): + react-remove-scroll-bar@2.3.8(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 - react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.31)(react@18.3.1) tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - react-remove-scroll@2.6.3(@types/react@18.3.23)(react@18.3.1): + react-remove-scroll@2.6.3(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.8(@types/react@18.3.23)(react@18.3.1) - react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll-bar: 2.3.8(@types/react@18.3.31)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.31)(react@18.3.1) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@18.3.23)(react@18.3.1) - use-sidecar: 1.1.3(@types/react@18.3.23)(react@18.3.1) + use-callback-ref: 1.3.3(@types/react@18.3.31)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - react-style-singleton@2.2.3(@types/react@18.3.23)(react@18.3.1): + react-style-singleton@2.2.3(@types/react@18.3.31)(react@18.3.1): dependencies: get-nonce: 1.0.1 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - react-textarea-autosize@8.5.9(@types/react@18.3.23)(react@18.3.1): + react-textarea-autosize@8.5.9(@types/react@18.3.31)(react@18.3.1): dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.29.7 react: 18.3.1 - use-composed-ref: 1.4.0(@types/react@18.3.23)(react@18.3.1) - use-latest: 1.3.0(@types/react@18.3.23)(react@18.3.1) + use-composed-ref: 1.4.0(@types/react@18.3.31)(react@18.3.1) + use-latest: 1.3.0(@types/react@18.3.31)(react@18.3.1) transitivePeerDependencies: - '@types/react' @@ -16954,14 +15763,14 @@ snapshots: react: 18.3.1 tslib: 2.8.1 - react-use@17.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-use@17.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@types/js-cookie': 2.2.7 + '@types/js-cookie': 3.0.6 '@xobotyi/scrollbar-width': 1.9.5 copy-to-clipboard: 3.3.3 fast-deep-equal: 3.1.3 fast-shallow-equal: 1.0.0 - js-cookie: 2.2.1 + js-cookie: 3.0.8 nano-css: 5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -16984,6 +15793,14 @@ snapshots: react@19.2.3: {} + read-pkg@9.0.1: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 6.0.2 + parse-json: 8.3.0 + type-fest: 4.41.0 + unicorn-magic: 0.1.0 + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -17022,10 +15839,6 @@ snapshots: dependencies: minimatch: 5.1.6 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 - readdirp@4.1.2: {} reconnecting-eventsource@1.6.4: {} @@ -17035,12 +15848,6 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 - redis-errors@1.2.0: {} - - redis-parser@3.0.0: - dependencies: - redis-errors: 1.2.0 - reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -17071,21 +15878,13 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - rehype-highlight@7.0.2: - dependencies: - '@types/hast': 3.0.4 - hast-util-to-text: 4.0.2 - lowlight: 3.3.0 - unist-util-visit: 5.0.0 - vfile: 6.0.3 - rehype-katex@7.0.1: dependencies: '@types/hast': 3.0.4 '@types/katex': 0.16.7 hast-util-from-html-isomorphic: 2.0.0 hast-util-to-text: 4.0.2 - katex: 0.16.22 + katex: 0.16.47 unist-util-visit-parents: 6.0.1 vfile: 6.0.3 @@ -17159,8 +15958,6 @@ snapshots: resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - resolve@2.0.0-next.5: dependencies: is-core-module: 2.16.1 @@ -17179,6 +15976,8 @@ snapshots: retry@0.12.0: {} + retry@0.13.1: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -17187,10 +15986,6 @@ snapshots: dependencies: glob: 11.1.0 - rimraf@5.0.10: - dependencies: - glob: 11.1.0 - rimraf@6.0.1: dependencies: glob: 11.1.0 @@ -17207,26 +16002,26 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.0.3: + rolldown@1.1.3: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.137.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 rollup@4.60.4: dependencies: @@ -17317,15 +16112,6 @@ snapshots: safer-buffer@2.1.2: {} - sambanova-ai-provider@1.2.2(zod@3.25.76): - dependencies: - '@ai-sdk/openai-compatible': 1.0.11(zod@3.25.76) - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.5(zod@3.25.76) - dotenv: 16.4.5 - transitivePeerDependencies: - - zod - sanitize-filename@1.6.3: dependencies: truncate-utf8-bytes: 1.0.2 @@ -17352,6 +16138,18 @@ snapshots: screenfull@5.2.0: {} + secretlint@10.2.2: + dependencies: + '@secretlint/config-creator': 10.2.2 + '@secretlint/formatter': 10.2.2 + '@secretlint/node': 10.2.2 + '@secretlint/profiler': 10.2.2 + debug: 4.4.3(supports-color@8.1.1) + globby: 14.1.0 + read-pkg: 9.0.1 + transitivePeerDependencies: + - supports-color + section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -17363,8 +16161,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.2: {} - semver@7.7.3: {} send@1.2.0: @@ -17374,12 +16170,12 @@ snapshots: escape-html: 1.0.3 etag: 1.8.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 mime-types: 3.0.1 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -17521,10 +16317,13 @@ snapshots: slash@3.0.0: {} - slice-ansi@7.1.0: + slash@5.1.0: {} + + slice-ansi@4.0.0: dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.0.0 + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 slice-ansi@7.1.2: dependencies: @@ -17536,7 +16335,7 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - smol-toml@1.3.4: {} + smol-toml@1.7.0: {} source-map-js@1.2.1: {} @@ -17559,6 +16358,20 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + split@0.3.3: dependencies: through: 2.3.8 @@ -17590,9 +16403,10 @@ snapshots: stack-generator: 2.0.10 stacktrace-gps: 3.1.2 - standard-as-callback@2.1.0: {} - - statuses@2.0.1: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 statuses@2.0.2: {} @@ -17634,11 +16448,6 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 - string-width@8.1.0: - dependencies: - get-east-asian-width: 1.3.0 - strip-ansi: 7.1.2 - string-width@8.2.1: dependencies: get-east-asian-width: 1.6.0 @@ -17703,10 +16512,6 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 - strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 @@ -17734,12 +16539,14 @@ snapshots: strip-json-comments@5.0.2: {} - strnum@2.1.1: {} - strong-type@0.1.6: {} strong-type@1.1.0: {} + structured-source@4.0.0: + dependencies: + boundary: 2.0.0 + style-to-js@1.1.16: dependencies: style-to-object: 1.0.8 @@ -17784,10 +16591,6 @@ snapshots: dependencies: copy-anything: 4.0.5 - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -17796,7 +16599,10 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@9.4.0: {} + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 supports-preserve-symlinks-flag@1.0.0: {} @@ -17804,6 +16610,14 @@ snapshots: tabbable@5.3.3: {} + table@6.9.0: + dependencies: + ajv: 8.20.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + tailwind-merge@3.3.0: {} tailwindcss-animate@1.0.7(tailwindcss@4.1.6): @@ -17851,17 +16665,22 @@ snapshots: term-size@2.2.1: {} - test-exclude@6.0.0: + terminal-link@4.0.0: dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 11.1.0 - minimatch: 3.1.2 + ansi-escapes: 7.3.0 + supports-hyperlinks: 3.2.0 text-decoder@1.2.3: dependencies: b4a: 1.6.7 optional: true + text-table@0.2.0: {} + + textextensions@6.11.0: + dependencies: + editions: 6.22.0 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -17879,21 +16698,14 @@ snapshots: through@2.3.8: {} - tiktoken@1.0.21: {} + tiktoken@1.0.22: {} tinybench@2.9.0: {} tinyexec@0.3.2: {} - tinyexec@1.2.3: {} - tinyexec@1.2.4: {} - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -17907,8 +16719,6 @@ snapshots: dependencies: tldts-core: 6.1.86 - tmp@0.2.3: {} - tmp@0.2.4: {} to-regex-range@5.0.1: @@ -17951,11 +16761,13 @@ snapshots: dependencies: utf8-byte-length: 1.0.5 - ts-api-utils@2.1.0(typescript@5.8.3): + ts-algebra@2.0.0: {} + + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: - typescript: 5.8.3 + typescript: 5.9.3 - ts-dedent@2.2.0: {} + ts-dedent@2.3.0: {} ts-easing@0.2.0: {} @@ -17967,7 +16779,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(jiti@2.4.2)(postcss@8.5.15)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.9.0): + tsup@8.5.0(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.28.1) cac: 6.7.14 @@ -17978,27 +16790,26 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.15)(tsx@4.19.4)(yaml@2.9.0) + postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0) resolve-from: 5.0.0 rollup: 4.60.4 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 tree-kill: 1.2.2 optionalDependencies: postcss: 8.5.15 - typescript: 5.8.3 + typescript: 5.9.3 transitivePeerDependencies: - jiti - supports-color - tsx - yaml - tsx@4.19.4: + tsx@4.22.4: dependencies: esbuild: 0.28.1 - get-tsconfig: 4.10.0 optionalDependencies: fsevents: 2.3.3 @@ -18009,14 +16820,14 @@ snapshots: tunnel@0.0.6: {} - turbo@2.9.14: + turbo@2.10.0: optionalDependencies: - '@turbo/darwin-64': 2.9.14 - '@turbo/darwin-arm64': 2.9.14 - '@turbo/linux-64': 2.9.14 - '@turbo/linux-arm64': 2.9.14 - '@turbo/windows-64': 2.9.14 - '@turbo/windows-arm64': 2.9.14 + '@turbo/darwin-64': 2.10.0 + '@turbo/darwin-arm64': 2.10.0 + '@turbo/linux-64': 2.10.0 + '@turbo/linux-arm64': 2.10.0 + '@turbo/windows-64': 2.10.0 + '@turbo/windows-arm64': 2.10.0 type-check@0.4.0: dependencies: @@ -18067,21 +16878,21 @@ snapshots: typed-rest-client@1.8.11: dependencies: - qs: 6.14.0 + qs: 6.15.2 tunnel: 0.0.6 - underscore: 1.13.7 + underscore: 1.13.8 - typescript-eslint@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.27.0(jiti@2.4.2) - typescript: 5.8.3 + '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript@5.8.3: {} + typescript@5.9.3: {} uc.micro@2.1.0: {} @@ -18094,17 +16905,13 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - underscore@1.13.7: {} - underscore@1.13.8: {} - undici-types@5.26.5: {} - undici-types@6.21.0: {} - undici-types@7.10.0: {} + undici@6.27.0: {} - undici@6.24.0: {} + unicorn-magic@0.1.0: {} unicorn-magic@0.3.0: {} @@ -18196,6 +17003,8 @@ snapshots: universalify@0.1.2: {} + universalify@2.0.1: {} + unpipe@1.0.0: {} untildify@4.0.0: {} @@ -18225,39 +17034,39 @@ snapshots: url-join@4.0.1: {} - use-callback-ref@1.3.3(@types/react@18.3.23)(react@18.3.1): + use-callback-ref@1.3.3(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - use-composed-ref@1.4.0(@types/react@18.3.23)(react@18.3.1): + use-composed-ref@1.4.0(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - use-isomorphic-layout-effect@1.2.0(@types/react@18.3.23)(react@18.3.1): + use-isomorphic-layout-effect@1.2.0(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - use-latest@1.3.0(@types/react@18.3.23)(react@18.3.1): + use-latest@1.3.0(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 - use-isomorphic-layout-effect: 1.2.0(@types/react@18.3.23)(react@18.3.1) + use-isomorphic-layout-effect: 1.2.0(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 - use-sidecar@1.1.3(@types/react@18.3.23)(react@18.3.1): + use-sidecar@1.1.3(@types/react@18.3.31)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 use-sound@5.0.0(react@18.3.1): dependencies: @@ -18270,20 +17079,19 @@ snapshots: uuid@11.1.1: {} - uuid@14.0.0: {} - uuid@8.3.2: {} uuid@9.0.1: {} - v8-to-istanbul@9.3.0: + validate-npm-package-license@3.0.4: dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 vary@1.1.2: {} + version-range@4.15.0: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -18311,120 +17119,30 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.3): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 20.19.41 - esbuild: 0.28.1 - fsevents: 2.3.3 - jiti: 2.4.2 - tsx: 4.19.4 - yaml: 2.8.3 - - vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 20.19.41 - esbuild: 0.28.1 - fsevents: 2.3.3 - jiti: 2.4.2 - tsx: 4.19.4 - yaml: 2.9.0 - - vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0): + vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.15 - rolldown: 1.0.3 + rolldown: 1.1.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 24.2.1 + '@types/node': 20.19.43 esbuild: 0.28.1 fsevents: 2.3.3 - jiti: 2.4.2 - tsx: 4.19.4 + jiti: 2.7.0 + tsx: 4.22.4 yaml: 2.9.0 - vitest@4.1.0(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.3)): - dependencies: - '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.0 - '@vitest/runner': 4.1.0 - '@vitest/snapshot': 4.1.0 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.2 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.3) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 20.19.41 - '@vitest/ui': 4.1.0(vitest@4.1.0) - jsdom: 26.1.0 - transitivePeerDependencies: - - msw - - vitest@4.1.0(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.0 - '@vitest/runner': 4.1.0 - '@vitest/snapshot': 4.1.0 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.2 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 20.19.41 - '@vitest/ui': 4.1.0(vitest@4.1.0) - jsdom: 26.1.0 - transitivePeerDependencies: - - msw - - vitest@4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.2.1)(@vitest/ui@4.1.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)): + vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@26.1.0)(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.0 - '@vitest/runner': 4.1.0 - '@vitest/snapshot': 4.1.0 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -18436,12 +17154,13 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@24.2.1)(esbuild@0.28.1)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.9.0) + vite: 8.1.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 - '@types/node': 24.2.1 - '@vitest/ui': 4.1.0(vitest@4.1.0) + '@types/node': 20.19.43 + '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) + '@vitest/ui': 4.1.9(vitest@4.1.9) jsdom: 26.1.0 transitivePeerDependencies: - msw @@ -18450,9 +17169,9 @@ snapshots: vscode-material-icons@0.1.1: {} - vscrui@0.2.2(@types/react@18.3.23)(react@18.3.1): + vscrui@0.2.2(@types/react@18.3.31)(react@18.3.1): dependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: 18.3.1 w3c-xmlserializer@5.0.0: @@ -18467,11 +17186,9 @@ snapshots: web-streams-polyfill@3.3.3: {} - web-streams-polyfill@4.0.0-beta.3: {} - web-tree-sitter@0.25.6: {} - web-vitals@4.2.4: {} + web-vitals@5.3.0: {} webidl-conversions@3.0.1: {} @@ -18534,8 +17251,6 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-pm-runs@1.1.0: {} - which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 @@ -18585,12 +17300,6 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 - wrap-ansi@9.0.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 7.2.0 - strip-ansi: 7.1.2 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -18599,11 +17308,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.2: {} - - ws@8.18.3: {} - - ws@8.20.1: {} + ws@8.21.0: {} xml-name-validator@5.0.0: {} @@ -18628,8 +17333,6 @@ snapshots: yallist@5.0.0: {} - yaml@2.8.3: {} - yaml@2.9.0: {} yargs-parser@20.2.9: {} @@ -18674,6 +17377,10 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yauzl@3.4.0: + dependencies: + pend: 1.2.0 + yazl@2.5.1: dependencies: buffer-crc32: 0.2.13 @@ -18686,19 +17393,10 @@ snapshots: dependencies: yoctocolors: 2.1.2 - yoctocolors@2.1.1: {} - yoctocolors@2.1.2: {} yoga-layout@3.2.1: {} - zhipu-ai-provider@0.2.2(zod@3.25.76): - dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.5(zod@3.25.76) - transitivePeerDependencies: - - zod - zip-stream@4.1.1: dependencies: archiver-utils: 3.0.4 @@ -18709,15 +17407,15 @@ snapshots: dependencies: zod: 3.25.76 - zod-validation-error@3.4.1(zod@3.25.76): + zod-validation-error@3.5.4(zod@3.25.76): dependencies: zod: 3.25.76 zod@3.25.76: {} - zustand@5.0.9(@types/react@18.3.23)(react@19.2.3): + zustand@5.0.9(@types/react@18.3.31)(react@19.2.3): optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.31 react: 19.2.3 zwitch@2.0.4: {} diff --git a/renovate.json b/renovate.json index e86916181e..69ba659080 100644 --- a/renovate.json +++ b/renovate.json @@ -3,6 +3,7 @@ "extends": ["config:best-practices"], "forkProcessing": "enabled", "timezone": "America/Los_Angeles", + "postUpdateOptions": ["pnpmDedupe"], "prConcurrentLimit": 5, "prCreation": "not-pending", "ignoreDeps": ["@vscode/vsce"], diff --git a/src/__tests__/delegation-concurrent.spec.ts b/src/__tests__/delegation-concurrent.spec.ts new file mode 100644 index 0000000000..40d9b49ee5 --- /dev/null +++ b/src/__tests__/delegation-concurrent.spec.ts @@ -0,0 +1,147 @@ +// npx vitest run __tests__/delegation-concurrent.spec.ts + +import { describe, it, expect, vi, beforeEach } from "vitest" +import type { HistoryItem } from "@roo-code/types" + +vi.mock("fs/promises", () => ({ + mkdir: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" })), + readdir: vi.fn().mockResolvedValue([]), + unlink: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("fs", () => ({ + default: { + watch: vi.fn().mockReturnValue({ on: vi.fn(), close: vi.fn() }), + existsSync: vi.fn().mockReturnValue(false), + }, + watch: vi.fn().mockReturnValue({ on: vi.fn(), close: vi.fn() }), + existsSync: vi.fn().mockReturnValue(false), +})) + +vi.mock("../utils/safeWriteJson", () => ({ + safeWriteJson: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../utils/storage", () => ({ + getStorageBasePath: vi.fn().mockResolvedValue("/tmp/test-storage"), +})) + +import { TaskHistoryStore } from "../core/task-persistence/TaskHistoryStore" + +const makeItem = (id: string, overrides: Partial = {}): HistoryItem => + ({ + id, + ts: Date.now(), + task: "test task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + status: "active", + mode: "code", + workspace: "/tmp", + ...overrides, + }) as HistoryItem + +describe("TaskHistoryStore.atomicReadAndUpdate", () => { + let store: TaskHistoryStore + + beforeEach(() => { + vi.clearAllMocks() + store = new TaskHistoryStore("/tmp/test-storage") + }) + + it("serializes concurrent operations — second caller reads the state written by the first", async () => { + // Seed the cache with an item that has no childIds yet. + const item = makeItem("parent-task", { childIds: [] }) + ;(store as any).cache.set(item.id, item) + + // Two concurrent delegations each append their child ID. + // Because they are serialized by the lock, the second caller must + // read the cache state that the first caller wrote — not the original. + const delegation1 = store.atomicReadAndUpdate("parent-task", (current) => ({ + ...current, + childIds: [...(current.childIds ?? []), "child-A"], + })) + + const delegation2 = store.atomicReadAndUpdate("parent-task", (current) => ({ + ...current, + childIds: [...(current.childIds ?? []), "child-B"], + })) + + await Promise.all([delegation1, delegation2]) + + // Both child IDs must be present: delegation2 saw delegation1's write. + const final = (store as any).cache.get("parent-task") as HistoryItem + expect(final.childIds).toContain("child-A") + expect(final.childIds).toContain("child-B") + }) + + it("two concurrent delegations produce consistent HistoryItem state (full field set)", async () => { + const item = makeItem("parent-task", { status: "active", childIds: [] }) + ;(store as any).cache.set(item.id, item) + + // Each delegation sets awaitingChildId and appends to childIds. + const delegation1 = store.atomicReadAndUpdate("parent-task", (historyItem) => { + const childIds = Array.from(new Set([...(historyItem.childIds ?? []), "child-A"])) + return { + ...historyItem, + status: "delegated", + delegatedToId: "child-A", + awaitingChildId: "child-A", + childIds, + } + }) + + const delegation2 = store.atomicReadAndUpdate("parent-task", (historyItem) => { + const childIds = Array.from(new Set([...(historyItem.childIds ?? []), "child-B"])) + return { + ...historyItem, + status: "delegated", + delegatedToId: "child-B", + awaitingChildId: "child-B", + childIds, + } + }) + + await Promise.all([delegation1, delegation2]) + + const final = (store as any).cache.get("parent-task") as HistoryItem + // Both child IDs present — neither write clobbered the other's childIds. + expect(final.childIds).toContain("child-A") + expect(final.childIds).toContain("child-B") + // The last writer wins on scalar fields; whichever child ran second is authoritative. + expect(final.status).toBe("delegated") + expect(["child-A", "child-B"]).toContain(final.awaitingChildId) + expect(final.delegatedToId).toBe(final.awaitingChildId) + expect(final.childIds).toContain(final.awaitingChildId) + }) + + it("completes without deadlock — updater is pure and does not re-acquire the lock", async () => { + const item = makeItem("task-1", { childIds: [] }) + ;(store as any).cache.set(item.id, item) + + // With the typed (taskId, updater) API, the updater is synchronous and + // cannot call upsert/withLock — no re-entrancy, no deadlock. + const result = await Promise.race([ + store + .atomicReadAndUpdate("task-1", (current) => ({ + ...current, + childIds: [...(current.childIds ?? []), "child-1"], + })) + .then(() => "completed"), + new Promise((resolve) => setTimeout(() => resolve("deadlocked"), 100)), + ]) + + expect(result).toBe("completed") + + const final = (store as any).cache.get("task-1") as HistoryItem + expect(final.childIds).toContain("child-1") + }) + + it("throws if the task ID is not in the cache", async () => { + await expect( + store.atomicReadAndUpdate("nonexistent-task", (current) => ({ ...current, status: "delegated" })), + ).rejects.toThrow("nonexistent-task") + }) +}) diff --git a/src/__tests__/helpers/provider-stub.ts b/src/__tests__/helpers/provider-stub.ts index d655c2dd6f..c9d77ad877 100644 --- a/src/__tests__/helpers/provider-stub.ts +++ b/src/__tests__/helpers/provider-stub.ts @@ -3,8 +3,8 @@ import { ClineProvider } from "../../core/webview/ClineProvider" /** * Augments a plain stub object with the instance fields and bound methods that * ClineProvider methods read from `this` (runDelegationTransition, - * delegationTransitionLocks, cancelledDelegationChildIds), so tests can call - * private methods via `(ClineProvider.prototype as any).method.call(stub, …)` + * delegationTransitionLocks, cancelledDelegationChildIds, cancellingDelegationChildIds), + * so tests can call private methods via `(ClineProvider.prototype as any).method.call(stub, …)` * without instantiating a real ClineProvider. */ export function makeProviderStub(stub: T): T { @@ -12,6 +12,9 @@ export function makeProviderStub(stub: T): T { const proto = ClineProvider.prototype as any s.delegationTransitionLocks ??= new Map() s.cancelledDelegationChildIds ??= new Set() + s.cancellingDelegationChildIds ??= new Set() + s.log ??= vi.fn() + s.taskHistoryStore ??= { get: () => undefined } s.runDelegationTransition = proto.runDelegationTransition.bind(s) return s } diff --git a/src/__tests__/history-resume-delegation.spec.ts b/src/__tests__/history-resume-delegation.spec.ts index 587ae917a6..0be2d28a3f 100644 --- a/src/__tests__/history-resume-delegation.spec.ts +++ b/src/__tests__/history-resume-delegation.spec.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { RooCodeEventName } from "@roo-code/types" +import type { HistoryItem } from "@roo-code/types" /* vscode mock for Task/Provider imports */ vi.mock("vscode", () => { @@ -29,17 +30,54 @@ vi.mock("vscode", () => { vi.mock("../core/task-persistence/taskMessages", () => ({ readTaskMessages: vi.fn().mockResolvedValue([]), })) -vi.mock("../core/task-persistence", () => ({ - readApiMessages: vi.fn().mockResolvedValue([]), - saveApiMessages: vi.fn().mockResolvedValue(undefined), - saveTaskMessages: vi.fn().mockResolvedValue(undefined), -})) +vi.mock("../core/task-persistence", async (importOriginal) => { + const real = await importOriginal() + return { + ...real, + readApiMessages: vi.fn().mockResolvedValue([]), + saveApiMessages: vi.fn().mockResolvedValue(undefined), + saveTaskMessages: vi.fn().mockResolvedValue(undefined), + } +}) import { ClineProvider } from "../core/webview/ClineProvider" import { readTaskMessages } from "../core/task-persistence/taskMessages" import { readApiMessages, saveApiMessages, saveTaskMessages } from "../core/task-persistence" import { makeProviderStub } from "./helpers/provider-stub" +/** + * Create a minimal taskHistoryStore stub whose atomicUpdatePair calls both updaters + * with the provided items and resolves, simulating the happy-path atomic write. + */ +function makeTaskHistoryStoreStub( + childItem: Record, + parentItem: Record, + overrides: { atomicUpdatePair?: ReturnType } = {}, +) { + const itemMap = new Map>([ + [childItem.id!, childItem], + [parentItem.id!, parentItem], + ]) + + const atomicUpdatePair = vi.fn( + async ( + firstId: string, + secondId: string, + firstUpdater: (h: HistoryItem) => HistoryItem, + secondUpdater: (h: HistoryItem) => HistoryItem, + ) => { + firstUpdater(itemMap.get(firstId) as HistoryItem) + secondUpdater(itemMap.get(secondId) as HistoryItem) + return [] + }, + ) + + return { + atomicUpdatePair: overrides.atomicUpdatePair ?? atomicUpdatePair, + get: vi.fn((id: string) => itemMap.get(id)), + } +} + describe("History resume delegation - parent metadata transitions", () => { beforeEach(() => { vi.clearAllMocks() @@ -47,24 +85,23 @@ describe("History resume delegation - parent metadata transitions", () => { it("reopenParentFromDelegation accepts an active parent awaiting the returning child", async () => { const providerEmit = vi.fn() - const getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { - id: "parent-1", - status: "active", - delegatedToId: "child-1", - awaitingChildId: "child-1", - childIds: ["child-1"], - ts: Date.now(), - task: "Parent task", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - mode: "code", - workspace: "/tmp", - }, - }) + const parentHistoryItem = { + id: "parent-1", + status: "active", + delegatedToId: "child-1", + awaitingChildId: "child-1", + childIds: ["child-1"], + ts: Date.now(), + task: "Parent task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + mode: "code", + workspace: "/tmp", + } + const getTaskWithId = vi.fn().mockResolvedValue({ historyItem: parentHistoryItem }) - const updateTaskHistory = vi.fn().mockResolvedValue([]) + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "child-1", status: "active" }, parentHistoryItem) const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const createTaskWithHistoryItem = vi.fn().mockResolvedValue({ taskId: "parent-1", @@ -79,10 +116,9 @@ describe("History resume delegation - parent metadata transitions", () => { getCurrentTask: vi.fn(() => ({ taskId: "child-1" })), removeClineFromStack, createTaskWithHistoryItem, - updateTaskHistory, + taskHistoryStore, } as any) - // Mock persistence reads to return empty arrays vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) @@ -92,22 +128,34 @@ describe("History resume delegation - parent metadata transitions", () => { completionResultSummary: "Child done", }) - // Assert: metadata updated BEFORE createTaskWithHistoryItem - expect(updateTaskHistory).toHaveBeenCalledWith( - expect.objectContaining({ - id: "parent-1", - status: "active", - completedByChildId: "child-1", - completionResultSummary: "Child done", - awaitingChildId: undefined, - childIds: ["child-1"], - }), - ) + // atomicUpdatePair called with child first, parent second + expect(taskHistoryStore.atomicUpdatePair).toHaveBeenCalledTimes(1) + const [firstId, secondId, firstUpdater, secondUpdater] = taskHistoryStore.atomicUpdatePair.mock.calls[0] + expect(firstId).toBe("child-1") + expect(secondId).toBe("parent-1") + + // Verify child updater produces completed status and persists completionResultSummary + // so startup reconciliation has the real result if the parent write fails. + const updatedChild = firstUpdater({ id: "child-1", status: "active" } as HistoryItem) + expect(updatedChild.status).toBe("completed") + expect(updatedChild.completionResultSummary).toBe("Child done") + + // Verify parent updater produces active status with correct fields + const updatedParent = secondUpdater(parentHistoryItem as HistoryItem) + expect(updatedParent).toMatchObject({ + id: "parent-1", + status: "active", + completedByChildId: "child-1", + completionResultSummary: "Child done", + awaitingChildId: undefined, + delegatedToId: undefined, + childIds: ["child-1"], + }) - // Verify call ordering: updateTaskHistory before createTaskWithHistoryItem - const updateCall = updateTaskHistory.mock.invocationCallOrder[0] + // atomicUpdatePair must happen before createTaskWithHistoryItem + const atomicCall = taskHistoryStore.atomicUpdatePair.mock.invocationCallOrder[0] const createCall = createTaskWithHistoryItem.mock.invocationCallOrder[0] - expect(updateCall).toBeLessThan(createCall) + expect(atomicCall).toBeLessThan(createCall) // Verify child closed and parent reopened with updated metadata expect(removeClineFromStack).toHaveBeenCalledTimes(1) @@ -122,21 +170,21 @@ describe("History resume delegation - parent metadata transitions", () => { }) it("reopenParentFromDelegation injects subtask_result into both UI and API histories", async () => { + const parentItem = { + id: "p1", + status: "delegated", + awaitingChildId: "c1", + childIds: [], + ts: 100, + task: "Parent", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "c1", status: "active" }, parentItem) const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/storage" } }, - getTaskWithId: vi.fn().mockResolvedValue({ - historyItem: { - id: "p1", - status: "delegated", - awaitingChildId: "c1", - childIds: [], - ts: 100, - task: "Parent", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: vi.fn(), getCurrentTask: vi.fn(() => ({ taskId: "c1" })), removeClineFromStack: vi.fn().mockResolvedValue(undefined), @@ -146,7 +194,7 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteClineMessages: vi.fn().mockResolvedValue(undefined), overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), - updateTaskHistory: vi.fn().mockResolvedValue([]), + taskHistoryStore, } as any) // Start with existing messages in history @@ -205,21 +253,21 @@ describe("History resume delegation - parent metadata transitions", () => { }) it("reopenParentFromDelegation injects tool_result when new_task tool_use exists in API history", async () => { + const parentItem = { + id: "p-tool", + status: "delegated", + awaitingChildId: "c-tool", + childIds: [], + ts: 100, + task: "Parent with tool_use", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "c-tool", status: "active" }, parentItem) const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/storage" } }, - getTaskWithId: vi.fn().mockResolvedValue({ - historyItem: { - id: "p-tool", - status: "delegated", - awaitingChildId: "c-tool", - childIds: [], - ts: 100, - task: "Parent with tool_use", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: vi.fn(), getCurrentTask: vi.fn(() => ({ taskId: "c-tool" })), removeClineFromStack: vi.fn().mockResolvedValue(undefined), @@ -229,7 +277,7 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteClineMessages: vi.fn().mockResolvedValue(undefined), overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), - updateTaskHistory: vi.fn().mockResolvedValue([]), + taskHistoryStore, } as any) // Include an assistant message with new_task tool_use to exercise the tool_result path @@ -291,21 +339,21 @@ describe("History resume delegation - parent metadata transitions", () => { }) it("reopenParentFromDelegation injects plain text when no new_task tool_use exists in API history", async () => { + const parentItem = { + id: "p-no-tool", + status: "delegated", + awaitingChildId: "c-no-tool", + childIds: [], + ts: 100, + task: "Parent without tool_use", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "c-no-tool", status: "active" }, parentItem) const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/storage" } }, - getTaskWithId: vi.fn().mockResolvedValue({ - historyItem: { - id: "p-no-tool", - status: "delegated", - awaitingChildId: "c-no-tool", - childIds: [], - ts: 100, - task: "Parent without tool_use", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: vi.fn(), getCurrentTask: vi.fn(() => ({ taskId: "c-no-tool" })), removeClineFromStack: vi.fn().mockResolvedValue(undefined), @@ -315,7 +363,7 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteClineMessages: vi.fn().mockResolvedValue(undefined), overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), - updateTaskHistory: vi.fn().mockResolvedValue([]), + taskHistoryStore, } as any) // No assistant tool_use in history @@ -351,26 +399,26 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), } + const parentItem = { + id: "parent-2", + status: "delegated", + awaitingChildId: "child-2", + childIds: [], + ts: 200, + task: "P", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "child-2", status: "active" }, parentItem) const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, - getTaskWithId: vi.fn().mockResolvedValue({ - historyItem: { - id: "parent-2", - status: "delegated", - awaitingChildId: "child-2", - childIds: [], - ts: 200, - task: "P", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: vi.fn(), getCurrentTask: vi.fn(() => ({ taskId: "child-2" })), removeClineFromStack: vi.fn().mockResolvedValue(undefined), createTaskWithHistoryItem: vi.fn().mockResolvedValue(parentInstance), - updateTaskHistory: vi.fn().mockResolvedValue([]), + taskHistoryStore, } as any) vi.mocked(readTaskMessages).mockResolvedValue([]) @@ -389,23 +437,22 @@ describe("History resume delegation - parent metadata transitions", () => { it("reopenParentFromDelegation emits events in correct order: TaskDelegationCompleted → TaskDelegationResumed", async () => { const emitSpy = vi.fn() - const updateTaskHistory = vi.fn().mockResolvedValue([]) + const parentItem = { + id: "p3", + status: "delegated", + awaitingChildId: "c3", + childIds: [], + ts: 300, + task: "P3", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "c3", status: "active" }, parentItem) const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, - getTaskWithId: vi.fn().mockResolvedValue({ - historyItem: { - id: "p3", - status: "delegated", - awaitingChildId: "c3", - childIds: [], - ts: 300, - task: "P3", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: emitSpy, getCurrentTask: vi.fn(() => ({ taskId: "c3" })), removeClineFromStack: vi.fn().mockResolvedValue(undefined), @@ -414,7 +461,7 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteClineMessages: vi.fn().mockResolvedValue(undefined), overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), - updateTaskHistory, + taskHistoryStore, } as any) vi.mocked(readTaskMessages).mockResolvedValue([]) @@ -437,16 +484,10 @@ describe("History resume delegation - parent metadata transitions", () => { expect(completedIdx).toBeGreaterThanOrEqual(0) expect(resumedIdx).toBeGreaterThan(completedIdx) - // RPD-05: verify parent metadata persistence happens before TaskDelegationCompleted emit - const parentUpdateCallIdx = updateTaskHistory.mock.calls.findIndex((call) => { - const item = call[0] as { id?: string; status?: string } | undefined - return item?.id === "p3" && item.status === "active" - }) - expect(parentUpdateCallIdx).toBeGreaterThanOrEqual(0) - - const parentUpdateCallOrder = updateTaskHistory.mock.invocationCallOrder[parentUpdateCallIdx] + // RPD-05: atomicUpdatePair must be called before TaskDelegationCompleted + const atomicCallOrder = taskHistoryStore.atomicUpdatePair.mock.invocationCallOrder[0] const completedEmitCallOrder = emitSpy.mock.invocationCallOrder[completedIdx] - expect(parentUpdateCallOrder).toBeLessThan(completedEmitCallOrder) + expect(atomicCallOrder).toBeLessThan(completedEmitCallOrder) }) it("reopenParentFromDelegation continues when overwrite operations fail and still resumes/emits (RPD-06)", async () => { @@ -457,42 +498,27 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteApiConversationHistory: vi.fn().mockRejectedValue(new Error("api overwrite failed")), } + const parentItem = { + id: "parent-rpd06", + status: "delegated", + awaitingChildId: "child-rpd06", + childIds: ["child-rpd06"], + ts: 800, + task: "Parent RPD-06", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "child-rpd06", status: "active" }, parentItem) + const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, - getTaskWithId: vi.fn().mockImplementation(async (id: string) => { - if (id === "parent-rpd06") { - return { - historyItem: { - id: "parent-rpd06", - status: "delegated", - awaitingChildId: "child-rpd06", - childIds: ["child-rpd06"], - ts: 800, - task: "Parent RPD-06", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - } - } - - return { - historyItem: { - id: "child-rpd06", - status: "active", - ts: 801, - task: "Child RPD-06", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - } - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: emitSpy, getCurrentTask: vi.fn(() => ({ taskId: "child-rpd06" })), removeClineFromStack: vi.fn().mockResolvedValue(undefined), createTaskWithHistoryItem: vi.fn().mockResolvedValue(parentInstance), - updateTaskHistory: vi.fn().mockResolvedValue([]), + taskHistoryStore, } as any) vi.mocked(readTaskMessages).mockResolvedValue([]) @@ -526,22 +552,22 @@ describe("History resume delegation - parent metadata transitions", () => { it("reopenParentFromDelegation does NOT emit TaskPaused or TaskUnpaused (new flow only)", async () => { const emitSpy = vi.fn() + const parentItem = { + id: "p4", + status: "delegated", + awaitingChildId: "c4", + childIds: [], + ts: 400, + task: "P4", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "c4", status: "active" }, parentItem) const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, - getTaskWithId: vi.fn().mockResolvedValue({ - historyItem: { - id: "p4", - status: "delegated", - awaitingChildId: "c4", - childIds: [], - ts: 400, - task: "P4", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: emitSpy, getCurrentTask: vi.fn(() => ({ taskId: "c4" })), removeClineFromStack: vi.fn().mockResolvedValue(undefined), @@ -550,7 +576,7 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteClineMessages: vi.fn().mockResolvedValue(undefined), overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), - updateTaskHistory: vi.fn().mockResolvedValue([]), + taskHistoryStore, } as any) vi.mocked(readTaskMessages).mockResolvedValue([]) @@ -576,45 +602,29 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), } - const updateTaskHistory = vi.fn().mockResolvedValue([]) + const parentItem = { + id: "parent-rpd02", + status: "delegated", + awaitingChildId: "child-rpd02", + childIds: ["child-rpd02"], + ts: 600, + task: "Parent RPD-02", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "child-rpd02", status: "active" }, parentItem) const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const createTaskWithHistoryItem = vi.fn().mockResolvedValue(parentInstance) const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, - getTaskWithId: vi.fn().mockImplementation(async (id: string) => { - if (id === "parent-rpd02") { - return { - historyItem: { - id: "parent-rpd02", - status: "delegated", - awaitingChildId: "child-rpd02", - childIds: ["child-rpd02"], - ts: 600, - task: "Parent RPD-02", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - } - } - return { - historyItem: { - id: "child-rpd02", - status: "active", - ts: 601, - task: "Child RPD-02", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - } - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: vi.fn(), getCurrentTask: vi.fn(() => ({ taskId: "different-open-task" })), removeClineFromStack, createTaskWithHistoryItem, - updateTaskHistory, + taskHistoryStore, } as any) vi.mocked(readTaskMessages).mockResolvedValue([]) @@ -627,12 +637,17 @@ describe("History resume delegation - parent metadata transitions", () => { }) expect(removeClineFromStack).not.toHaveBeenCalled() - expect(updateTaskHistory).toHaveBeenCalledWith( - expect.objectContaining({ - id: "child-rpd02", - status: "completed", - }), - ) + + // Verify atomicUpdatePair called with child first (completed) and parent second (active) + expect(taskHistoryStore.atomicUpdatePair).toHaveBeenCalledTimes(1) + const [firstId, secondId, firstUpdater, secondUpdater] = taskHistoryStore.atomicUpdatePair.mock.calls[0] + expect(firstId).toBe("child-rpd02") + expect(secondId).toBe("parent-rpd02") + const updatedChild = firstUpdater({ id: "child-rpd02", status: "active" } as HistoryItem) + expect(updatedChild.status).toBe("completed") + const updatedParent = secondUpdater(parentItem as HistoryItem) + expect(updatedParent).toMatchObject({ id: "parent-rpd02", status: "active", completedByChildId: "child-rpd02" }) + expect(createTaskWithHistoryItem).toHaveBeenCalledWith( expect.objectContaining({ id: "parent-rpd02", @@ -644,110 +659,80 @@ describe("History resume delegation - parent metadata transitions", () => { expect(parentInstance.resumeAfterDelegation).toHaveBeenCalledTimes(1) }) - it("reopenParentFromDelegation logs child status persistence failure and continues reopen flow (RPD-04)", async () => { - const logSpy = vi.fn() - const emitSpy = vi.fn() - const parentInstance = { - resumeAfterDelegation: vi.fn().mockResolvedValue(undefined), - overwriteClineMessages: vi.fn().mockResolvedValue(undefined), - overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), + it("reopenParentFromDelegation propagates atomicUpdatePair failure — parent not reopened (RPD-04)", async () => { + const parentItem = { + id: "parent-rpd04", + status: "delegated", + awaitingChildId: "child-rpd04", + childIds: ["child-rpd04"], + ts: 700, + task: "Parent RPD-04", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, } - - const updateTaskHistory = vi.fn().mockImplementation(async (historyItem: { id?: string }) => { - if (historyItem.id === "child-rpd04") { - throw new Error("child status persist failed") - } - return [] + const persistError = new Error("atomic pair write failed") + const atomicUpdatePair = vi.fn().mockRejectedValue(persistError) + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "child-rpd04", status: "active" }, parentItem, { + atomicUpdatePair, }) + const createTaskWithHistoryItem = vi.fn() const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, - getTaskWithId: vi.fn().mockImplementation(async (id: string) => { - if (id === "parent-rpd04") { - return { - historyItem: { - id: "parent-rpd04", - status: "delegated", - awaitingChildId: "child-rpd04", - childIds: ["child-rpd04"], - ts: 700, - task: "Parent RPD-04", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - } - } - return { - historyItem: { - id: "child-rpd04", - status: "active", - ts: 701, - task: "Child RPD-04", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - } - }), - emit: emitSpy, - log: logSpy, + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), + emit: vi.fn(), getCurrentTask: vi.fn(() => ({ taskId: "child-rpd04" })), removeClineFromStack: vi.fn().mockResolvedValue(undefined), - createTaskWithHistoryItem: vi.fn().mockResolvedValue(parentInstance), - updateTaskHistory, + createTaskWithHistoryItem, + taskHistoryStore, } as any) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) + // Failure propagates — child is closed (step 4 already ran) but parent is NOT reopened await expect( (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { parentTaskId: "parent-rpd04", childTaskId: "child-rpd04", completionResultSummary: "Child completion with persistence failure", }), - ).resolves.toBe(true) + ).rejects.toThrow(persistError) - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining( - "[reopenParentFromDelegation] Failed to persist child completed status for child-rpd04:", - ), - ) - expect(updateTaskHistory).toHaveBeenCalledWith( - expect.objectContaining({ - id: "parent-rpd04", - status: "active", - completedByChildId: "child-rpd04", - }), - ) - expect(parentInstance.resumeAfterDelegation).toHaveBeenCalledTimes(1) - expect(emitSpy).toHaveBeenCalledWith(RooCodeEventName.TaskDelegationResumed, "parent-rpd04", "child-rpd04") + expect(createTaskWithHistoryItem).not.toHaveBeenCalled() }) - it("reopenParentFromDelegation keeps the child open when parent metadata persistence fails", async () => { + it("reopenParentFromDelegation aborts parent reopen when all persistence paths fail (RPD-05)", async () => { const persistError = new Error("parent status persist failed") const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const createTaskWithHistoryItem = vi.fn() + const parentItem = { + id: "parent-rpd05", + status: "delegated", + awaitingChildId: "child-rpd05", + childIds: ["child-rpd05"], + ts: 800, + task: "Parent RPD-05", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + // atomicUpdatePair failure aborts the reopen flow. + const atomicUpdatePair = vi.fn().mockRejectedValue(persistError) + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "child-rpd05", status: "active" }, parentItem, { + atomicUpdatePair, + }) + const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, - getTaskWithId: vi.fn().mockResolvedValue({ - historyItem: { - id: "parent-rpd05", - status: "delegated", - awaitingChildId: "child-rpd05", - childIds: ["child-rpd05"], - ts: 800, - task: "Parent RPD-05", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: vi.fn(), + log: vi.fn(), getCurrentTask: vi.fn(() => ({ taskId: "child-rpd05" })), removeClineFromStack, createTaskWithHistoryItem, + taskHistoryStore, updateTaskHistory: vi.fn().mockRejectedValue(persistError), } as any) @@ -762,26 +747,27 @@ describe("History resume delegation - parent metadata transitions", () => { }), ).rejects.toThrow(persistError) - expect(removeClineFromStack).not.toHaveBeenCalled() + // Child is closed before the atomic write (new ordering) — child closed, parent not reopened + expect(removeClineFromStack).toHaveBeenCalledTimes(1) expect(createTaskWithHistoryItem).not.toHaveBeenCalled() }) it("handles empty history gracefully when injecting synthetic messages", async () => { + const parentItem = { + id: "p5", + status: "delegated", + awaitingChildId: "c5", + childIds: [], + ts: 500, + task: "P5", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub({ id: "c5", status: "active" }, parentItem) const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, - getTaskWithId: vi.fn().mockResolvedValue({ - historyItem: { - id: "p5", - status: "delegated", - awaitingChildId: "c5", - childIds: [], - ts: 500, - task: "P5", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - }), + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), emit: vi.fn(), getCurrentTask: vi.fn(() => ({ taskId: "c5" })), removeClineFromStack: vi.fn().mockResolvedValue(undefined), @@ -790,7 +776,7 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteClineMessages: vi.fn().mockResolvedValue(undefined), overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), - updateTaskHistory: vi.fn().mockResolvedValue([]), + taskHistoryStore, } as any) // Mock read failures or empty returns @@ -830,7 +816,7 @@ describe("History resume delegation - parent metadata transitions", () => { it("reopenParentFromDelegation aborts when parent is already active (stale-delegation guard)", async () => { const logSpy = vi.fn() - const updateTaskHistory = vi.fn() + const atomicUpdatePair = vi.fn() const saveTaskMessagesMock = vi.mocked(saveTaskMessages) const saveApiMessagesMock = vi.mocked(saveApiMessages) @@ -843,7 +829,7 @@ describe("History resume delegation - parent metadata transitions", () => { getCurrentTask: vi.fn(() => null), removeClineFromStack: vi.fn(), createTaskWithHistoryItem: vi.fn(), - updateTaskHistory, + taskHistoryStore: { atomicUpdatePair, get: vi.fn() }, } as any) const providerActive = makeProvider({ @@ -860,13 +846,13 @@ describe("History resume delegation - parent metadata transitions", () => { ).resolves.toBe(false) expect(saveTaskMessagesMock).not.toHaveBeenCalled() expect(saveApiMessagesMock).not.toHaveBeenCalled() - expect(updateTaskHistory).not.toHaveBeenCalled() + expect(atomicUpdatePair).not.toHaveBeenCalled() expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[reopenParentFromDelegation] Aborting")) }) it("reopenParentFromDelegation aborts when in-process cancellation failed closed", async () => { const logSpy = vi.fn() - const updateTaskHistory = vi.fn() + const atomicUpdatePair = vi.fn() const saveTaskMessagesMock = vi.mocked(saveTaskMessages) const saveApiMessagesMock = vi.mocked(saveApiMessages) const provider = makeProviderStub({ @@ -883,7 +869,7 @@ describe("History resume delegation - parent metadata transitions", () => { getCurrentTask: vi.fn(() => null), removeClineFromStack: vi.fn(), createTaskWithHistoryItem: vi.fn(), - updateTaskHistory, + taskHistoryStore: { atomicUpdatePair, get: vi.fn() }, cancelledDelegationChildIds: new Set(["child-guard"]), } as any) @@ -897,13 +883,13 @@ describe("History resume delegation - parent metadata transitions", () => { expect(saveTaskMessagesMock).not.toHaveBeenCalled() expect(saveApiMessagesMock).not.toHaveBeenCalled() - expect(updateTaskHistory).not.toHaveBeenCalled() + expect(atomicUpdatePair).not.toHaveBeenCalled() expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[reopenParentFromDelegation] Aborting")) }) it("reopenParentFromDelegation aborts when parent awaits a different child (stale-delegation guard)", async () => { const logSpy = vi.fn() - const updateTaskHistory = vi.fn() + const atomicUpdatePair = vi.fn() const saveTaskMessagesMock = vi.mocked(saveTaskMessages) const saveApiMessagesMock = vi.mocked(saveApiMessages) @@ -916,7 +902,7 @@ describe("History resume delegation - parent metadata transitions", () => { getCurrentTask: vi.fn(() => null), removeClineFromStack: vi.fn(), createTaskWithHistoryItem: vi.fn(), - updateTaskHistory, + taskHistoryStore: { atomicUpdatePair, get: vi.fn() }, } as any) const providerWrongChild = makeProvider({ @@ -933,7 +919,7 @@ describe("History resume delegation - parent metadata transitions", () => { ).resolves.toBe(false) expect(saveTaskMessagesMock).not.toHaveBeenCalled() expect(saveApiMessagesMock).not.toHaveBeenCalled() - expect(updateTaskHistory).not.toHaveBeenCalled() + expect(atomicUpdatePair).not.toHaveBeenCalled() expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[reopenParentFromDelegation] Aborting")) }) @@ -961,4 +947,192 @@ describe("History resume delegation - parent metadata transitions", () => { await expect(second).resolves.toBe("done") expect(calls).toEqual(["first", "second"]) }) + + it("reopenParentFromDelegation posts taskHistoryItemUpdated for both records when view is launched", async () => { + const childItem = { id: "c-webview", status: "active" } + const parentItem = { + id: "p-webview", + status: "delegated", + awaitingChildId: "c-webview", + childIds: [], + ts: 100, + task: "Parent webview", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + + // After atomicUpdatePair resolves, get() returns the merged committed items. + const updatedChild = { ...childItem, status: "completed" } + const updatedParent = { + ...parentItem, + status: "active", + awaitingChildId: undefined, + completedByChildId: "c-webview", + } + const itemMap = new Map([ + ["c-webview", updatedChild], + ["p-webview", updatedParent], + ]) + const taskHistoryStore = { + atomicUpdatePair: vi.fn(async (_fId: string, _sId: string, fU: (h: any) => any, sU: (h: any) => any) => { + fU(childItem) + sU(parentItem) + return [] + }), + get: vi.fn((id: string) => itemMap.get(id)), + } + + const postMessageToWebview = vi.fn().mockResolvedValue(undefined) + + const provider = makeProviderStub({ + contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), + emit: vi.fn(), + isViewLaunched: true, + getCurrentTask: vi.fn(() => ({ taskId: "c-webview" })), + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTaskWithHistoryItem: vi.fn().mockResolvedValue({ + resumeAfterDelegation: vi.fn().mockResolvedValue(undefined), + overwriteClineMessages: vi.fn().mockResolvedValue(undefined), + overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), + }), + taskHistoryStore, + postMessageToWebview, + } as any) + + vi.mocked(readTaskMessages).mockResolvedValue([]) + vi.mocked(readApiMessages).mockResolvedValue([]) + + await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + parentTaskId: "p-webview", + childTaskId: "c-webview", + completionResultSummary: "Done", + }) + + // Must post taskHistoryItemUpdated for both the child (completed) and parent (active) + const webviewCalls = postMessageToWebview.mock.calls.filter((c) => c[0]?.type === "taskHistoryItemUpdated") + expect(webviewCalls).toHaveLength(2) + + const childMsg = webviewCalls.find((c) => c[0].taskHistoryItem.id === "c-webview") + expect(childMsg?.[0].taskHistoryItem).toMatchObject({ id: "c-webview", status: "completed" }) + + const parentMsg = webviewCalls.find((c) => c[0].taskHistoryItem.id === "p-webview") + expect(parentMsg?.[0].taskHistoryItem).toMatchObject({ id: "p-webview", status: "active" }) + + // Both must be sent after atomicUpdatePair (verified by call order) + const atomicOrder = taskHistoryStore.atomicUpdatePair.mock.invocationCallOrder[0] + for (const call of webviewCalls) { + const msgOrder = + postMessageToWebview.mock.invocationCallOrder[postMessageToWebview.mock.calls.indexOf(call)] + expect(atomicOrder).toBeLessThan(msgOrder) + } + }) + + it("reopenParentFromDelegation does NOT post taskHistoryItemUpdated when view is not launched", async () => { + const childItem = { id: "c-noview", status: "active" } + const parentItem = { + id: "p-noview", + status: "delegated", + awaitingChildId: "c-noview", + childIds: [], + ts: 100, + task: "Parent no-view", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const taskHistoryStore = makeTaskHistoryStoreStub(childItem, parentItem) + const postMessageToWebview = vi.fn().mockResolvedValue(undefined) + + const provider = makeProviderStub({ + contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), + emit: vi.fn(), + isViewLaunched: false, + getCurrentTask: vi.fn(() => ({ taskId: "c-noview" })), + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTaskWithHistoryItem: vi.fn().mockResolvedValue({ + resumeAfterDelegation: vi.fn().mockResolvedValue(undefined), + overwriteClineMessages: vi.fn().mockResolvedValue(undefined), + overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), + }), + taskHistoryStore, + postMessageToWebview, + } as any) + + vi.mocked(readTaskMessages).mockResolvedValue([]) + vi.mocked(readApiMessages).mockResolvedValue([]) + + await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + parentTaskId: "p-noview", + childTaskId: "c-noview", + completionResultSummary: "Done", + }) + + const webviewCalls = postMessageToWebview.mock.calls.filter((c) => c[0]?.type === "taskHistoryItemUpdated") + expect(webviewCalls).toHaveLength(0) + }) + + describe("atomicUpdatePair handoff correctness", () => { + it("after reopenParentFromDelegation, child is completed and parent is active atomically", async () => { + const parentItem = { + id: "p-handoff", + status: "delegated", + awaitingChildId: "c-handoff", + childIds: [], + ts: 100, + task: "Parent", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + const childItem = { id: "c-handoff", status: "active" } + + let capturedChildResult: HistoryItem | undefined + let capturedParentResult: HistoryItem | undefined + + const atomicUpdatePair = vi.fn( + async ( + firstId: string, + secondId: string, + firstUpdater: (h: HistoryItem) => HistoryItem, + secondUpdater: (h: HistoryItem) => HistoryItem, + ) => { + // Both updaters must be applied atomically + capturedChildResult = firstUpdater(childItem as unknown as HistoryItem) + capturedParentResult = secondUpdater(parentItem as unknown as HistoryItem) + return [] + }, + ) + const taskHistoryStore = { atomicUpdatePair, get: vi.fn() } + + const provider = makeProviderStub({ + contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, + getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentItem }), + emit: vi.fn(), + getCurrentTask: vi.fn(() => ({ taskId: "c-handoff" })), + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTaskWithHistoryItem: vi.fn().mockResolvedValue({ + resumeAfterDelegation: vi.fn().mockResolvedValue(undefined), + }), + taskHistoryStore, + } as any) + + vi.mocked(readTaskMessages).mockResolvedValue([]) + vi.mocked(readApiMessages).mockResolvedValue([]) + + await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + parentTaskId: "p-handoff", + childTaskId: "c-handoff", + completionResultSummary: "Done", + }) + + // After atomicUpdatePair: child completed AND parent active — never one without the other + expect(capturedChildResult?.status).toBe("completed") + expect(capturedParentResult?.status).toBe("active") + expect(capturedParentResult?.awaitingChildId).toBeUndefined() + expect(capturedParentResult?.completedByChildId).toBe("c-handoff") + }) + }) }) diff --git a/src/__tests__/nested-delegation-resume.spec.ts b/src/__tests__/nested-delegation-resume.spec.ts index 9f78ba14bb..35341f935c 100644 --- a/src/__tests__/nested-delegation-resume.spec.ts +++ b/src/__tests__/nested-delegation-resume.spec.ts @@ -44,7 +44,8 @@ vi.mock("vscode", () => { vi.mock("../core/task-persistence/taskMessages", () => ({ readTaskMessages: vi.fn().mockResolvedValue([]), })) -vi.mock("../core/task-persistence", () => ({ +vi.mock("../core/task-persistence", async (importOriginal) => ({ + ...(await importOriginal()), readApiMessages: vi.fn().mockResolvedValue([]), saveApiMessages: vi.fn().mockResolvedValue(undefined), saveTaskMessages: vi.fn().mockResolvedValue(undefined), @@ -149,6 +150,25 @@ describe("Nested delegation resume (A → B → C)", () => { return Object.values(historyIndex) }) + const taskHistoryStore = { + atomicUpdatePair: vi.fn( + async ( + firstId: string, + secondId: string, + firstUpdater: (h: any) => any, + secondUpdater: (h: any) => any, + ) => { + // Apply both updaters and persist to historyIndex atomically + const updatedFirst = firstUpdater(historyIndex[firstId]) + const updatedSecond = secondUpdater(historyIndex[secondId]) + historyIndex[firstId] = updatedFirst + historyIndex[secondId] = updatedSecond + return Object.values(historyIndex) + }, + ), + get: vi.fn((id: string) => historyIndex[id]), + } + const provider = makeProviderStub({ contextProxy: { globalStorageUri: { fsPath: "/tmp" } }, getTaskWithId, @@ -157,6 +177,7 @@ describe("Nested delegation resume (A → B → C)", () => { removeClineFromStack, createTaskWithHistoryItem, updateTaskHistory, + taskHistoryStore, // Wire through provider method so attemptCompletionTool can call it reopenParentFromDelegation: vi.fn(async (params: any) => { return await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, params) diff --git a/src/__tests__/provider-delegation.spec.ts b/src/__tests__/provider-delegation.spec.ts index bd4519b970..ddc6a3c4f4 100644 --- a/src/__tests__/provider-delegation.spec.ts +++ b/src/__tests__/provider-delegation.spec.ts @@ -1,127 +1,193 @@ // npx vitest run __tests__/provider-delegation.spec.ts import { describe, it, expect, vi } from "vitest" +import type { HistoryItem } from "@roo-code/types" import { RooCodeEventName } from "@roo-code/types" import { ClineProvider } from "../core/webview/ClineProvider" +const parentHistoryItem: HistoryItem = { + id: "parent-1", + task: "Parent", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + childIds: [], +} as unknown as HistoryItem + +/** Minimal taskHistoryStore stub whose atomicReadAndUpdate calls the updater with the parent item. */ +function makeStoreStub( + overrides: Partial<{ atomicReadAndUpdate: ReturnType; get: ReturnType }> = {}, +) { + return { + atomicReadAndUpdate: vi.fn(async (_taskId: string, updater: (h: HistoryItem) => HistoryItem) => { + updater(parentHistoryItem) + return [] + }), + get: vi.fn().mockReturnValue(undefined), + ...overrides, + } +} + +/** + * Parent task double with the methods delegateParentAndOpenChild reads from + * `parent`. Without flushPendingToolResultsToHistory the method hits its + * non-fatal flush-error branch and never reaches the happy delegation path. + */ +const makeParentTask = () => + ({ + taskId: "parent-1", + emit: vi.fn(), + flushPendingToolResultsToHistory: vi.fn().mockResolvedValue(true), + retrySaveApiConversationHistory: vi.fn(), + }) as any + describe("ClineProvider.delegateParentAndOpenChild()", () => { - it("persists parent delegation metadata and emits TaskDelegated", async () => { + it("persists parent delegation metadata via atomicReadAndUpdate and emits TaskDelegated", async () => { const providerEmit = vi.fn() - const parentTask = { taskId: "parent-1", emit: vi.fn() } as any + const parentTask = makeParentTask() const childStart = vi.fn() - const updateTaskHistory = vi.fn() const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const createTask = vi.fn().mockResolvedValue({ taskId: "child-1", start: childStart }) const handleModeSwitch = vi.fn().mockResolvedValue(undefined) - const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { - if (id === "parent-1") { - return { - historyItem: { - id: "parent-1", - task: "Parent", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - childIds: [], - }, - } - } - // child-1 - return { - historyItem: { - id: "child-1", - task: "Do something", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - }, - } - }) + const taskHistoryStore = makeStoreStub() const provider = { emit: providerEmit, getCurrentTask: vi.fn(() => parentTask), removeClineFromStack, createTask, - getTaskWithId, - updateTaskHistory, handleModeSwitch, log: vi.fn(), + isViewLaunched: false, + recentTasksCache: undefined, + taskHistoryStore, } as unknown as ClineProvider - const params = { + const child = await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { parentTaskId: "parent-1", message: "Do something", initialTodos: [], mode: "code", - } - - const child = await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, params) + }) expect(child.taskId).toBe("child-1") // Invariant: parent closed before child creation expect(removeClineFromStack).toHaveBeenCalledTimes(1) - // Child task is created with startTask: false and initialStatus: "active" + + // Child task created with startTask: false and initialStatus: "active" expect(createTask).toHaveBeenCalledWith("Do something", undefined, parentTask, { initialTodos: [], initialStatus: "active", startTask: false, }) - // Metadata persistence - parent gets "delegated" status (child status is set at creation via initialStatus) - expect(updateTaskHistory).toHaveBeenCalledTimes(1) - - // Parent set to "delegated" - const parentSaved = updateTaskHistory.mock.calls[0][0] - expect(parentSaved).toEqual( - expect.objectContaining({ - id: "parent-1", - status: "delegated", - delegatedToId: "child-1", - awaitingChildId: "child-1", - childIds: expect.arrayContaining(["child-1"]), - }), - ) + // Delegation metadata written via atomicReadAndUpdate with correct taskId + expect(taskHistoryStore.atomicReadAndUpdate).toHaveBeenCalledTimes(1) + const [calledTaskId, updater] = taskHistoryStore.atomicReadAndUpdate.mock.calls[0] + expect(calledTaskId).toBe("parent-1") + + // The updater must produce the correct delegation fields + const result = updater(parentHistoryItem) + expect(result).toMatchObject({ + id: "parent-1", + status: "delegated", + delegatedToId: "child-1", + awaitingChildId: "child-1", + childIds: expect.arrayContaining(["child-1"]), + }) - // child.start() must be called AFTER parent metadata is persisted + // child.start() called AFTER parent metadata is persisted expect(childStart).toHaveBeenCalledTimes(1) - // Event emission (provider-level) + // Provider-level event expect(providerEmit).toHaveBeenCalledWith(RooCodeEventName.TaskDelegated, "parent-1", "child-1") // Mode switch expect(handleModeSwitch).toHaveBeenCalledWith("code") }) - it("calls child.start() only after parent metadata is persisted (no race condition)", async () => { - const callOrder: string[] = [] + it("posts taskHistoryItemUpdated to the webview when isViewLaunched is true", async () => { + const updatedParent = { ...parentHistoryItem, status: "delegated" } as HistoryItem + const postMessageToWebview = vi.fn().mockResolvedValue(undefined) + const parentTask = makeParentTask() + const taskHistoryStore = makeStoreStub({ + get: vi.fn().mockReturnValue(updatedParent), + }) - const parentTask = { - taskId: "parent-1", + const provider = { emit: vi.fn(), - } as any - const childStart = vi.fn(() => callOrder.push("child.start")) + getCurrentTask: vi.fn(() => parentTask), + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTask: vi.fn().mockResolvedValue({ taskId: "child-1", start: vi.fn() }), + handleModeSwitch: vi.fn().mockResolvedValue(undefined), + postMessageToWebview, + log: vi.fn(), + isViewLaunched: true, + recentTasksCache: undefined, + taskHistoryStore, + } as unknown as ClineProvider + + await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + parentTaskId: "parent-1", + message: "Do something", + initialTodos: [], + mode: "code", + }) + + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "taskHistoryItemUpdated", + taskHistoryItem: updatedParent, + }) + }) + + it("skips postMessageToWebview when isViewLaunched is true but store returns undefined", async () => { + const postMessageToWebview = vi.fn().mockResolvedValue(undefined) + const parentTask = makeParentTask() + const taskHistoryStore = makeStoreStub({ + get: vi.fn().mockReturnValue(undefined), + }) + + const provider = { + emit: vi.fn(), + getCurrentTask: vi.fn(() => parentTask), + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTask: vi.fn().mockResolvedValue({ taskId: "child-1", start: vi.fn() }), + handleModeSwitch: vi.fn().mockResolvedValue(undefined), + postMessageToWebview, + log: vi.fn(), + isViewLaunched: true, + recentTasksCache: undefined, + taskHistoryStore, + } as unknown as ClineProvider - const updateTaskHistory = vi.fn(async () => { - callOrder.push("updateTaskHistory") + await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + parentTaskId: "parent-1", + message: "Do something", + initialTodos: [], + mode: "code", }) + + expect(postMessageToWebview).not.toHaveBeenCalled() + }) + + it("calls child.start() only after atomicReadAndUpdate completes (no race condition)", async () => { + const callOrder: string[] = [] + + const parentTask = makeParentTask() + const childStart = vi.fn(() => callOrder.push("child.start")) const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const createTask = vi.fn(async () => { callOrder.push("createTask") return { taskId: "child-1", start: childStart } }) const handleModeSwitch = vi.fn().mockResolvedValue(undefined) - const getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { - id: "parent-1", - task: "Parent", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - childIds: [], - }, + const taskHistoryStore = makeStoreStub({ + atomicReadAndUpdate: vi.fn(async (_taskId: string, _updater: (h: HistoryItem) => HistoryItem) => { + callOrder.push("atomicReadAndUpdate") + return [] + }), }) const provider = { @@ -129,10 +195,11 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { getCurrentTask: vi.fn(() => parentTask), removeClineFromStack, createTask, - getTaskWithId, - updateTaskHistory, handleModeSwitch, log: vi.fn(), + isViewLaunched: false, + recentTasksCache: undefined, + taskHistoryStore, } as unknown as ClineProvider await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { @@ -142,40 +209,45 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { mode: "code", }) - // Verify ordering: createTask → updateTaskHistory → child.start - expect(callOrder).toEqual(["createTask", "updateTaskHistory", "child.start"]) + // createTask → atomicReadAndUpdate → child.start: lock must release before start + expect(callOrder).toEqual(["createTask", "atomicReadAndUpdate", "child.start"]) }) - it("rolls back the paused child and restores the parent when metadata persistence fails", async () => { + it("rolls back the paused child and restores the parent when atomicReadAndUpdate fails", async () => { const persistError = new Error("parent metadata persist failed") - const parentHistory = { - id: "parent-1", - task: "Parent", - tokensIn: 0, - tokensOut: 0, - totalCost: 0, - mode: "code", - } - const parentTask = { - taskId: "parent-1", - emit: vi.fn(), - } as any + const parentTask = makeParentTask() const childStart = vi.fn() const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const deleteTaskWithId = vi.fn().mockResolvedValue(undefined) const createTaskWithHistoryItem = vi.fn().mockResolvedValue(undefined) + const getTaskWithId = vi.fn().mockResolvedValue({ historyItem: parentHistoryItem }) + + const taskHistoryStore = makeStoreStub({ + atomicReadAndUpdate: vi.fn().mockRejectedValue(persistError), + }) + + const child = { taskId: "child-1", start: childStart } + // Before createTask: getCurrentTask returns parent (used by step 3 close). + // After createTask: returns child so the rollback guard passes and the child is popped. + const getCurrentTask = vi.fn().mockReturnValue(parentTask) + const createTask = vi.fn().mockImplementation(async () => { + getCurrentTask.mockReturnValue(child) + return child + }) const provider = { emit: vi.fn(), - getCurrentTask: vi.fn(() => parentTask), + getCurrentTask, removeClineFromStack, - createTask: vi.fn().mockResolvedValue({ taskId: "child-1", start: childStart }), - getTaskWithId: vi.fn().mockResolvedValue({ historyItem: parentHistory }), - updateTaskHistory: vi.fn().mockRejectedValue(persistError), + createTask, + getTaskWithId, handleModeSwitch: vi.fn().mockResolvedValue(undefined), deleteTaskWithId, createTaskWithHistoryItem, log: vi.fn(), + isViewLaunched: false, + recentTasksCache: undefined, + taskHistoryStore, } as unknown as ClineProvider await expect( @@ -191,6 +263,6 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { expect(removeClineFromStack).toHaveBeenNthCalledWith(1, { skipDelegationRepair: true }) expect(removeClineFromStack).toHaveBeenNthCalledWith(2, { skipDelegationRepair: true }) expect(deleteTaskWithId).toHaveBeenCalledWith("child-1", false) - expect(createTaskWithHistoryItem).toHaveBeenCalledWith(parentHistory) + expect(createTaskWithHistoryItem).toHaveBeenCalledWith(parentHistoryItem) }) }) diff --git a/src/__tests__/removeClineFromStack-delegation.spec.ts b/src/__tests__/removeClineFromStack-delegation.spec.ts index 6ed2a5c221..6013d8db99 100644 --- a/src/__tests__/removeClineFromStack-delegation.spec.ts +++ b/src/__tests__/removeClineFromStack-delegation.spec.ts @@ -79,6 +79,7 @@ describe("ClineProvider.removeClineFromStack() delegation awareness", () => { id: "parent-1", status: "active", awaitingChildId: undefined, + delegatedToId: undefined, }), ) @@ -279,4 +280,61 @@ describe("ClineProvider.removeClineFromStack() delegation awareness", () => { // Grandparent A's metadata remains intact (delegated, awaitingChildId: task-B) // The caller (delegateParentAndOpenChild) will update A to point to C separately. }) + + it("does NOT repair parent when child has 'interrupted' status (cancel already persisted it)", async () => { + // cancelTask() writes child status: "interrupted" and leaves parent "delegated". + // When rehydrate then calls removeClineFromStack, parent must stay delegated. + const childTaskId = "child-1" + const parentTaskId = "parent-1" + + const childTask = { + taskId: childTaskId, + instanceId: "inst-child", + parentTaskId, + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + + const updateTaskHistory = vi.fn().mockResolvedValue([]) + const getTaskWithId = vi.fn().mockImplementation(async (id: string) => { + if (id === parentTaskId) { + return { + historyItem: { + id: parentTaskId, + task: "Parent task", + ts: 1000, + number: 1, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + status: "delegated", + awaitingChildId: childTaskId, + }, + } + } + throw new Error("Task not found") + }) + + const provider = makeProviderStub({ + clineStack: [childTask] as any[], + taskEventListeners: new Map(), + log: vi.fn(), + getTaskWithId, + updateTaskHistory, + // Seed the in-memory store with the interrupted child — mirrors what cancelTask + // writes before rehydrating, and is what removeClineFromStack now reads directly. + taskHistoryStore: { get: (id: string) => (id === childTaskId ? { status: "interrupted" } : undefined) }, + }) + + await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + + // Stack is emptied + expect(provider.clineStack).toHaveLength(0) + + // Parent must NOT be transitioned to active — it stays "delegated" + // so the interrupted child can resume and report back later + expect(updateTaskHistory).not.toHaveBeenCalledWith( + expect.objectContaining({ id: parentTaskId, status: "active" }), + ) + }) }) diff --git a/src/__tests__/single-open-invariant.spec.ts b/src/__tests__/single-open-invariant.spec.ts index 2dd466a992..af5b429a80 100644 --- a/src/__tests__/single-open-invariant.spec.ts +++ b/src/__tests__/single-open-invariant.spec.ts @@ -73,6 +73,46 @@ describe("Single-open-task invariant", () => { expect(addClineToStack).toHaveBeenCalledTimes(1) }) + it("Subtask create: keeps existing task open when parentTask is provided", async () => { + vi.spyOn(ProfileValidatorMod.ProfileValidator, "isProfileAllowed").mockReturnValue(true) + + const removeClineFromStack = vi.fn().mockResolvedValue(undefined) + const addClineToStack = vi.fn().mockResolvedValue(undefined) + const parentTask = { taskId: "parent-1" } + + const provider = { + clineStack: [parentTask], + setValues: vi.fn(), + getState: vi.fn().mockResolvedValue({ + apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 0 }, + organizationAllowList: "*", + enableCheckpoints: true, + checkpointTimeout: 60, + cloudUserInfo: null, + }), + removeClineFromStack, + addClineToStack, + setProviderProfile: vi.fn(), + log: vi.fn(), + getStateToPostToWebview: vi.fn(), + providerSettingsManager: { getModeConfigId: vi.fn(), listConfig: vi.fn() }, + customModesManager: { getCustomModes: vi.fn().mockResolvedValue([]) }, + taskCreationCallback: vi.fn(), + contextProxy: { + extensionUri: {}, + setValue: vi.fn(), + getValue: vi.fn(), + setProviderSettings: vi.fn(), + getProviderSettings: vi.fn(() => ({})), + }, + } as unknown as ClineProvider + + await (ClineProvider.prototype as any).createTask.call(provider, "Subtask", undefined, parentTask as any) + + expect(removeClineFromStack).not.toHaveBeenCalled() + expect(addClineToStack).toHaveBeenCalledTimes(1) + }) + it("History resume path always closes current before rehydration (non-rehydrating case)", async () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const addClineToStack = vi.fn().mockResolvedValue(undefined) diff --git a/src/api/index.ts b/src/api/index.ts index e52b41200b..6faa524e99 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -31,6 +31,7 @@ import { SambaNovaHandler, ZAiHandler, FireworksHandler, + FriendliHandler, VercelAiGatewayHandler, OpencodeGoHandler, ZooGatewayHandler, @@ -90,6 +91,12 @@ export interface ApiHandlerCreateMessageMetadata { * Only applies to providers that support function calling restrictions (e.g., Gemini). */ allowedFunctionNames?: string[] + /** + * Abort signal for cancelling the HTTP request mid-stream. + * Passed through to AI SDK's streamText() so the underlying HTTP request is aborted + * when the user clicks stop, preventing wasted API tokens/compute on the provider side. + */ + abortSignal?: AbortSignal } export interface ApiHandler { @@ -101,6 +108,13 @@ export interface ApiHandler { getModel(): { id: string; info: ModelInfo } + /** + * Optional context window for context-management / auto-condense when it must differ from + * getModel().info.contextWindow. Only VS Code LM overrides it (static `maxInputTokens` vs its + * inflated live window); others leave it undefined and callers fall back. + */ + getCondenseContextWindow?(): number + /** * Counts tokens for content blocks * All providers extend BaseProvider which provides a default tiktoken implementation, @@ -176,6 +190,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new ZAiHandler(options) case "fireworks": return new FireworksHandler(options) + case "friendli": + return new FriendliHandler(options) case "vercel-ai-gateway": return new VercelAiGatewayHandler(options) case "opencode-go": diff --git a/src/api/providers/__tests__/anthropic-vertex.spec.ts b/src/api/providers/__tests__/anthropic-vertex.spec.ts index 2cf9c8a21a..36afc8557d 100644 --- a/src/api/providers/__tests__/anthropic-vertex.spec.ts +++ b/src/api/providers/__tests__/anthropic-vertex.spec.ts @@ -895,6 +895,41 @@ describe("VertexHandler", () => { const result = await handler.completePrompt("Test prompt") expect(result).toBe("") }) + + it("should handle empty content array for Claude", async () => { + handler = new AnthropicVertexHandler({ + apiModelId: "claude-3-5-sonnet-v2@20241022", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + + const mockCreate = vitest.fn().mockResolvedValue({ + content: [], + }) + ;(handler["client"].messages as any).create = mockCreate + + const result = await handler.completePrompt("Test prompt") + expect(result).toBe("") + }) + + it("should return text from first text block when mixed content for Claude", async () => { + handler = new AnthropicVertexHandler({ + apiModelId: "claude-3-5-sonnet-v2@20241022", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + + const mockCreate = vitest.fn().mockResolvedValue({ + content: [ + { type: "thinking", thinking: "internal reasoning" }, + { type: "text", text: "visible response" }, + ], + }) + ;(handler["client"].messages as any).create = mockCreate + + const result = await handler.completePrompt("Test prompt") + expect(result).toBe("visible response") + }) }) describe("getModel", () => { @@ -1037,6 +1072,26 @@ describe("VertexHandler", () => { expect(model.info.supportsTemperature).toBe(false) }) + it("should return Claude Sonnet 5 model info", () => { + const handler = new AnthropicVertexHandler({ + apiModelId: "claude-sonnet-5", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + + const model = handler.getModel() + expect(model.id).toBe("claude-sonnet-5") + expect(model.info.maxTokens).toBe(8192) + expect(model.info.contextWindow).toBe(1_000_000) + // Fork shapes Sonnet 5 as effort/adaptive on Vertex (mirrors Opus 4.8), not budget/binary. + expect(model.info.supportsReasoningEffort).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(model.info.requiredReasoningEffort).toBe(true) + expect(model.info.supportsReasoningBudget).toBeUndefined() + expect(model.info.supportsReasoningBinary).toBeUndefined() + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.supportsTemperature).toBe(false) + }) + it("should not enable 1M context when flag is disabled", () => { const handler = new AnthropicVertexHandler({ apiModelId: VERTEX_1M_CONTEXT_MODEL_IDS[0], @@ -1313,6 +1368,37 @@ describe("VertexHandler", () => { expect(request.thinking).not.toHaveProperty("budget_tokens") expect(request.temperature).toBeUndefined() }) + + it("should use adaptive thinking for Claude Sonnet 5", async () => { + const sonnetHandler = new AnthropicVertexHandler({ + apiModelId: "claude-sonnet-5", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + enableReasoningEffort: true, + }) + + const mockCreate = vitest.fn().mockImplementation(async () => ({ + async *[Symbol.asyncIterator]() { + yield { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 5 } } } + }, + })) + ;(sonnetHandler["client"].messages as any).create = mockCreate + + await sonnetHandler + .createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }]) + .next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + thinking: { type: "adaptive" }, + }), + undefined, + ) + + const request = mockCreate.mock.calls[0][0] + expect(request.thinking).not.toHaveProperty("budget_tokens") + expect(request.temperature).toBeUndefined() + }) }) describe("native tool calling", () => { diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index 55fa92adfb..614e3bec6c 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -478,6 +478,56 @@ describe("AnthropicHandler", () => { expect(requestBody?.max_tokens).toBe(128000) expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31") }) + + it("should use adaptive thinking for Claude Sonnet 5 when reasoning is enabled", async () => { + const sonnetHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-5", + enableReasoningEffort: true, + modelMaxTokens: 32768, + }) + + const stream = sonnetHandler.createMessage(systemPrompt, [ + { + role: "user", + content: [{ type: "text" as const, text: "Hello" }], + }, + ]) + + for await (const _chunk of stream) { + // Consume stream + } + + const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] + const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1] + expect(requestBody?.thinking).toEqual({ type: "adaptive" }) + expect((requestBody as any)?.output_config).toEqual({ effort: "high" }) + expect(requestBody?.temperature).toBeUndefined() + expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31") + }) + + it("should send the custom model ID as-is and use adaptive thinking for a custom Sonnet-5-family model", async () => { + const customHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-5-bf", + enableReasoningEffort: true, + }) + + const stream = customHandler.createMessage(systemPrompt, [ + { + role: "user", + content: [{ type: "text" as const, text: "Hello" }], + }, + ]) + + for await (const _chunk of stream) { + // Consume stream + } + + const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] + expect(requestBody?.model).toBe("claude-sonnet-5-bf") + expect(requestBody?.thinking).toEqual({ type: "adaptive" }) + }) }) describe("completePrompt", () => { @@ -648,6 +698,27 @@ describe("AnthropicHandler", () => { expect(model.maxTokens).toBe(128000) }) + it("should handle Claude Sonnet 5 model correctly", () => { + const handler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-5", + }) + const model = handler.getModel() + expect(model.id).toBe("claude-sonnet-5") + expect(model.info.maxTokens).toBe(128000) + expect(model.info.contextWindow).toBe(1000000) + // Local fork uses the effort/adaptive shape for Sonnet 5, not upstream's budget/binary. + expect(model.info.supportsReasoningEffort).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(model.info.requiredReasoningEffort).toBe(true) + expect(model.info.supportsReasoningBudget).toBeUndefined() + expect(model.info.supportsReasoningBinary).toBeUndefined() + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.supportsTemperature).toBe(false) + expect(model.reasoningBudget).toBeUndefined() + expect(model.reasoningEffort).toBe("high") + expect(model.maxTokens).toBe(128000) + }) + it("should enable 1M context for Claude 4.5 Sonnet when beta flag is set", () => { const handler = new AnthropicHandler({ apiKey: "test-api-key", @@ -671,6 +742,41 @@ describe("AnthropicHandler", () => { expect(model.info.inputPrice).toBe(6.0) expect(model.info.outputPrice).toBe(22.5) }) + + it("should honor a custom/unrecognized model ID instead of silently falling back to anthropicDefaultModelId", () => { + const handler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-5-bf", + }) + const model = handler.getModel() + expect(model.id).toBe("claude-sonnet-5-bf") + }) + + it("should guess capabilities for a custom/unrecognized model ID from known model-family substrings", () => { + const handler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-5-bf", + }) + const model = handler.getModel() + // Guessed from the claude-sonnet-5 family, which the fork shapes as effort/adaptive. + expect(model.info.supportsReasoningEffort).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(model.info.requiredReasoningEffort).toBe(true) + expect(model.info.supportsReasoningBinary).toBeUndefined() + expect(model.info.maxTokens).toBe(128000) + expect(model.info.contextWindow).toBe(1000000) + }) + + it("should fall back to anthropicDefaultModelId's info when a custom model ID matches no known family", () => { + const handler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "totally-unknown-custom-model", + }) + const model = handler.getModel() + expect(model.id).toBe("totally-unknown-custom-model") + expect(model.info.maxTokens).toBe(64000) + expect(model.info.contextWindow).toBe(200000) + expect(model.info.supportsReasoningBinary).toBeUndefined() + }) }) describe("reasoning block filtering", () => { diff --git a/src/api/providers/__tests__/base-openai-compatible-provider.spec.ts b/src/api/providers/__tests__/base-openai-compatible-provider.spec.ts index 847aa6e4dc..cbb2a91333 100644 --- a/src/api/providers/__tests__/base-openai-compatible-provider.spec.ts +++ b/src/api/providers/__tests__/base-openai-compatible-provider.spec.ts @@ -97,6 +97,75 @@ describe("BaseOpenAiCompatibleProvider", () => { ]) }) + it("should handle reasoning tags () from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "Deep thought" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: " here" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "Result: 42" } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + const stream = handler.createMessage("system prompt", []) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + expect(chunks).toEqual([ + { type: "reasoning", text: "Deep thought" }, + { type: "reasoning", text: " here" }, + { type: "text", text: "Result: 42" }, + ]) + }) + + it("should not close tag with tag", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "Thinking" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: " but closing with wrong tag
" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: " still thinking" } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + const stream = handler.createMessage("system prompt", []) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + // The tag should be treated as text since it doesn't match the active tag + expect(chunks).toEqual([ + { type: "reasoning", text: "Thinking" }, + { type: "reasoning", text: " but closing with wrong tag" }, + { type: "reasoning", text: " still thinking" }, + ]) + }) + it("should handle complete tag in a single chunk", async () => { mockCreate.mockImplementationOnce(() => { return { @@ -153,13 +222,8 @@ describe("BaseOpenAiCompatibleProvider", () => { chunks.push(chunk) } - // TagMatcher should handle incomplete tags and flush remaining content - expect(chunks.length).toBeGreaterThan(0) - expect( - chunks.some( - (c) => (c.type === "text" || c.type === "reasoning") && c.text.includes("Incomplete thought"), - ), - ).toBe(true) + // TagMatcher should flush incomplete reasoning content on stream end + expect(chunks).toContainEqual({ type: "reasoning", text: "Incomplete thought" }) }) it("should handle text without any tags", async () => { diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index 156df8e540..e2b17e4e87 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -724,6 +724,37 @@ describe("AwsBedrockHandler", () => { const model = handler.getModel() expect(model.id).toBe("global.anthropic.claude-fable-5") }) + + it("should return Claude Sonnet 5 model info", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-sonnet-5", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const model = handler.getModel() + expect(model.id).toBe("anthropic.claude-sonnet-5") + expect(model.info.contextWindow).toBe(1_000_000) + expect(model.info.supportsReasoningBinary).toBe(true) + expect(model.info.supportsReasoningBudget).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.supportsTemperature).toBe(false) + expect(model.maxTokens).toBe(8192) + }) + + it("should apply global inference prefix for Claude Sonnet 5 when awsUseGlobalInference is true", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-sonnet-5", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsUseGlobalInference: true, + }) + + const model = handler.getModel() + expect(model.id).toBe("global.anthropic.claude-sonnet-5") + }) }) describe("1M context beta feature", () => { @@ -1426,6 +1457,36 @@ describe("AwsBedrockHandler", () => { expect(commandArg.inferenceConfig?.temperature).toBeUndefined() }) + it("should send adaptive thinking with effort xhigh for Claude Sonnet 5 when reasoning is enabled", async () => { + // End-to-end regression guard for the Sonnet 5 handler branch. The + // isAdaptiveThinkingModel predicate is unit-covered, but a regression in + // the createMessage adaptive-thinking branch for this specific model + // wouldn't be caught without a request-level test (see review feedback). + const sonnet5Handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-sonnet-5", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + enableReasoningEffort: true, + }) + + const generator = sonnet5Handler.createMessage("System prompt", messages) + await generator.next() + + expect(mockConverseStreamCommand).toHaveBeenCalled() + const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any + + // Sonnet 5 uses the same adaptive-thinking contract as Opus 4.7/4.8 — + // budget_tokens causes a 400, so thinking.type is "adaptive" with effort. + expect(commandArg.additionalModelRequestFields?.thinking).toEqual({ + type: "adaptive", + display: "summarized", + }) + expect(commandArg.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" }) + // Sonnet 5 rejects sampling parameters: temperature must be omitted entirely. + expect(commandArg.inferenceConfig?.temperature).toBeUndefined() + }) + it("should omit thinking and temperature for Claude Opus 4.8 when reasoning is disabled", async () => { const opus48Handler = new AwsBedrockHandler({ apiModelId: "anthropic.claude-opus-4-8", @@ -1558,6 +1619,7 @@ describe("AwsBedrockHandler", () => { expect(isAdaptiveThinkingModel("anthropic.claude-opus-4-7")).toBe(true) expect(isAdaptiveThinkingModel("anthropic.claude-opus-4-8")).toBe(true) expect(isAdaptiveThinkingModel("anthropic.claude-fable-5")).toBe(true) + expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-5")).toBe(true) // Future-proof Sonnet patterns — guarded even before a registry entry exists. expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-4-7")).toBe(true) expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-4-8")).toBe(true) @@ -1566,6 +1628,7 @@ describe("AwsBedrockHandler", () => { it("returns true when the id carries a cross-region or global prefix", () => { expect(isAdaptiveThinkingModel("us.anthropic.claude-opus-4-8")).toBe(true) expect(isAdaptiveThinkingModel("global.anthropic.claude-fable-5")).toBe(true) + expect(isAdaptiveThinkingModel("global.anthropic.claude-sonnet-5")).toBe(true) expect(isAdaptiveThinkingModel("eu.anthropic.claude-sonnet-4-7")).toBe(true) expect(isAdaptiveThinkingModel("global.anthropic.claude-opus-4-8")).toBe(true) }) diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 2f0482eeef..563b49bce8 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -426,6 +426,17 @@ describe("DeepSeekHandler", () => { expect(chunks).toContainEqual({ type: "reasoning", text: "router-style thought" }) }) + it("should throw a wrapped error when the API call fails", async () => { + const apiError = Object.assign(new Error("Invalid API key"), { status: 401 }) + mockCreate.mockRejectedValueOnce(apiError) + + const stream = handler.createMessage(systemPrompt, messages) + const err = await stream.next().catch((e) => e) + + expect(err.message).toBe("DeepSeek completion error: Invalid API key") + expect((err as any).status).toBe(401) + }) + it("prefers delta.reasoning_content over delta.reasoning when both are present", async () => { mockCreate.mockImplementationOnce(async () => ({ [Symbol.asyncIterator]: async function* () { diff --git a/src/api/providers/__tests__/fireworks.spec.ts b/src/api/providers/__tests__/fireworks.spec.ts index 18fded5c05..33d50ab7b2 100644 --- a/src/api/providers/__tests__/fireworks.spec.ts +++ b/src/api/providers/__tests__/fireworks.spec.ts @@ -183,6 +183,30 @@ describe("FireworksHandler", () => { ) }) + it("should return Kimi K2.7 code model with correct configuration", () => { + const testModelId: FireworksModelId = "accounts/fireworks/models/kimi-k2p7-code" + const handlerWithModel = new FireworksHandler({ + apiModelId: testModelId, + fireworksApiKey: "test-fireworks-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 16384, + contextWindow: 262144, + supportsImages: true, + supportsPromptCache: true, + supportsTemperature: true, + preserveReasoning: true, + defaultTemperature: 1.0, + inputPrice: 0.95, + outputPrice: 4.0, + cacheReadsPrice: 0.19, + }), + ) + }) + it("should return MiniMax M2 model with correct configuration", () => { const testModelId: FireworksModelId = "accounts/fireworks/models/minimax-m2" const handlerWithModel = new FireworksHandler({ diff --git a/src/api/providers/__tests__/friendli.spec.ts b/src/api/providers/__tests__/friendli.spec.ts new file mode 100644 index 0000000000..79ab6115ab --- /dev/null +++ b/src/api/providers/__tests__/friendli.spec.ts @@ -0,0 +1,405 @@ +// npx vitest run api/providers/__tests__/friendli.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { friendliDefaultModelId, friendliModels } from "@roo-code/types" + +import { buildApiHandler } from "../../index" +import { getModelMaxOutputTokens } from "../../../shared/api" +import { FriendliHandler } from "../friendli" + +// Create mock functions +const mockCreate = vi.fn() + +// Mock OpenAI module +vi.mock("openai", () => ({ + default: vi.fn(function () { + return { + chat: { + completions: { + create: mockCreate, + }, + }, + } + }), +})) + +describe("FriendliHandler", () => { + let handler: FriendliHandler + + beforeEach(() => { + vi.clearAllMocks() + // Set up default mock implementation + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Test response" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + } + }, + })) + handler = new FriendliHandler({ friendliApiKey: "test-key" }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should use the correct Friendli base URL", () => { + new FriendliHandler({ friendliApiKey: "test-friendli-api-key" }) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ baseURL: "https://api.friendli.ai/serverless/v1" }), + ) + }) + + it("should use the provided API key", () => { + const friendliApiKey = "test-friendli-api-key" + new FriendliHandler({ friendliApiKey }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: friendliApiKey })) + }) + + it("should throw error when API key is not provided", () => { + expect(() => new FriendliHandler({})).toThrow("API key is required") + }) + + it("should return default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(friendliDefaultModelId) + expect(model.info).toEqual(expect.objectContaining(friendliModels[friendliDefaultModelId])) + }) + + it("should return GLM-5.2 model with correct configuration", () => { + const handlerWithModel = new FriendliHandler({ + apiModelId: "zai-org/GLM-5.2", + friendliApiKey: "test-...ey", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe("zai-org/GLM-5.2") + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + inputPrice: 1.4, + outputPrice: 4.4, + cacheWritesPrice: 0, + cacheReadsPrice: 0.26, + }), + ) + }) + + it.each([ + { + modelId: "zai-org/GLM-5.1" as const, + contextWindow: 200_000, + maxTokens: 131_072, + supportsMaxTokens: true, + inputPrice: 1.4, + outputPrice: 4.4, + cacheWritesPrice: 0, + cacheReadsPrice: 0.26, + }, + { + modelId: "deepseek-ai/DeepSeek-V3.2" as const, + contextWindow: 163_840, + maxTokens: 16384, + supportsMaxTokens: undefined, + inputPrice: 0.5, + outputPrice: 1.5, + cacheWritesPrice: 0, + cacheReadsPrice: 0.25, + }, + { + modelId: "MiniMaxAI/MiniMax-M2.5" as const, + contextWindow: 204_800, + maxTokens: 4096, + supportsMaxTokens: undefined, + inputPrice: 0.3, + outputPrice: 1.2, + cacheWritesPrice: 0, + cacheReadsPrice: 0.06, + }, + ])( + "should expose newly added model $modelId", + ({ + modelId, + contextWindow, + maxTokens, + supportsMaxTokens, + inputPrice, + outputPrice, + cacheWritesPrice, + cacheReadsPrice, + }) => { + expect(friendliModels[modelId]).toBeDefined() + const info = friendliModels[modelId] as import("@roo-code/types").ModelInfo + expect(info.maxTokens).toBe(maxTokens) + expect(info.contextWindow).toBe(contextWindow) + expect(info.supportsMaxTokens).toBe(supportsMaxTokens) + expect(info.inputPrice).toBe(inputPrice) + expect(info.outputPrice).toBe(outputPrice) + expect(info.cacheWritesPrice).toBe(cacheWritesPrice) + expect(info.cacheReadsPrice).toBe(cacheReadsPrice) + expect(info.description).toBeTruthy() + + const handlerWithModel = new FriendliHandler({ + apiModelId: modelId, + friendliApiKey: "test-friendli-api-key", + }) + expect(handlerWithModel.getModel().id).toBe(modelId) + }, + ) + + it("completePrompt method should return text from Friendli API", async () => { + const expectedResponse = "This is a test response from Friendli" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "Friendli API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow( + `Friendli completion error: ${errorMessage}`, + ) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from Friendli stream" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("createMessage should yield usage data from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toMatchObject({ type: "usage", inputTokens: 10, outputTokens: 20 }) + }) + + it("createMessage should pass correct parameters to Friendli client", async () => { + const modelId = "zai-org/GLM-5.2" + const modelInfo = friendliModels[modelId] + const handlerWithModel = new FriendliHandler({ + apiModelId: modelId, + friendliApiKey: "test-friendli-api-key", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt for Friendli" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Friendli" }] + + const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + max_tokens: modelInfo.maxTokens, + temperature: 0.6, + messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), + stream: true, + stream_options: { include_usage: true }, + }), + undefined, + ) + }) + + it("should use user-specified temperature over provider default", async () => { + const handlerWithModel = new FriendliHandler({ + apiModelId: "zai-org/GLM-5.2", + friendliApiKey: "test-friendli-api-key", + modelTemperature: 0.3, + }) + + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + })) + + const messageGenerator = handlerWithModel.createMessage("system", []) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + temperature: 0.3, + }), + undefined, + ) + }) + + it("should handle empty response in completePrompt", async () => { + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: null } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe("") + }) + + it("should handle missing choices in completePrompt", async () => { + mockCreate.mockResolvedValueOnce({ choices: [] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe("") + }) + + it("createMessage should handle stream with multiple chunks", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Hello" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: { content: " world" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 5, + completion_tokens: 10, + total_tokens: 15, + }, + } + }, + })) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + + const stream = handler.createMessage(systemPrompt, messages) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks[0]).toEqual({ type: "text", text: "Hello" }) + expect(chunks[1]).toEqual({ type: "text", text: " world" }) + expect(chunks[2]).toMatchObject({ type: "usage", inputTokens: 5, outputTokens: 10 }) + }) +}) + +describe("buildApiHandler friendli wiring", () => { + it("returns a FriendliHandler for apiProvider='friendli'", () => { + const handler = buildApiHandler({ apiProvider: "friendli", friendliApiKey: "test-key" }) + expect(handler).toBeInstanceOf(FriendliHandler) + }) +}) + +describe("Friendli model max output tokens (clamping behavior)", () => { + it("GLM-5.2: maxTokens (131072) is under 20% of 1M context window — clamp is no-op", () => { + const model = friendliModels["zai-org/GLM-5.2"] + const result = getModelMaxOutputTokens({ + modelId: "zai-org/GLM-5.2", + model, + settings: { apiProvider: "friendli" }, + format: "openai", + }) + // 1_000_000 * 0.2 = 200_000 > 131_072 → no clamping + expect(result).toBe(131_072) + }) + + it("GLM-5.1: maxTokens (131072) exceeds 20% of 200k context window — clamp binds to 40000", () => { + const model = friendliModels["zai-org/GLM-5.1"] + const result = getModelMaxOutputTokens({ + modelId: "zai-org/GLM-5.1", + model, + settings: { apiProvider: "friendli" }, + format: "openai", + }) + // 200_000 * 0.2 = 40_000 < 131_072 → clamped to 40_000 + expect(result).toBe(40_000) + }) + + it("GLM-5.1 with user modelMaxTokens override: honors override capped at model maxTokens", () => { + const model = friendliModels["zai-org/GLM-5.1"] + const result = getModelMaxOutputTokens({ + modelId: "zai-org/GLM-5.1", + model, + settings: { apiProvider: "friendli", modelMaxTokens: 80_000 }, + format: "openai", + }) + // supportsMaxTokens=true, user set 80k, model ceiling 131072 → min(80000, 131072) = 80000 + expect(result).toBe(80_000) + }) +}) diff --git a/src/api/providers/__tests__/gemini.spec.ts b/src/api/providers/__tests__/gemini.spec.ts index e2633474a3..76b0b3abbe 100644 --- a/src/api/providers/__tests__/gemini.spec.ts +++ b/src/api/providers/__tests__/gemini.spec.ts @@ -54,6 +54,215 @@ describe("GeminiHandler", () => { }) }) + describe("thoughtSignature round-trip (issue #536)", () => { + const systemPrompt = "You are a helpful assistant" + const toolMetadata = { tools: [{ function: { name: "read_file", description: "", parameters: {} } }] } as any + + // Helper: build a mock async-iterable stream from chunks + function makeStream(chunks: unknown[]) { + return { + [Symbol.asyncIterator]: async function* () { + for (const chunk of chunks) yield chunk + }, + } + } + + // Simulate a Gemini 3.x response: thoughtSignature arrives on its own part, + // alongside a functionCall part (the way the real Gemini 3 API returns it). + const turn1Response = makeStream([ + { + candidates: [ + { + content: { + parts: [ + { thought: true, text: "thinking…" }, + { functionCall: { name: "read_file", args: { path: "foo.ts" } } }, + { thoughtSignature: "sig-abc123" }, + ], + }, + }, + ], + }, + { usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 } }, + ]) + + it("captures thoughtSignature from the stream after turn 1", async () => { + ;(handler["client"].models.generateContentStream as any).mockResolvedValue(turn1Response) + + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Read foo.ts" }] + + for await (const _chunk of handler.createMessage(systemPrompt, messages, toolMetadata)) { + // drain + } + + expect(handler.getThoughtSignature()).toBe("sig-abc123") + }) + + it("sends thoughtSignature from history on turn 2 (core regression)", async () => { + // This is the bug from issue #536: after turn 1 the thoughtSignature block is + // persisted into apiConversationHistory. On turn 2 the handler must include it + // in the outgoing request, otherwise Gemini 3.x returns an empty response. + const historyAfterTurn1: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Read foo.ts" }, + { + role: "assistant", + // assistant turn as stored by prepareApiConversationMessage: + // tool_use block + appended thoughtSignature block + content: [ + { type: "tool_use", id: "call-1", name: "read_file", input: { path: "foo.ts" } }, + { type: "thoughtSignature", thoughtSignature: "sig-abc123" } as any, + ], + }, + { + role: "user", + content: [{ type: "tool_result", tool_use_id: "call-1", content: "file contents here" }], + }, + ] + + ;(handler["client"].models.generateContentStream as any).mockResolvedValue( + makeStream([ + { candidates: [{ content: { parts: [{ text: "Done." }] } }] }, + { usageMetadata: { promptTokenCount: 20, candidatesTokenCount: 5 } }, + ]), + ) + + for await (const _chunk of handler.createMessage(systemPrompt, historyAfterTurn1, toolMetadata)) { + // drain + } + + const callArgs = (handler["client"].models.generateContentStream as any).mock.calls[0][0] + const contents: any[] = callArgs.contents + + // The model turn in the outgoing request must carry the thoughtSignature on its functionCall part + const modelTurn = contents.find((c: any) => c.role === "model") + expect(modelTurn).toBeDefined() + const fnPart = modelTurn.parts.find((p: any) => p.functionCall) + expect(fnPart).toBeDefined() + expect(fnPart.thoughtSignature).toBe("sig-abc123") + }) + + it("falls back to base64-encoded skip_thought_signature_validator when history has no signature", async () => { + // Cross-model history scenario: prior session used a non-Gemini model, no signature stored. + // The fallback bypass token must be base64-encoded because Part.thoughtSignature is + // documented as a base64 field. Vertex AI validates this strictly. + const historyNoSig: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Read foo.ts" }, + { + role: "assistant", + content: [{ type: "tool_use", id: "call-1", name: "read_file", input: { path: "foo.ts" } }], + }, + { + role: "user", + content: [{ type: "tool_result", tool_use_id: "call-1", content: "file contents" }], + }, + ] + + ;(handler["client"].models.generateContentStream as any).mockResolvedValue( + makeStream([ + { candidates: [{ content: { parts: [{ text: "Done." }] } }] }, + { usageMetadata: { promptTokenCount: 20, candidatesTokenCount: 5 } }, + ]), + ) + + for await (const _chunk of handler.createMessage(systemPrompt, historyNoSig, toolMetadata)) { + // drain + } + + const callArgs = (handler["client"].models.generateContentStream as any).mock.calls[0][0] + const contents: any[] = callArgs.contents + const modelTurn = contents.find((c: any) => c.role === "model") + const fnPart = modelTurn?.parts.find((p: any) => p.functionCall) + expect(fnPart).toBeDefined() + const expectedBypass = Buffer.from("skip_thought_signature_validator").toString("base64") + expect(fnPart.thoughtSignature).toBe(expectedBypass) + }) + + it("sends thoughtSignature even when reasoningEffort is disabled", async () => { + // If the user disables reasoning effort, thinkingConfig=undefined. + // The old code: includeThoughtSignatures = Boolean(thinkingConfig) || Boolean(metadata?.tools?.length) + // With tools present this is still true — but if called with no tools it would be false. + // Verify the signature is sent regardless when tools are in the metadata. + const handlerNoReasoning = new GeminiHandler({ + apiKey: "test-key", + geminiApiKey: "test-key", + apiModelId: GEMINI_MODEL_NAME, + reasoningEffort: "disable" as any, + }) + handlerNoReasoning["client"] = handler["client"] as any + + const historyWithSig: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Read foo.ts" }, + { + role: "assistant", + content: [ + { type: "tool_use", id: "call-1", name: "read_file", input: { path: "foo.ts" } }, + { type: "thoughtSignature", thoughtSignature: "sig-xyz" } as any, + ], + }, + { + role: "user", + content: [{ type: "tool_result", tool_use_id: "call-1", content: "file contents" }], + }, + ] + + ;(handler["client"].models.generateContentStream as any).mockResolvedValue( + makeStream([ + { candidates: [{ content: { parts: [{ text: "Done." }] } }] }, + { usageMetadata: { promptTokenCount: 20, candidatesTokenCount: 5 } }, + ]), + ) + + for await (const _chunk of handlerNoReasoning.createMessage(systemPrompt, historyWithSig, toolMetadata)) { + // drain + } + + const callArgs = (handler["client"].models.generateContentStream as any).mock.calls[0][0] + const contents: any[] = callArgs.contents + const modelTurn = contents.find((c: any) => c.role === "model") + const fnPart = modelTurn?.parts.find((p: any) => p.functionCall) + expect(fnPart).toBeDefined() + expect(fnPart.thoughtSignature).toBe("sig-xyz") + }) + + it("does NOT capture thoughtSignature when there are no tools in metadata", async () => { + // Without tools, includeThoughtSignatures=false when thinkingConfig is also absent. + // This tests the boundary so we don't over-eagerly store signatures for non-tool calls. + const handlerNoReasoning = new GeminiHandler({ + apiKey: "test-key", + geminiApiKey: "test-key", + apiModelId: GEMINI_MODEL_NAME, + reasoningEffort: "disable" as any, + }) + handlerNoReasoning["client"] = handler["client"] as any + ;(handler["client"].models.generateContentStream as any).mockResolvedValue( + makeStream([ + { + candidates: [ + { + content: { + parts: [ + { functionCall: { name: "read_file", args: { path: "foo.ts" } } }, + { thoughtSignature: "sig-should-not-be-captured" }, + ], + }, + }, + ], + }, + { usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 } }, + ]), + ) + + // No tools in metadata, no thinkingConfig → includeThoughtSignatures=false + for await (const _chunk of handlerNoReasoning.createMessage(systemPrompt, [ + { role: "user", content: "hi" }, + ])) { + // drain + } + + expect(handlerNoReasoning.getThoughtSignature()).toBeUndefined() + }) + }) + describe("createMessage", () => { const mockMessages: Anthropic.Messages.MessageParam[] = [ { diff --git a/src/api/providers/__tests__/native-ollama.spec.ts b/src/api/providers/__tests__/native-ollama.spec.ts index 200868022b..f3f312d296 100644 --- a/src/api/providers/__tests__/native-ollama.spec.ts +++ b/src/api/providers/__tests__/native-ollama.spec.ts @@ -1,4 +1,6 @@ -// npx vitest run api/providers/__tests__/native-ollama.spec.ts +// pnpm exec vitest run api/providers/__tests__/native-ollama.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" import { NativeOllamaHandler } from "../native-ollama" import { ApiHandlerOptions } from "../../../shared/api" @@ -81,6 +83,107 @@ describe("NativeOllamaHandler", () => { expect(results[2]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 2 }) }) + it("should map tool_result array content to a concatenated string, flushing base64 images", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-1", + content: [ + { type: "text", text: "line one" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "imgdata", + }, + }, + { type: "text", text: "line two" }, + ], + }, + ], + }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume stream + } + + // Text blocks are joined with "\n"; the image emits a placeholder. + // Tool results are text-only in Ollama, so the image is flushed onto a + // separate adjacent user message via the `images` field rather than + // inlined into the tool result. + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "user", + content: "line one\n(see following user message for image)\nline two", + }), + expect.objectContaining({ + role: "user", + images: ["imgdata"], + }), + ]), + }), + ) + + // The tool result message itself must not carry an images field. + const callArgs = mockChat.mock.calls[0][0] as any + const toolResultMessage = callArgs.messages.find( + (m: any) => typeof m.content === "string" && m.content.includes("line one"), + ) + expect(toolResultMessage.images).toBeUndefined() + }) + + it("should drop unknown block types in tool_result content (empty string contribution)", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-1", + content: [ + { type: "text", text: "before" }, + { type: "document" } as any, + { type: "text", text: "after" }, + ], + }, + ], + }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + // The unknown block contributes "" so the join produces "before\n\nafter" + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "user", + content: "before\n\nafter", + }), + ]), + }), + ) + }) + it("should not include num_ctx by default", async () => { // Mock the chat response mockChat.mockImplementation(async function* () { @@ -163,6 +266,431 @@ describe("NativeOllamaHandler", () => { expect(results.some((r) => r.type === "reasoning")).toBe(true) expect(results.some((r) => r.type === "text")).toBe(true) }) + + it("should surface Ollama's native message.thinking field as reasoning", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "", thinking: "Reasoning step one" } } + yield { message: { content: "", thinking: " step two" } } + yield { message: { content: "The answer" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Question?" }]) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + const reasoningChunks = results.filter((r) => r.type === "reasoning") + expect(reasoningChunks).toHaveLength(2) + expect(reasoningChunks[0]).toEqual({ type: "reasoning", text: "Reasoning step one" }) + expect(reasoningChunks[1]).toEqual({ type: "reasoning", text: " step two" }) + expect(results.some((r) => r.type === "text" && r.text === "The answer")).toBe(true) + }) + + it("should send think parameter when reasoningEffort is set", async () => { + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok", thinking: "hmm" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + think: "high", + }), + ) + }) + + it("should map reasoningEffort levels to Ollama think values", async () => { + const cases: Array< + [NonNullable, boolean | "high" | "medium" | "low"] + > = [ + ["low", "low"], + ["medium", "medium"], + ["high", "high"], + ["xhigh", "high"], + ["max", "high"], + ["none", true], + ["minimal", true], + ["disable", false], + ] + + for (const [effort, expected] of cases) { + vitest.clearAllMocks() + mockGetOllamaModels.mockResolvedValue({ + qwen3: { contextWindow: 4096, maxTokens: 4096, supportsImages: false, supportsPromptCache: false }, + }) + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: effort, + } + + handler = new NativeOllamaHandler(options) + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + think: expected, + }), + ) + } + }) + + it("should not send think parameter when reasoningEffort is undefined", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + + it("should not send think parameter when enableReasoningEffort is false", async () => { + // When the Ollama UI checkbox is unchecked, enableReasoningEffort + // is false. The handler must not send any think param (undefined), + // leaving the model/Modelfile in control rather than forcing + // thinking off. A stale reasoningEffort value must not override + // the explicit opt-out. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: false, + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + + it("should not send think parameter when enableReasoningEffort is undefined but reasoningEffort is set", async () => { + // This guards against a stale reasoningEffort inherited from + // another provider config. Without an explicit Ollama opt-in, + // the handler must not emit a think param. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + // enableReasoningEffort intentionally undefined + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + + it("should send think=false when reasoningEffort is disable and enableReasoningEffort is true", async () => { + // The only way to explicitly force thinking off via the think + // parameter is to set reasoningEffort to "disable" while opted in. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: "disable", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + think: false, + }), + ) + }) + + it("should round-trip reasoning blocks as the thinking field on assistant messages", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "Prior reasoning", summary: [] } as any, + { type: "text", text: "Prior answer" }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: "Prior reasoning", + }), + ]), + }), + ) + }) + it("should round-trip Anthropic-protocol thinking blocks as the thinking field on assistant messages", async () => { + // Covers the `block.type === "thinking"` branch in the assistant + // message converter. Anthropic-protocol thinking blocks carry the + // reasoning text in a `thinking` field (not `text`). + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "thinking", thinking: "Anthropic thinking text" } as any, + { type: "text", text: "Prior answer" }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: "Anthropic thinking text", + }), + ]), + }), + ) + }) + + it("should concatenate multiple reasoning and thinking blocks into the thinking field", async () => { + // Multiple reasoning/thinking blocks are joined with newlines so the + // full thinking context is preserved across turns. + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "First reasoning", summary: [] } as any, + { type: "thinking", thinking: "Second thinking" } as any, + { type: "text", text: "Answer" }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: "First reasoning\nSecond thinking", + }), + ]), + }), + ) + }) + + it("should not set thinking field when assistant reasoning/thinking blocks are empty", async () => { + // Covers the `block.text.length > 0` and `block.thinking.length > 0` + // false branches, and the `reasoningText || undefined` falsy branch. + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "", summary: [] } as any, + { type: "thinking", thinking: "" } as any, + { type: "text", text: "Answer" }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: undefined, + }), + ]), + }), + ) + }) + + it("should not set thinking field on assistant messages without reasoning blocks", async () => { + // Covers the `reasoningText || undefined` falsy branch for a plain + // assistant text+tool_use message (no reasoning/thinking blocks). + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "text", text: "Answer" }, + { + type: "tool_use", + id: "tool-1", + name: "get_weather", + input: { location: "SF" }, + }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: undefined, + }), + ]), + }), + ) + }) + + it("should not send think parameter for an unknown reasoningEffort value", async () => { + // Covers the `default` branch of getOllamaThinkParam's switch, + // which returns undefined for unrecognized effort values. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: "bogus" as any, + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + }) + + it("should not send think parameter when enableReasoningEffort is true but reasoningEffort is undefined", async () => { + // This is the state the UI checkbox would produce if it only set + // enableReasoningEffort without a default reasoningEffort. The + // handler must not send a think param in that case. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + // reasoningEffort intentionally undefined + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() }) describe("completePrompt", () => { @@ -209,23 +737,87 @@ describe("NativeOllamaHandler", () => { ollamaNumCtx: 4096, // Explicitly set num_ctx } - handler = new NativeOllamaHandler(options) + handler = new NativeOllamaHandler(options) + + mockChat.mockResolvedValue({ + message: { content: "Response" }, + }) + + await handler.completePrompt("Test prompt") + + // Verify that num_ctx was included with the specified value + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + num_ctx: 4096, + }), + }), + ) + }) + }) + + it("should send think parameter in completePrompt when reasoningEffort is set", async () => { + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockResolvedValue({ + message: { content: "Response" }, + }) + + await handler.completePrompt("Test prompt") + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + think: "high", + }), + ) + }) + + it("should not send think parameter in completePrompt when reasoningEffort is undefined", async () => { + mockChat.mockResolvedValue({ + message: { content: "Response" }, + }) + + await handler.completePrompt("Test prompt") + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + + it("should not send think parameter in completePrompt when enableReasoningEffort is false", async () => { + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: false, + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockResolvedValue({ + message: { content: "Response" }, + }) - mockChat.mockResolvedValue({ - message: { content: "Response" }, - }) + await handler.completePrompt("Test prompt") - await handler.completePrompt("Test prompt") + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) - // Verify that num_ctx was included with the specified value - expect(mockChat).toHaveBeenCalledWith( - expect.objectContaining({ - options: expect.objectContaining({ - num_ctx: 4096, - }), - }), - ) - }) + it("should wrap non-Error throws from completePrompt", async () => { + // Covers the `throw error` branch when the rejected value is not an + // Error instance (e.g. a plain object or string). + mockChat.mockRejectedValue("boom") + + await expect(handler.completePrompt("Test prompt")).rejects.toBe("boom") }) describe("error handling", () => { @@ -256,6 +848,58 @@ describe("NativeOllamaHandler", () => { } }).rejects.toThrow("Model llama2 not found in Ollama") }) + + it("should wrap stream processing errors with a descriptive message", async () => { + // Covers the `catch (streamError)` branch: the chat() call + // resolves and returns an async iterable, but iterating it throws. + // The handler must wrap the error with "Ollama stream processing + // error: ..." and rethrow. + mockChat.mockImplementation(async function* () { + yield { message: { content: "partial" } } + throw new Error("stream blew up") + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }]) + + await expect(async () => { + for await (const _ of stream) { + // consume stream + } + }).rejects.toThrow("Ollama stream processing error: stream blew up") + }) + + it("should wrap stream processing errors with unknown message fallback", async () => { + // Covers the `streamError.message || "Unknown error"` fallback in + // the stream processing catch block when the error has no message. + mockChat.mockImplementation(async function* () { + yield { message: { content: "partial" } } + throw {} + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }]) + + await expect(async () => { + for await (const _ of stream) { + // consume stream + } + }).rejects.toThrow("Ollama stream processing error: Unknown error") + }) + + it("should rethrow non-ECONNREFUSED non-404 errors from chat()", async () => { + // Covers the fall-through `throw error` branch in the outer catch + // when the error is neither ECONNREFUSED nor a 404. + const error = new Error("something else") as any + error.status = 500 + mockChat.mockRejectedValue(error) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }]) + + await expect(async () => { + for await (const _ of stream) { + // consume stream + } + }).rejects.toThrow("something else") + }) }) describe("getModel", () => { @@ -606,5 +1250,355 @@ describe("NativeOllamaHandler", () => { const firstEndIndex = results.findIndex((r) => r.type === "tool_call_end") expect(firstEndIndex).toBeGreaterThan(lastPartialIndex) }) + + it("should send tool results with role 'tool' and tool_name when preceded by a tool_use", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "tool-abc", + name: "apply_diff", + input: { + path: "foo.ts", + diff: "SEARCH_REPLACE_DIFF_CONTENT", + }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-abc", + content: "Diff applied successfully", + }, + ], + }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume stream + } + + // The tool result should use Ollama's native "tool" role with tool_name + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "tool", + tool_name: "apply_diff", + content: "Diff applied successfully", + }), + ]), + }), + ) + }) + + it("should fall back to role 'user' for tool results when no matching tool_use is found", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "unknown-id", + content: "orphan result", + }, + ], + }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume stream + } + + // No preceding tool_use -> fall back to "user" role + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "user", + content: "orphan result", + }), + ]), + }), + ) + }) + + it("should strip additionalProperties from tool schema parameters", async () => { + mockGetOllamaModels.mockResolvedValue({ + "llama3.2": { + contextWindow: 128000, + maxTokens: 4096, + supportsImages: true, + supportsPromptCache: false, + }, + }) + + const options: ApiHandlerOptions = { + apiModelId: "llama3.2", + ollamaModelId: "llama3.2", + ollamaBaseUrl: "http://localhost:11434", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const tools = [ + { + type: "function" as const, + function: { + name: "apply_diff", + description: "Apply a diff", + parameters: { + type: "object", + properties: { + path: { type: "string", description: "File path" }, + diff: { type: "string", description: "Diff content" }, + }, + required: ["path", "diff"], + additionalProperties: false, + }, + }, + }, + ] + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Edit the file" }], { + taskId: "test", + tools, + }) + + for await (const _ of stream) { + // consume stream + } + + // additionalProperties should be stripped from the parameters + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + tools: [ + { + type: "function", + function: { + name: "apply_diff", + description: "Apply a diff", + parameters: { + type: "object", + properties: { + path: { type: "string", description: "File path" }, + diff: { type: "string", description: "Diff content" }, + }, + required: ["path", "diff"], + }, + }, + }, + ], + }), + ) + + // Explicitly verify additionalProperties is not present + const callArgs = mockChat.mock.calls[0][0] as any + expect(callArgs.tools[0].function.parameters).not.toHaveProperty("additionalProperties") + }) + + it("should recursively strip additionalProperties from nested tool schema parameters", async () => { + mockGetOllamaModels.mockResolvedValue({ + "llama3.2": { + contextWindow: 128000, + maxTokens: 4096, + supportsImages: true, + supportsPromptCache: false, + }, + }) + + const options: ApiHandlerOptions = { + apiModelId: "llama3.2", + ollamaModelId: "llama3.2", + ollamaBaseUrl: "http://localhost:11434", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const tools = [ + { + type: "function" as const, + function: { + name: "apply_diff", + description: "Apply a diff", + parameters: { + type: "object", + properties: { + path: { type: "string", description: "File path" }, + options: { + type: "object", + properties: { + dry_run: { type: "boolean" }, + backup: { type: "boolean" }, + }, + required: ["dry_run"], + additionalProperties: false, + }, + }, + required: ["path", "options"], + additionalProperties: false, + }, + }, + }, + ] + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Edit the file" }], { + taskId: "test", + tools, + }) + + for await (const _ of stream) { + // consume stream + } + + const callArgs = mockChat.mock.calls[0][0] as any + const params = callArgs.tools[0].function.parameters + + // Top-level additionalProperties stripped + expect(params).not.toHaveProperty("additionalProperties") + + // Nested additionalProperties also stripped + expect(params.properties.options).not.toHaveProperty("additionalProperties") + expect(params.properties.options.properties.dry_run).toEqual({ type: "boolean" }) + }) + + it("should keep tool results text-only and move images onto the adjacent user message", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "tool-img", + name: "read_file", + input: { path: "foo.ts" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-img", + content: [ + { type: "text", text: "screenshot" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "imgdata", + }, + }, + ], + }, + { type: "text", text: "please continue" }, + ], + }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume stream + } + + const callArgs = mockChat.mock.calls[0][0] as any + + // The tool result uses the native "tool" role and is text-only. + const toolMessage = callArgs.messages.find((m: any) => m.role === "tool") + expect(toolMessage).toBeDefined() + expect(toolMessage.tool_name).toBe("read_file") + expect(toolMessage.images).toBeUndefined() + + // The image is carried by the adjacent user message. + const userMessage = callArgs.messages.find( + (m: any) => m.role === "user" && Array.isArray(m.images) && m.images.includes("imgdata"), + ) + expect(userMessage).toBeDefined() + }) + + it("should not leak images from one tool result into another", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "tool_use", id: "tool-a", name: "read_file", input: { path: "a.ts" } }, + { type: "tool_use", id: "tool-b", name: "read_file", input: { path: "b.ts" } }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-a", + content: [ + { type: "text", text: "a" }, + { + type: "image", + source: { type: "base64", media_type: "image/png", data: "img-a" }, + }, + ], + }, + { + type: "tool_result", + tool_use_id: "tool-b", + content: [{ type: "text", text: "b" }], + }, + ], + }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume stream + } + + const callArgs = mockChat.mock.calls[0][0] as any + const toolMessages = callArgs.messages.filter((m: any) => m.role === "tool") + + // Neither tool result should carry an images field. + expect(toolMessages).toHaveLength(2) + for (const m of toolMessages) { + expect(m.images).toBeUndefined() + } + + // The single image is delivered once via the adjacent user message. + const userImageMessages = callArgs.messages.filter((m: any) => m.role === "user" && Array.isArray(m.images)) + expect(userImageMessages).toHaveLength(1) + expect(userImageMessages[0].images).toEqual(["img-a"]) + }) }) }) diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index dcc0c4d035..4ab9755252 100644 --- a/src/api/providers/__tests__/openai-codex.spec.ts +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -1,6 +1,8 @@ // npx vitest run api/providers/__tests__/openai-codex.spec.ts +import { Anthropic } from "@anthropic-ai/sdk" import { OpenAiCodexHandler } from "../openai-codex" +import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth" describe("OpenAiCodexHandler.getModel", () => { it.each(["gpt-5.1", "gpt-5", "gpt-5.1-codex", "gpt-5-codex", "gpt-5-codex-mini", "gpt-5.3-codex-spark"])( @@ -42,3 +44,104 @@ describe("OpenAiCodexHandler.getModel", () => { expect(model.info).toBeDefined() }) }) + +describe("OpenAiCodexHandler.createMessage", () => { + it("should skip URL-sourced images in formatFullConversation (only base64 emits input_image)", async () => { + const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.1-codex" }) + + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + + const capturedInput: any[] = [] + ;(handler as any).client = { + responses: { + create: vitest.fn().mockImplementation(async (body: any) => { + capturedInput.push(...(body.input ?? [])) + return { + async *[Symbol.asyncIterator]() { + yield { + type: "response.completed", + response: { + id: "r1", + status: "completed", + output: [], + usage: { input_tokens: 1, output_tokens: 1 }, + }, + } + }, + } + }), + }, + } + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this:" }, + { type: "image", source: { type: "url", url: "https://example.com/img.png" } as any }, + ], + }, + ] + + const stream = handler.createMessage("system", messages) + for await (const _ of stream) { + // consume + } + + // URL image is skipped; only the text input_text block should be present + const userMsg = capturedInput.find((item: any) => item.role === "user") + expect(userMsg?.content).toEqual([{ type: "input_text", text: "Look at this:" }]) + expect(JSON.stringify(capturedInput)).not.toContain("input_image") + }) + + it("should emit input_image for base64 images in formatFullConversation", async () => { + const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.1-codex" }) + + vitest.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") + vitest.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") + + const capturedInput: any[] = [] + ;(handler as any).client = { + responses: { + create: vitest.fn().mockImplementation(async (body: any) => { + capturedInput.push(...(body.input ?? [])) + return { + async *[Symbol.asyncIterator]() { + yield { + type: "response.completed", + response: { + id: "r1", + status: "completed", + output: [], + usage: { input_tokens: 1, output_tokens: 1 }, + }, + } + }, + } + }), + }, + } + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this:" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "abc123" } }, + ], + }, + ] + + const stream = handler.createMessage("system", messages) + for await (const _ of stream) { + // consume + } + + const userMsg = capturedInput.find((item: any) => item.role === "user") + expect(userMsg?.content).toContainEqual({ + type: "input_image", + image_url: "data:image/png;base64,abc123", + }) + }) +}) diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 4d35387992..2a3ec0afb2 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -1845,4 +1845,100 @@ describe("GPT-5 streaming event coverage (additional)", () => { }) }) }) + + describe("URL image handling", () => { + it("should skip URL-sourced images in formatFullConversation (only base64 emits input_image)", async () => { + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.output_text.delta","delta":"ok"}\n\n'), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + mockResponsesCreate.mockRejectedValue(new Error("SDK not available")) + + const localHandler = new OpenAiNativeHandler({ + apiModelId: "gpt-4.1", + openAiNativeApiKey: "test-api-key", + }) + + const urlImageMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this:" }, + { + type: "image", + source: { type: "url", url: "https://example.com/img.png" } as any, + }, + ], + }, + ] + + const stream = localHandler.createMessage("You are a helpful assistant.", urlImageMessages) + for await (const _ of stream) { + // consume + } + + const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string + const parsedBody = JSON.parse(bodyStr) + // URL image is skipped; only the text part is in the input + const userMsg = parsedBody.input[0] + expect(userMsg.content).toEqual([{ type: "input_text", text: "Look at this:" }]) + expect(bodyStr).not.toContain("input_image") + }) + + it("should emit input_image for base64 images in formatFullConversation", async () => { + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.output_text.delta","delta":"ok"}\n\n'), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + mockResponsesCreate.mockRejectedValue(new Error("SDK not available")) + + const localHandler = new OpenAiNativeHandler({ + apiModelId: "gpt-4.1", + openAiNativeApiKey: "test-api-key", + }) + + const b64ImageMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this:" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "abc123" } }, + ], + }, + ] + + const stream = localHandler.createMessage("You are a helpful assistant.", b64ImageMessages) + for await (const _ of stream) { + // consume + } + + const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string + const parsedBody = JSON.parse(bodyStr) + const userMsg = parsedBody.input[0] + expect(userMsg.content).toContainEqual({ + type: "input_image", + image_url: "data:image/png;base64,abc123", + }) + }) + }) }) diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 3c006f8318..332fc39602 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -636,6 +636,214 @@ describe("OpenAiHandler", () => { const callArgs = mockCreate.mock.calls[0][0] expect(callArgs.max_completion_tokens).toBe(4096) }) + + describe("TagMatcher reasoning tags", () => { + it("should treat stray closing tag as plain text when no tag is open", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "finaltext" } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + })) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([{ type: "text", text: "finaltext" }]) + }) + + it("should treat extra closing tag after a closed block as plain text", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { + choices: [{ delta: { content: "thinkingfinaltext" } }], + }, + }) + .mockResolvedValueOnce({ done: true }), + }), + })) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { type: "reasoning", text: "thinking" }, + { type: "text", text: "finaltext" }, + ]) + }) + + it("should handle nested mixed tags with correct closure matching", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "outer" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "inner" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: " middle" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "final text" } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + })) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // With the tag stack fix, closes inner tag, + // and correctly closes the outer tag. + // inner content inside is reasoning, middle is still reasoning under + expect(chunks).toEqual([ + { type: "reasoning", text: "outer" }, + { type: "reasoning", text: "inner" }, + { type: "reasoning", text: " middle" }, + { type: "text", text: "final text" }, + ]) + }) + + it("should handle nested tags with correct stack unwinding", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "outer" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "inner" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: " middle" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "final text" } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + })) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // With the tag stack fix, closes inner tag, + // and correctly closes the outer tag. + // inner content inside is reasoning, middle is still reasoning under + expect(chunks).toEqual([ + { type: "reasoning", text: "outer" }, + { type: "reasoning", text: "inner" }, + { type: "reasoning", text: " middle" }, + { type: "text", text: "final text" }, + ]) + }) + + it("should handle reasoning_content alongside tag matching", async () => { + mockCreate.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { reasoning_content: "native reasoning" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: "tag based" } }] }, + }) + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: " final output" } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + })) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { type: "reasoning", text: "native reasoning" }, + { type: "reasoning", text: "tag based" }, + { type: "text", text: " final output" }, + ]) + }) + }) + + it("should include reasoning_content on assistant history messages when preserveReasoning is set", async () => { + // Regression guard for issue #201: OpenAI-compatible providers (e.g. DeepSeek via custom + // base URL) must pass reasoning_content back in history when thinking mode is active. + // This exercises OpenAiHandler -> convertToOpenAiMessages directly. + const thinkingHandler = new OpenAiHandler({ + ...mockOptions, + openAiCustomModelInfo: { + contextWindow: 128_000, + supportsPromptCache: false, + preserveReasoning: true, + }, + }) + + const messagesWithReasoning: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "What files are in the project?" }, + { + role: "assistant", + content: [ + { type: "reasoning", text: "I should use the read_file tool.", summary: [] } as any, + { type: "tool_use", id: "call_001", name: "read_file", input: { path: "README.md" } }, + ], + }, + { + role: "user", + content: [{ type: "tool_result", tool_use_id: "call_001", content: "# Project\nHello." }], + }, + ] + + const stream = thinkingHandler.createMessage(systemPrompt, messagesWithReasoning) + for await (const _chunk of stream) { + } + + expect(mockCreate).toHaveBeenCalled() + const sentMessages: any[] = mockCreate.mock.calls[0][0].messages + const assistantMsg = sentMessages.find((m: any) => m.role === "assistant" && m.tool_calls?.length) + expect(assistantMsg).toBeDefined() + expect(assistantMsg.reasoning_content).toBe("I should use the read_file tool.") + }) }) describe("error handling", () => { diff --git a/src/api/providers/__tests__/requesty.spec.ts b/src/api/providers/__tests__/requesty.spec.ts index cdf6512324..b0e86f35fa 100644 --- a/src/api/providers/__tests__/requesty.spec.ts +++ b/src/api/providers/__tests__/requesty.spec.ts @@ -68,6 +68,24 @@ vitest.mock("../fetchers/modelCache", () => ({ cacheReadsPrice: 1, description: "Claude Fable 5", }, + "anthropic/claude-sonnet-5": { + maxTokens: 128000, + contextWindow: 1000000, + supportsImages: true, + supportsPromptCache: true, + // Local fork: Sonnet 5 uses the effort/adaptive shape (not budget/binary), mirroring + // the requesty fetcher's claude-sonnet-5 patch. + supportsReasoningEffort: ["low", "medium", "high", "xhigh", "max"], + requiredReasoningEffort: true, + reasoningEffort: "high", + supportsReasoningDisplay: true, + supportsTemperature: false, + inputPrice: 3, + outputPrice: 15, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + description: "Claude Sonnet 5", + }, }) }), })) @@ -261,6 +279,41 @@ describe("RequestyHandler", () => { ) }) + it("uses adaptive thinking for Claude Sonnet 5 when reasoning is enabled", async () => { + const handler = new RequestyHandler({ + requestyApiKey: "test-key", + requestyModelId: "anthropic/claude-sonnet-5", + enableReasoningEffort: true, + modelMaxTokens: 32768, + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + id: "test-id", + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, + } + }, + } + + mockCreate.mockResolvedValue(mockStream) + + const generator = handler.createMessage("test system prompt", [{ role: "user" as const, content: "test" }]) + await generator.next() + + // Effort models ignore modelMaxTokens, and the fork's requesty `thinking` param + // carries the AnthropicProviderReasoningParams envelope (the provider unwraps it). + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "anthropic/claude-sonnet-5", + max_tokens: 128000, + thinking: { thinking: { type: "adaptive" }, output_config: { effort: "high" } }, + temperature: undefined, + }), + ) + }) + it("handles API errors", async () => { const handler = new RequestyHandler(mockOptions) const mockError = new Error("API Error") @@ -532,6 +585,25 @@ describe("RequestyHandler", () => { }) }) + it("omits temperature for Claude Sonnet 5 in completePrompt", async () => { + const handler = new RequestyHandler({ + requestyApiKey: "test-key", + requestyModelId: "anthropic/claude-sonnet-5", + }) + mockCreate.mockResolvedValue({ choices: [{ message: { content: "test completion" } }] }) + + await handler.completePrompt("test prompt") + + // Effort-shape Sonnet 5: max_tokens is the model ceiling (128k), not the budget-path + // default of 8192. completePrompt sends no reasoning fields. + expect(mockCreate).toHaveBeenCalledWith({ + model: "anthropic/claude-sonnet-5", + max_tokens: 128000, + messages: [{ role: "system", content: "test prompt" }], + temperature: undefined, + }) + }) + it("handles API errors", async () => { const handler = new RequestyHandler(mockOptions) const mockError = new Error("API Error") diff --git a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts index 3370f87dd2..4669904e2f 100644 --- a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts @@ -49,6 +49,18 @@ vitest.mock("../fetchers/modelCache", () => ({ cacheReadsPrice: 1, description: "Claude Fable 5", }, + "anthropic/claude-sonnet-5": { + maxTokens: 128000, + contextWindow: 1000000, + supportsImages: true, + supportsPromptCache: true, + supportsTemperature: false, + inputPrice: 3, + outputPrice: 15, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + description: "Claude Sonnet 5", + }, "anthropic/claude-3.5-haiku": { maxTokens: 32000, contextWindow: 200000, @@ -305,6 +317,24 @@ describe("VercelAiGatewayHandler", () => { ) }) + it("omits temperature for Claude Sonnet 5", async () => { + const handler = new VercelAiGatewayHandler({ + ...mockOptions, + vercelAiGatewayModelId: "anthropic/claude-sonnet-5", + }) + + await handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello" }]).next() + + // Assert directly on the extracted call arg. `objectContaining({ + // temperature: undefined })` passes whether temperature is explicitly + // undefined or simply absent, so it wouldn't catch a regression where the + // handler stops consulting supportsTemperature. + const call = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(call.model).toBe("anthropic/claude-sonnet-5") + expect(call.temperature).toBeUndefined() + expect(call.max_completion_tokens).toBe(128000) + }) + it("adds cache breakpoints for supported models", async () => { const { addCacheBreakpoints } = await import("../../transform/caching/vercel-ai-gateway") const handler = new VercelAiGatewayHandler({ diff --git a/src/api/providers/__tests__/vscode-lm.spec.ts b/src/api/providers/__tests__/vscode-lm.spec.ts index a79a5a4bcb..5227c4b289 100644 --- a/src/api/providers/__tests__/vscode-lm.spec.ts +++ b/src/api/providers/__tests__/vscode-lm.spec.ts @@ -63,6 +63,7 @@ import * as vscode from "vscode" import { VsCodeLmHandler } from "../vscode-lm" import type { ApiHandlerOptions } from "../../../shared/api" import type { Anthropic } from "@anthropic-ai/sdk" +import { openAiModelInfoSaneDefaults, vscodeLlmDefaultModelId, vscodeLlmModels } from "@roo-code/types" const mockLanguageModelChat = { id: "test-model", @@ -440,6 +441,86 @@ describe("VsCodeLmHandler", () => { const model = handler.getModel() expect(model.info).toBeDefined() }) + + it("should use the full advertised maxInputTokens without an upper cap", async () => { + // A large advertised window is surfaced as-is, not clamped to a smaller default. + const mockModel = { ...mockLanguageModelChat, maxInputTokens: 936000 } + ;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel]) + handler["client"] = null + await handler.initializeClient() + + const model = handler.getModel() + expect(model.info.contextWindow).toBe(936000) + }) + + it("should pass through a small maxInputTokens unchanged", async () => { + const mockModel = { ...mockLanguageModelChat, maxInputTokens: 4096 } + ;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel]) + handler["client"] = null + await handler.initializeClient() + + const model = handler.getModel() + expect(model.info.contextWindow).toBe(4096) + }) + + it("should fall back to sane defaults when maxInputTokens is not a number", async () => { + const mockModel = { ...mockLanguageModelChat, maxInputTokens: undefined as unknown as number } + ;(vscode.lm.selectChatModels as Mock).mockResolvedValue([mockModel]) + handler["client"] = null + await handler.initializeClient() + + const model = handler.getModel() + expect(model.info.contextWindow).toBe(openAiModelInfoSaneDefaults.contextWindow) + }) + }) + + describe("getCondenseContextWindow", () => { + it("uses the static-table maxInputTokens for a known VS Code LM family", () => { + const opusHandler = new VsCodeLmHandler({ + vsCodeLmModelSelector: { vendor: "copilot", family: "claude-opus-4.8" }, + }) + expect(opusHandler.getCondenseContextWindow()).toBe(vscodeLlmModels["claude-opus-4.8"].maxInputTokens) + opusHandler.dispose() + }) + + it("falls back to the default-row maxInputTokens for an unknown family (catalog drift)", () => { + // `test-family` isn't a curated row (e.g. a selector left over from a dropped model), so the + // gate resolves the default row instead of the inflated live window. + handler["client"] = mockLanguageModelChat as unknown as vscode.LanguageModelChat + expect(handler.getCondenseContextWindow()).toBe(vscodeLlmModels[vscodeLlmDefaultModelId].maxInputTokens) + }) + + it("falls back to the default-row maxInputTokens when no family is resolvable (no client, no selector family)", () => { + // No client and no selector family means `family` is undefined, so the gate uses the default + // row's maxInputTokens rather than the live getModel().info.contextWindow. + const noFamilyHandler = new VsCodeLmHandler({ vsCodeLmModelSelector: { vendor: "copilot" } }) + noFamilyHandler["client"] = null + expect(noFamilyHandler.getCondenseContextWindow()).toBe( + vscodeLlmModels[vscodeLlmDefaultModelId].maxInputTokens, + ) + noFamilyHandler.dispose() + }) + + it("falls back to the derived window when the static row exists but maxInputTokens is non-positive", () => { + // A curated row exists but its maxInputTokens is <= 0, so the `> 0` guard fails and the gate + // falls back to getModel().info.contextWindow. + const family = "claude-opus-4.8" + const original = vscodeLlmModels[family].maxInputTokens + try { + ;(vscodeLlmModels[family] as { maxInputTokens: number }).maxInputTokens = 0 + const guardHandler = new VsCodeLmHandler({ + vsCodeLmModelSelector: { vendor: "copilot", family }, + }) + // Leave the client unset so `family` resolves from the selector, forcing the zeroed + // static row to be read instead of a live client's family. + guardHandler["client"] = null + expect(guardHandler.getCondenseContextWindow()).toBe(guardHandler.getModel().info.contextWindow) + expect(guardHandler.getCondenseContextWindow()).toBe(openAiModelInfoSaneDefaults.contextWindow) + guardHandler.dispose() + } finally { + ;(vscodeLlmModels[family] as { maxInputTokens: number }).maxInputTokens = original + } + }) }) describe("countTokens", () => { diff --git a/src/api/providers/anthropic-vertex.ts b/src/api/providers/anthropic-vertex.ts index 4c6f327cd3..c5a00d8dc3 100644 --- a/src/api/providers/anthropic-vertex.ts +++ b/src/api/providers/anthropic-vertex.ts @@ -296,13 +296,9 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple } as Anthropic.Messages.MessageCreateParamsNonStreaming const response = await this.client.messages.create(params) - const content = response.content[0] + const content = response.content.find(({ type }) => type === "text") - if (content.type === "text") { - return content.text - } - - return "" + return content?.type === "text" ? content.text : "" } catch (error) { if (error instanceof Error) { throw new Error(`Vertex completion error: ${error.message}`) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 7402d6e40d..a727ad1481 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -29,6 +29,16 @@ import { convertOpenAIToolChoiceToAnthropic, } from "../../core/prompts/tools/native-tools/converters" +// Pre-sorted list of known Anthropic model IDs (lowercased) by length (descending) for case-insensitive substring matching. +const ANTHROPIC_MODEL_IDS_SORTED_LOWER = (Object.keys(anthropicModels) as AnthropicModelId[]) + .map((id) => id.toLowerCase()) + .sort((a, b) => b.length - a.length) as string[] + +// Original-case mapping: lowercase key → original AnthropicModelId for lookup. +const ANTHROPIC_MODEL_ID_LOWER_TO_ORIGINAL = Object.fromEntries( + (Object.keys(anthropicModels) as AnthropicModelId[]).map((id) => [id.toLowerCase(), id]), +) + export class AnthropicHandler extends BaseProvider implements SingleCompletionHandler { private options: ApiHandlerOptions private client: Anthropic @@ -87,6 +97,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa switch (modelId) { case "claude-fable-5": + case "claude-sonnet-5": case "claude-sonnet-4-6": case "claude-sonnet-4-5": case "claude-sonnet-4-20250514": @@ -158,6 +169,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa // Then check for models that support prompt caching switch (modelId) { case "claude-fable-5": + case "claude-sonnet-5": case "claude-sonnet-4-6": case "claude-sonnet-4-5": case "claude-sonnet-4-20250514": @@ -379,10 +391,29 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa } } + // Guesses capabilities for an unrecognized model ID via known-family substring match. + private guessModelInfoFromId(modelId: string): ModelInfo { + const lowerModelId = modelId.toLowerCase() + const matchedLower = ANTHROPIC_MODEL_IDS_SORTED_LOWER.find((knownId) => lowerModelId.includes(knownId)) + + if (!matchedLower) { + return anthropicModels[anthropicDefaultModelId] + } + + const originalId = ANTHROPIC_MODEL_ID_LOWER_TO_ORIGINAL[matchedLower] as AnthropicModelId + return anthropicModels[originalId] + } + getModel() { const modelId = this.options.apiModelId - const id = modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId - let info: ModelInfo = anthropicModels[id] + const isKnownModel = modelId !== undefined && modelId in anthropicModels + + // Always honor a user-configured apiModelId, even if it's not a known model. + const id = isKnownModel ? (modelId as AnthropicModelId) : (modelId ?? anthropicDefaultModelId) + + let info: ModelInfo = isKnownModel + ? anthropicModels[modelId as AnthropicModelId] + : this.guessModelInfoFromId(id) // If 1M context beta is enabled for supported models, update the model info if ( diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index 28c812660f..d6406f2a67 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -11,7 +11,7 @@ import { convertToOpenAiMessages } from "../transform/openai-format" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { calculateApiCostOpenAI } from "../../shared/cost" import { extractReasoningFromDelta } from "./utils/extract-reasoning" @@ -118,7 +118,7 @@ export abstract class BaseOpenAiCompatibleProvider const stream = await this.createStream(systemPrompt, messages, metadata) const matcher = new TagMatcher( - "think", + ["think", "thought"], (chunk) => ({ type: chunk.matched ? "reasoning" : "text", diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index d92e993d58..8c2b5ace68 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -301,13 +301,14 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH * Detect models that require the adaptive-thinking API contract. * * Starting with Claude Opus 4.7 (and the matching Sonnet 4.7), and continuing - * in Opus 4.8 / Sonnet 4.8 and Claude Fable 5, Anthropic removed sampling parameters - * (temperature/top_p/top_k) and replaced budget_tokens-based thinking with - * `thinking.type: "adaptive"` plus `output_config.effort`. The migration guide - * from 4.7 → 4.8 confirms there are no further breaking API changes, and Fable 5 - * keeps the same adaptive-thinking contract, so a single - * guard matches both generations. Shared by createMessage and completePrompt so - * both request paths omit temperature for these models (sending it causes a 400). + * in Opus 4.8 / Sonnet 4.8, Claude Fable 5, and Claude Sonnet 5, Anthropic + * removed sampling parameters (temperature/top_p/top_k) and replaced + * budget_tokens-based thinking with `thinking.type: "adaptive"` plus + * `output_config.effort`. The migration guide from 4.7 → 4.8 confirms there + * are no further breaking API changes, and Fable 5 / Sonnet 5 keep the same + * adaptive-thinking contract, so a single guard matches all generations. + * Shared by createMessage and completePrompt so both request paths omit + * temperature for these models (sending it causes a 400). * * Accepts a model ID (with or without a cross-region/global prefix) and strips * the prefix via parseBaseModelId before matching. @@ -319,7 +320,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH baseModelId.includes("opus-4-8") || baseModelId.includes("fable-5") || baseModelId.includes("sonnet-4-7") || - baseModelId.includes("sonnet-4-8") + baseModelId.includes("sonnet-4-8") || + baseModelId.includes("sonnet-5") ) } diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 819fe6c7bc..ef7839ad34 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -18,6 +18,7 @@ import { convertToR1Format } from "../transform/r1-format" import { OpenAiHandler } from "./openai" import { extractReasoningFromDelta } from "./utils/extract-reasoning" import type { ApiHandlerCreateMessageMetadata } from "../index" +import { handleOpenAIError } from "./utils/error-handler" // Custom interface for DeepSeek params to support thinking mode type DeepSeekChatCompletionParams = Omit & { @@ -137,7 +138,6 @@ export class DeepSeekHandler extends OpenAiHandler { isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {}, ) } catch (error) { - const { handleOpenAIError } = await import("./utils/openai-error-handler") throw handleOpenAIError(error, "DeepSeek") } diff --git a/src/api/providers/fetchers/__tests__/modelCache.spec.ts b/src/api/providers/fetchers/__tests__/modelCache.spec.ts index 11395485a9..5b771f2bda 100644 --- a/src/api/providers/fetchers/__tests__/modelCache.spec.ts +++ b/src/api/providers/fetchers/__tests__/modelCache.spec.ts @@ -432,5 +432,187 @@ describe("empty cache protection", () => { expect(result1).toEqual(mockModels) expect(result2).toEqual(mockModels) }) + + it("scopes in-flight dedup by API key for key-scoped providers", async () => { + // In-flight dedup is keyed on the compound cache key, so concurrent refreshes for a + // key-scoped provider must dedup only when the API key matches. Two different keys + // (different compound keys) each trigger their own fetch; the same key shares one. + const mockModels = { + "requesty/model": { + maxTokens: 4096, + contextWindow: 200000, + supportsPromptCache: false, + description: "Requesty model", + }, + } + mockGetRequestyModels.mockResolvedValue(mockModels) + + const { refreshModels } = await import("../modelCache") + + // Different keys -> separate compound keys -> two distinct fetches. + const [a, b] = await Promise.all([ + refreshModels({ provider: "requesty", apiKey: "key-one" }), + refreshModels({ provider: "requesty", apiKey: "key-two" }), + ]) + expect(mockGetRequestyModels).toHaveBeenCalledTimes(2) + expect(a).toEqual(mockModels) + expect(b).toEqual(mockModels) + + mockGetRequestyModels.mockClear() + + // Same key -> same compound key -> a single shared in-flight fetch. + let resolveShared: (value: typeof mockModels) => void + mockGetRequestyModels.mockReturnValue( + new Promise((resolve) => { + resolveShared = resolve + }), + ) + + const shared1 = refreshModels({ provider: "requesty", apiKey: "same-key" }) + const shared2 = refreshModels({ provider: "requesty", apiKey: "same-key" }) + + expect(mockGetRequestyModels).toHaveBeenCalledTimes(1) + + resolveShared!(mockModels) + const [s1, s2] = await Promise.all([shared1, shared2]) + expect(s1).toEqual(mockModels) + expect(s2).toEqual(mockModels) + }) + }) +}) + +describe("key-scoped cache key derivation", () => { + // Exercises the per-API-key cache discriminator that all KEY_SCOPED_PROVIDERS share. + // Requesty is used only because it is a key-scoped provider with a mocked fetcher; the + // behavior under test is provider-agnostic. + const keyScopedProvider = "requesty" as const + + let mockCache: any + let mockSet: Mock + + const mockModels = { + "key-scoped/model": { + maxTokens: 4096, + contextWindow: 200000, + supportsPromptCache: false, + description: "Key-scoped provider model", + }, + } + + beforeEach(() => { + vi.clearAllMocks() + const MockedNodeCache = vi.mocked(NodeCache) + mockCache = new MockedNodeCache() + mockCache.get.mockReturnValue(undefined) + mockSet = mockCache.set + mockGetRequestyModels.mockResolvedValue(mockModels) + }) + + // Returns the cache key the result was written under (first arg of the matching set call). + const writtenCacheKey = (): string => { + const call = mockSet.mock.calls.find((c) => c[1] === mockModels) + return call?.[0] as string + } + + it("writes different cache keys for different API keys", async () => { + await getModels({ provider: keyScopedProvider, apiKey: "key-one" }) + const firstKey = writtenCacheKey() + + mockSet.mockClear() + await getModels({ provider: keyScopedProvider, apiKey: "key-two" }) + const secondKey = writtenCacheKey() + + expect(firstKey).toBeDefined() + expect(secondKey).toBeDefined() + expect(firstKey).not.toEqual(secondKey) + }) + + it("writes the same cache key for repeated calls with the same API key", async () => { + await getModels({ provider: keyScopedProvider, apiKey: "stable-key" }) + const firstKey = writtenCacheKey() + + mockSet.mockClear() + await getModels({ provider: keyScopedProvider, apiKey: "stable-key" }) + const secondKey = writtenCacheKey() + + expect(firstKey).toEqual(secondKey) + }) + + it("does not embed the raw API key in the cache key and truncates the discriminator", async () => { + const apiKey = "super-secret-api-key-value" + await getModels({ provider: keyScopedProvider, apiKey }) + const cacheKey = writtenCacheKey() + + // The raw secret must never appear in the on-disk-bound cache key. + expect(cacheKey).not.toContain(apiKey) + // The discriminator is the trailing key-component: an 8-char (32-bit) hex string. + const discriminator = cacheKey.split(":").pop() as string + expect(discriminator).toMatch(/^[0-9a-f]{8}$/) + }) +}) + +describe("compound cache key derivation across scoping dimensions", () => { + // Exercises every branch of getCacheKey via the public getModels() entry point. + // litellm is url-scoped AND key-scoped; openrouter is neither, so it hits the bare + // provider fallback. The fetcher mocks let us observe the cache key the result is + // written under (first arg of the matching memoryCache.set call). + const mockModels = { + "compound/model": { + maxTokens: 4096, + contextWindow: 200000, + supportsPromptCache: false, + description: "Compound cache key model", + }, + } + + let mockSet: Mock + + beforeEach(() => { + vi.clearAllMocks() + const MockedNodeCache = vi.mocked(NodeCache) + const mockCache = new MockedNodeCache() + ;(mockCache.get as Mock).mockReturnValue(undefined) + mockSet = mockCache.set as unknown as Mock + mockGetLiteLLMModels.mockResolvedValue(mockModels) + mockGetOpenRouterModels.mockResolvedValue(mockModels) + }) + + const writtenCacheKey = (): string => { + const call = mockSet.mock.calls.find((c) => c[1] === mockModels) + return call?.[0] as string + } + + it("includes both the server URL and the key discriminator for url+key-scoped providers", async () => { + await getModels({ provider: "litellm", apiKey: "compound-key", baseUrl: "http://host:4000" }) + const cacheKey = writtenCacheKey() + + // Expected shape: provider:url:keyDiscriminator + expect(cacheKey).toMatch(/^litellm:http:\/\/host:4000:[0-9a-f]{8}$/) + }) + + it("normalizes trailing slashes in the server URL so equivalent URLs share a cache key", async () => { + await getModels({ provider: "litellm", apiKey: "compound-key", baseUrl: "http://host:4000/" }) + const withSlash = writtenCacheKey() + + mockSet.mockClear() + await getModels({ provider: "litellm", apiKey: "compound-key", baseUrl: "http://host:4000" }) + const withoutSlash = writtenCacheKey() + + expect(withSlash).toEqual(withoutSlash) + }) + + it("includes only the server URL when a url-scoped provider has no API key", async () => { + await getModels({ provider: "litellm", baseUrl: "http://host:4000" }) + const cacheKey = writtenCacheKey() + + // No trailing key discriminator when apiKey is absent. + expect(cacheKey).toBe("litellm:http://host:4000") + }) + + it("falls back to the bare provider name for providers that are neither url- nor key-scoped", async () => { + await getModels({ provider: "openrouter", apiKey: "ignored-key", baseUrl: "http://ignored:4000" }) + const cacheKey = writtenCacheKey() + + expect(cacheKey).toBe("openrouter") }) }) diff --git a/src/api/providers/fetchers/__tests__/ollama.test.ts b/src/api/providers/fetchers/__tests__/ollama.test.ts index d9d1832175..9c0b547e88 100644 --- a/src/api/providers/fetchers/__tests__/ollama.test.ts +++ b/src/api/providers/fetchers/__tests__/ollama.test.ts @@ -18,7 +18,7 @@ describe("Ollama Fetcher", () => { const parsedModel = parseOllamaModel(modelData) expect(parsedModel).toEqual({ - maxTokens: 40960, + maxTokens: 4096, contextWindow: 40960, supportsImages: false, supportsPromptCache: true, @@ -42,7 +42,7 @@ describe("Ollama Fetcher", () => { const parsedModel = parseOllamaModel(modelDataWithNullFamilies as any) expect(parsedModel).toEqual({ - maxTokens: 40960, + maxTokens: 4096, contextWindow: 40960, supportsImages: false, supportsPromptCache: true, @@ -398,5 +398,92 @@ describe("Ollama Fetcher", () => { expect(Object.keys(result).length).toBe(1) expect(result[modelName]).toBeDefined() }) + + it("should still list healthy models when a single /api/show request fails", async () => { + const baseUrl = "http://localhost:11434" + const healthyModel = "healthy-model:latest" + const failingModel = "failing-model:latest" + + const mockApiTagsResponse = { + models: [ + { + name: healthyModel, + model: healthyModel, + modified_at: "2025-06-03T09:23:22.610222878-04:00", + size: 14333928010, + digest: "6a5f0c01d2c96c687d79e32fdd25b87087feb376bf9838f854d10be8cf3c10a5", + details: { + family: "llama", + families: ["llama"], + format: "gguf", + parameter_size: "23.6B", + parent_model: "", + quantization_level: "Q4_K_M", + }, + }, + { + name: failingModel, + model: failingModel, + modified_at: "2025-06-03T09:23:22.610222878-04:00", + size: 14333928010, + digest: "6a5f0c01d2c96c687d79e32fdd25b87087feb376bf9838f854d10be8cf3c10a5", + details: { + family: "llama", + families: ["llama"], + format: "gguf", + parameter_size: "23.6B", + parent_model: "", + quantization_level: "Q4_K_M", + }, + }, + ], + } + const mockApiShowResponse = { + license: "Mock License", + modelfile: "FROM /path/to/blob\nTEMPLATE {{ .Prompt }}", + parameters: "num_ctx 4096\nstop_token ", + template: "{{ .System }}USER: {{ .Prompt }}ASSISTANT:", + modified_at: "2025-06-03T09:23:22.610222878-04:00", + details: { + parent_model: "", + format: "gguf", + family: "llama", + families: ["llama"], + parameter_size: "23.6B", + quantization_level: "Q4_K_M", + }, + model_info: { + "ollama.context_length": 4096, + "some.other.info": "value", + }, + capabilities: ["completion", "tools"], // Has tools capability + } + + mockedAxios.get.mockResolvedValueOnce({ data: mockApiTagsResponse }) + // First /api/show succeeds (healthy model), second rejects (failing model) + mockedAxios.post + .mockResolvedValueOnce({ data: mockApiShowResponse }) + .mockRejectedValueOnce(new Error("model not found")) + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(function () {}) + + const result = await getOllamaModels(baseUrl) + + expect(mockedAxios.get).toHaveBeenCalledTimes(1) + expect(mockedAxios.post).toHaveBeenCalledTimes(2) + + // The healthy model should still be present despite the sibling failure + expect(Object.keys(result).length).toBe(1) + expect(result[healthyModel]).toBeDefined() + expect(result[failingModel]).toBeUndefined() + + // The individual failure should be logged, not swallowed silently + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error fetching details for model ${failingModel}:`, + expect.any(Error), + ) + + consoleErrorSpy.mockRestore() + }) }) }) diff --git a/src/api/providers/fetchers/__tests__/openrouter.spec.ts b/src/api/providers/fetchers/__tests__/openrouter.spec.ts index ad5e3232eb..268f83b92e 100644 --- a/src/api/providers/fetchers/__tests__/openrouter.spec.ts +++ b/src/api/providers/fetchers/__tests__/openrouter.spec.ts @@ -348,6 +348,36 @@ describe("OpenRouter API", () => { expect(result.supportsReasoningBudget).toBe(false) }) + it("sets claude-sonnet-5 model to Anthropic max tokens and omits temperature", () => { + const mockModel = { + name: "Claude Sonnet 5", + description: "Test model", + context_length: 1000000, + max_completion_tokens: 128000, + pricing: { + prompt: "0.000003", + completion: "0.000015", + }, + } + + const result = parseOpenRouterModel({ + id: "anthropic/claude-sonnet-5", + model: mockModel, + inputModality: ["text", "image"], + outputModality: ["text"], + maxTokens: 128000, + supportedParameters: ["reasoning", "include_reasoning"], + }) + + expect(result.maxTokens).toBe(128000) + expect(result.contextWindow).toBe(1000000) + expect(result.supportsTemperature).toBe(false) + // Fork shapes Sonnet 5 as effort/adaptive (mirrors opus/fable), not budget/binary. + expect(result.supportsReasoningEffort).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.requiredReasoningEffort).toBe(true) + expect(result.supportsReasoningBudget).toBe(false) + }) + it("sets horizon-alpha model to 32k max tokens", () => { const mockModel = { name: "Horizon Alpha", diff --git a/src/api/providers/fetchers/__tests__/requesty.spec.ts b/src/api/providers/fetchers/__tests__/requesty.spec.ts index ced138554c..38a4147925 100644 --- a/src/api/providers/fetchers/__tests__/requesty.spec.ts +++ b/src/api/providers/fetchers/__tests__/requesty.spec.ts @@ -54,6 +54,33 @@ describe("getRequestyModels", () => { expect(fable5.supportsTemperature).toBe(false) }) + it("applies Sonnet 5 overrides when parsing anthropic/claude-sonnet-5", async () => { + const rawSonnet5 = makeRawModel({ + id: "anthropic/claude-sonnet-5", + max_output_tokens: 128000, + context_window: 1000000, + supports_caching: true, + supports_vision: true, + supports_reasoning: true, + input_price: "0.000003", + output_price: "0.000015", + caching_price: "0.00000375", + cached_price: "0.0000003", + }) + + mockAxiosGet.mockResolvedValueOnce({ data: { data: [rawSonnet5] } }) + + const models = await getRequestyModels() + const sonnet5 = models["anthropic/claude-sonnet-5"] + + expect(sonnet5).toBeDefined() + // Fork shapes Sonnet 5 as effort/adaptive (mirrors the fable-5 override), not budget/binary. + expect(sonnet5.supportsReasoningEffort).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(sonnet5.requiredReasoningEffort).toBe(true) + expect(sonnet5.supportsReasoningBudget).toBe(false) + expect(sonnet5.supportsTemperature).toBe(false) + }) + it("does not apply Fable 5 overrides to other models", async () => { const rawSonnet = makeRawModel({ id: "anthropic/claude-sonnet-4.6", diff --git a/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts index 1bc109f006..dfb8ad4b3b 100644 --- a/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts @@ -221,6 +221,22 @@ describe("Vercel AI Gateway Fetchers", () => { expect(result.supportsTemperature).toBe(false) }) + it("marks Claude Sonnet 5 as not supporting temperature", () => { + const result = parseVercelAiGatewayModel({ + id: "anthropic/claude-sonnet-5", + model: { + ...baseModel, + id: "anthropic/claude-sonnet-5", + context_window: 1000000, + max_tokens: 128000, + }, + }) + + expect(result.maxTokens).toBe(128000) + expect(result.contextWindow).toBe(1000000) + expect(result.supportsTemperature).toBe(false) + }) + it("detects vision-only models", () => { // claude 3.5 haiku in VERCEL_AI_GATEWAY_VISION_ONLY_MODELS const visionModel = { diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 404a60cd85..17e88c7ea9 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -1,6 +1,7 @@ import * as path from "path" import fs from "fs/promises" import * as fsSync from "fs" +import { pbkdf2Sync } from "crypto" import NodeCache from "node-cache" import { z } from "zod" @@ -34,9 +35,10 @@ const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) // Zod schema for validating ModelRecord structure from disk cache const modelRecordSchema = z.record(z.string(), modelInfoSchema) -// Track in-flight refresh requests to prevent concurrent API calls for the same provider -// This prevents race conditions where multiple calls might overwrite each other's results -const inFlightRefresh = new Map>() +// Track in-flight refresh requests to prevent concurrent API calls for the same provider+url. +// Keyed on the compound cache key (see getCacheKey) so that two different URL-scoped servers never +// deduplicate each other's in-flight refreshes. +const inFlightRefresh = new Map>() // Providers whose model lists are scoped to the signed-in user (e.g. per-account // allowlists or org policies). For these we MUST NOT cache results on disk or @@ -44,18 +46,127 @@ const inFlightRefresh = new Map>() // list to the next user, and stale data could mask backend allowlist updates. const AUTH_SCOPED_PROVIDERS: ReadonlySet = new Set(["zoo-gateway"]) +// Providers whose model list is determined by the server URL, not just by the provider name. +// Each unique baseUrl must be cached independently so that switching endpoints never serves +// stale results from a previously-cached server. +const URL_SCOPED_PROVIDERS: ReadonlySet = new Set([ + "litellm", + "poe", + "deepseek", + "ollama", + "lmstudio", + "requesty", +]) + +// Providers where the API key itself determines which models are visible (e.g. per-key +// allowlists). For these the cache key also includes a short hash of +// the API key so that two different keys on the same server never share a cache entry. +const KEY_SCOPED_PROVIDERS: ReadonlySet = new Set([ + "litellm", // Per-key model allowlists are a first-class LiteLLM proxy feature + "poe", // Per-account model availability + "requesty", // Per-account custom model policies +]) + function isAuthScopedProvider(provider: RouterName): boolean { return AUTH_SCOPED_PROVIDERS.has(provider) } -async function writeModels(router: RouterName, data: ModelRecord) { - const filename = `${router}_models.json` +// Memoize derived digests so the deliberately-structureless KDF runs at most once per +// distinct input per session (getCacheKey / cacheKeyToFilename run on every cache lookup). +const cacheDigestCache = new Map() + +// Fixed, non-secret application salt. This is NOT credential storage: it derives short, +// stable cache-key components from the API key and the compound cache key so that distinct +// inputs map to distinct cache entries / filenames. PBKDF2 is used (over a plain hash) only +// to obtain a uniform, structureless mapping with no exploitable internal structure; the +// iteration count is intentionally modest because security here rests on truncation, not on +// KDF slowness. Using a KDF rather than a plain digest also keeps API-key-derived values off +// CodeQL's js/insufficient-password-hash sink, which flags any password-tainted value flowing +// into a non-password hashing operation -- and that taint propagates to anything derived from +// the key, including the compound cache key hashed for the on-disk filename. +const CACHE_DIGEST_SALT = "zoo-model-cache-key-v1" +const CACHE_DIGEST_ITERATIONS = 10_000 + +/** + * Derive a short, irreversible, truncated digest of a cache input. + * + * The output is deliberately far smaller than the entropy of a real API key: collisions + * across the handful of keys/servers a single user configures are negligible (birthday bound + * ~ n^2 / 2^(8*bytes)), while the truncated output is small enough that any preimage search + * yields an astronomically large set of candidate inputs -- so a value written to an on-disk + * cache filename cannot be reversed to identify the API key it was derived from. + */ +function deriveCacheDigest(value: string, bytes: number): string { + const memoKey = `${bytes}:${value}` + const cached = cacheDigestCache.get(memoKey) + if (cached) return cached + const digest = pbkdf2Sync(value, CACHE_DIGEST_SALT, CACHE_DIGEST_ITERATIONS, bytes, "sha256").toString("hex") + cacheDigestCache.set(memoKey, digest) + return digest +} + +// 4 bytes (8 hex chars) = 32 bits for the per-API-key discriminator embedded in the cache key. +const API_KEY_DISCRIMINATOR_BYTES = 4 +// 8 bytes (16 hex chars) = 64 bits for the filename digest, preserving the prior filename width. +const FILENAME_DIGEST_BYTES = 8 + +/** + * Derive a short, irreversible, non-identifying cache-key discriminator from an API key. + */ +function deriveApiKeyDiscriminator(apiKey: string): string { + return deriveCacheDigest(apiKey, API_KEY_DISCRIMINATOR_BYTES) +} + +/** + * Build a cache key that is unique per provider+server+key combination. + * + * - URL-scoped providers include the normalized baseUrl so that two different servers + * of the same provider type never share a cache entry. + * - Key-scoped providers additionally fold in a short, irreversible discriminator derived + * from the API key so that two different API keys on the same server never share a cache + * entry (relevant when the server enforces per-key model allowlists, e.g. LiteLLM, Poe, + * Requesty). See deriveApiKeyDiscriminator for why the value cannot be reversed to the key. + */ +function getCacheKey(options: GetModelsOptions): string { + const { provider } = options + const isUrlScoped = URL_SCOPED_PROVIDERS.has(provider as RouterName) + const isKeyScoped = KEY_SCOPED_PROVIDERS.has(provider as RouterName) + + // Build URL and key components independently so that key-scoped providers + // without a custom baseUrl still get a per-key cache entry (otherwise two + // different keys on the default server would collapse to the same entry). + // Strip trailing slashes so "http://host:4000/" and "http://host:4000" map to the same key. + const urlPart = isUrlScoped && options.baseUrl ? options.baseUrl.replace(/\/+$/, "") : undefined + const keyPart = isKeyScoped && options.apiKey ? deriveApiKeyDiscriminator(options.apiKey) : undefined + + if (urlPart && keyPart) return `${provider}:${urlPart}:${keyPart}` + if (urlPart) return `${provider}:${urlPart}` + if (keyPart) return `${provider}:${keyPart}` + return provider +} + +/** + * Convert a cache key to a filesystem-safe filename component. + * Hashes the full key to guarantee uniqueness while preserving a readable + * provider prefix at the start of the filename. + */ +function cacheKeyToFilename(cacheKey: string): string { + const prefix = cacheKey.split(":")[0] // provider name -- always filesystem-safe + // The compound cache key embeds the API-key discriminator, so it is treated as + // password-tainted by static analysis; deriveCacheDigest keeps the filename derivation + // off the weak-hash sink while still producing a collision-free, irreversible component. + const hash = deriveCacheDigest(cacheKey, FILENAME_DIGEST_BYTES) + return `${prefix}_${hash}` +} + +async function writeModels(cacheKey: string, data: ModelRecord) { + const filename = `${cacheKeyToFilename(cacheKey)}_models.json` const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath) await safeWriteJson(path.join(cacheDir, filename), data) } -async function readModels(router: RouterName): Promise { - const filename = `${router}_models.json` +async function readModels(cacheKey: string): Promise { + const filename = `${cacheKeyToFilename(cacheKey)}_models.json` const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath) const filePath = path.join(cacheDir, filename) const exists = await fileExistsAtPath(filePath) @@ -86,8 +197,7 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise => { const { provider } = options + const cacheKey = getCacheKey(options) const shouldSkipCache = isAuthScopedProvider(provider) - let models = shouldSkipCache ? undefined : getModelsFromCache(provider) + let models = shouldSkipCache ? undefined : getModelsFromCache(options) if (models) { return models @@ -149,10 +260,10 @@ export const getModels = async (options: GetModelsOptions): Promise // Only cache non-empty results so a failed API response doesn't get persisted // as if the provider had no models. Auth-scoped providers skip caching entirely. if (modelCount > 0 && !shouldSkipCache) { - memoryCache.set(provider, models) + memoryCache.set(cacheKey, models) - await writeModels(provider, models).catch((err) => - console.error(`[MODEL_CACHE] Error writing ${provider} models to file cache:`, err), + await writeModels(cacheKey, models).catch((err) => + console.error(`[MODEL_CACHE] Error writing ${cacheKey} models to file cache:`, err), ) } else if (modelCount === 0) { TelemetryService.instance.captureEvent(TelemetryEventName.MODEL_CACHE_EMPTY_RESPONSE, { @@ -182,23 +293,30 @@ export const getModels = async (options: GetModelsOptions): Promise */ export const refreshModels = async (options: GetModelsOptions): Promise => { const { provider } = options + const cacheKey = getCacheKey(options) const shouldSkipCache = isAuthScopedProvider(provider) - // Check if there's already an in-flight refresh for this provider. + // Check if there's already an in-flight refresh for this provider+url combination. // This prevents race conditions where multiple concurrent refreshes might // overwrite each other's results. Skip de-duplication for auth-scoped // providers because two concurrent calls may carry different tokens // (e.g., after a sign-out/sign-in within the same session) and we must // not return the first caller's results to the second caller. if (!shouldSkipCache) { - const existingRequest = inFlightRefresh.get(provider) + const existingRequest = inFlightRefresh.get(cacheKey) if (existingRequest) { return existingRequest } } - // Create the refresh promise and track it + // Create the refresh promise and track it. + // + // The `finally` cleanup below runs only after the first `await` inside this async + // function yields, which cannot happen until the current synchronous run -- including + // the `inFlightRefresh.set(cacheKey, ...)` registration below -- has completed. So the + // entry is always present in the map before `finally` can delete it; the registration + // can never be lost to a microtask race even if the fetch resolves immediately. const refreshPromise = (async (): Promise => { try { // Force fresh API fetch - skip getModelsFromCache() check @@ -206,7 +324,7 @@ export const refreshModels = async (options: GetModelsOptions): Promise - console.error(`[refreshModels] Error writing ${provider} models to disk:`, err), + await writeModels(cacheKey, models).catch((err) => + console.error(`[refreshModels] Error writing ${cacheKey} models to disk:`, err), ) } @@ -235,23 +353,23 @@ export const refreshModels = async (options: GetModelsOptions): Promise { * @param refresh - If true, immediately fetch fresh data from API */ export const flushModels = async (options: GetModelsOptions, refresh: boolean = false): Promise => { - const { provider } = options if (refresh) { // Don't delete memory cache - let refreshModels atomically replace it // This prevents a race condition where getModels() might be called @@ -298,8 +415,10 @@ export const flushModels = async (options: GetModelsOptions, refresh: boolean = // Await the refresh to ensure the cache is updated before returning await refreshModels(options) } else { - // Only delete memory cache when not refreshing - memoryCache.del(provider) + // Only delete memory cache when not refreshing. Use the compound cache key so that + // URL-scoped providers (litellm, poe, etc.) actually evict the per-server entry rather + // than a bare provider-name entry that was never written. + memoryCache.del(getCacheKey(options)) } } @@ -311,9 +430,18 @@ export const flushModels = async (options: GetModelsOptions, refresh: boolean = * @param provider - The provider to get models for. * @returns Models from memory cache, disk cache, or undefined if not cached. */ -export function getModelsFromCache(provider: ProviderName): ModelRecord | undefined { +export function getModelsFromCache(options: GetModelsOptions | ProviderName): ModelRecord | undefined { + // Auth-scoped providers (e.g. zoo-gateway) must never be served from cache -- + // their model lists are user-specific and a stale file left over from a previous + // session could leak another user's list. Mirror the guards in getModels/refreshModels. + const providerName = typeof options === "string" ? options : options.provider + if (isAuthScopedProvider(providerName as RouterName)) { + return undefined + } + + const cacheKey = typeof options === "string" ? options : getCacheKey(options) // Check memory cache first (fast) - const memoryModels = memoryCache.get(provider) + const memoryModels = memoryCache.get(cacheKey) if (memoryModels) { return memoryModels } @@ -321,7 +449,7 @@ export function getModelsFromCache(provider: ProviderName): ModelRecord | undefi // Memory cache miss - try to load from disk synchronously // This is acceptable because it only happens on cold start or after cache expiry try { - const filename = `${provider}_models.json` + const filename = `${cacheKeyToFilename(cacheKey)}_models.json` const cacheDir = getCacheDirectoryPathSync() if (!cacheDir) { return undefined @@ -339,19 +467,19 @@ export function getModelsFromCache(provider: ProviderName): ModelRecord | undefi const validation = modelRecordSchema.safeParse(models) if (!validation.success) { console.error( - `[MODEL_CACHE] Invalid disk cache data structure for ${provider}:`, + `[MODEL_CACHE] Invalid disk cache data structure for ${cacheKey}:`, validation.error.format(), ) return undefined } // Populate memory cache for future fast access - memoryCache.set(provider, validation.data) + memoryCache.set(cacheKey, validation.data) return validation.data } } catch (error) { - console.error(`[MODEL_CACHE] Error loading ${provider} models from disk:`, error) + console.error(`[MODEL_CACHE] Error loading ${cacheKey} models from disk:`, error) } return undefined diff --git a/src/api/providers/fetchers/ollama.ts b/src/api/providers/fetchers/ollama.ts index a81a1e2896..9f88d75327 100644 --- a/src/api/providers/fetchers/ollama.ts +++ b/src/api/providers/fetchers/ollama.ts @@ -53,7 +53,10 @@ export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo | contextWindow: contextWindow || ollamaDefaultModelInfo.contextWindow, supportsPromptCache: true, supportsImages: rawModel.capabilities?.includes("vision"), - maxTokens: contextWindow || ollamaDefaultModelInfo.contextWindow, + // maxTokens represents max OUTPUT tokens, not the context window. + // Setting it to the full contextWindow causes getModelMaxOutputTokens to + // reserve 20% of the window for output, triggering premature condensing. + // Inherit the sane default (4096) from ollamaDefaultModelInfo instead. }) return modelInfo @@ -100,6 +103,13 @@ export async function getOllamaModels( if (modelInfo) { models[ollamaModel.name] = modelInfo } + }) + // A single failing /api/show request (corrupt model, timeout, + // server overload, etc.) must not reject the whole Promise.all + // and wipe out all otherwise healthy models. Log and swallow + // the individual failure so the remaining models still load. + .catch((error) => { + console.error(`Error fetching details for model ${ollamaModel.model}:`, error) }), ) } diff --git a/src/api/providers/fetchers/openrouter.ts b/src/api/providers/fetchers/openrouter.ts index fc4151a114..4d03332420 100644 --- a/src/api/providers/fetchers/openrouter.ts +++ b/src/api/providers/fetchers/openrouter.ts @@ -300,6 +300,20 @@ export const parseOpenRouterModel = ({ modelInfo.supportsTemperature = staticDef.supportsTemperature } + // Sonnet 5 is adaptive-thinking only and rejects budget_tokens; mirror the static + // effort shape (like the opus/fable rule above) so xhigh/max stay reachable and + // getAnthropicReasoning takes the effort/adaptive branch, not the legacy budget one. + if (id === "anthropic/claude-sonnet-5") { + const staticDef = anthropicModels["claude-sonnet-5"] + modelInfo.maxTokens = staticDef.maxTokens + modelInfo.supportsReasoningEffort = staticDef.supportsReasoningEffort + modelInfo.reasoningEffort = staticDef.reasoningEffort + modelInfo.requiredReasoningEffort = staticDef.requiredReasoningEffort + modelInfo.supportsReasoningBudget = false + modelInfo.supportsReasoningDisplay = staticDef.supportsReasoningDisplay + modelInfo.supportsTemperature = staticDef.supportsTemperature + } + // Ensure correct reasoning handling for Claude Haiku 4.5 on OpenRouter // Use budget control and disable effort-based reasoning fallback if (id === "anthropic/claude-haiku-4.5") { diff --git a/src/api/providers/fetchers/requesty.ts b/src/api/providers/fetchers/requesty.ts index a93af16ce5..6909c12766 100644 --- a/src/api/providers/fetchers/requesty.ts +++ b/src/api/providers/fetchers/requesty.ts @@ -59,6 +59,18 @@ export async function getRequestyModels(baseUrl?: string, apiKey?: string): Prom modelInfo.supportsTemperature = false } + if (rawModel.id === "anthropic/claude-sonnet-5") { + // Sonnet 5 is adaptive-only and rejects budget_tokens; mirror the static + // effort shape (same as the fable-5 rule above) so it takes the effort branch. + const staticDef = anthropicModels["claude-sonnet-5"] + modelInfo.supportsReasoningEffort = staticDef.supportsReasoningEffort + modelInfo.reasoningEffort = staticDef.reasoningEffort + modelInfo.requiredReasoningEffort = staticDef.requiredReasoningEffort + modelInfo.supportsReasoningDisplay = staticDef.supportsReasoningDisplay + modelInfo.supportsReasoningBudget = false + modelInfo.supportsTemperature = false + } + models[rawModel.id] = modelInfo } } catch (error) { diff --git a/src/api/providers/fetchers/vercel-ai-gateway.ts b/src/api/providers/fetchers/vercel-ai-gateway.ts index 50c3035084..ba748ddd51 100644 --- a/src/api/providers/fetchers/vercel-ai-gateway.ts +++ b/src/api/providers/fetchers/vercel-ai-gateway.ts @@ -118,5 +118,9 @@ export const parseVercelAiGatewayModel = ({ id, model }: { id: string; model: Ve modelInfo.supportsTemperature = false } + if (id === "anthropic/claude-sonnet-5") { + modelInfo.supportsTemperature = false + } + return modelInfo } diff --git a/src/api/providers/friendli.ts b/src/api/providers/friendli.ts new file mode 100644 index 0000000000..f9aa4fa20c --- /dev/null +++ b/src/api/providers/friendli.ts @@ -0,0 +1,26 @@ +import { type FriendliModelId, friendliDefaultModelId, friendliModels } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" + +/** + * Handler for the Friendli Model APIs (OpenAI-compatible). + * Routes chat completions to `https://api.friendli.ai/serverless/v1`. + */ +export class FriendliHandler extends BaseOpenAiCompatibleProvider { + /** + * @param options Provider settings; `friendliApiKey` is required. + */ + constructor(options: ApiHandlerOptions) { + super({ + ...options, + providerName: "Friendli", + baseURL: "https://api.friendli.ai/serverless/v1", + apiKey: options.friendliApiKey, + defaultProviderModelId: friendliDefaultModelId, + providerModels: friendliModels, + defaultTemperature: 0.6, + }) + } +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 3c0d1e03e3..6b7a74f543 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -24,6 +24,7 @@ export { VsCodeLmHandler } from "./vscode-lm" export { XAIHandler } from "./xai" export { ZAiHandler } from "./zai" export { FireworksHandler } from "./fireworks" +export { FriendliHandler } from "./friendli" export { VercelAiGatewayHandler } from "./vercel-ai-gateway" export { OpencodeGoHandler } from "./opencode-go" export { ZooGatewayHandler } from "./zoo-gateway" diff --git a/src/api/providers/lite-llm.ts b/src/api/providers/lite-llm.ts index 981f984ded..3f9b3732e5 100644 --- a/src/api/providers/lite-llm.ts +++ b/src/api/providers/lite-llm.ts @@ -9,6 +9,7 @@ import { ApiHandlerOptions } from "../../shared/api" import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" +import { GEMINI_THOUGHT_SIGNATURE_BYPASS } from "../transform/gemini-format" import { sanitizeOpenAiCallId } from "../../utils/tool-id" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" @@ -72,7 +73,7 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa * * Per LiteLLM documentation: * - Thought signatures are stored in provider_specific_fields.thought_signature of tool calls - * - The dummy signature base64("skip_thought_signature_validator") bypasses validation + * - The bypass token (GEMINI_THOUGHT_SIGNATURE_BYPASS) skips signature validation * * We inject the dummy signature on EVERY tool call unconditionally to ensure Gemini * doesn't complain about missing/corrupted signatures when conversation history @@ -81,8 +82,7 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa private injectThoughtSignatureForGemini( openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[], ): OpenAI.Chat.ChatCompletionMessageParam[] { - // Base64 encoded "skip_thought_signature_validator" as per LiteLLM docs - const dummySignature = Buffer.from("skip_thought_signature_validator").toString("base64") + const dummySignature = GEMINI_THOUGHT_SIGNATURE_BYPASS return openAiMessages.map((msg) => { if (msg.role === "assistant") { diff --git a/src/api/providers/lm-studio.ts b/src/api/providers/lm-studio.ts index d04bd157c7..891695b5fc 100644 --- a/src/api/providers/lm-studio.ts +++ b/src/api/providers/lm-studio.ts @@ -15,7 +15,7 @@ import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { getModelsFromCache } from "./fetchers/modelCache" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions @@ -104,7 +104,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan } const matcher = new TagMatcher( - "think", + ["think", "thought"], (chunk) => ({ type: chunk.matched ? "reasoning" : "text", @@ -170,7 +170,10 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan } override getModel(): { id: string; info: ModelInfo } { - const models = getModelsFromCache("lmstudio") + const models = getModelsFromCache({ + provider: "lmstudio", + baseUrl: this.options.lmStudioBaseUrl, + }) if (models && this.options.lmStudioModelId && models[this.options.lmStudioModelId]) { return { id: this.options.lmStudioModelId, diff --git a/src/api/providers/native-ollama.ts b/src/api/providers/native-ollama.ts index 99c1dc03cf..2a14885e11 100644 --- a/src/api/providers/native-ollama.ts +++ b/src/api/providers/native-ollama.ts @@ -14,8 +14,20 @@ interface OllamaChatOptions { num_ctx?: number } +// Narrow local types for non-Anthropic content blocks that may be carried in +// the conversation history. The Anthropic SDK union does not include the +// custom `reasoning` block (used by non-Anthropic protocols) or the +// Anthropic-protocol `thinking` block, so we declare them here to keep type +// checking intact for the rest of the union instead of falling back to `any`. +type ReasoningContentBlock = { type: "reasoning"; text: string } +type ThinkingContentBlock = { type: "thinking"; thinking: string } +type AssistantContentBlock = Anthropic.ContentBlock | ReasoningContentBlock | ThinkingContentBlock + function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): Message[] { const ollamaMessages: Message[] = [] + // Track tool use IDs to tool names so tool results can be sent with + // Ollama's native "tool" role and tool_name field instead of "user". + const toolUseIdToName = new Map() for (const anthropicMessage of anthropicMessages) { if (typeof anthropicMessage.content === "string") { @@ -40,7 +52,11 @@ function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessagePa { nonToolMessages: [], toolMessages: [] }, ) - // Process tool result messages FIRST since they must follow the tool use messages + // Process tool result messages FIRST since they must follow the tool use messages. + // Images extracted from tool results are collected here and attached to the + // adjacent user message, because Ollama's "tool" role only supports text + // content (content + tool_name); attaching images there can invalidate the + // request. const toolResultImages: string[] = [] toolMessages.forEach((toolMessage) => { // The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the Ollama SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility. @@ -49,6 +65,10 @@ function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessagePa if (typeof toolMessage.content === "string") { content = toolMessage.content } else { + // Collect this result's images in a local accumulator so they + // cannot leak into sibling tool results, then fold them into + // the shared accumulator for the adjacent user message. + const resultImages: string[] = [] content = toolMessage.content ?.map((part) => { @@ -56,17 +76,26 @@ function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessagePa // Handle base64 images only (Anthropic SDK uses base64) // Ollama expects raw base64 strings, not data URLs if ("source" in part && part.source.type === "base64") { - toolResultImages.push(part.source.data) + resultImages.push(part.source.data) } return "(see following user message for image)" } - return part.text + if (part.type === "text") { + return part.text + } + return "" }) .join("\n") ?? "" + toolResultImages.push(...resultImages) } + // Look up the tool name from the corresponding tool_use block. + // When found, use Ollama's native "tool" role with tool_name so the + // model can distinguish tool results from user messages. Tool messages + // stay text-only; images are delivered via the adjacent user message. + const toolName = toolUseIdToName.get(toolMessage.tool_use_id) ollamaMessages.push({ - role: "user", - images: toolResultImages.length > 0 ? toolResultImages : undefined, + role: toolName ? "tool" : "user", + tool_name: toolName, content: content, }) }) @@ -86,57 +115,106 @@ function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessagePa imageData.push(part.source.data) } }) + // Attach images extracted from tool results to the adjacent user + // message, the only role that supports images in Ollama's chat API. + imageData.push(...toolResultImages) ollamaMessages.push({ role: "user", content: textContent, images: imageData.length > 0 ? imageData : undefined, }) + } else if (toolResultImages.length > 0) { + // No adjacent user message exists to carry tool-result images, so + // emit a dedicated user message to ensure they still reach the model. + ollamaMessages.push({ + role: "user", + content: "", + images: toolResultImages, + }) } } else if (anthropicMessage.role === "assistant") { - const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{ - nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] + // Assistant message conversion: only `text`, `tool_use`, and the + // custom `reasoning`/`thinking` blocks are relevant here. + // + // Note on the removed `image` branch: the previous code checked + // `block.type === "image"` and typed `nonToolMessages` as + // `(Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]`. This + // was removed because: + // 1. The Anthropic API only accepts image blocks in *user* + // messages — assistants cannot produce or send images, so + // the branch was dead code (the original comment even said + // "impossible as the assistant cannot send images"). + // 2. `Anthropic.ContentBlock` (the response/output union) does + // not include an `image` variant, so the comparison + // `block.type === "image"` triggered TS2367 ("this comparison + // appears to be unintentional because the types have no + // overlap"). + // 3. Image handling for *user* messages (where images are + // actually sent to the model) is preserved unchanged in the + // `anthropicMessage.role === "user"` branch above. + const { nonToolMessages, toolMessages, reasoningText } = anthropicMessage.content.reduce<{ + nonToolMessages: Anthropic.TextBlockParam[] toolMessages: Anthropic.ToolUseBlockParam[] + reasoningText: string }>( (acc, part) => { - if (part.type === "tool_use") { - acc.toolMessages.push(part) - } else if (part.type === "text" || part.type === "image") { - acc.nonToolMessages.push(part) + // `part` is typed as an Anthropic content block, but the + // conversation history may also carry custom `reasoning` + // blocks (used by non-Anthropic protocols) or Anthropic + // `thinking` blocks that are not part of the SDK union. + // Cast to the augmented union to access them while + // preserving type safety for the rest of the block. + const block = part as AssistantContentBlock + if (block.type === "tool_use") { + acc.toolMessages.push(block) + } else if (block.type === "text") { + acc.nonToolMessages.push(block) + } else if (block.type === "reasoning") { + // Non-Anthropic protocols store reasoning as a block + // with a `text` field. Pass it back so Ollama can + // preserve thinking context across turns. + if (block.text.length > 0) { + acc.reasoningText += (acc.reasoningText ? "\n" : "") + block.text + } + } else if (block.type === "thinking") { + // Anthropic-protocol thinking blocks carry `thinking`. + if (block.thinking.length > 0) { + acc.reasoningText += (acc.reasoningText ? "\n" : "") + block.thinking + } } // assistant cannot send tool_result messages return acc }, - { nonToolMessages: [], toolMessages: [] }, + { nonToolMessages: [], toolMessages: [], reasoningText: "" }, ) // Process non-tool messages let content: string = "" if (nonToolMessages.length > 0) { - content = nonToolMessages - .map((part) => { - if (part.type === "image") { - return "" // impossible as the assistant cannot send images - } - return part.text - }) - .join("\n") + content = nonToolMessages.map((part) => part.text).join("\n") } // Convert tool_use blocks to Ollama tool_calls format const toolCalls = toolMessages.length > 0 - ? toolMessages.map((tool) => ({ - function: { - name: tool.name, - arguments: tool.input as Record, - }, - })) + ? toolMessages.map((tool) => { + // Track tool use ID → name so tool results can use the "tool" role + toolUseIdToName.set(tool.id, tool.name) + return { + function: { + name: tool.name, + arguments: tool.input as Record, + }, + } + }) : undefined ollamaMessages.push({ role: "assistant", content, tool_calls: toolCalls, + // Round-trip prior reasoning so multi-turn thinking is preserved. + thinking: reasoningText || undefined, }) } } @@ -178,6 +256,29 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio return this.client } + /** + * Recursively strips `additionalProperties` from a JSON schema (including + * nested `properties` objects and `items` arrays). `additionalProperties` + * is not part of Ollama's tool schema definition and can break tool-calling + * templates on some models, so it must be removed at every nesting level. + */ + private stripAdditionalProperties(schema: unknown): unknown { + if (!schema || typeof schema !== "object") { + return schema + } + if (Array.isArray(schema)) { + return schema.map((item) => this.stripAdditionalProperties(item)) + } + const result: Record = {} + for (const [key, value] of Object.entries(schema)) { + if (key === "additionalProperties") { + continue + } + result[key] = this.stripAdditionalProperties(value) + } + return result + } + /** * Converts OpenAI-format tools to Ollama's native tool format. * This allows NativeOllamaHandler to use the same tool definitions @@ -190,14 +291,106 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio return tools .filter((tool): tool is OpenAI.Chat.ChatCompletionTool & { type: "function" } => tool.type === "function") - .map((tool) => ({ - type: tool.type, - function: { - name: tool.function.name, - description: tool.function.description, - parameters: tool.function.parameters as OllamaTool["function"]["parameters"], - }, - })) + .map((tool) => { + // Recursively strip additionalProperties from the parameters schema + // (top-level and nested). This field is not part of Ollama's tool + // schema definition and can cause issues with some models' + // tool-calling templates. + const rawParams = tool.function.parameters as Record | undefined + const parameters = rawParams ? this.stripAdditionalProperties(rawParams) : undefined + return { + type: tool.type, + function: { + name: tool.function.name, + description: tool.function.description, + parameters: parameters as OllamaTool["function"]["parameters"], + }, + } + }) + } + + /** + * Maps the configured reasoning effort setting to Ollama's native `think` + * request parameter (boolean | "high" | "medium" | "low"). + * + * Requires an explicit Ollama opt-in (`enableReasoningEffort === true`) + * before translating `reasoningEffort`. This prevents inherited + * `apiConfiguration.reasoningEffort` values (left over from another + * provider) from silently emitting a `think` param when the Ollama UI + * checkbox is unchecked. + * + * Returns undefined when reasoning is not explicitly enabled, leaving + * the model/Modelfile in control (preserving prior behavior where models + * that emit think/thought tags in content are still handled by TagMatcher). + * + * Note: The Ollama API itself also accepts `"max"` (see + * https://docs.ollama.com/capabilities/thinking), but the installed + * `ollama` SDK (v0.6.x) only types `think` as + * `boolean | "high" | "medium" | "low"`. Until the SDK types catch up, + * "xhigh"/"max" efforts are clamped to "high". + * + * - enableReasoningEffort !== true -> undefined (no think param sent) + * - "disable" -> false (thinking off) + * - "none" / "minimal" -> true (enable thinking with default budget) + * - "low" / "medium" / "high" -> the matching effort level + * - "xhigh" / "max" -> "high" (highest level the SDK currently supports) + */ + private getOllamaThinkParam(): boolean | "high" | "medium" | "low" | undefined { + // Require an explicit Ollama opt-in before mapping reasoningEffort. + // Without this guard, a stale reasoningEffort inherited from another + // provider config could still emit a think param when the UI checkbox + // is unchecked. + if (this.options.enableReasoningEffort !== true) { + return undefined + } + + const effort = this.options.reasoningEffort + if (effort === undefined) { + return undefined + } + + switch (effort) { + case "disable": + return false + case "none": + case "minimal": + return true + case "low": + return "low" + case "medium": + return "medium" + case "high": + case "xhigh": + case "max": + return "high" + default: + return undefined + } + } + + /** + * Builds the shared chat request options (temperature, num_ctx) and the + * conditional `think` parameter used by both `createMessage` and + * `completePrompt`. Centralizing this avoids drift between the streaming + * and single-shot request paths. + * + * Returns a tuple of `[chatOptions, thinkParam]` where `thinkParam` is + * `undefined` when no `think` field should be sent to Ollama. + */ + private buildChatRequestOptions( + useR1Format: boolean, + ): [OllamaChatOptions, boolean | "high" | "medium" | "low" | undefined] { + const chatOptions: OllamaChatOptions = { + temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0), + } + + // Only include num_ctx if explicitly set via ollamaNumCtx + if (this.options.ollamaNumCtx !== undefined) { + chatOptions.num_ctx = this.options.ollamaNumCtx + } + + const thinkParam = this.getOllamaThinkParam() + return [chatOptions, thinkParam] } override async *createMessage( @@ -215,7 +408,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio ] const matcher = new TagMatcher( - "think", + ["think", "thought"], (chunk) => ({ type: chunk.matched ? "reasoning" : "text", @@ -224,23 +417,24 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio ) try { - // Build options object conditionally - const chatOptions: OllamaChatOptions = { - temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0), - } - - // Only include num_ctx if explicitly set via ollamaNumCtx - if (this.options.ollamaNumCtx !== undefined) { - chatOptions.num_ctx = this.options.ollamaNumCtx - } - - // Create the actual API request promise + // Build the shared chat options and conditional think parameter. + // Conditionally enabling Ollama's native think parameter lets + // reasoning models (qwen3, deepseek-r1, etc.) emit thinking via + // the dedicated message.thinking field instead of (or in addition + // to) think/thought tags embedded in content. + const [chatOptions, thinkParam] = this.buildChatRequestOptions(useR1Format) + + // Create the actual API request promise. The `stream: true` literal + // is kept inline so TypeScript selects the streaming overload of + // client.chat. The `think` parameter is spread conditionally to + // avoid sending an explicit `think: undefined` to the runtime. const stream = await client.chat({ model: modelId, messages: ollamaMessages, stream: true, options: chatOptions, tools: this.convertToolsToOllama(metadata?.tools), + ...(thinkParam !== undefined ? { think: thinkParam } : {}), }) let totalInputTokens = 0 @@ -252,6 +446,18 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio try { for await (const chunk of stream) { + // Process Ollama's native thinking field. When the think + // parameter is enabled (or the model thinks by default), + // Ollama streams reasoning via message.thinking separately + // from message.content. Surface it as a reasoning chunk so + // it is rendered and preserved like other providers. + if (typeof chunk.message.thinking === "string" && chunk.message.thinking.length > 0) { + yield { + type: "reasoning", + text: chunk.message.thinking, + } + } + if (typeof chunk.message.content === "string" && chunk.message.content.length > 0) { // Process content through matcher for reasoning detection for (const matcherChunk of matcher.update(chunk.message.content)) { @@ -350,21 +556,17 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio const { id: modelId } = await this.fetchModel() const useR1Format = modelId.toLowerCase().includes("deepseek-r1") - // Build options object conditionally - const chatOptions: OllamaChatOptions = { - temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0), - } - - // Only include num_ctx if explicitly set via ollamaNumCtx - if (this.options.ollamaNumCtx !== undefined) { - chatOptions.num_ctx = this.options.ollamaNumCtx - } + // Reuse the shared request-option builder so single-shot + // completions respect the same reasoning configuration as the + // streaming path. + const [chatOptions, thinkParam] = this.buildChatRequestOptions(useR1Format) const response = await client.chat({ model: modelId, messages: [{ role: "user", content: prompt }], stream: false, options: chatOptions, + ...(thinkParam !== undefined ? { think: thinkParam } : {}), }) return response.message?.content || "" diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index bc9d4cd26a..2b2a599c0f 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -429,8 +429,10 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion content.push({ type: "input_text", text: block.text }) } else if (block.type === "image") { const image = block as Anthropic.Messages.ImageBlockParam - const imageUrl = `data:${image.source.media_type};base64,${image.source.data}` - content.push({ type: "input_image", image_url: imageUrl }) + if (image.source.type === "base64") { + const imageUrl = `data:${image.source.media_type};base64,${image.source.data}` + content.push({ type: "input_image", image_url: imageUrl }) + } } else if (block.type === "tool_result") { const result = typeof block.content === "string" diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index ea7a0667f3..8f86f46751 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -483,8 +483,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio content.push({ type: "input_text", text: block.text }) } else if (block.type === "image") { const image = block as Anthropic.Messages.ImageBlockParam - const imageUrl = `data:${image.source.media_type};base64,${image.source.data}` - content.push({ type: "input_image", image_url: imageUrl }) + if (image.source.type === "base64") { + const imageUrl = `data:${image.source.media_type};base64,${image.source.data}` + content.push({ type: "input_image", image_url: imageUrl }) + } } else if (block.type === "tool_result") { // Map Anthropic tool_result to Responses API function_call_output item const result = diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index abef612d88..07427c7dbe 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -22,7 +22,7 @@ import { getModelParams } from "../transform/model-params" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { extractReasoningFromDelta } from "./utils/extract-reasoning" // TODO: Rename this to OpenAICompatibleHandler. Also, I think the @@ -184,7 +184,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl } const matcher = new TagMatcher( - "think", + ["think", "thought"], (chunk) => ({ type: chunk.matched ? "reasoning" : "text", diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 1ac9c465b6..360d4c7f81 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -36,7 +36,7 @@ import { getModelEndpoints } from "./fetchers/modelEndpointCache" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-generation" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" diff --git a/src/api/providers/poe.ts b/src/api/providers/poe.ts index 536d222acd..9513a1445d 100644 --- a/src/api/providers/poe.ts +++ b/src/api/providers/poe.ts @@ -38,7 +38,11 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler override getModel() { const id = this.options.apiModelId ?? poeDefaultModelId - const cached = getModelsFromCache("poe") + const cached = getModelsFromCache({ + provider: "poe", + apiKey: this.options.poeApiKey, + baseUrl: this.options.poeBaseUrl, + }) const info: ModelInfo = cached?.[id] ?? getPoeDefaultModelInfo() return { id, info } } diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index c490227d44..006cd9cdfb 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -16,7 +16,7 @@ import { getModels } from "./fetchers/modelCache" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { toRequestyServiceUrl } from "../../shared/utils/requesty" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" import { extractReasoningFromDelta } from "./utils/extract-reasoning" diff --git a/src/api/providers/router-provider.ts b/src/api/providers/router-provider.ts index 20419bb45a..7a983ed12e 100644 --- a/src/api/providers/router-provider.ts +++ b/src/api/providers/router-provider.ts @@ -62,24 +62,38 @@ export abstract class RouterProvider extends BaseProvider { } override getModel(): { id: string; info: ModelInfo } { - const id = this.modelId ?? this.defaultModelId + // Use `||` (not `??`) so an empty-string modelId also falls back to the default, + // guaranteeing a non-empty id rather than forwarding "" to the API as an invalid + // request. Note this guarantees non-empty, not viable: defaultModelId is provider- + // supplied and may not be a model that actually exists on the user's server (e.g. + // OpenAI-compatible have no inherent default), so a configured-but-empty selection + // can still resolve to a model the server rejects. + const id = this.modelId || this.defaultModelId // First check instance models (populated by fetchModel) if (this.models[id]) { return { id, info: this.models[id] } } - // Fall back to global cache (synchronous disk/memory cache) - // This ensures models are available before fetchModel() is called - const cachedModels = getModelsFromCache(this.name) + // Fall back to global cache (synchronous disk/memory cache). + // Pass the full options so URL-scoped providers (litellm, ollama, etc.) + // resolve the same compound cache key that fetchModel() wrote under. + const cachedModels = getModelsFromCache({ + provider: this.name, + baseUrl: this.client.baseURL, + apiKey: this.client.apiKey, + }) if (cachedModels?.[id]) { // Also populate instance models for future calls this.models = cachedModels return { id, info: cachedModels[id] } } - // Last resort: return default model - return { id: this.defaultModelId, info: this.defaultModelInfo } + // Last resort: preserve the configured model ID (falling back to the default + // only when none is configured) so an as-yet-unfetched model isn't silently + // swapped for the hardcoded default. info still comes from defaults since we + // have no fetched or cached metadata for the configured model at this point. + return { id, info: this.defaultModelInfo } } protected supportsTemperature(modelId: string): boolean { diff --git a/src/api/providers/unbound.ts b/src/api/providers/unbound.ts index 0ec7a2466f..cb41591634 100644 --- a/src/api/providers/unbound.ts +++ b/src/api/providers/unbound.ts @@ -15,7 +15,7 @@ import { DEFAULT_HEADERS } from "./constants" import { getModels } from "./fetchers/modelCache" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { applyRouterToolPreferences } from "./utils/router-tool-preferences" import { extractReasoningFromDelta } from "./utils/extract-reasoning" diff --git a/src/api/providers/utils/__tests__/openai-error-handler.spec.ts b/src/api/providers/utils/__tests__/openai-error-handler.spec.ts deleted file mode 100644 index 740d060ac8..0000000000 --- a/src/api/providers/utils/__tests__/openai-error-handler.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { handleOpenAIError } from "../openai-error-handler" - -describe("handleOpenAIError", () => { - const providerName = "TestProvider" - - describe("HTTP status preservation", () => { - it("should preserve status code from Error with status field", () => { - const error = new Error("API request failed") as any - error.status = 401 - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect(result.message).toContain("TestProvider completion error") - expect((result as any).status).toBe(401) - }) - - it("should preserve status code from Error with nested error structure", () => { - const error = new Error("Wrapped error") as any - error.status = 429 - error.errorDetails = [{ "@type": "type.googleapis.com/google.rpc.RetryInfo" }] - - const result = handleOpenAIError(error, providerName) - - expect((result as any).status).toBe(429) - expect((result as any).errorDetails).toBeDefined() - }) - - it("should preserve status from non-Error exception", () => { - const error = { - status: 500, - message: "Internal server error", - } - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect((result as any).status).toBe(500) - }) - - it("should not add status field if original error lacks it", () => { - const error = new Error("Generic error") - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect((result as any).status).toBeUndefined() - }) - }) - - describe("errorDetails preservation", () => { - it("should preserve errorDetails array from original error", () => { - const error = new Error("Rate limited") as any - error.status = 429 - error.errorDetails = [{ "@type": "type.googleapis.com/google.rpc.RetryInfo", retryDelay: "5s" }] - - const result = handleOpenAIError(error, providerName) - - expect((result as any).errorDetails).toEqual(error.errorDetails) - }) - - it("should preserve code field from original error", () => { - const error = new Error("Bad request") as any - error.code = "invalid_request" - - const result = handleOpenAIError(error, providerName) - - expect((result as any).code).toBe("invalid_request") - }) - }) - - describe("ByteString conversion errors", () => { - it("should return localized message for ByteString conversion errors", () => { - const error = new Error("Cannot convert argument to a ByteString") - - const result = handleOpenAIError(error, providerName) - - expect(result.message).not.toContain("TestProvider completion error") - // The actual translated message depends on i18n setup - expect(result.message).toBeTruthy() - }) - - it("should preserve status even for ByteString errors", () => { - const error = new Error("Cannot convert argument to a ByteString") as any - error.status = 400 - - const result = handleOpenAIError(error, providerName) - - // Even though ByteString errors are typically client-side, - // we preserve any status metadata that exists for debugging purposes - expect((result as any).status).toBe(400) - }) - }) - - describe("error message formatting", () => { - it("should wrap error message with provider name prefix", () => { - const error = new Error("Authentication failed") - - const result = handleOpenAIError(error, providerName) - - expect(result.message).toBe("TestProvider completion error: Authentication failed") - }) - - it("should handle error with nested metadata", () => { - const error = new Error("Network error") as any - error.error = { - metadata: { - raw: "Connection refused", - }, - } - - const result = handleOpenAIError(error, providerName) - - expect(result.message).toContain("Connection refused") - expect(result.message).toContain("TestProvider completion error") - }) - - it("should handle non-Error exceptions", () => { - const error = { message: "Something went wrong" } - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect(result.message).toContain("TestProvider completion error") - expect(result.message).toContain("[object Object]") - }) - - it("should handle string exceptions", () => { - const error = "Connection timeout" - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect(result.message).toBe("TestProvider completion error: Connection timeout") - }) - }) - - describe("real-world error scenarios", () => { - it("should handle 401 Unauthorized with status and message", () => { - const error = new Error("Unauthorized") as any - error.status = 401 - - const result = handleOpenAIError(error, providerName) - - expect(result.message).toContain("Unauthorized") - expect((result as any).status).toBe(401) - }) - - it("should handle 429 Rate Limit with RetryInfo", () => { - const error = new Error("Rate limit exceeded") as any - error.status = 429 - error.errorDetails = [ - { - "@type": "type.googleapis.com/google.rpc.RetryInfo", - retryDelay: "10s", - }, - ] - - const result = handleOpenAIError(error, providerName) - - expect((result as any).status).toBe(429) - expect((result as any).errorDetails).toBeDefined() - expect((result as any).errorDetails[0].retryDelay).toBe("10s") - }) - - it("should handle 500 Internal Server Error", () => { - const error = new Error("Internal server error") as any - error.status = 500 - - const result = handleOpenAIError(error, providerName) - - expect((result as any).status).toBe(500) - expect(result.message).toContain("Internal server error") - }) - - it("should handle errors without status gracefully", () => { - const error = new Error("Network connectivity issue") - - const result = handleOpenAIError(error, providerName) - - expect(result).toBeInstanceOf(Error) - expect((result as any).status).toBeUndefined() - expect(result.message).toContain("Network connectivity issue") - }) - }) -}) diff --git a/src/api/providers/utils/openai-error-handler.ts b/src/api/providers/utils/openai-error-handler.ts deleted file mode 100644 index f4bd7d0348..0000000000 --- a/src/api/providers/utils/openai-error-handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * General error handler for OpenAI client errors - * Transforms technical errors into user-friendly messages - * - * @deprecated Use handleProviderError from './error-handler' instead - * This file is kept for backward compatibility - */ - -import { handleProviderError } from "./error-handler" - -/** - * Handles OpenAI client errors and transforms them into user-friendly messages - * @param error - The error to handle - * @param providerName - The name of the provider for context in error messages - * @returns The original error or a transformed user-friendly error - */ -export function handleOpenAIError(error: unknown, providerName: string): Error { - return handleProviderError(error, providerName, { messagePrefix: "completion" }) -} diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index 8fb564a9d5..5b9064363f 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import * as vscode from "vscode" import OpenAI from "openai" -import { type ModelInfo, openAiModelInfoSaneDefaults } from "@roo-code/types" +import { type ModelInfo, openAiModelInfoSaneDefaults, vscodeLlmDefaultModelId, vscodeLlmModels } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils" @@ -562,6 +562,26 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } } + /** + * Context window for auto-condense. The API's advertised `client.maxInputTokens` is far larger + * than usable, so relying on it stops auto-condense from firing; measure against the curated + * static table's `maxInputTokens` instead (the same value the bar uses). An unknown family (e.g. + * a selector left over from a model dropped from the catalog) resolves to the default row rather + * than the inflated live window; only a non-positive static `maxInputTokens` falls back to it. + */ + getCondenseContextWindow(): number { + const family = this.client?.family ?? this.options.vsCodeLmModelSelector?.family + const staticModel = family + ? (vscodeLlmModels[family as keyof typeof vscodeLlmModels] ?? vscodeLlmModels[vscodeLlmDefaultModelId]) + : vscodeLlmModels[vscodeLlmDefaultModelId] + + if (staticModel && typeof staticModel.maxInputTokens === "number" && staticModel.maxInputTokens > 0) { + return staticModel.maxInputTokens + } + + return this.getModel().info.contextWindow + } + async completePrompt(prompt: string): Promise { try { const client = await this.getClient() diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index e5c0ba0a81..17ac40c8bf 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -14,7 +14,7 @@ import { getModelParams } from "../transform/model-params" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" import { isMcpTool } from "../../utils/mcp-name" const XAI_DEFAULT_TEMPERATURE = 0 diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts index c8f720a971..4854c814fd 100644 --- a/src/api/providers/zai.ts +++ b/src/api/providers/zai.ts @@ -16,7 +16,7 @@ import { convertToZAiFormat } from "../transform/zai-format" import type { ApiHandlerCreateMessageMetadata } from "../index" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" -import { handleOpenAIError } from "./utils/openai-error-handler" +import { handleOpenAIError } from "./utils/error-handler" // Custom interface for Z.ai params to support thinking mode and reasoning effort tiers. // Z.ai accepts the standard `reasoning_effort` ladder (none/minimal/low/medium/high/xhigh/max) diff --git a/src/api/transform/__tests__/gemini-format.spec.ts b/src/api/transform/__tests__/gemini-format.spec.ts index 23f752e207..327a05479b 100644 --- a/src/api/transform/__tests__/gemini-format.spec.ts +++ b/src/api/transform/__tests__/gemini-format.spec.ts @@ -123,6 +123,11 @@ describe("convertAnthropicMessageToGemini", () => { const result = convertAnthropicMessageToGemini(anthropicMessage) + // thoughtSignature must be base64-encoded: the Gemini API documents Part.thoughtSignature + // as "Encoded as base64 string". Sending the raw bypass token without base64 encoding fails + // on Vertex AI (Gemini 3.1/3.5 strict validation), causing empty-response loops on turn 2+. + const expectedBypassToken = Buffer.from("skip_thought_signature_validator").toString("base64") + expect(result).toEqual([ { role: "model", @@ -133,7 +138,7 @@ describe("convertAnthropicMessageToGemini", () => { name: "calculator", args: { operation: "add", numbers: [2, 3] }, }, - thoughtSignature: "skip_thought_signature_validator", + thoughtSignature: expectedBypassToken, }, ], }, diff --git a/src/api/transform/__tests__/mistral-format.spec.ts b/src/api/transform/__tests__/mistral-format.spec.ts index 290bea1ec5..6f3984bcc7 100644 --- a/src/api/transform/__tests__/mistral-format.spec.ts +++ b/src/api/transform/__tests__/mistral-format.spec.ts @@ -106,6 +106,45 @@ describe("convertToMistralMessages", () => { }) }) + it("should handle user messages with URL image content", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Describe this:" }, + { + type: "image", + source: { type: "url", url: "https://example.com/photo.jpg" } as any, + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + const content = mistralMessages[0].content as Array<{ type: string; imageUrl?: { url: string }; text?: string }> + + expect(content[1]).toEqual({ type: "image_url", imageUrl: { url: "https://example.com/photo.jpg" } }) + }) + + it("should fall back to [Image] placeholder for unsupported image source types", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "image", + source: { type: "file" } as any, + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + const content = mistralMessages[0].content as Array<{ type: string; text?: string }> + + expect(content[0]).toEqual({ type: "text", text: "[Image]" }) + }) + it("should handle user messages with only tool results", () => { const anthropicMessages: Anthropic.Messages.MessageParam[] = [ { diff --git a/src/api/transform/__tests__/openai-format.spec.ts b/src/api/transform/__tests__/openai-format.spec.ts index 1a4c7f6518..214531312c 100644 --- a/src/api/transform/__tests__/openai-format.spec.ts +++ b/src/api/transform/__tests__/openai-format.spec.ts @@ -1,4 +1,4 @@ -// npx vitest run api/transform/__tests__/openai-format.spec.ts +// pnpm exec vitest run api/transform/__tests__/openai-format.spec.ts import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" @@ -76,6 +76,123 @@ describe("convertToOpenAiMessages", () => { }) }) + it("should handle messages with URL image content", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image", + source: { type: "url", url: "https://example.com/image.png" } as any, + }, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + const content = openAiMessages[0].content as Array<{ type: string; text?: string; image_url?: { url: string } }> + + expect(content[1]).toEqual({ + type: "image_url", + image_url: { url: "https://example.com/image.png" }, + }) + }) + + it("should fall back to [Image] placeholder for unsupported image source types", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "image", + source: { type: "file" } as any, + }, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + const content = openAiMessages[0].content as Array<{ type: string; text?: string }> + + expect(content[0]).toEqual({ type: "text", text: "[Image]" }) + }) + + it("should handle non-text/non-image blocks in tool results as empty string", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-1", + content: [ + { type: "text", text: "some text" }, + { + type: "document", + source: { type: "base64", media_type: "application/pdf", data: "abc" }, + } as any, + ], + }, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages[0].role).toBe("tool") + expect((openAiMessages[0] as any).content).toBe("some text\n") + }) + + it("should handle base64 image in tool result with placeholder", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-1", + content: [ + { + type: "image", + source: { type: "base64", media_type: "image/png", data: "base64data" }, + }, + ], + }, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages[0].role).toBe("tool") + // base64 images in tool results emit a placeholder; the image itself is + // flushed in a separate user message (see comment block in openai-format.ts) + expect((openAiMessages[0] as any).content).toBe("(see following user message for image)") + }) + + it("should render [Image] placeholder for URL image in tool result", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-1", + content: [ + { + type: "image", + source: { type: "url", url: "https://example.com/img.png" } as any, + }, + ], + }, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages[0].role).toBe("tool") + expect((openAiMessages[0] as any).content).toBe("[Image]") + }) + it("should handle assistant messages with tool use (no normalization without normalizeToolCallId)", () => { const anthropicMessages: Anthropic.Messages.MessageParam[] = [ { @@ -967,6 +1084,157 @@ describe("convertToOpenAiMessages", () => { expect(assistantMessage.reasoning_details[2].data).toBe("encrypted_data") }) }) + + describe("reasoning_content round-trip for DeepSeek / Z.ai thinking mode", () => { + it("should pass through top-level reasoning_content on assistant messages", () => { + const anthropicMessages = [ + { + role: "assistant" as const, + content: "Here is my answer.", + reasoning_content: "Let me think about this carefully...", + }, + ] as any as Anthropic.Messages.MessageParam[] + + const result = convertToOpenAiMessages(anthropicMessages) + + expect(result).toHaveLength(1) + expect((result[0] as any).reasoning_content).toBe("Let me think about this carefully...") + }) + + it("should extract reasoning_content from reasoning content block", () => { + // buildCleanConversationHistory stores reasoning as a content block when preserveReasoning=true + const anthropicMessages = [ + { + role: "assistant" as const, + content: [ + { type: "reasoning", text: "Let me think...", summary: [] }, + { type: "text", text: "My answer." }, + ], + }, + ] as any as Anthropic.Messages.MessageParam[] + + const result = convertToOpenAiMessages(anthropicMessages) + + expect(result).toHaveLength(1) + const msg = result[0] as any + expect(msg.reasoning_content).toBe("Let me think...") + expect(msg.content).toBe("My answer.") + }) + + it("should extract reasoning_content from reasoning block alongside tool calls", () => { + // The critical case: DeepSeek thinking + tool call in the same turn. + // Without reasoning_content on the second request, DeepSeek returns 400: + // "The reasoning_content in the thinking mode must be passed back to the API." + const anthropicMessages = [ + { + role: "assistant" as const, + content: [ + { type: "reasoning", text: "I need to read a file.", summary: [] }, + { + type: "tool_use", + id: "call_abc", + name: "read_file", + input: { path: "foo.txt" }, + }, + ], + }, + ] as any as Anthropic.Messages.MessageParam[] + + const result = convertToOpenAiMessages(anthropicMessages) + + expect(result).toHaveLength(1) + const msg = result[0] as any + expect(msg.reasoning_content).toBe("I need to read a file.") + expect(msg.tool_calls).toHaveLength(1) + expect(msg.tool_calls[0].id).toBe("call_abc") + }) + + it("should accumulate multiple reasoning blocks in order, separated by a tool call", () => { + // DeepSeek / Z.ai interleaved thinking can emit more than one reasoning block per + // turn. A regression that overwrites (instead of accumulates) would silently drop + // all but the last block. + const anthropicMessages = [ + { + role: "assistant" as const, + content: [ + { type: "reasoning", text: "First, I should check the file.", summary: [] }, + { + type: "tool_use", + id: "call_abc", + name: "read_file", + input: { path: "foo.txt" }, + }, + { type: "reasoning", text: "Now I know what to do next.", summary: [] }, + ], + }, + ] as any as Anthropic.Messages.MessageParam[] + + const result = convertToOpenAiMessages(anthropicMessages) + + expect(result).toHaveLength(1) + const msg = result[0] as any + expect(msg.reasoning_content).toBe("First, I should check the file.Now I know what to do next.") + expect(msg.tool_calls).toHaveLength(1) + expect(msg.tool_calls[0].id).toBe("call_abc") + }) + + it("should prefer top-level reasoning_content over content block", () => { + const anthropicMessages = [ + { + role: "assistant" as const, + content: [ + { type: "reasoning", text: "block reasoning", summary: [] }, + { type: "text", text: "answer" }, + ], + reasoning_content: "top-level reasoning", + }, + ] as any as Anthropic.Messages.MessageParam[] + + const result = convertToOpenAiMessages(anthropicMessages) + + expect((result[0] as any).reasoning_content).toBe("top-level reasoning") + }) + + it("should not set reasoning_content when there is none", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "call_abc", + name: "read_file", + input: { path: "foo.txt" }, + }, + ], + }, + ] + + const result = convertToOpenAiMessages(anthropicMessages) + + expect(result).toHaveLength(1) + expect((result[0] as any).reasoning_content).toBeUndefined() + }) + + it("should ignore non-object content parts without crashing (defensive guard)", () => { + // getReasoningBlockText guards against non-object parts (e.g. a stray + // string in the content array). Such parts are not reasoning blocks and + // must be skipped rather than crashing or being misread as reasoning. + const anthropicMessages = [ + { + role: "assistant" as const, + content: ["not-a-block" as any, { type: "text", text: "answer" }], + }, + ] as any as Anthropic.Messages.MessageParam[] + + const result = convertToOpenAiMessages(anthropicMessages) + + expect(result).toHaveLength(1) + const msg = result[0] as any + expect(msg.content).toBe("answer") + expect(msg.reasoning_content).toBeUndefined() + }) + }) }) describe("consolidateReasoningDetails", () => { diff --git a/src/api/transform/__tests__/r1-format.spec.ts b/src/api/transform/__tests__/r1-format.spec.ts index 3d875e9392..7def13f1fe 100644 --- a/src/api/transform/__tests__/r1-format.spec.ts +++ b/src/api/transform/__tests__/r1-format.spec.ts @@ -69,6 +69,26 @@ describe("convertToR1Format", () => { expect(convertToR1Format(input)).toEqual(expected) }) + it("should skip non-base64 images (e.g. URL-sourced)", () => { + const input: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this:" }, + { + type: "image", + source: { type: "url", url: "https://example.com/img.png" } as any, + }, + ], + }, + ] + + const result = convertToR1Format(input) + // URL image is skipped; only the text part survives + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ role: "user", content: "Look at this:" }) + }) + it("should handle mixed text and image content", () => { const input: Anthropic.Messages.MessageParam[] = [ { diff --git a/src/api/transform/__tests__/responses-api-input.spec.ts b/src/api/transform/__tests__/responses-api-input.spec.ts index c57b61d895..dc13ebfce0 100644 --- a/src/api/transform/__tests__/responses-api-input.spec.ts +++ b/src/api/transform/__tests__/responses-api-input.spec.ts @@ -73,6 +73,25 @@ describe("convertToResponsesApiInput", () => { ]) }) + it("should skip non-base64 images (e.g. URL-sourced)", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this:" }, + { + type: "image", + source: { type: "url", url: "https://example.com/img.png" } as any, + }, + ], + }, + ] + + const result = convertToResponsesApiInput(messages) + // URL image is not emitted; only the text part survives + expect(result).toEqual([{ role: "user", content: [{ type: "input_text", text: "Look at this:" }] }]) + }) + it("should convert tool_result to function_call_output", () => { const messages: Anthropic.Messages.MessageParam[] = [ { diff --git a/src/api/transform/__tests__/vscode-lm-format.spec.ts b/src/api/transform/__tests__/vscode-lm-format.spec.ts index 67f835f6ab..36325e9c93 100644 --- a/src/api/transform/__tests__/vscode-lm-format.spec.ts +++ b/src/api/transform/__tests__/vscode-lm-format.spec.ts @@ -1,4 +1,4 @@ -// npx vitest run src/api/transform/__tests__/vscode-lm-format.spec.ts +// pnpm exec vitest run api/transform/__tests__/vscode-lm-format.spec.ts import { Anthropic } from "@anthropic-ai/sdk" import * as vscode from "vscode" @@ -178,6 +178,91 @@ describe("convertToVsCodeLmMessages", () => { const imagePlaceholder = result[0].content[1] as MockLanguageModelTextPart expect(imagePlaceholder.value).toContain("[Image (base64): image/png not supported by VSCode LM API]") }) + + it("should produce correct placeholder for URL image in non-tool messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "image", + source: { type: "url", url: "https://example.com/img.png" } as any, + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + const imagePlaceholder = result[0].content[0] as MockLanguageModelTextPart + expect(imagePlaceholder.value).toContain("[Image (url): not supported by VSCode LM API]") + }) + + it("should produce correct placeholder for URL image inside tool result", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-1", + content: [ + { + type: "image", + source: { type: "url", url: "https://example.com/img.png" } as any, + }, + ], + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + const toolResult = result[0].content[0] as any + expect(toolResult.content[0].value).toContain("[Image (url): not supported by VSCode LM API]") + }) + + it("should produce base64 image placeholder inside tool result", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-1", + content: [ + { + type: "image", + source: { type: "base64", media_type: "image/jpeg", data: "abc" }, + }, + ], + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + const toolResult = result[0].content[0] as any + expect(toolResult.content[0].value).toBe("[Image (base64): image/jpeg not supported by VSCode LM API]") + }) + + it("should return empty string for unknown block types inside tool result", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-1", + content: [{ type: "document" } as any], + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + const toolResult = result[0].content[0] as any + expect(toolResult.content[0].value).toBe("") + }) }) describe("convertToAnthropicRole", () => { diff --git a/src/api/transform/__tests__/zai-format.spec.ts b/src/api/transform/__tests__/zai-format.spec.ts new file mode 100644 index 0000000000..27ad4a2c90 --- /dev/null +++ b/src/api/transform/__tests__/zai-format.spec.ts @@ -0,0 +1,79 @@ +// pnpm exec vitest run api/transform/__tests__/zai-format.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" + +import { convertToZAiFormat } from "../zai-format" + +describe("convertToZAiFormat", () => { + it("should convert simple user text messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] + + const result = convertToZAiFormat(messages) + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ role: "user", content: "Hello" }) + }) + + it("should handle base64 image in user message", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Describe this:" }, + { + type: "image", + source: { type: "base64", media_type: "image/png", data: "abc123" }, + }, + ], + }, + ] + + const result = convertToZAiFormat(messages) + expect(result).toHaveLength(1) + const content = (result[0] as any).content as Array<{ + type: string + image_url?: { url: string } + text?: string + }> + expect(content[0]).toEqual({ type: "text", text: "Describe this:" }) + expect(content[1]).toEqual({ type: "image_url", image_url: { url: "data:image/png;base64,abc123" } }) + }) + + it("should skip non-base64 images in user messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Hello" }, + { + type: "image", + source: { type: "url", url: "https://example.com/img.png" } as any, + }, + ], + }, + ] + + const result = convertToZAiFormat(messages) + expect(result).toHaveLength(1) + // URL image is skipped — only text remains, so content is a plain string + expect((result[0] as any).content).toBe("Hello") + }) + + it("should convert tool_result content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [{ type: "tool_result", tool_use_id: "tool-1", content: "Result text" }], + }, + ] + + const result = convertToZAiFormat(messages) + expect(result[0]).toEqual({ role: "tool", tool_call_id: "tool-1", content: "Result text" }) + }) + + it("should convert assistant messages with text", () => { + const messages: Anthropic.Messages.MessageParam[] = [{ role: "assistant", content: "I can help with that." }] + + const result = convertToZAiFormat(messages) + expect(result[0]).toEqual({ role: "assistant", content: "I can help with that." }) + }) +}) diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts index 6f24036296..53bab11c52 100644 --- a/src/api/transform/gemini-format.ts +++ b/src/api/transform/gemini-format.ts @@ -1,6 +1,11 @@ import { Anthropic } from "@anthropic-ai/sdk" import { Content, Part } from "@google/genai" +// Gemini documents Part.thoughtSignature as "Encoded as base64 string". Vertex AI enforces +// this strictly — sending the plain string causes empty responses after the first tool call. +// This bypass token tells Gemini to skip signature validation for cross-model history entries. +export const GEMINI_THOUGHT_SIGNATURE_BYPASS = Buffer.from("skip_thought_signature_validator").toString("base64") + type ThoughtSignatureContentBlock = { type: "thoughtSignature" thoughtSignature?: string @@ -42,10 +47,12 @@ export function convertAnthropicContentToGemini( // Determine the signature to attach to function calls. // If we're in a mode that expects signatures (includeThoughtSignatures is true): // 1. Use the actual signature if we found one in the history/content. - // 2. Fallback to "skip_thought_signature_validator" if missing (e.g. cross-model history). + // 2. Fallback to a base64-encoded bypass token if missing (e.g. cross-model history). + // Part.thoughtSignature is documented as "Encoded as base64 string" — Vertex AI validates + // this strictly and returns empty responses when a non-base64 value is sent. let functionCallSignature: string | undefined if (includeThoughtSignatures) { - functionCallSignature = activeThoughtSignature || "skip_thought_signature_validator" + functionCallSignature = activeThoughtSignature || GEMINI_THOUGHT_SIGNATURE_BYPASS } if (typeof content === "string") { diff --git a/src/api/transform/mistral-format.ts b/src/api/transform/mistral-format.ts index d32f84d6e0..c29d248714 100644 --- a/src/api/transform/mistral-format.ts +++ b/src/api/transform/mistral-format.ts @@ -104,12 +104,21 @@ export function convertToMistralMessages(anthropicMessages: Anthropic.Messages.M role: "user", content: nonToolMessages.map((part) => { if (part.type === "image") { - return { - type: "image_url", - imageUrl: { - url: `data:${part.source.media_type};base64,${part.source.data}`, - }, + if (part.source.type === "base64") { + return { + type: "image_url", + imageUrl: { + url: `data:${part.source.media_type};base64,${part.source.data}`, + }, + } } + if (part.source.type === "url") { + return { + type: "image_url", + imageUrl: { url: part.source.url }, + } + } + return { type: "text", text: "[Image]" } } return { type: "text", text: part.text } }), diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts index 38719feba1..c06064f1dd 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -253,6 +253,52 @@ export function sanitizeGeminiMessages( return sanitized } +/** + * A reasoning content block (DeepSeek / Z.ai interleaved thinking) that may + * appear in an assistant message's content array. Not part of Anthropic's + * ContentBlockParam union, so it's declared separately here. + */ +interface ReasoningContentBlock { + type: "reasoning" + text?: string +} + +/** + * Extracts the reasoning text if `part` is a {@link ReasoningContentBlock}, or `undefined` + * otherwise. Providers like DeepSeek / Z.ai emit a `"reasoning"` content block that isn't part of + * Anthropic's `ContentBlockParam` union, so `part` is treated as `unknown` and its shape is + * validated at runtime rather than assumed via a cast. A boolean type-guard isn't viable here: + * TypeScript can't narrow `part` to `ReasoningContentBlock` because their `type` literals don't + * overlap with any variant of `ContentBlockParam`, so the narrowed type collapses to `never`. + */ +function getReasoningBlockText(part: unknown): string | undefined { + if (!part || typeof part !== "object") { + return undefined + } + const block = part as { type?: unknown; text?: unknown } + return block.type === "reasoning" && typeof block.text === "string" ? block.text : undefined +} + +/** + * Non-standard fields Zoo Code attaches to Anthropic message params to + * round-trip provider-specific reasoning state (e.g. DeepSeek's + * `reasoning_content`, which must be echoed back or the API returns a 400). + */ +type AssistantMessageWithReasoning = Anthropic.Messages.MessageParam & { + reasoning_details?: any[] + reasoning_content?: string +} + +/** + * Extra fields layered onto an outgoing OpenAI chat message to round-trip + * provider-specific reasoning state. Shared by both branches of the + * conversion below (string-content and content-block assistant messages). + */ +type ReasoningPassthroughFields = { + reasoning_details?: any[] + reasoning_content?: string +} + /** * Options for converting Anthropic messages to OpenAI format. */ @@ -306,8 +352,8 @@ export function convertToOpenAiMessages( // will convert a single text block into a string for compactness. // If a message also contains reasoning_details (Gemini 3 / xAI / o-series, etc.), // we must preserve it here as well. - const messageWithDetails = anthropicMessage as any - const baseMessage: OpenAI.Chat.ChatCompletionMessageParam & { reasoning_details?: any[] } = { + const messageWithDetails = anthropicMessage as AssistantMessageWithReasoning + const baseMessage: OpenAI.Chat.ChatCompletionMessageParam & ReasoningPassthroughFields = { role: anthropicMessage.role, content: anthropicMessage.content, } @@ -315,7 +361,11 @@ export function convertToOpenAiMessages( if (anthropicMessage.role === "assistant") { const mapped = mapReasoningDetails(messageWithDetails.reasoning_details) if (mapped) { - ;(baseMessage as any).reasoning_details = mapped + baseMessage.reasoning_details = mapped + } + // Pass through reasoning_content for DeepSeek / Z.ai thinking mode. + if (typeof messageWithDetails.reasoning_content === "string" && messageWithDetails.reasoning_content) { + baseMessage.reasoning_content = messageWithDetails.reasoning_content } } @@ -358,10 +408,16 @@ export function convertToOpenAiMessages( toolMessage.content ?.map((part) => { if (part.type === "image") { - toolResultImages.push(part) - return "(see following user message for image)" + if (part.source.type === "base64") { + toolResultImages.push(part) + return "(see following user message for image)" + } + return "[Image]" + } + if (part.type === "text") { + return part.text } - return part.text + return "" }) .join("\n") ?? "" } @@ -422,10 +478,21 @@ export function convertToOpenAiMessages( role: "user", content: filteredNonToolMessages.map((part) => { if (part.type === "image") { - return { - type: "image_url", - image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` }, + if (part.source.type === "base64") { + return { + type: "image_url", + image_url: { + url: `data:${part.source.media_type};base64,${part.source.data}`, + }, + } } + if (part.source.type === "url") { + return { + type: "image_url", + image_url: { url: part.source.url }, + } + } + return { type: "text", text: "[Image]" } } return { type: "text", text: part.text } }), @@ -433,6 +500,9 @@ export function convertToOpenAiMessages( } } } else if (anthropicMessage.role === "assistant") { + const messageWithDetails = anthropicMessage as AssistantMessageWithReasoning + + let extractedReasoning: string | undefined const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{ nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] toolMessages: Anthropic.ToolUseBlockParam[] @@ -442,6 +512,17 @@ export function convertToOpenAiMessages( acc.toolMessages.push(part) } else if (part.type === "text" || part.type === "image") { acc.nonToolMessages.push(part) + } else { + const reasoningText = getReasoningBlockText(part) + if (reasoningText) { + // Extract reasoning stored as a content block (DeepSeek / Z.ai interleaved thinking). + // Must be passed back as top-level reasoning_content so providers like DeepSeek + // don't reject the request with "reasoning_content must be passed back to the API". + // Accumulate all blocks (a turn may have more than one) to preserve order. + extractedReasoning = extractedReasoning + ? extractedReasoning + reasoningText + : reasoningText + } } // assistant cannot send tool_result messages return acc }, @@ -472,15 +553,10 @@ export function convertToOpenAiMessages( }, })) - // Check if the message has reasoning_details (used by Gemini 3, xAI, etc.) - const messageWithDetails = anthropicMessage as any - // Build message with reasoning_details BEFORE tool_calls to preserve // the order expected by providers like Roo. Property order matters // when sending messages back to some APIs. - const baseMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam & { - reasoning_details?: any[] - } = { + const baseMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam & ReasoningPassthroughFields = { role: "assistant", // Use empty string instead of undefined for providers like Gemini (via OpenRouter) // that require every message to have content in the "parts" field @@ -494,6 +570,18 @@ export function convertToOpenAiMessages( baseMessage.reasoning_details = mapped } + // Pass through reasoning_content for providers that require it in history + // (e.g. DeepSeek thinking mode: "reasoning_content must be passed back to the API"). + // Prefer top-level field (already round-tripped); fall back to reasoning from content blocks. + const outgoingReasoningContent: string | undefined = + (typeof messageWithDetails.reasoning_content === "string" && + messageWithDetails.reasoning_content.length > 0 + ? messageWithDetails.reasoning_content + : undefined) ?? extractedReasoning + if (outgoingReasoningContent) { + baseMessage.reasoning_content = outgoingReasoningContent + } + // Add tool_calls after reasoning_details // Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty if (tool_calls.length > 0) { diff --git a/src/api/transform/r1-format.ts b/src/api/transform/r1-format.ts index 6e8b782047..e59b282be2 100644 --- a/src/api/transform/r1-format.ts +++ b/src/api/transform/r1-format.ts @@ -61,10 +61,12 @@ export function convertToR1Format( if (part.type === "text") { textParts.push(part.text) } else if (part.type === "image") { - imageParts.push({ - type: "image_url", - image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` }, - }) + if (part.source.type === "base64") { + imageParts.push({ + type: "image_url", + image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` }, + }) + } } else if (part.type === "tool_result") { // Convert tool_result to OpenAI tool message format let content: string diff --git a/src/api/transform/responses-api-input.ts b/src/api/transform/responses-api-input.ts index a766dfef6e..9ef31be280 100644 --- a/src/api/transform/responses-api-input.ts +++ b/src/api/transform/responses-api-input.ts @@ -73,11 +73,13 @@ export function convertToResponsesApiInput(messages: Anthropic.Messages.MessageP contentParts.push({ type: "input_text", text: part.text }) break case "image": - contentParts.push({ - type: "input_image", - detail: "auto", - image_url: `data:${part.source.media_type};base64,${part.source.data}`, - }) + if (part.source.type === "base64") { + contentParts.push({ + type: "input_image", + detail: "auto", + image_url: `data:${part.source.media_type};base64,${part.source.data}`, + }) + } break case "tool_result": { // Flush any pending user content before the tool result diff --git a/src/api/transform/vscode-lm-format.ts b/src/api/transform/vscode-lm-format.ts index 388197c2c2..94716b209d 100644 --- a/src/api/transform/vscode-lm-format.ts +++ b/src/api/transform/vscode-lm-format.ts @@ -72,11 +72,19 @@ export function convertToVsCodeLmMessages( ? [new vscode.LanguageModelTextPart(toolMessage.content)] : (toolMessage.content?.map((part) => { if (part.type === "image") { + if (part.source.type === "base64") { + return new vscode.LanguageModelTextPart( + `[Image (base64): ${part.source.media_type} not supported by VSCode LM API]`, + ) + } return new vscode.LanguageModelTextPart( - `[Image (${part.source?.type || "Unknown source-type"}): ${part.source?.media_type || "unknown media-type"} not supported by VSCode LM API]`, + `[Image (${part.source.type}): not supported by VSCode LM API]`, ) } - return new vscode.LanguageModelTextPart(part.text) + if (part.type === "text") { + return new vscode.LanguageModelTextPart(part.text) + } + return new vscode.LanguageModelTextPart("") }) ?? [new vscode.LanguageModelTextPart("")]) return new vscode.LanguageModelToolResultPart(toolMessage.tool_use_id, toolContentParts) @@ -85,8 +93,13 @@ export function convertToVsCodeLmMessages( // Convert non-tool messages to TextParts after tool messages ...nonToolMessages.map((part) => { if (part.type === "image") { + if (part.source.type === "base64") { + return new vscode.LanguageModelTextPart( + `[Image (base64): ${part.source.media_type} not supported by VSCode LM API]`, + ) + } return new vscode.LanguageModelTextPart( - `[Image (${part.source?.type || "Unknown source-type"}): ${part.source?.media_type || "unknown media-type"} not supported by VSCode LM API]`, + `[Image (${part.source.type}): not supported by VSCode LM API]`, ) } return new vscode.LanguageModelTextPart(part.text) diff --git a/src/api/transform/zai-format.ts b/src/api/transform/zai-format.ts index 79b2e88aeb..0bc8353d8e 100644 --- a/src/api/transform/zai-format.ts +++ b/src/api/transform/zai-format.ts @@ -56,10 +56,12 @@ export function convertToZAiFormat( if (part.type === "text") { textParts.push(part.text) } else if (part.type === "image") { - imageParts.push({ - type: "image_url", - image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` }, - }) + if (part.source.type === "base64") { + imageParts.push({ + type: "image_url", + image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` }, + }) + } } else if (part.type === "tool_result") { // Convert tool_result to OpenAI tool message format let content: string diff --git a/src/assets/marketplace/mcps.yml b/src/assets/marketplace/mcps.yml index 47709f6bdf..47b6fd57d5 100644 --- a/src/assets/marketplace/mcps.yml +++ b/src/assets/marketplace/mcps.yml @@ -1214,7 +1214,7 @@ items: parameters: - name: GitHub Personal Access Token key: GITHUB_PERSONAL_ACCESS_TOKEN - placeholder: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (classic PAT from https://github.com/settings/tokens/new) + placeholder: "YOUR_GITHUB_PAT (classic PAT from https://github.com/settings/tokens/new)" - id: gitlab name: GitLab description: Enables comprehensive GitLab project management including file operations, issue tracking, merge requests, @@ -2634,7 +2634,7 @@ items: parameters: - name: Slack Bot Token key: SLACK_BOT_TOKEN - placeholder: xoxb-your-bot-token + placeholder: YOUR_SLACK_BOT_TOKEN - name: Slack Team ID key: SLACK_TEAM_ID placeholder: T01234567 diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index bda7c71eb8..9639ae1baa 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -819,6 +819,12 @@ export class NativeToolCallParser { break case "ask_followup_question": + // Require a question and a present follow_up. When follow_up is + // present-but-not-an-array (e.g. an object/string/number produced by + // incremental JSON parsing), we still construct nativeArgs and forward + // the raw value so the tool can emit a precise "must be an array" error + // instead of the generic parser failure, which would surface as a + // misleading "Missing value for required parameter 'follow_up'" error. if (args.question !== undefined && args.follow_up !== undefined) { nativeArgs = { question: args.question, diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index cc675dd948..f71b5cc1bd 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -959,8 +959,7 @@ export async function presentAssistantMessage(cline: Task) { if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) { // There are already more content blocks to stream, so we'll call // this function ourselves. - presentAssistantMessage(cline) - return + return presentAssistantMessage(cline) } else { // CRITICAL FIX: If we're out of bounds and the stream is complete, set userMessageContentReady // This handles the case where assistantMessageContent is empty or becomes empty after processing @@ -972,7 +971,7 @@ export async function presentAssistantMessage(cline: Task) { // Block is partial, but the read stream may have finished. if (cline.presentAssistantMessageHasPendingUpdates) { - presentAssistantMessage(cline) + return presentAssistantMessage(cline) } } diff --git a/src/core/context-management/__tests__/context-management.spec.ts b/src/core/context-management/__tests__/context-management.spec.ts index 9950ec536b..ce7db70fa7 100644 --- a/src/core/context-management/__tests__/context-management.spec.ts +++ b/src/core/context-management/__tests__/context-management.spec.ts @@ -810,9 +810,9 @@ describe("Context Management", () => { const summarizeSpy = vi.spyOn(condenseModule, "summarizeConversation") const modelInfo = createModelInfo(100000, 30000) - // Set tokens to be below both the allowedTokens threshold and the percentage threshold + // Usage measured against available input space stays below the threshold. const contextWindow = modelInfo.contextWindow - const totalTokens = 40000 // 40% of context window + const totalTokens = 30000 const messagesWithSmallContent = [ ...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }, @@ -825,7 +825,7 @@ describe("Context Management", () => { maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, autoCondenseContext: true, - autoCondenseContextPercent: 50, // Set threshold to 50% - our tokens are at 40% + autoCondenseContextPercent: 50, // Set threshold to 50% - usage is ~43% of available input systemPrompt: "System prompt", taskId, profileThresholds: {}, @@ -1507,19 +1507,36 @@ describe("Context Management", () => { }) it("should return false when context percent is below threshold", () => { + // Opt-in available-input denominator: usage stays below threshold. const result = willManageContext({ - totalTokens: 40000, - contextWindow: 100000, // 40% of context window + totalTokens: 30000, + contextWindow: 100000, maxTokens: 30000, autoCondenseContext: true, - autoCondenseContextPercent: 50, // 50% threshold + autoCondenseContextPercent: 50, // 50% threshold; usage is ~43% of available input profileThresholds: {}, currentProfileId: "default", lastMessageTokens: 0, + useAvailableInputForContextPercent: true, }) expect(result).toBe(false) }) + it("should treat a negative maxTokens (vscode-lm reports -1) as the default reserve, not -1", () => { + // A -1 reserve must be treated as unknown (default reserve), not kept as -1. + const result = willManageContext({ + totalTokens: 85000, + contextWindow: 100000, + maxTokens: -1, + autoCondenseContext: false, + autoCondenseContextPercent: 50, + profileThresholds: {}, + currentProfileId: "default", + lastMessageTokens: 0, + }) + expect(result).toBe(true) + }) + it("should return true when tokens exceed allowedTokens even if autoCondenseContext is false", () => { // allowedTokens = contextWindow * (1 - 0.1) - reservedTokens = 100000 * 0.9 - 30000 = 60000 const result = willManageContext({ @@ -1581,10 +1598,9 @@ describe("Context Management", () => { }) it("should include lastMessageTokens in the calculation", () => { - // Without lastMessageTokens: 49000 tokens = 49% - // With lastMessageTokens: 49000 + 2000 = 51000 tokens = 51% + // Adding lastMessageTokens pushes usage over the threshold (opt-in available-input denominator). const resultWithoutLastMessage = willManageContext({ - totalTokens: 49000, + totalTokens: 34000, contextWindow: 100000, maxTokens: 30000, autoCondenseContext: true, @@ -1592,18 +1608,20 @@ describe("Context Management", () => { profileThresholds: {}, currentProfileId: "default", lastMessageTokens: 0, + useAvailableInputForContextPercent: true, }) expect(resultWithoutLastMessage).toBe(false) const resultWithLastMessage = willManageContext({ - totalTokens: 49000, + totalTokens: 34000, contextWindow: 100000, maxTokens: 30000, autoCondenseContext: true, autoCondenseContextPercent: 50, // 50% threshold profileThresholds: {}, currentProfileId: "default", - lastMessageTokens: 2000, // Pushes total to 51% + lastMessageTokens: 2000, // Pushes usage over 50% of available input + useAvailableInputForContextPercent: true, }) expect(resultWithLastMessage).toBe(true) }) @@ -1701,4 +1719,329 @@ describe("Context Management", () => { expect(result.newContextTokensAfterTruncation).toBeGreaterThan(0) }) }) + + /** + * Regression: with the opt-in flag on, the gate measures usage against available input space + * (contextWindow - reserved output) so it stays in lockstep with the UI gauge and fires for vscode-lm. + */ + describe("contextPercent uses available input space (opt-in, regression)", () => { + const createModelInfo = (contextWindow: number, maxTokens?: number): ModelInfo => ({ + contextWindow, + supportsPromptCache: true, + maxTokens, + }) + + const messages: ApiMessage[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "Second message" }, + { role: "user", content: "Third message" }, + { role: "assistant", content: "Fourth message" }, + { role: "user", content: "Fifth message" }, + ] + + it("willManageContext measures the percentage against available input, not the full window", () => { + // Dividing by available input clears the threshold; the full window would keep the gate closed. + const result = willManageContext({ + totalTokens: 100000, + contextWindow: 200000, + maxTokens: 64000, + autoCondenseContext: true, + autoCondenseContextPercent: 70, + profileThresholds: {}, + currentProfileId: "default", + lastMessageTokens: 0, + useAvailableInputForContextPercent: true, + }) + expect(result).toBe(true) + }) + + it("willManageContext stays below threshold when usage is under available input", () => { + // Usage under available input stays below threshold. + const result = willManageContext({ + totalTokens: 90000, + contextWindow: 200000, + maxTokens: 64000, + autoCondenseContext: true, + autoCondenseContextPercent: 70, + profileThresholds: {}, + currentProfileId: "default", + lastMessageTokens: 0, + useAvailableInputForContextPercent: true, + }) + expect(result).toBe(false) + }) + + it("willManageContext treats a negative (unlimited) reserve as zero reserve for the percentage", () => { + // A strongly-negative reserve must clamp to zero, not be subtracted into a larger denominator: + // guard on → availableInput 200000 → 75% ≥ 70 (fires); without the > 0 guard → availableInput + // 400000 → 37.5% < 70 (would not fire). A small -1 can't tell the paths apart; -200000 can. + const result = willManageContext({ + totalTokens: 150000, + contextWindow: 200000, + maxTokens: -200000, + autoCondenseContext: true, + autoCondenseContextPercent: 70, + profileThresholds: {}, + currentProfileId: "default", + lastMessageTokens: 0, + useAvailableInputForContextPercent: true, + }) + expect(result).toBe(true) + }) + + it("willManageContext falls back to 100% when the reserve is >= the window (availableInput <= 0)", () => { + // Non-positive available input must short-circuit contextPercent to 100 rather than divide. + const result = willManageContext({ + totalTokens: 1, + contextWindow: 50000, + maxTokens: 60000, // reserve > window → availableInput = -10000 + autoCondenseContext: true, + autoCondenseContextPercent: 80, + profileThresholds: {}, + currentProfileId: "default", + lastMessageTokens: 0, + useAvailableInputForContextPercent: true, + }) + expect(result).toBe(true) + }) + + it("willManageContext falls back to 100% when the reserve exactly equals the window (availableInput === 0)", () => { + // Boundary: reserve === window → available input 0, still the non-positive guard. + const result = willManageContext({ + totalTokens: 1, + contextWindow: 50000, + maxTokens: 50000, + autoCondenseContext: true, + autoCondenseContextPercent: 90, + profileThresholds: {}, + currentProfileId: "default", + lastMessageTokens: 0, + useAvailableInputForContextPercent: true, + }) + expect(result).toBe(true) + }) + + it("manageContext summarizes via the 100% fallback when the reserve >= the window (availableInput <= 0)", async () => { + // reserve >= window forces contextPercent to 100, so summarization triggers. + const mockSummary = "Reserve-exceeds-window summary" + const mockSummarizeResponse: condenseModule.SummarizeResponse = { + messages: [ + { role: "user", content: "First message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, + ], + summary: mockSummary, + cost: 0.05, + newContextTokens: 100, + } + const summarizeSpy = vi + .spyOn(condenseModule, "summarizeConversation") + .mockResolvedValue(mockSummarizeResponse) + + const messagesWithSmallContent = [ + ...messages.slice(0, -1), + { ...messages[messages.length - 1], content: "" }, + ] + + const result = await manageContext({ + messages: messagesWithSmallContent, + totalTokens: 1, + contextWindow: 50000, + maxTokens: 60000, + apiHandler: mockApiHandler, + autoCondenseContext: true, + autoCondenseContextPercent: 80, + systemPrompt: "System prompt", + taskId, + profileThresholds: {}, + currentProfileId: "default", + useAvailableInputForContextPercent: true, + }) + + expect(summarizeSpy).toHaveBeenCalled() + expect(result).toMatchObject({ + summary: mockSummary, + prevContextTokens: 1, + }) + + summarizeSpy.mockRestore() + }) + + it("manageContext summarizes based on available input space, end-to-end", async () => { + const mockSummary = "Available-input summary" + const mockSummarizeResponse: condenseModule.SummarizeResponse = { + messages: [ + { role: "user", content: "First message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, + ], + summary: mockSummary, + cost: 0.05, + newContextTokens: 100, + } + const summarizeSpy = vi + .spyOn(condenseModule, "summarizeConversation") + .mockResolvedValue(mockSummarizeResponse) + + const modelInfo = createModelInfo(200000, 64000) + // Clears the threshold against available input but not the raw window; end-to-end must summarize. + const totalTokens = 100000 + const messagesWithSmallContent = [ + ...messages.slice(0, -1), + { ...messages[messages.length - 1], content: "" }, + ] + + const result = await manageContext({ + messages: messagesWithSmallContent, + totalTokens, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + apiHandler: mockApiHandler, + autoCondenseContext: true, + autoCondenseContextPercent: 70, + systemPrompt: "System prompt", + taskId, + profileThresholds: {}, + currentProfileId: "default", + useAvailableInputForContextPercent: true, + }) + + expect(summarizeSpy).toHaveBeenCalled() + expect(result).toMatchObject({ + summary: mockSummary, + prevContextTokens: totalTokens, + }) + + summarizeSpy.mockRestore() + }) + }) + + /** + * Scoping: the available-input denominator is opt-in; default divides by the full window. + * The maxTokens: -1 reserve guard stays global on the default path. + */ + describe("contextPercent denominator is opt-in (default = full window)", () => { + const messages: ApiMessage[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "Second message" }, + { role: "user", content: "Third message" }, + { role: "assistant", content: "Fourth message" }, + { role: "user", content: "Fifth message" }, + ] + + it("willManageContext divides by the full window when the flag is omitted (default)", () => { + // Default divides by the full window, staying below threshold where the opt-in math would fire. + const result = willManageContext({ + totalTokens: 100000, + contextWindow: 200000, + maxTokens: 64000, + autoCondenseContext: true, + autoCondenseContextPercent: 70, + profileThresholds: {}, + currentProfileId: "default", + lastMessageTokens: 0, + }) + expect(result).toBe(false) + }) + + it("willManageContext fires on the same inputs when the opt-in flag is true", () => { + // Same inputs, flag on: dividing by available input clears the threshold. + const result = willManageContext({ + totalTokens: 100000, + contextWindow: 200000, + maxTokens: 64000, + autoCondenseContext: true, + autoCondenseContextPercent: 70, + profileThresholds: {}, + currentProfileId: "default", + lastMessageTokens: 0, + useAvailableInputForContextPercent: true, + }) + expect(result).toBe(true) + }) + + it("keeps the maxTokens:-1 reserve guard on the default (full-window) path", () => { + // The -1 reserve guard is global, independent of the percent denominator. + const result = willManageContext({ + totalTokens: 85000, + contextWindow: 100000, + maxTokens: -1, + autoCondenseContext: false, + autoCondenseContextPercent: 50, + profileThresholds: {}, + currentProfileId: "default", + lastMessageTokens: 0, + }) + expect(result).toBe(true) + }) + + it("manageContext treats an unlimited (-1) reserve as default reserve on the truncation path", async () => { + // manageContext duplicates willManageContext's reserve guard, so both must live or die together. + // maxTokens -1 → reserve ANTHROPIC_DEFAULT_MAX_TOKENS (8192): allowedTokens = 100000*0.9 − 8192 = + // 81808; empty last message keeps prevContextTokens = 81809 > 81808 → truncation runs. A mutant + // that left -1 unclamped would give allowedTokens 90001, and 81809 > 90001 is false → no truncation. + const summarizeSpy = vi.spyOn(condenseModule, "summarizeConversation") + + const messagesWithSmallContent = [ + ...messages.slice(0, -1), + { ...messages[messages.length - 1], content: "" }, + ] + + const result = await manageContext({ + messages: messagesWithSmallContent, + totalTokens: 81809, + contextWindow: 100000, + maxTokens: -1, + apiHandler: mockApiHandler, + autoCondenseContext: false, + autoCondenseContextPercent: 70, + systemPrompt: "System prompt", + taskId, + profileThresholds: {}, + currentProfileId: "default", + }) + + expect(summarizeSpy).not.toHaveBeenCalled() + expect(result.truncationId).toBeDefined() + expect(result.messagesRemoved).toBe(2) + expect(result.summary).toBe("") + expect(result.prevContextTokens).toBe(81809) + + summarizeSpy.mockRestore() + }) + + it("manageContext does NOT summarize on the default path where the opt-in math would have", async () => { + // Default full-window math leaves this case below threshold; the opt-in flag would summarize it. + const summarizeSpy = vi.spyOn(condenseModule, "summarizeConversation") + + const messagesWithSmallContent = [ + ...messages.slice(0, -1), + { ...messages[messages.length - 1], content: "" }, + ] + + const result = await manageContext({ + messages: messagesWithSmallContent, + totalTokens: 100000, + contextWindow: 200000, + maxTokens: 64000, + apiHandler: mockApiHandler, + autoCondenseContext: true, + autoCondenseContextPercent: 70, + systemPrompt: "System prompt", + taskId, + profileThresholds: {}, + currentProfileId: "default", + }) + + expect(summarizeSpy).not.toHaveBeenCalled() + expect(result).toEqual({ + messages: messagesWithSmallContent, + summary: "", + cost: 0, + prevContextTokens: 100000, + }) + + summarizeSpy.mockRestore() + }) + }) }) diff --git a/src/core/context-management/index.ts b/src/core/context-management/index.ts index 243d7bd797..82cb4fdf2e 100644 --- a/src/core/context-management/index.ts +++ b/src/core/context-management/index.ts @@ -40,6 +40,32 @@ export async function estimateTokenCount( return apiHandler.countTokens(content) } +/** + * Computes the percentage of the context budget consumed by the prior context. + * + * Default: divide by the full context window. Opt-in (vscode-lm) divides by available input + * (window minus reserved output); an unknown/unlimited reserve (maxTokens -1) falls back to the + * full window. Shared by `willManageContext` and `manageContext` so the two stay in lockstep. + */ +function computeContextPercent({ + prevContextTokens, + contextWindow, + maxTokens, + useAvailableInputForContextPercent, +}: { + prevContextTokens: number + contextWindow: number + maxTokens?: number | null + useAvailableInputForContextPercent?: boolean +}): number { + if (!useAvailableInputForContextPercent) { + return (100 * prevContextTokens) / contextWindow + } + const reservedForOutput = maxTokens && maxTokens > 0 ? maxTokens : 0 + const availableInputTokens = contextWindow - reservedForOutput + return availableInputTokens > 0 ? (100 * prevContextTokens) / availableInputTokens : 100 +} + /** * Result of truncation operation, includes the truncation ID for UI events. */ @@ -147,6 +173,11 @@ export type WillManageContextOptions = { profileThresholds: Record currentProfileId: string lastMessageTokens: number + /** + * Opt-in (vscode-lm): measure the condense percentage against available input space + * (contextWindow - reserved output) instead of the full window. Others leave it undefined. + */ + useAvailableInputForContextPercent?: boolean } /** @@ -167,16 +198,19 @@ export function willManageContext({ profileThresholds, currentProfileId, lastMessageTokens, + useAvailableInputForContextPercent, }: WillManageContextOptions): boolean { if (!autoCondenseContext) { // When auto-condense is disabled, only truncation can occur - const reservedTokens = maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS + // vscode-lm reports maxTokens: -1 (unlimited); a negative reserve must not distort the window math. + const reservedTokens = maxTokens && maxTokens > 0 ? maxTokens : ANTHROPIC_DEFAULT_MAX_TOKENS const prevContextTokens = totalTokens + lastMessageTokens const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens return prevContextTokens > allowedTokens } - const reservedTokens = maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS + // vscode-lm reports maxTokens: -1 (unlimited); a negative reserve must not distort the window math. + const reservedTokens = maxTokens && maxTokens > 0 ? maxTokens : ANTHROPIC_DEFAULT_MAX_TOKENS const prevContextTokens = totalTokens + lastMessageTokens const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens @@ -192,7 +226,12 @@ export function willManageContext({ // Invalid values fall back to global setting (effectiveThreshold already set) } - const contextPercent = (100 * prevContextTokens) / contextWindow + const contextPercent = computeContextPercent({ + prevContextTokens, + contextWindow, + maxTokens, + useAvailableInputForContextPercent, + }) return contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens } @@ -229,6 +268,11 @@ export type ContextManagementOptions = { cwd?: string /** Optional controller for file access validation */ rooIgnoreController?: RooIgnoreController + /** + * Opt-in (vscode-lm): measure the condense percentage against available input space + * (contextWindow - reserved output) instead of the full window. Others leave it undefined. + */ + useAvailableInputForContextPercent?: boolean } export type ContextManagementResult = SummarizeResponse & { @@ -262,12 +306,14 @@ export async function manageContext({ filesReadByRoo, cwd, rooIgnoreController, + useAvailableInputForContextPercent, }: ContextManagementOptions): Promise { let error: string | undefined let errorDetails: string | undefined let cost = 0 // Calculate the maximum tokens reserved for response - const reservedTokens = maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS + // vscode-lm reports maxTokens: -1 (unlimited); a negative reserve must not distort the window math. + const reservedTokens = maxTokens && maxTokens > 0 ? maxTokens : ANTHROPIC_DEFAULT_MAX_TOKENS // Estimate tokens for the last message (which is always a user message) const lastMessage = messages[messages.length - 1] @@ -304,7 +350,12 @@ export async function manageContext({ // If no specific threshold is found for the profile, fall back to global setting if (autoCondenseContext) { - const contextPercent = (100 * prevContextTokens) / contextWindow + const contextPercent = computeContextPercent({ + prevContextTokens, + contextWindow, + maxTokens, + useAvailableInputForContextPercent, + }) if (contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens) { // Attempt to intelligently condense the context const result = await summarizeConversation({ diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts index b58e331f8f..297e64fa83 100644 --- a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts +++ b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts @@ -878,6 +878,119 @@ function processData(data) { expect(result.success).toBe(false) }) + it("should include line range debug info when search fails with start_line marker", async () => { + const originalContent = "line one\nline two" + const diffContent = `test.ts +<<<<<<< SEARCH +:start_line:999 +------- +non-existent content that cannot be found in the file +======= +replacement content here +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(false) + const error = + !result.success && result.failParts?.[0] + ? "error" in result.failParts[0] + ? result.failParts[0].error + : "" + : "" + expect(error).toContain("No sufficiently similar match found") + expect(error).toContain("at line: 999") + expect(error).toContain("Search Range: starting at line 999") + expect(error).toContain("Best Match Found:\n(no match)") + expect(error).toContain("Levenshtein Distance: N/A") + expect(error).toContain("Best Match Length: 0 characters") + }) + + it("should include scoped original content when search fails with start_line that has a low-score match", async () => { + const originalContent = "function existing() {\n return 42;\n}\n" + const diffContent = `test.ts +<<<<<<< SEARCH +:start_line:1 +------- +function different() { + return 99; +} +======= +function newVersion() { + return 99; +} +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(false) + const error = + !result.success && result.failParts?.[0] + ? "error" in result.failParts[0] + ? result.failParts[0].error + : "" + : "" + expect(error).toContain("No sufficiently similar match found") + expect(error).toContain("at line: 1") + expect(error).toContain("Search Range: starting at line 1") + expect(error).toContain("Best Match Found:") + expect(error).toContain("Original Content:\n1 | function existing()") + }) + + it("should include best-match debug info when unscoped search is below threshold", async () => { + const strictStrategy = new MultiSearchReplaceDiffStrategy(1, 5) + const originalContent = "function processUsers(data) {\n return data.map(user => user.name);\n}\n" + const diffContent = `test.ts +<<<<<<< SEARCH +function processUsers(data) { + return data.map(user => user.username); +} +======= +function processUsers(data) { + return data.map(user => user.displayName); +} +>>>>>>> REPLACE` + + const result = await strictStrategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(false) + const error = + !result.success && result.failParts?.[0] + ? "error" in result.failParts[0] + ? result.failParts[0].error + : "" + : "" + expect(error).toContain("Search Range: start to end") + expect(error).toContain("Levenshtein Distance:") + expect(error).toContain("characters") + expect(error).toContain("Best Match Length:") + expect(error).toContain("Best Match Found:\n1 | function processUsers(data)") + }) + + it("should include zero-match info when unscoped search finds no similarity at all", async () => { + const strictStrategy = new MultiSearchReplaceDiffStrategy(1, 0) + const originalContent = "xxxxxx\nyyyyyy\nzzzzzz" + const diffContent = `test.ts +<<<<<<< SEARCH +!!!!!! +======= +aaaaaa +>>>>>>> REPLACE` + + const result = await strictStrategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(false) + const error = + !result.success && result.failParts?.[0] + ? "error" in result.failParts[0] + ? result.failParts[0].error + : "" + : "" + expect(error).toContain("No sufficiently similar match found") + expect(error).toContain("Search Range: start to end") + expect(error).toContain("Best Match Found:\n(no match)") + expect(error).toContain("Levenshtein Distance: N/A") + expect(error).toContain("Best Match Length: 0 characters") + expect(error).toContain("Original Content:") + expect(error).toContain("1 | xxxxxx") + }) + it("should match content with extra whitespace", async () => { const originalContent = "function sum(a, b) {\n return a + b;\n}" const diffContent = `test.ts @@ -1374,4 +1487,109 @@ function sum(a, b) { } }) }) + + describe("fuzzyThreshold and diagnostics", () => { + const originalContent = + "function calculateTotal(price: number, tax: number) {\n\tconst subtotal = price;\n\treturn subtotal + tax;\n}\n" + + it("should succeed with near-miss match (e.g. minor whitespace diff) when threshold is 0.90", async () => { + const strategy = new MultiSearchReplaceDiffStrategy(0.9) + // Near-miss search block with slightly different formatting/whitespace (e.g., spaces instead of tab, missing semicolon) + const diff = + "<<<<<<< SEARCH\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price\n" + + " return subtotal + tax;\n" + + "}\n" + + "=======\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price;\n" + + " return (subtotal + tax) * 1.1;\n" + + "}\n" + + ">>>>>>> REPLACE" + + const result = await strategy.applyDiff(originalContent, diff) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toContain("(subtotal + tax) * 1.1") + } + }) + + it("should fail with near-miss match when threshold is set to 1.0", async () => { + const strategy = new MultiSearchReplaceDiffStrategy(1.0) + const diff = + "<<<<<<< SEARCH\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price\n" + + " return subtotal + tax;\n" + + "}\n" + + "=======\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price;\n" + + " return (subtotal + tax) * 1.1;\n" + + "}\n" + + ">>>>>>> REPLACE" + + const result = await strategy.applyDiff(originalContent, diff) + expect(result.success).toBe(false) + expect(result.failParts).toBeDefined() + expect(result.failParts!.length).toBeGreaterThan(0) + const failedPart = result.failParts![0] + expect(failedPart).toHaveProperty("error") + expect((failedPart as { error: string }).error).toContain("No sufficiently similar match found") + }) + + it("should output enhanced error diagnostics (Levenshtein distance, character counts) when a match fails", async () => { + const strategy = new MultiSearchReplaceDiffStrategy(0.95) + const diff = + "<<<<<<< SEARCH\n" + + "function calculateGrandTotal(initialPrice: number, standardTax: number) {\n" + + " const totalVal = initialPrice\n" + + " return totalVal + standardTax;\n" + + "}\n" + + "=======\n" + + "function calculateTotal(price: number, tax: number) {\n" + + " const subtotal = price;\n" + + " return (subtotal + tax) * 1.1;\n" + + "}\n" + + ">>>>>>> REPLACE" + + const result = await strategy.applyDiff(originalContent, diff) + expect(result.success).toBe(false) + expect(result.failParts).toBeDefined() + expect(result.failParts!.length).toBeGreaterThan(0) + const failedPart = result.failParts![0] + expect(failedPart).toHaveProperty("error") + const errorMsg = (failedPart as { error: string }).error + expect(errorMsg).toContain("Debug Info:") + expect(errorMsg).toContain("Similarity Score:") + expect(errorMsg).toContain("Required Threshold: 95%") + expect(errorMsg).toContain("Levenshtein Distance:") + expect(errorMsg).toContain("Search Length:") + expect(errorMsg).toContain("Best Match Length:") + }) + it("should report no-match diagnostics when search content is completely different and no :start_line: is given", async () => { + const strategy = new MultiSearchReplaceDiffStrategy(0.9) + const diff = + "<<<<<<< SEARCH\n" + + "§§§§§§§§§§§§\n" + + "§§§§§§§§§§§§\n" + + "=======\n" + + "¤¤¤¤¤¤¤¤¤¤¤¤\n" + + "¤¤¤¤¤¤¤¤¤¤¤¤\n" + + ">>>>>>> REPLACE" + + const result = await strategy.applyDiff(originalContent, diff) + expect(result.success).toBe(false) + expect(result.failParts).toBeDefined() + expect(result.failParts!.length).toBeGreaterThan(0) + const failedPart = result.failParts![0] + expect(failedPart).toHaveProperty("error") + const errorMsg = (failedPart as { error: string }).error + expect(errorMsg).toContain("No sufficiently similar match found") + expect(errorMsg).toContain("Best Match Found:") + expect(errorMsg).toContain("Levenshtein Distance:") + expect(errorMsg).toContain("Search Range: start to end") + }) + }) }) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index c74faedbfe..a8ba0215d3 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -1,6 +1,6 @@ import { distance } from "fastest-levenshtein" -import { ToolProgressStatus } from "@roo-code/types" +import { ToolProgressStatus, DEFAULT_DIFF_FUZZY_THRESHOLD } from "@roo-code/types" import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text" import { ToolUse, DiffStrategy, DiffResult } from "../../../shared/tools" @@ -82,9 +82,12 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { constructor(fuzzyThreshold?: number, bufferLines?: number) { // Use provided threshold or default to exact matching (1.0) - // Note: fuzzyThreshold is inverted in UI (0% = 1.0, 10% = 0.9) - // so we use it directly here - this.fuzzyThreshold = fuzzyThreshold ?? 1.0 + // A value of 0.9 means 90% similarity is required for a match, + // but the default remains 1.0 (exact match). Users can opt in + // to relaxed matching via diffFuzzyThreshold in settings. + // Clamp the threshold to [0.5, 1.0] as a defence-in-depth guard. + const thresholdVal = fuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD + this.fuzzyThreshold = Math.max(0.5, Math.min(1.0, thresholdVal)) this.bufferLines = bufferLines ?? BUFFER_LINES } @@ -537,7 +540,7 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { } else { // No match found with either method const originalContentSection = - startLine !== undefined && endLine !== undefined + startLine && endLine ? `\n\nOriginal Content:\n${addLineNumbers( resultLines .slice( @@ -547,7 +550,20 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { .join("\n"), Math.max(1, startLine - this.bufferLines), )}` - : `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}` + : `\n\nOriginal Content:\n${addLineNumbers( + resultLines + .slice( + Math.max(0, (matchIndex >= 0 ? matchIndex : 0) - this.bufferLines), + Math.min( + resultLines.length, + (matchIndex >= 0 ? matchIndex : 0) + + searchLines.length + + this.bufferLines, + ), + ) + .join("\n"), + Math.max(1, (matchIndex >= 0 ? matchIndex : 0) - this.bufferLines + 1), + )}` const bestMatchSection = bestMatchContent ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}` @@ -555,9 +571,13 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { const lineRange = startLine ? ` at line: ${startLine}` : "" + const levenDist = bestMatchContent + ? distance(normalizeString(searchChunk), normalizeString(bestMatchContent)) + : -1 + diffResults.push({ success: false, - error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine ? `starting at line ${startLine}` : "start to end"}\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`, + error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine ? `starting at line ${startLine}` : "start to end"}\n- Levenshtein Distance: ${levenDist >= 0 ? `${levenDist} characters` : "N/A"}\n- Search Length: ${searchChunk.length} characters\n- Best Match Length: ${bestMatchContent ? bestMatchContent.length : 0} characters\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`, }) continue } diff --git a/src/core/prompts/tools/native-tools/apply_diff.ts b/src/core/prompts/tools/native-tools/apply_diff.ts index 3938e4886a..cdbd99df33 100644 --- a/src/core/prompts/tools/native-tools/apply_diff.ts +++ b/src/core/prompts/tools/native-tools/apply_diff.ts @@ -2,14 +2,19 @@ import type OpenAI from "openai" const APPLY_DIFF_DESCRIPTION = `Apply precise, targeted modifications to an existing file using one or more search/replace blocks. This tool is for surgical edits only; the 'SEARCH' block must exactly match the existing content, including whitespace and indentation. To make multiple targeted changes, provide multiple SEARCH/REPLACE blocks in the 'diff' parameter. Use the 'read_file' tool first if you are not confident in the exact content to search for.` -const DIFF_PARAMETER_DESCRIPTION = `A string containing one or more search/replace blocks defining the changes. The ':start_line:' is required and indicates the starting line number of the original content. You must not add a start line for the replacement content. Each block must follow this format: +const DIFF_PARAMETER_DESCRIPTION = `A string containing one or more search/replace blocks defining the changes. The ':start_line:' is strongly recommended and indicates the starting line number of the original content. You must not add a start line for the replacement content. Each block must follow this format: <<<<<<< SEARCH :start_line:[line_number] ------- [exact content to find] ======= [new content to replace with] ->>>>>>> REPLACE` +>>>>>>> REPLACE + +CRITICAL: +- The ':start_line:[line_number]' header is strongly recommended for accurate matching. When provided, it must follow the exact syntax ':start_line:[integer]' (for example: ':start_line:220'). Do not write headers with shorthand forms like ':220' or variations like ':start_line=220'. +- Copy the exact lines from the source file for a 100% string match including all whitespace, indentation, and newlines. +- Ensure the separator '-------' is on its own line immediately following ':start_line:[line_number]' with a newline.` export const apply_diff = { type: "function", diff --git a/src/core/task-persistence/TaskHistoryStore.ts b/src/core/task-persistence/TaskHistoryStore.ts index 4157d8b9fb..219918b029 100644 --- a/src/core/task-persistence/TaskHistoryStore.ts +++ b/src/core/task-persistence/TaskHistoryStore.ts @@ -8,6 +8,29 @@ import { GlobalFileNames } from "../../shared/globalFileNames" import { safeWriteJson } from "../../utils/safeWriteJson" import { getStorageBasePath } from "../../utils/storage" +/** Valid status values for a task's HistoryItem. */ +export type HistoryItemStatus = NonNullable + +const VALID_TRANSITIONS: Record = { + active: ["delegated", "completed", "interrupted"], + delegated: ["active"], + interrupted: ["completed"], + completed: [], +} + +/** + * Asserts that a task status transition is valid, throwing if not. + * + * @throws {Error} When the transition is not allowed by the state machine. + */ +export function assertValidTransition(from: HistoryItemStatus | undefined, to: HistoryItemStatus): void { + const fromStatus: HistoryItemStatus = from ?? "active" + const validTargets = VALID_TRANSITIONS[fromStatus] + if (!validTargets.includes(to)) { + throw new Error(`Invalid task status transition: ${fromStatus} → ${to}`) + } +} + /** * Index file format for fast startup reads. */ @@ -88,10 +111,13 @@ export class TaskHistoryStore { // 2. Reconcile cache against actual task directories on disk await this.reconcile() - // 3. Start fs.watch for cross-instance reactivity + // 3. Repair delegation inconsistencies left by a previous crash + await this.reconcileDelegationState() + + // 4. Start fs.watch for cross-instance reactivity this.startWatcher() - // 4. Start periodic reconciliation as a defensive fallback + // 5. Start periodic reconciliation as a defensive fallback this.startPeriodicReconciliation() } finally { // Mark initialization as complete so callers awaiting `initialized` can proceed @@ -158,30 +184,53 @@ export class TaskHistoryStore { * updates the in-memory Map, and schedules a debounced index write. */ async upsert(item: HistoryItem): Promise { - return this.withLock(async () => { - const existing = this.cache.get(item.id) + return this.withLock(() => this.upsertCore(item)) + } - // Merge: preserve existing metadata unless explicitly overwritten - const merged = existing ? { ...existing, ...item } : item + /** + * Core upsert logic — must only be called from within `withLock`. + * + * Enforces state-machine transition rules when `item.status` changes. + * Pass `skipTransitionCheck: true` only for administrative repairs (reconciliation, + * migration) that need to write corrected state outside the normal task lifecycle. + */ + private async upsertCore( + item: HistoryItem, + options: { skipTransitionCheck?: boolean } = {}, + ): Promise { + const existing = this.cache.get(item.id) + + // Enforce transition validity at the write boundary so that any caller + // (including fire-and-forget saves) cannot silently stomp a terminal status. + // Skip when there is no existing record — first insert has no prior state to transition from. + // Normalize existing.status (undefined = legacy "active") before comparing so that writing + // status: "active" onto a legacy item without a status field is not treated as a transition. + if (!options.skipTransitionCheck && existing && item.status !== undefined) { + const normalizedExisting: HistoryItemStatus = existing.status ?? "active" + if (item.status !== normalizedExisting) { + assertValidTransition(existing.status, item.status) + } + } - // Write per-task file (source of truth) - await this.writeTaskFile(merged) + // Merge: preserve existing metadata unless explicitly overwritten + const merged = existing ? { ...existing, ...item } : item - // Update in-memory cache - this.cache.set(merged.id, merged) + // Write per-task file (source of truth) + await this.writeTaskFile(merged) - // Schedule debounced index write - this.scheduleIndexWrite() + // Update in-memory cache + this.cache.set(merged.id, merged) + // Schedule debounced index write + this.scheduleIndexWrite() - const all = this.getAll() + const all = this.getAll() - // Call onWrite callback inside the lock for serialized write-through - if (this.onWrite) { - await this.onWrite(all) - } + // Call onWrite callback inside the lock for serialized write-through + if (this.onWrite) { + await this.onWrite(all) + } - return all - }) + return all } /** @@ -289,6 +338,92 @@ export class TaskHistoryStore { }) } + /** + * Repair delegation inconsistencies left by a crash mid-transition. + * + * Called once from `initialize()` after `reconcile()`. Runs inside `withLock` to + * prevent interleaving with watcher-triggered reconcile() calls. Iterates until + * convergence so that one-level chained delegations visible at startup are resolved. + * + * Must NOT be called from within `withLock` — `withLock` is non-reentrant (promise + * chain); calling `upsert` (which acquires the lock) from inside would deadlock. + * `upsertCore` is called directly here instead, bypassing transition validation via + * `skipTransitionCheck: true` because these writes are administrative repairs, not + * runtime state-machine transitions. + * + * Cases repaired per pass: + * - Parent `delegated` with no `awaitingChildId` → parent → `active` (invalid state) + * - Parent `delegated`, child not found → parent → `active` (orphaned delegation) + * - Parent `delegated`, child `completed` → parent → `active` (interrupted handoff) + * + * A parent awaiting an `active`, `interrupted`, or `delegated` child is left as-is — the child is resumable. + */ + private async reconcileDelegationState(): Promise { + return this.withLock(async () => { + let repairsInThisPass: number + do { + repairsInThisPass = 0 + // Rebuild the lookup map each pass so repairs from the previous pass + // are visible when evaluating chained delegations. + const byId = new Map(Array.from(this.cache.values()).map((i) => [i.id, i])) + + for (const [, item] of byId) { + if (item.status !== "delegated") { + continue + } + + if (!item.awaitingChildId) { + await this.upsertCore( + { ...item, status: "active", awaitingChildId: undefined, delegatedToId: undefined }, + { skipTransitionCheck: true }, + ) + console.warn( + `[TaskHistoryStore] Reconciled invalid delegation: task ${item.id} → active (no awaitingChildId)`, + ) + repairsInThisPass++ + continue + } + + const child = byId.get(item.awaitingChildId) + + if (!child) { + await this.upsertCore( + { + ...item, + status: "active", + awaitingChildId: undefined, + delegatedToId: undefined, + }, + { skipTransitionCheck: true }, + ) + console.warn( + `[TaskHistoryStore] Reconciled orphaned delegation: task ${item.id} → active (child ${item.awaitingChildId} not found)`, + ) + repairsInThisPass++ + } else if (child.status === "completed") { + await this.upsertCore( + { + ...item, + status: "active", + awaitingChildId: undefined, + delegatedToId: undefined, + completedByChildId: child.id, + completionResultSummary: + child.completionResultSummary ?? "Task completed (recovered after interruption)", + }, + { skipTransitionCheck: true }, + ) + console.warn( + `[TaskHistoryStore] Reconciled interrupted handoff: task ${item.id} → active (child ${item.awaitingChildId} already completed)`, + ) + repairsInThisPass++ + } + // child.status === "active", "interrupted", or "delegated" → leave as-is this pass + } + } while (repairsInThisPass > 0) + }) + } + // ────────────────────────────── Cache invalidation ────────────────────────────── /** @@ -357,6 +492,10 @@ export class TaskHistoryStore { // Write the index await this.writeIndex() + + // Repair any delegation inconsistencies introduced by the migrated entries. + // reconcileDelegationState() is idempotent so running it again is safe. + await this.reconcileDelegationState() } // ────────────────────────────── Private: Index management ────────────────────────────── @@ -529,6 +668,105 @@ export class TaskHistoryStore { }, TaskHistoryStore.RECONCILE_INTERVAL_MS) } + // ────────────────────────────── Atomic read-modify-write ────────────────────────────── + + /** + * Read a HistoryItem from the in-memory cache and write back an updated version, + * all within a single lock acquisition so no concurrent writer can interleave + * between the read and the write. + * + * The `updater` receives the current cached item and must return the new item + * synchronously. It must not perform I/O or acquire any other lock. + * + * @throws If the task ID is not present in the cache. + */ + public atomicReadAndUpdate(taskId: string, updater: (current: HistoryItem) => HistoryItem): Promise { + return this.withLock(async () => { + const current = this.cache.get(taskId) + if (!current) { + throw new Error(`[TaskHistoryStore] atomicReadAndUpdate: task ${taskId} not found in cache`) + } + // Deep-copy so a mutating updater cannot alter cached state before persistence. + const snapshot = structuredClone(current) + const updated = updater(snapshot) + if (updated.id !== taskId) { + throw new Error( + `[TaskHistoryStore] atomicReadAndUpdate: updater changed task id from ${taskId} to ${updated.id}`, + ) + } + return this.upsertCore(updated) + }) + } + + /** + * Atomically update two related HistoryItems within a single lock acquisition. + * Both updaters run synchronously (no I/O, no lock re-entry). Both writes are + * committed before the lock releases — no concurrent writer can observe an + * intermediate state. + * + * @throws If either task ID is not present in the cache. + */ + public atomicUpdatePair( + firstId: string, + secondId: string, + firstUpdater: (current: HistoryItem) => HistoryItem, + secondUpdater: (current: HistoryItem) => HistoryItem, + ): Promise { + return this.withLock(async () => { + const first = this.cache.get(firstId) + if (!first) throw new Error(`[TaskHistoryStore] atomicUpdatePair: ${firstId} not found`) + const second = this.cache.get(secondId) + if (!second) throw new Error(`[TaskHistoryStore] atomicUpdatePair: ${secondId} not found`) + + const updatedFirst = firstUpdater(structuredClone(first)) + const updatedSecond = secondUpdater(structuredClone(second)) + + if (updatedFirst.id !== firstId) { + throw new Error( + `[TaskHistoryStore] atomicUpdatePair: first updater changed id from ${firstId} to ${updatedFirst.id}`, + ) + } + if (updatedSecond.id !== secondId) { + throw new Error( + `[TaskHistoryStore] atomicUpdatePair: second updater changed id from ${secondId} to ${updatedSecond.id}`, + ) + } + + // Validate status transitions before any disk write — mirrors upsertCore guard. + for (const [existing, updated] of [ + [first, updatedFirst], + [second, updatedSecond], + ] as const) { + if (updated.status !== undefined) { + const normalizedExisting: HistoryItemStatus = existing.status ?? "active" + if (updated.status !== normalizedExisting) { + assertValidTransition(existing.status, updated.status) + } + } + } + + // Merge with existing cache entries before writing, mirroring upsertCore. + const mergedFirst = { ...first, ...updatedFirst } + const mergedSecond = { ...second, ...updatedSecond } + + // Write both files before touching the cache so readers never observe a + // half-updated in-memory state between the two await points. + await this.writeTaskFile(mergedFirst) + await this.writeTaskFile(mergedSecond) + + // Both disk writes succeeded — now update the cache atomically. + this.cache.set(firstId, mergedFirst) + this.cache.set(secondId, mergedSecond) + + this.scheduleIndexWrite() + const all = this.getAll() + if (this.onWrite) { + await this.onWrite(all) + } + return all + }) + } + // ────────────────────────────── Private: Write lock ────────────────────────────── /** diff --git a/src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts b/src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts new file mode 100644 index 0000000000..2888c0f7b2 --- /dev/null +++ b/src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts @@ -0,0 +1,521 @@ +// pnpm --filter zoo-code test core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts + +import * as fs from "fs/promises" +import * as path from "path" +import * as os from "os" + +import type { HistoryItem } from "@roo-code/types" + +import { TaskHistoryStore, assertValidTransition } from "../TaskHistoryStore" + +vi.mock("../../../utils/storage", () => ({ + getStorageBasePath: vi.fn().mockImplementation((defaultPath: string) => defaultPath), +})) + +vi.mock("../../../utils/safeWriteJson", () => ({ + safeWriteJson: vi.fn().mockImplementation(async (filePath: string, data: any) => { + await fs.mkdir(path.dirname(filePath), { recursive: true }) + await fs.writeFile(filePath, JSON.stringify(data, null, "\t"), "utf8") + }), +})) + +function makeItem(overrides: Partial = {}): HistoryItem { + return { + id: `task-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`, + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + ...overrides, + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// assertValidTransition — pure function tests +// ───────────────────────────────────────────────────────────────────────────── + +describe("assertValidTransition", () => { + describe("valid transitions", () => { + it("active → delegated", () => { + expect(() => assertValidTransition("active", "delegated")).not.toThrow() + }) + + it("active → completed", () => { + expect(() => assertValidTransition("active", "completed")).not.toThrow() + }) + + it("active → interrupted", () => { + expect(() => assertValidTransition("active", "interrupted")).not.toThrow() + }) + + it("delegated → active", () => { + expect(() => assertValidTransition("delegated", "active")).not.toThrow() + }) + + it("interrupted → completed", () => { + expect(() => assertValidTransition("interrupted", "completed")).not.toThrow() + }) + + it("undefined (implicit active) → delegated", () => { + expect(() => assertValidTransition(undefined, "delegated")).not.toThrow() + }) + + it("undefined (implicit active) → completed", () => { + expect(() => assertValidTransition(undefined, "completed")).not.toThrow() + }) + }) + + describe("invalid transitions — throw", () => { + it("delegated → completed", () => { + expect(() => assertValidTransition("delegated", "completed")).toThrow( + "Invalid task status transition: delegated → completed", + ) + }) + + it("delegated → delegated (self-loop)", () => { + expect(() => assertValidTransition("delegated", "delegated")).toThrow( + "Invalid task status transition: delegated → delegated", + ) + }) + + it("completed → active", () => { + expect(() => assertValidTransition("completed", "active")).toThrow( + "Invalid task status transition: completed → active", + ) + }) + + it("completed → delegated", () => { + expect(() => assertValidTransition("completed", "delegated")).toThrow( + "Invalid task status transition: completed → delegated", + ) + }) + + it("interrupted → active", () => { + expect(() => assertValidTransition("interrupted", "active")).toThrow( + "Invalid task status transition: interrupted → active", + ) + }) + + it("active → active (self-loop)", () => { + expect(() => assertValidTransition("active", "active")).toThrow( + "Invalid task status transition: active → active", + ) + }) + + it("undefined (implicit active) → delegated is valid", () => { + expect(() => assertValidTransition(undefined, "delegated")).not.toThrow() + }) + }) +}) + +// ───────────────────────────────────────────────────────────────────────────── +// reconcileDelegationState — integration tests via initialize() +// ───────────────────────────────────────────────────────────────────────────── + +describe("TaskHistoryStore reconcileDelegationState", () => { + let tmpDir: string + let store: TaskHistoryStore + + async function seedItems(items: HistoryItem[]): Promise { + const tasksDir = path.join(tmpDir, "tasks") + await fs.mkdir(tasksDir, { recursive: true }) + for (const item of items) { + const taskDir = path.join(tasksDir, item.id) + await fs.mkdir(taskDir, { recursive: true }) + await fs.writeFile(path.join(taskDir, "history_item.json"), JSON.stringify(item)) + } + } + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "reconcile-test-")) + store = new TaskHistoryStore(tmpDir) + }) + + afterEach(async () => { + store.dispose() + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}) + }) + + it("repairs orphaned delegation: delegated parent whose child does not exist → active", async () => { + const parent = makeItem({ id: "parent-1", status: "delegated", awaitingChildId: "missing-child" }) + await seedItems([parent]) + + await store.initialize() + + const repaired = store.get("parent-1") + expect(repaired?.status).toBe("active") + expect(repaired?.awaitingChildId).toBeUndefined() + expect(repaired?.delegatedToId).toBeUndefined() + }) + + it("repairs interrupted handoff: delegated parent with completed child → active", async () => { + const child = makeItem({ + id: "child-2", + status: "completed", + completionResultSummary: "Child result", + }) + const parent = makeItem({ + id: "parent-2", + status: "delegated", + awaitingChildId: "child-2", + delegatedToId: "child-2", + }) + await seedItems([parent, child]) + + await store.initialize() + + const repaired = store.get("parent-2") + expect(repaired?.status).toBe("active") + expect(repaired?.awaitingChildId).toBeUndefined() + expect(repaired?.delegatedToId).toBeUndefined() + expect(repaired?.completedByChildId).toBe("child-2") + expect(repaired?.completionResultSummary).toBe("Child result") + }) + + it("uses fallback summary when child has no completionResultSummary", async () => { + const child = makeItem({ id: "child-3", status: "completed" }) + const parent = makeItem({ id: "parent-3", status: "delegated", awaitingChildId: "child-3" }) + await seedItems([parent, child]) + + await store.initialize() + + const repaired = store.get("parent-3") + expect(repaired?.completionResultSummary).toBe("Task completed (recovered after interruption)") + }) + + it("leaves delegated parent alone when child is still active", async () => { + const child = makeItem({ id: "child-4", status: "active" }) + const parent = makeItem({ id: "parent-4", status: "delegated", awaitingChildId: "child-4" }) + await seedItems([parent, child]) + + await store.initialize() + + const unchanged = store.get("parent-4") + expect(unchanged?.status).toBe("delegated") + expect(unchanged?.awaitingChildId).toBe("child-4") + }) + + it("repairs invalid delegation: delegated parent with no awaitingChildId → active (clears delegatedToId and awaitingChildId)", async () => { + // awaitingChildId is falsy but explicitly set (empty string), delegatedToId is stale + const parent = makeItem({ + id: "parent-5", + status: "delegated", + delegatedToId: "stale-child", + awaitingChildId: "", + } as any) + await seedItems([parent]) + + await store.initialize() + + const repaired = store.get("parent-5") + expect(repaired?.status).toBe("active") + expect(repaired?.delegatedToId).toBeUndefined() + // Fix #4: falsy awaitingChildId must also be cleared + expect(repaired?.awaitingChildId).toBeUndefined() + }) + + it("does not touch active or completed tasks", async () => { + const active = makeItem({ id: "task-active", status: "active" }) + const completed = makeItem({ id: "task-completed", status: "completed" }) + await seedItems([active, completed]) + + await store.initialize() + + expect(store.get("task-active")?.status).toBe("active") + expect(store.get("task-completed")?.status).toBe("completed") + }) + + it("repairs multiple delegated parents in a single initialize()", async () => { + const childA = makeItem({ id: "child-a", status: "completed" }) + const parentA = makeItem({ id: "parent-a", status: "delegated", awaitingChildId: "child-a" }) + const parentB = makeItem({ id: "parent-b", status: "delegated", awaitingChildId: "missing-b" }) + await seedItems([childA, parentA, parentB]) + + await store.initialize() + + expect(store.get("parent-a")?.status).toBe("active") + expect(store.get("parent-b")?.status).toBe("active") + }) + + it("handles chained delegation (A→B→C): repairs B first, then A sees B as active and is left delegated", async () => { + // C doesn't exist (orphaned). B is delegated waiting for C → repaired to active. + // A is delegated waiting for B → left delegated (B is now active, resumable by user). + const parentA = makeItem({ id: "parent-a-chain", status: "delegated", awaitingChildId: "parent-b-chain" }) + const parentB = makeItem({ + id: "parent-b-chain", + status: "delegated", + awaitingChildId: "missing-child-chain", + }) + await seedItems([parentA, parentB]) + + await store.initialize() + + // B is repaired: its child (C) was missing + expect(store.get("parent-b-chain")?.status).toBe("active") + // A stays delegated: its child (B) is now active, which is a valid state + expect(store.get("parent-a-chain")?.status).toBe("delegated") + expect(store.get("parent-a-chain")?.awaitingChildId).toBe("parent-b-chain") + }) + + it("is idempotent: running initialize twice produces the same result", async () => { + const child = makeItem({ id: "child-6", status: "completed", completionResultSummary: "Done" }) + const parent = makeItem({ id: "parent-6", status: "delegated", awaitingChildId: "child-6" }) + await seedItems([parent, child]) + + await store.initialize() + const afterFirst = { ...store.get("parent-6") } + + store.dispose() + const store2 = new TaskHistoryStore(tmpDir) + await store2.initialize() + const afterSecond = { ...store2.get("parent-6") } + store2.dispose() + + expect(afterFirst.status).toBe("active") + expect(afterSecond.status).toBe("active") + expect(afterSecond.completedByChildId).toBe(afterFirst.completedByChildId) + expect(afterSecond.completionResultSummary).toBe(afterFirst.completionResultSummary) + }) + + it("logs repairs to console.warn", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + + const parent = makeItem({ id: "parent-log", status: "delegated", awaitingChildId: "nonexistent" }) + await seedItems([parent]) + + await store.initialize() + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Reconciled orphaned delegation")) + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("parent-log")) + + warnSpy.mockRestore() + }) + + it("invokes onWrite callback after startup repairs", async () => { + const onWrite = vi.fn().mockResolvedValue(undefined) + store.dispose() + store = new TaskHistoryStore(tmpDir, { onWrite }) + + const parent = makeItem({ id: "parent-onwrite", status: "delegated", awaitingChildId: "nonexistent-child" }) + await seedItems([parent]) + + await store.initialize() + + // The startup repair writes the repaired item, which must trigger onWrite + expect(onWrite).toHaveBeenCalled() + // The final state passed to onWrite must contain the repaired item + const lastCall = onWrite.mock.calls[onWrite.mock.calls.length - 1][0] as HistoryItem[] + const repaired = lastCall.find((i) => i.id === "parent-onwrite") + expect(repaired?.status).toBe("active") + }) +}) + +// ───────────────────────────────────────────────────────────────────────────── +// migrateFromGlobalState — reconciliation runs after migration +// ───────────────────────────────────────────────────────────────────────────── + +describe("TaskHistoryStore migrateFromGlobalState reconciliation", () => { + let tmpDir: string + let store: TaskHistoryStore + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "zoo-migrate-test-")) + store = new TaskHistoryStore(tmpDir) + await store.initialize() + }) + + afterEach(async () => { + store.dispose() + await fs.rm(tmpDir, { recursive: true, force: true }) + }) + + it("repairs a delegated parent introduced by migrateFromGlobalState on the same startup", async () => { + // Simulate a first-upgrade scenario: the child task file exists on disk + // (from a pre-migration run) but the parent arrives via migrateFromGlobalState + // with status "delegated" and an awaitingChildId whose task dir is also present. + // The child's history_item.json does NOT exist yet — it too will be migrated. + const tasksDir = path.join(tmpDir, "tasks") + const childId = "migrate-child-1" + const parentId = "migrate-parent-1" + + // Create task directories (simulating existing task folders) + await fs.mkdir(path.join(tasksDir, childId), { recursive: true }) + await fs.mkdir(path.join(tasksDir, parentId), { recursive: true }) + + const child = makeItem({ id: childId, status: "completed", completionResultSummary: "Done" }) + const parent = makeItem({ id: parentId, status: "delegated", awaitingChildId: childId, delegatedToId: childId }) + + // Migrate both — parent is delegated with a completed child + await store.migrateFromGlobalState([child, parent]) + + // The parent should be repaired to active by the post-migration reconciliation + const repairedParent = store.get(parentId) + expect(repairedParent?.status).toBe("active") + expect(repairedParent?.awaitingChildId).toBeUndefined() + expect(repairedParent?.delegatedToId).toBeUndefined() + expect(repairedParent?.completedByChildId).toBe(childId) + expect(repairedParent?.completionResultSummary).toBe("Done") + }) +}) + +// ───────────────────────────────────────────────────────────────────────────── +// upsert — transition guard enforcement at the write boundary +// ───────────────────────────────────────────────────────────────────────────── + +describe("TaskHistoryStore upsert transition guard", () => { + let tmpDir: string + let store: TaskHistoryStore + + async function seedItems(items: HistoryItem[]): Promise { + const tasksDir = path.join(tmpDir, "tasks") + await fs.mkdir(tasksDir, { recursive: true }) + for (const item of items) { + const taskDir = path.join(tasksDir, item.id) + await fs.mkdir(taskDir, { recursive: true }) + await fs.writeFile(path.join(taskDir, "history_item.json"), JSON.stringify(item)) + } + } + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "upsert-guard-test-")) + store = new TaskHistoryStore(tmpDir) + await store.initialize() + }) + + afterEach(async () => { + store.dispose() + await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}) + }) + + it("rejects completed → active transition, preserving the completed status", async () => { + const item = makeItem({ id: "task-guard-1", status: "completed" }) + await seedItems([item]) + store.dispose() + store = new TaskHistoryStore(tmpDir) + await store.initialize() + + // Fire-and-forget late save: tries to write status: "active" over "completed" + await expect(store.upsert({ ...item, status: "active" })).rejects.toThrow( + "Invalid task status transition: completed → active", + ) + + // The completed status must be preserved in the cache + expect(store.get("task-guard-1")?.status).toBe("completed") + }) + + it("rejects delegated → completed transition", async () => { + // Must include a live active child so reconciliation doesn't repair the parent to active + const child = makeItem({ id: "child-guard-2", status: "active" }) + const item = makeItem({ id: "task-guard-2", status: "delegated", awaitingChildId: "child-guard-2" }) + await seedItems([child, item]) + store.dispose() + store = new TaskHistoryStore(tmpDir) + await store.initialize() + + // Confirm reconciliation left the delegated status alone + expect(store.get("task-guard-2")?.status).toBe("delegated") + + await expect(store.upsert({ ...item, status: "completed" })).rejects.toThrow( + "Invalid task status transition: delegated → completed", + ) + + expect(store.get("task-guard-2")?.status).toBe("delegated") + }) + + it("allows valid active → completed transition", async () => { + const item = makeItem({ id: "task-guard-3", status: "active" }) + await seedItems([item]) + store.dispose() + store = new TaskHistoryStore(tmpDir) + await store.initialize() + + await expect(store.upsert({ ...item, status: "completed" })).resolves.toBeDefined() + expect(store.get("task-guard-3")?.status).toBe("completed") + }) + + it("rejects interrupted → active transition, preserving the interrupted status", async () => { + const item = makeItem({ id: "task-guard-interrupted", status: "interrupted" }) + await seedItems([item]) + store.dispose() + store = new TaskHistoryStore(tmpDir) + await store.initialize() + + await expect(store.upsert({ ...item, status: "active" })).rejects.toThrow( + "Invalid task status transition: interrupted → active", + ) + expect(store.get("task-guard-interrupted")?.status).toBe("interrupted") + }) + + it("allows valid interrupted → completed transition", async () => { + const item = makeItem({ id: "task-guard-interrupted-complete", status: "interrupted" }) + await seedItems([item]) + store.dispose() + store = new TaskHistoryStore(tmpDir) + await store.initialize() + + await expect(store.upsert({ ...item, status: "completed" })).resolves.toBeDefined() + expect(store.get("task-guard-interrupted-complete")?.status).toBe("completed") + }) + + it("allows first insert with status: active (no prior record to transition from)", async () => { + const item = makeItem({ id: "task-guard-new", status: "active" }) + // Do NOT seed — this is the very first write for this task + await expect(store.upsert(item)).resolves.toBeDefined() + expect(store.get("task-guard-new")?.status).toBe("active") + }) + + it("allows writing status: active over a legacy item with status: undefined (implicit active → active no-op)", async () => { + // Legacy items pre-dating the status field have status: undefined, which normalizes + // to "active". Writing status: "active" must not throw as an invalid self-loop. + const item = makeItem({ id: "task-guard-legacy" }) + delete (item as any).status + await seedItems([item]) + store.dispose() + store = new TaskHistoryStore(tmpDir) + await store.initialize() + + await expect(store.upsert({ ...item, status: "active" })).resolves.toBeDefined() + expect(store.get("task-guard-legacy")?.status).toBe("active") + }) + + it("allows upsert without a status field (no-op on status)", async () => { + const item = makeItem({ id: "task-guard-4", status: "completed" }) + await seedItems([item]) + store.dispose() + store = new TaskHistoryStore(tmpDir) + await store.initialize() + + // Omitting status entirely — no transition should be validated + const { status: _omit, ...noStatus } = item + await expect(store.upsert(noStatus as HistoryItem)).resolves.toBeDefined() + // Status is preserved from the existing cache entry + expect(store.get("task-guard-4")?.status).toBe("completed") + }) + + it("atomicReadAndUpdate enforces the upsertCore transition guard on status changes", async () => { + // atomicReadAndUpdate now flows through upsertCore without skipTransitionCheck, + // so invalid transitions are rejected at the store boundary. + const item = makeItem({ id: "task-atomic-guard", status: "active" }) + await store.upsert(item) + + // active → delegated via atomicReadAndUpdate — valid, must succeed + await expect( + store.atomicReadAndUpdate("task-atomic-guard", (current) => ({ + ...current, + status: "delegated" as const, + awaitingChildId: "some-child", + })), + ).resolves.toBeDefined() + expect(store.get("task-atomic-guard")?.status).toBe("delegated") + + // delegated → completed via atomicReadAndUpdate — invalid, must throw + await expect( + store.atomicReadAndUpdate("task-atomic-guard", (current) => ({ + ...current, + status: "completed" as const, + })), + ).rejects.toThrow("Invalid task status transition: delegated → completed") + }) +}) diff --git a/src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts b/src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts index ebf00d4954..75e0a16253 100644 --- a/src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts +++ b/src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts @@ -6,7 +6,7 @@ import * as os from "os" import type { HistoryItem } from "@roo-code/types" -import { TaskHistoryStore } from "../TaskHistoryStore" +import { TaskHistoryStore, assertValidTransition } from "../TaskHistoryStore" import { GlobalFileNames } from "../../../shared/globalFileNames" vi.mock("../../../utils/storage", () => ({ @@ -441,4 +441,278 @@ describe("TaskHistoryStore", () => { expect(store.get("gone-task")).toBeUndefined() }) }) + + describe("atomicUpdatePair()", () => { + it("updates both records and both files are written before lock releases", async () => { + await store.initialize() + + const child = makeHistoryItem({ id: "child-pair", status: "active", ts: 1000 }) + const parent = makeHistoryItem({ id: "parent-pair", status: "delegated", ts: 2000 }) + await store.upsert(child) + await store.upsert(parent) + + await store.atomicUpdatePair( + "child-pair", + "parent-pair", + (c) => ({ ...c, status: "completed" as const }), + (p) => ({ ...p, status: "active" as const, awaitingChildId: undefined }), + ) + + expect(store.get("child-pair")?.status).toBe("completed") + expect(store.get("parent-pair")?.status).toBe("active") + expect(store.get("parent-pair")?.awaitingChildId).toBeUndefined() + + // Both per-task files must be persisted + const childFile = path.join(tmpDir, "tasks", "child-pair", GlobalFileNames.historyItem) + const parentFile = path.join(tmpDir, "tasks", "parent-pair", GlobalFileNames.historyItem) + const childDisk = JSON.parse(await fs.readFile(childFile, "utf8")) + const parentDisk = JSON.parse(await fs.readFile(parentFile, "utf8")) + expect(childDisk.status).toBe("completed") + expect(parentDisk.status).toBe("active") + }) + + it("a concurrent upsert queued during the pair write sees completed state of both records", async () => { + await store.initialize() + + const child = makeHistoryItem({ id: "child-concurrent", status: "active", ts: 1000 }) + const parent = makeHistoryItem({ id: "parent-concurrent", status: "delegated", ts: 2000 }) + await store.upsert(child) + await store.upsert(parent) + + // Queue atomicUpdatePair and a follow-up upsert concurrently + const pairPromise = store.atomicUpdatePair( + "child-concurrent", + "parent-concurrent", + (c) => ({ ...c, status: "completed" as const }), + (p) => ({ ...p, status: "active" as const }), + ) + + // This upsert is queued while the pair lock is held — it must see the final state + const followUp = store.upsert({ ...parent, status: "active" as const, tokensOut: 999 }) + + await pairPromise + await followUp + + // Both pair updates landed; follow-up should have merged on top + expect(store.get("child-concurrent")?.status).toBe("completed") + expect(store.get("parent-concurrent")?.status).toBe("active") + expect(store.get("parent-concurrent")?.tokensOut).toBe(999) + }) + + it("throws when first updater returns a different id", async () => { + await store.initialize() + + const child = makeHistoryItem({ id: "child-id-check" }) + const parent = makeHistoryItem({ id: "parent-id-check" }) + await store.upsert(child) + await store.upsert(parent) + + await expect( + store.atomicUpdatePair( + "child-id-check", + "parent-id-check", + (c) => ({ ...c, id: "wrong-id" }), + (p) => p, + ), + ).rejects.toThrow( + "[TaskHistoryStore] atomicUpdatePair: first updater changed id from child-id-check to wrong-id", + ) + }) + + it("throws when second updater returns a different id", async () => { + await store.initialize() + + const child = makeHistoryItem({ id: "child-id-check2" }) + const parent = makeHistoryItem({ id: "parent-id-check2" }) + await store.upsert(child) + await store.upsert(parent) + + await expect( + store.atomicUpdatePair( + "child-id-check2", + "parent-id-check2", + (c) => c, + (p) => ({ ...p, id: "wrong-id" }), + ), + ).rejects.toThrow( + "[TaskHistoryStore] atomicUpdatePair: second updater changed id from parent-id-check2 to wrong-id", + ) + }) + + it("throws when first task ID is not in cache", async () => { + await store.initialize() + + const parent = makeHistoryItem({ id: "parent-missing-child" }) + await store.upsert(parent) + + await expect( + store.atomicUpdatePair( + "nonexistent-child", + "parent-missing-child", + (c) => c, + (p) => p, + ), + ).rejects.toThrow("[TaskHistoryStore] atomicUpdatePair: nonexistent-child not found") + }) + + it("throws when second task ID is not in cache", async () => { + await store.initialize() + + const child = makeHistoryItem({ id: "child-missing-parent" }) + await store.upsert(child) + + await expect( + store.atomicUpdatePair( + "child-missing-parent", + "nonexistent-parent", + (c) => c, + (p) => p, + ), + ).rejects.toThrow("[TaskHistoryStore] atomicUpdatePair: nonexistent-parent not found") + }) + + it("partial failure: first file write lands, second throws — error surfaces; cache unchanged (known disk/cache divergence)", async () => { + await store.initialize() + + const child = makeHistoryItem({ id: "child-partial", status: "active", ts: 1000 }) + const parent = makeHistoryItem({ id: "parent-partial", status: "delegated", ts: 2000 }) + await store.upsert(child) + await store.upsert(parent) + + let writeCallCount = 0 + const storeAny = store as any + const originalWriteTaskFile = storeAny.writeTaskFile.bind(store) + vi.spyOn(storeAny, "writeTaskFile").mockImplementation(async (...args: unknown[]) => { + writeCallCount++ + if (writeCallCount === 2) throw new Error("second writeTaskFile failed") + return originalWriteTaskFile(...args) + }) + + await expect( + store.atomicUpdatePair( + "child-partial", + "parent-partial", + (c) => ({ ...c, status: "completed" as const }), + (p) => ({ ...p, status: "active" as const }), + ), + ).rejects.toThrow("second writeTaskFile failed") + + // First file write landed on disk (known limitation — no rollback) + const childFile = path.join(tmpDir, "tasks", "child-partial", GlobalFileNames.historyItem) + const childDisk = JSON.parse(await fs.readFile(childFile, "utf8")) + expect(childDisk.status).toBe("completed") + + // Second file write did not land + const parentFile = path.join(tmpDir, "tasks", "parent-partial", GlobalFileNames.historyItem) + const parentDisk = JSON.parse(await fs.readFile(parentFile, "utf8")) + expect(parentDisk.status).toBe("delegated") + + // Cache was NOT updated (cache set is deferred until after both writes succeed) + expect(store.get("child-partial")?.status).toBe("active") + expect(store.get("parent-partial")?.status).toBe("delegated") + }) + + it("invokes onWrite callback once with both records updated", async () => { + const onWrite = vi.fn().mockResolvedValue(undefined) + const storeWithCallback = new TaskHistoryStore(tmpDir, { onWrite }) + await storeWithCallback.initialize() + + const child = makeHistoryItem({ id: "child-onwrite", status: "active" }) + const parent = makeHistoryItem({ id: "parent-onwrite", status: "delegated" }) + await storeWithCallback.upsert(child) + await storeWithCallback.upsert(parent) + + onWrite.mockClear() + + await storeWithCallback.atomicUpdatePair( + "child-onwrite", + "parent-onwrite", + (c) => ({ ...c, status: "completed" as const }), + (p) => ({ ...p, status: "active" as const }), + ) + + // onWrite called exactly once (not once per record) + expect(onWrite).toHaveBeenCalledTimes(1) + const writtenItems = onWrite.mock.calls[0][0] as HistoryItem[] + const childWritten = writtenItems.find((i) => i.id === "child-onwrite") + const parentWritten = writtenItems.find((i) => i.id === "parent-onwrite") + expect(childWritten?.status).toBe("completed") + expect(parentWritten?.status).toBe("active") + + storeWithCallback.dispose() + }) + + it("propagates throw from first updater — neither record is written", async () => { + await store.initialize() + + const child = makeHistoryItem({ id: "child-updater-throw", status: "active" }) + const parent = makeHistoryItem({ id: "parent-updater-throw", status: "delegated" }) + await store.upsert(child) + await store.upsert(parent) + + await expect( + store.atomicUpdatePair( + "child-updater-throw", + "parent-updater-throw", + (_c) => { + throw new Error("updater exploded") + }, + (p) => p, + ), + ).rejects.toThrow("updater exploded") + + // Neither record should have been modified + expect(store.get("child-updater-throw")?.status).toBe("active") + expect(store.get("parent-updater-throw")?.status).toBe("delegated") + }) + + it("transition guard in first updater: delegated → completed throws assertValidTransition", async () => { + await store.initialize() + + const child = makeHistoryItem({ id: "child-guard-pair", status: "delegated" }) + const parent = makeHistoryItem({ id: "parent-guard-pair", status: "active" }) + await store.upsert(child) + await store.upsert(parent) + + await expect( + store.atomicUpdatePair( + "child-guard-pair", + "parent-guard-pair", + (c) => { + assertValidTransition(c.status, "completed") + return { ...c, status: "completed" as const } + }, + (p) => p, + ), + ).rejects.toThrow("Invalid task status transition: delegated → completed") + + // Original statuses preserved + expect(store.get("child-guard-pair")?.status).toBe("delegated") + expect(store.get("parent-guard-pair")?.status).toBe("active") + }) + + it("store-level guard in atomicUpdatePair rejects invalid transition even without explicit updater assertion", async () => { + await store.initialize() + + const child = makeHistoryItem({ id: "child-store-guard", status: "delegated" }) + const parent = makeHistoryItem({ id: "parent-store-guard", status: "active" }) + await store.upsert(child) + await store.upsert(parent) + + // Updater returns delegated → completed without calling assertValidTransition. + // The store's internal guard must still catch this. + await expect( + store.atomicUpdatePair( + "child-store-guard", + "parent-store-guard", + (c) => ({ ...c, status: "completed" as const }), + (p) => p, + ), + ).rejects.toThrow("Invalid task status transition: delegated → completed") + + // Neither record modified + expect(store.get("child-store-guard")?.status).toBe("delegated") + expect(store.get("parent-store-guard")?.status).toBe("active") + }) + }) }) diff --git a/src/core/task-persistence/__tests__/importRooTaskHistory.spec.ts b/src/core/task-persistence/__tests__/importRooTaskHistory.spec.ts new file mode 100644 index 0000000000..017f77a3ec --- /dev/null +++ b/src/core/task-persistence/__tests__/importRooTaskHistory.spec.ts @@ -0,0 +1,620 @@ +import * as fs from "fs/promises" +import * as os from "os" +import * as path from "path" +import * as vscode from "vscode" + +import { + importRooTaskHistory, + isConcurrentDestinationClaimError, + resolveRooHistoryImportPaths, +} from "../importRooTaskHistory" + +vi.mock("vscode", () => ({ + workspace: { + getConfiguration: vi.fn(), + }, +})) + +const makeHistoryItem = (id: string, extra: Record = {}) => + JSON.stringify({ id, number: 1, ts: 1000, task: "t", tokensIn: 0, tokensOut: 0, totalCost: 0, ...extra }) + +describe("importRooTaskHistory", () => { + let tempRoot: string + + const mockStorageConfiguration = ({ + roo = "", + zoo = "", + throwOnRoo = false, + }: { + roo?: string + zoo?: string + throwOnRoo?: boolean + } = {}) => { + const getConfigurationMock = vi.mocked(vscode.workspace.getConfiguration) + + getConfigurationMock.mockImplementation((section?: string) => { + const resolvedSection = section ?? "" + return { + get: vi.fn().mockImplementation(() => { + if (resolvedSection === "roo-cline" && throwOnRoo) { + throw new Error("roo config unavailable") + } + + if (resolvedSection === "roo-cline") { + return roo + } + + if (resolvedSection === "zoo-code") { + return zoo + } + + return "" + }), + } as any + }) + } + + beforeEach(async () => { + tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "roo-history-import-")) + vi.clearAllMocks() + }) + + afterEach(async () => { + await fs.rm(tempRoot, { recursive: true, force: true }) + }) + + it("resolves Roo and Zoo storage roots from extension domains and configured custom paths", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooCustomStoragePath = path.join(tempRoot, "roo-custom") + const zooCustomStoragePath = path.join(tempRoot, "zoo-custom") + + mockStorageConfiguration({ + roo: rooCustomStoragePath, + zoo: zooCustomStoragePath, + }) + + const result = await resolveRooHistoryImportPaths(zooGlobalStoragePath) + + expect(result.rooExtensionDomain).toBe("RooVeterinaryInc.roo-cline") + expect(result.zooExtensionDomain).toBe("ZooCodeOrganization.zoo-code") + expect(result.rooStorageRoots).toEqual([ + path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline"), + rooCustomStoragePath, + ]) + expect(result.zooStorageRoot).toBe(zooCustomStoragePath) + }) + + it("falls back to the default Roo storage root when reading Roo custom storage fails", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + + mockStorageConfiguration({ throwOnRoo: true }) + + const result = await resolveRooHistoryImportPaths(zooGlobalStoragePath) + + expect(result.rooStorageRoots).toEqual([path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline")]) + expect(result.zooStorageRoot).toBe(zooGlobalStoragePath) + }) + + it("dedupes Roo storage roots when the custom path matches the default Roo storage root", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration({ roo: rooDefaultStorageRoot }) + + const result = await resolveRooHistoryImportPaths(zooGlobalStoragePath) + + expect(result.rooStorageRoots).toEqual([rooDefaultStorageRoot]) + }) + + it("copies Roo task directories into the active Zoo storage root", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const rooCustomStorageRoot = path.join(tempRoot, "roo-custom") + const zooCustomStorageRoot = path.join(tempRoot, "zoo-custom") + + mockStorageConfiguration({ + roo: rooCustomStorageRoot, + zoo: zooCustomStorageRoot, + }) + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-default"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-default", "history_item.json"), + makeHistoryItem("task-default"), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-default", "ui_messages.json"), "default") + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "_index.json"), "{}") + + await fs.mkdir(path.join(rooCustomStorageRoot, "tasks", "task-custom"), { recursive: true }) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-custom", "history_item.json"), + makeHistoryItem("task-custom"), + ) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-custom", "api_conversation_history.json"), + "custom", + ) + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.foundTaskCount).toBe(2) + expect(result.importedTaskCount).toBe(2) + expect(result.importedFileCount).toBe(4) + expect( + await fs.readFile(path.join(zooCustomStorageRoot, "tasks", "task-default", "ui_messages.json"), "utf8"), + ).toBe("default") + expect( + await fs.readFile( + path.join(zooCustomStorageRoot, "tasks", "task-custom", "api_conversation_history.json"), + "utf8", + ), + ).toBe("custom") + await expect(fs.access(path.join(zooCustomStorageRoot, "tasks", "_index.json"))).rejects.toMatchObject({ + code: "ENOENT", + }) + }) + + it("does not overwrite an existing Zoo task directory when the same Roo history is imported again", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-repeat"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-repeat", "history_item.json"), + makeHistoryItem("task-repeat", { source: "first-import" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-repeat", "ui_messages.json"), "first-ui") + + const firstImportResult = await importRooTaskHistory(zooGlobalStoragePath) + + expect(firstImportResult.importedTaskCount).toBe(1) + expect(firstImportResult.importedFileCount).toBe(2) + + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-repeat", "history_item.json"), + makeHistoryItem("task-repeat", { source: "second-import" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-repeat", "ui_messages.json"), "second-ui") + + const secondImportResult = await importRooTaskHistory(zooGlobalStoragePath) + + expect(secondImportResult.foundTaskCount).toBe(1) + expect(secondImportResult.importedTaskCount).toBe(0) + expect(secondImportResult.importedFileCount).toBe(0) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-repeat", "history_item.json"), "utf8"), + ).toBe(makeHistoryItem("task-repeat", { source: "first-import" })) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-repeat", "ui_messages.json"), "utf8"), + ).toBe("first-ui") + }) + + it("deterministically keeps the first importable Roo task when duplicate task IDs exist across roots", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const rooCustomStorageRoot = path.join(tempRoot, "roo-custom") + + mockStorageConfiguration({ roo: rooCustomStorageRoot }) + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-shared"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-shared", "history_item.json"), + makeHistoryItem("task-shared", { source: "default-root" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-shared", "ui_messages.json"), "default-ui") + + await fs.mkdir(path.join(rooCustomStorageRoot, "tasks", "task-shared"), { recursive: true }) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-shared", "history_item.json"), + makeHistoryItem("task-shared", { source: "custom-root" }), + ) + await fs.writeFile(path.join(rooCustomStorageRoot, "tasks", "task-shared", "ui_messages.json"), "custom-ui") + + await fs.mkdir(path.join(rooCustomStorageRoot, "tasks", "task-custom-only"), { recursive: true }) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-custom-only", "history_item.json"), + makeHistoryItem("task-custom-only", { source: "custom-root" }), + ) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-custom-only", "ui_messages.json"), + "custom-only-ui", + ) + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(2) + expect(result.importedFileCount).toBe(4) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-shared", "history_item.json"), "utf8"), + ).toBe(makeHistoryItem("task-shared", { source: "default-root" })) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-shared", "ui_messages.json"), "utf8"), + ).toBe("default-ui") + expect( + await fs.readFile( + path.join(zooGlobalStoragePath, "tasks", "task-custom-only", "history_item.json"), + "utf8", + ), + ).toBe(makeHistoryItem("task-custom-only", { source: "custom-root" })) + }) + + it("reports Roo history import progress as files are copied", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const onProgress = vi.fn() + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-progress"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-progress", "history_item.json"), + makeHistoryItem("task-progress"), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-progress", "ui_messages.json"), "ui") + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-progress", "api_conversation_history.json"), + "api", + ) + + await importRooTaskHistory(zooGlobalStoragePath, onProgress) + + expect(onProgress.mock.calls).toEqual([ + [ + { + copiedFileCount: 0, + totalFileCount: 3, + importedTaskCount: 0, + totalTaskCount: 1, + currentTaskId: undefined, + currentFileName: undefined, + }, + ], + [ + { + copiedFileCount: 1, + totalFileCount: 3, + importedTaskCount: 1, + totalTaskCount: 1, + currentTaskId: "task-progress", + currentFileName: "history_item.json", + }, + ], + [ + { + copiedFileCount: 2, + totalFileCount: 3, + importedTaskCount: 1, + totalTaskCount: 1, + currentTaskId: "task-progress", + currentFileName: "ui_messages.json", + }, + ], + [ + { + copiedFileCount: 3, + totalFileCount: 3, + importedTaskCount: 1, + totalTaskCount: 1, + currentTaskId: "task-progress", + currentFileName: "api_conversation_history.json", + }, + ], + ]) + }) + + it("imports only top-level task history files and skips checkpoint directories", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const zooCustomStorageRoot = path.join(tempRoot, "shared-storage") + + mockStorageConfiguration({ + roo: zooCustomStorageRoot, + zoo: zooCustomStorageRoot, + }) + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-visible", "checkpoints", ".git", "objects"), { + recursive: true, + }) + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", ".task-hidden"), { recursive: true }) + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "_task-hidden"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-visible", "history_item.json"), + makeHistoryItem("task-visible"), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-visible", "ui_messages.json"), "visible-ui") + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-visible", "api_conversation_history.json"), + "visible-api", + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-visible", "task_metadata.json"), "metadata") + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "loose.json"), "loose") + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-visible", "checkpoints", ".git", "objects", "object"), + "git-object", + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", ".task-hidden", "history_item.json"), "hidden-dir") + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "_task-hidden", "history_item.json"), "hidden-dir") + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.rooStorageRoots).toEqual([rooDefaultStorageRoot]) + expect(result.importedTaskCount).toBe(1) + expect(result.importedFileCount).toBe(4) + expect( + await fs.readFile(path.join(zooCustomStorageRoot, "tasks", "task-visible", "ui_messages.json"), "utf8"), + ).toBe("visible-ui") + expect( + await fs.readFile( + path.join(zooCustomStorageRoot, "tasks", "task-visible", "api_conversation_history.json"), + "utf8", + ), + ).toBe("visible-api") + expect( + await fs.readFile(path.join(zooCustomStorageRoot, "tasks", "task-visible", "task_metadata.json"), "utf8"), + ).toBe("metadata") + await expect(fs.access(path.join(zooCustomStorageRoot, "tasks", ".task-hidden"))).rejects.toMatchObject({ + code: "ENOENT", + }) + await expect(fs.access(path.join(zooCustomStorageRoot, "tasks", "_task-hidden"))).rejects.toMatchObject({ + code: "ENOENT", + }) + await expect( + fs.access( + path.join(zooCustomStorageRoot, "tasks", "task-visible", "checkpoints", ".git", "objects", "object"), + ), + ).rejects.toMatchObject({ + code: "ENOENT", + }) + await expect(fs.access(path.join(zooCustomStorageRoot, "tasks", "loose.json"))).rejects.toMatchObject({ + code: "ENOENT", + }) + }) + + it("ignores missing Roo task roots while still importing from available roots", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const rooMissingCustomStorageRoot = path.join(tempRoot, "roo-missing") + + mockStorageConfiguration({ roo: rooMissingCustomStorageRoot }) + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-default"), { recursive: true }) + const taskDefaultHistoryJson = JSON.stringify({ + id: "task-default", + number: 1, + ts: 1000, + task: "t", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-default", "history_item.json"), + taskDefaultHistoryJson, + ) + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.rooStorageRoots).toEqual([rooDefaultStorageRoot, rooMissingCustomStorageRoot]) + expect(result.importedTaskCount).toBe(1) + expect(result.importedFileCount).toBe(1) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-default", "history_item.json"), "utf8"), + ).toBe(taskDefaultHistoryJson) + }) + + it("skips tasks that do not have an importable history_item.json", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-missing-history"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-missing-history", "ui_messages.json"), + "ui only", + ) + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(0) + expect(result.importedFileCount).toBe(0) + await expect(fs.access(path.join(zooGlobalStoragePath, "tasks", "task-missing-history"))).rejects.toMatchObject( + { + code: "ENOENT", + }, + ) + }) + + it("does not delete an existing Zoo task when the Roo task is missing history_item.json", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const existingZooTaskDirectory = path.join(zooGlobalStoragePath, "tasks", "task-existing") + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-existing"), { recursive: true }) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-existing", "ui_messages.json"), "ui only") + await fs.mkdir(existingZooTaskDirectory, { recursive: true }) + await fs.writeFile(path.join(existingZooTaskDirectory, "history_item.json"), "existing") + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(0) + expect(result.importedFileCount).toBe(0) + expect(await fs.readFile(path.join(existingZooTaskDirectory, "history_item.json"), "utf8")).toBe("existing") + }) + + it("does not overwrite an existing Zoo task when the Roo task is otherwise importable", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const existingZooTaskDirectory = path.join(zooGlobalStoragePath, "tasks", "task-existing") + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-existing"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-existing", "history_item.json"), + makeHistoryItem("task-existing", { source: "roo" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-existing", "ui_messages.json"), "roo-ui") + await fs.mkdir(existingZooTaskDirectory, { recursive: true }) + await fs.writeFile(path.join(existingZooTaskDirectory, "history_item.json"), "existing") + await fs.writeFile(path.join(existingZooTaskDirectory, "ui_messages.json"), "existing-ui") + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(0) + expect(result.importedFileCount).toBe(0) + expect(await fs.readFile(path.join(existingZooTaskDirectory, "history_item.json"), "utf8")).toBe("existing") + expect(await fs.readFile(path.join(existingZooTaskDirectory, "ui_messages.json"), "utf8")).toBe("existing-ui") + }) + + it("rejects task IDs containing dots or underscore prefixes to prevent traversal", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration() + + // These directory names look like valid FS entries but should be rejected. + // (Slash/backslash can't exist in directory names on Linux/Mac/Windows.) + const unsafeCandidates = ["task.id", ".hidden-task", "_reserved-task"] + for (const name of unsafeCandidates) { + const dir = path.join(rooDefaultStorageRoot, "tasks", name) + await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(path.join(dir, "history_item.json"), "unsafe") + } + + // A safe task alongside unsafe ones should still be imported. + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-safe"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-safe", "history_item.json"), + makeHistoryItem("task-safe"), + ) + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(1) + for (const name of unsafeCandidates) { + await expect(fs.access(path.join(zooGlobalStoragePath, "tasks", name))).rejects.toMatchObject({ + code: "ENOENT", + }) + } + }) + + it("rejects tasks whose history_item.json id field does not match the directory name or contains unsafe characters", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration() + + // id mismatches the directory name — could drive path traversal in TaskHistoryStore + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-mismatch"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-mismatch", "history_item.json"), + makeHistoryItem("different-task-id"), + ) + + // id contains a path traversal sequence + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-traversal"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-traversal", "history_item.json"), + makeHistoryItem("../../evil"), + ) + + // id fails schema validation (not a valid HistoryItem) + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-invalid-schema"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-invalid-schema", "history_item.json"), + JSON.stringify({ id: "task-invalid-schema" }), + ) + + // A task with a correct, schema-valid id should still import. + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-valid"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-valid", "history_item.json"), + makeHistoryItem("task-valid"), + ) + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(1) + await expect(fs.access(path.join(zooGlobalStoragePath, "tasks", "task-mismatch"))).rejects.toMatchObject({ + code: "ENOENT", + }) + await expect(fs.access(path.join(zooGlobalStoragePath, "tasks", "task-traversal"))).rejects.toMatchObject({ + code: "ENOENT", + }) + await expect(fs.access(path.join(zooGlobalStoragePath, "tasks", "task-invalid-schema"))).rejects.toMatchObject({ + code: "ENOENT", + }) + await expect(fs.access(path.join(zooGlobalStoragePath, "tasks", "task-valid"))).resolves.toBeUndefined() + }) + + it("rethrows unexpected task-root errors while importing Roo history", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration() + + await fs.mkdir(rooDefaultStorageRoot, { recursive: true }) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks"), "not a directory") + + await expect(importRooTaskHistory(zooGlobalStoragePath)).rejects.toMatchObject({ + code: "ENOTDIR", + }) + }) + + it("treats Windows EPERM rename as a concurrent destination claim only when the destination exists", () => { + const error = new Error("operation not permitted") as NodeJS.ErrnoException + error.code = "EPERM" + + expect(isConcurrentDestinationClaimError(error, true)).toBe(true) + expect(isConcurrentDestinationClaimError(error, false)).toBe(false) + }) + + it("imports the complete file set when two calls run concurrently against the same Roo task", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-concurrent"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-concurrent", "history_item.json"), + makeHistoryItem("task-concurrent"), + ) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-concurrent", "ui_messages.json"), + "concurrent-ui", + ) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-concurrent", "api_conversation_history.json"), + "concurrent-api", + ) + + const [result1, result2] = await Promise.all([ + importRooTaskHistory(zooGlobalStoragePath), + importRooTaskHistory(zooGlobalStoragePath), + ]) + + // Exactly one call should win the atomic rename; the other should skip gracefully. + expect(result1.importedTaskCount + result2.importedTaskCount).toBe(1) + + const destTaskDir = path.join(zooGlobalStoragePath, "tasks", "task-concurrent") + + // The winning import must have written all three files completely. + expect(await fs.readFile(path.join(destTaskDir, "history_item.json"), "utf8")).toBe( + makeHistoryItem("task-concurrent"), + ) + expect(await fs.readFile(path.join(destTaskDir, "ui_messages.json"), "utf8")).toBe("concurrent-ui") + expect(await fs.readFile(path.join(destTaskDir, "api_conversation_history.json"), "utf8")).toBe( + "concurrent-api", + ) + + // No staging directories should be left behind. + const tasksEntries = await fs.readdir(path.join(zooGlobalStoragePath, "tasks")) + expect(tasksEntries.filter((e) => e.startsWith("_staging_"))).toHaveLength(0) + }) +}) diff --git a/src/core/task-persistence/importRooTaskHistory.ts b/src/core/task-persistence/importRooTaskHistory.ts new file mode 100644 index 0000000000..92dc1cafa7 --- /dev/null +++ b/src/core/task-persistence/importRooTaskHistory.ts @@ -0,0 +1,368 @@ +import type { Dirent } from "fs" +import * as fs from "fs/promises" +import * as path from "path" +import * as vscode from "vscode" + +import { historyItemSchema } from "@roo-code/types" + +import { GlobalFileNames } from "../../shared/globalFileNames" +import { Package } from "../../shared/package" +import { getStorageBasePath } from "../../utils/storage" + +const ROO_EXTENSION_DOMAIN = "RooVeterinaryInc.roo-cline" +const ROO_STORAGE_DIRECTORY = ROO_EXTENSION_DOMAIN.toLowerCase() +const ROO_CONFIGURATION_SECTION = "roo-cline" +const IMPORTABLE_TASK_FILE_NAMES = [ + GlobalFileNames.historyItem, + GlobalFileNames.uiMessages, + GlobalFileNames.apiConversationHistory, + GlobalFileNames.taskMetadata, +] + +// Reject task IDs that could cause path traversal or filesystem confusion. +// Valid Roo task IDs are Unix-millisecond timestamps (all digits), but we +// conservatively allow any name that doesn't contain path separators or dots, +// and doesn't start with a hidden/reserved prefix. +const UNSAFE_TASK_ID_RE = /[/\\.]|^_/ + +export interface RooHistoryImportPaths { + rooExtensionDomain: string + zooExtensionDomain: string + rooStorageRoots: string[] + zooStorageRoot: string +} + +export interface RooHistoryImportResult extends RooHistoryImportPaths { + foundTaskCount: number + importedTaskCount: number + importedFileCount: number +} + +export interface RooHistoryImportProgress { + copiedFileCount: number + totalFileCount: number + importedTaskCount: number + totalTaskCount: number + currentTaskId?: string + currentFileName?: string +} + +interface ImportableTaskPlan { + taskId: string + sourceTaskDirectory: string + fileNames: string[] +} + +const toComparablePath = (candidatePath: string) => { + const resolvedPath = path.resolve(candidatePath) + return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath +} + +const dedupePaths = (paths: string[]) => { + const seen = new Set() + return paths.filter((candidatePath) => { + const comparablePath = toComparablePath(candidatePath) + if (seen.has(comparablePath)) { + return false + } + seen.add(comparablePath) + return true + }) +} + +const getConfiguredCustomStoragePath = (configurationSection: string) => { + try { + const configuredPath = vscode.workspace + .getConfiguration(configurationSection) + .get("customStoragePath", "") + .trim() + return configuredPath || undefined + } catch { + return undefined + } +} + +// Only treat missing files as skippable — permission errors should propagate. +const isAbsent = (error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT" + +// Validate that history_item.json parses as a HistoryItem and that its `id` +// field matches the task directory name. Prevents imported metadata from +// driving unsafe path operations downstream (TaskHistoryStore uses item.id +// directly in path.join calls). +const validateHistoryItem = async (filePath: string, expectedTaskId: string): Promise => { + try { + const raw = await fs.readFile(filePath, "utf8") + const parsed = historyItemSchema.safeParse(JSON.parse(raw)) + if (!parsed.success) { + return false + } + return parsed.data.id === expectedTaskId && !UNSAFE_TASK_ID_RE.test(parsed.data.id) + } catch { + return false + } +} + +const isRegularFile = async (filePath: string): Promise => { + try { + const stat = await fs.lstat(filePath) + return stat.isFile() + } catch { + return false + } +} + +const copyTaskFileIfPresent = async ( + sourceTaskDirectory: string, + stagingTaskDirectory: string, + fileName: string, +): Promise => { + const sourcePath = path.join(sourceTaskDirectory, fileName) + + if (!(await isRegularFile(sourcePath))) { + return false + } + + await fs.mkdir(stagingTaskDirectory, { recursive: true }) + await fs.copyFile(sourcePath, path.join(stagingTaskDirectory, fileName)) + return true +} + +const pathExists = async (candidatePath: string) => { + try { + await fs.access(candidatePath) + return true + } catch { + return false + } +} + +export const isConcurrentDestinationClaimError = (error: unknown, destinationWasClaimed: boolean) => { + const nodeError = error as NodeJS.ErrnoException + return ( + destinationWasClaimed && + (nodeError.code === "EEXIST" || nodeError.code === "ENOTEMPTY" || nodeError.code === "EPERM") + ) +} + +const getImportableTaskFileNames = async (sourceTaskDirectory: string) => { + const fileNames: string[] = [] + + for (const fileName of IMPORTABLE_TASK_FILE_NAMES) { + try { + const filePath = path.join(sourceTaskDirectory, fileName) + if (await isRegularFile(filePath)) { + fileNames.push(fileName) + } + } catch (error) { + if (isAbsent(error)) { + continue + } + throw error + } + } + + return fileNames +} + +const collectImportableTaskPlans = async (sourceRoots: string[]) => { + const taskPlans: ImportableTaskPlan[] = [] + const taskIds = new Set() + + for (const sourceRoot of sourceRoots) { + const sourceTasksRoot = path.join(sourceRoot, "tasks") + let entries: Dirent[] + + try { + entries = await fs.readdir(sourceTasksRoot, { withFileTypes: true }) + } catch (error) { + const nodeError = error as NodeJS.ErrnoException + if (nodeError.code === "ENOENT") { + continue + } + throw error + } + + for (const entry of entries) { + if (!entry.isDirectory() || UNSAFE_TASK_ID_RE.test(entry.name)) { + continue + } + + // Preserve source-root priority: the first importable occurrence of a task ID wins. + if (taskIds.has(entry.name)) { + continue + } + + const sourceTaskDirectory = path.join(sourceTasksRoot, entry.name) + const fileNames = await getImportableTaskFileNames(sourceTaskDirectory) + + if (!fileNames.includes(GlobalFileNames.historyItem)) { + continue + } + + taskPlans.push({ + taskId: entry.name, + sourceTaskDirectory, + fileNames, + }) + taskIds.add(entry.name) + } + } + + return { + taskPlans, + totalTaskCount: taskPlans.length, + } +} + +export const resolveRooHistoryImportPaths = async (globalStoragePath: string): Promise => { + const zooExtensionDomain = `${Package.publisher}.${Package.name}` + const zooStorageRoot = await getStorageBasePath(globalStoragePath) + const rooDefaultStorageRoot = path.join(path.dirname(globalStoragePath), ROO_STORAGE_DIRECTORY) + const rooCustomStorageRoot = getConfiguredCustomStoragePath(ROO_CONFIGURATION_SECTION) + + return { + rooExtensionDomain: ROO_EXTENSION_DOMAIN, + zooExtensionDomain, + rooStorageRoots: dedupePaths([rooDefaultStorageRoot, ...(rooCustomStorageRoot ? [rooCustomStorageRoot] : [])]), + zooStorageRoot, + } +} + +export const importRooTaskHistory = async ( + globalStoragePath: string, + onProgress?: (progress: RooHistoryImportProgress) => Promise | void, +): Promise => { + const paths = await resolveRooHistoryImportPaths(globalStoragePath) + const destinationComparablePath = toComparablePath(paths.zooStorageRoot) + const sourceRoots = paths.rooStorageRoots.filter( + (sourceRoot) => toComparablePath(sourceRoot) !== destinationComparablePath, + ) + const destinationTasksRoot = path.join(paths.zooStorageRoot, "tasks") + const { taskPlans, totalTaskCount: foundTaskCount } = await collectImportableTaskPlans(sourceRoots) + const importedTaskIds = new Set() + let importedFileCount = 0 + let copiedFileCount = 0 + let stagingFileCount = 0 + // Tracks tasks whose history_item has been staged but not yet atomically + // promoted — used so progress reports show 1-based task count during copy. + let inFlightTaskCount = 0 + const importableTaskPlans: ImportableTaskPlan[] = [] + + await fs.mkdir(destinationTasksRoot, { recursive: true }) + + for (const taskPlan of taskPlans) { + const destinationTaskDirectory = path.join(destinationTasksRoot, taskPlan.taskId) + if (await pathExists(destinationTaskDirectory)) { + continue + } + + importableTaskPlans.push(taskPlan) + } + + const totalTaskCount = importableTaskPlans.length + let totalFileCount = importableTaskPlans.reduce((count, taskPlan) => count + taskPlan.fileNames.length, 0) + + const reportProgress = async (currentTaskId?: string, currentFileName?: string) => { + if (!onProgress) { + return + } + + await onProgress({ + copiedFileCount, + totalFileCount, + importedTaskCount: importedTaskIds.size + inFlightTaskCount, + totalTaskCount, + currentTaskId, + currentFileName, + }) + } + + await reportProgress() + + for (const taskPlan of importableTaskPlans) { + const destinationTaskDirectory = path.join(destinationTasksRoot, taskPlan.taskId) + + // Re-check under the loop — a concurrent import may have claimed this task. + if (await pathExists(destinationTaskDirectory)) { + totalFileCount -= taskPlan.fileNames.length + continue + } + + // Stage into a unique temp directory, then atomically rename to avoid + // leaving partial task directories that a retry would skip as already-present. + // Using mkdtemp ensures concurrent imports for the same task ID don't collide. + const stagingDirectory = await fs.mkdtemp(path.join(destinationTasksRoot, `_staging_${taskPlan.taskId}_`)) + stagingFileCount = 0 + + try { + const historyItemCopied = await copyTaskFileIfPresent( + taskPlan.sourceTaskDirectory, + stagingDirectory, + GlobalFileNames.historyItem, + ) + + if (!historyItemCopied) { + totalFileCount -= taskPlan.fileNames.length + await reportProgress(taskPlan.taskId, GlobalFileNames.historyItem) + continue + } + + // Validate the staged history_item.json: it must parse as a valid + // HistoryItem and its id must match the directory name exactly. + const stagedHistoryItemPath = path.join(stagingDirectory, GlobalFileNames.historyItem) + if (!(await validateHistoryItem(stagedHistoryItemPath, taskPlan.taskId))) { + totalFileCount -= taskPlan.fileNames.length + await reportProgress(taskPlan.taskId, GlobalFileNames.historyItem) + continue + } + + stagingFileCount += 1 + copiedFileCount += 1 + inFlightTaskCount = 1 + await reportProgress(taskPlan.taskId, GlobalFileNames.historyItem) + + for (const fileName of taskPlan.fileNames) { + if (fileName === GlobalFileNames.historyItem) { + continue + } + + if (await copyTaskFileIfPresent(taskPlan.sourceTaskDirectory, stagingDirectory, fileName)) { + stagingFileCount += 1 + copiedFileCount += 1 + } else { + totalFileCount -= 1 + } + + await reportProgress(taskPlan.taskId, fileName) + } + + // Atomic promotion: rename staging → destination. + // If destination was concurrently created, treat it as a skip. + try { + await fs.rename(stagingDirectory, destinationTaskDirectory) + importedTaskIds.add(taskPlan.taskId) + importedFileCount += stagingFileCount + } catch (renameError) { + const destinationWasClaimed = await pathExists(destinationTaskDirectory) + if (!isConcurrentDestinationClaimError(renameError, destinationWasClaimed)) { + throw renameError + } + // Destination claimed concurrently — discard staging, uncount progress. + copiedFileCount -= stagingFileCount + totalFileCount -= taskPlan.fileNames.length + } finally { + inFlightTaskCount = 0 + } + } finally { + await fs.rm(stagingDirectory, { recursive: true, force: true }) + } + } + + return { + ...paths, + rooStorageRoots: sourceRoots, + foundTaskCount, + importedTaskCount: importedTaskIds.size, + importedFileCount, + } +} diff --git a/src/core/task-persistence/index.ts b/src/core/task-persistence/index.ts index 115711e6fd..edc4d860b5 100644 --- a/src/core/task-persistence/index.ts +++ b/src/core/task-persistence/index.ts @@ -1,4 +1,4 @@ export { type ApiMessage, readApiMessages, saveApiMessages } from "./apiMessages" export { readTaskMessages, saveTaskMessages } from "./taskMessages" export { taskMetadata } from "./taskMetadata" -export { TaskHistoryStore } from "./TaskHistoryStore" +export { TaskHistoryStore, assertValidTransition } from "./TaskHistoryStore" diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index 4b77126971..ec2e6cceeb 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -24,7 +24,7 @@ export type TaskMetadataOptions = { /** Provider profile name for the task (sticky profile feature) */ apiConfigName?: string /** Initial status for the task (e.g., "active" for child tasks) */ - initialStatus?: "active" | "delegated" | "completed" + initialStatus?: "active" | "delegated" | "completed" | "interrupted" } export async function taskMetadata({ diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 2f1b370b48..7bee3c5e14 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -158,8 +158,9 @@ export interface TaskOptions extends CreateTaskOptions { initialTodos?: TodoItem[] workspacePath?: string /** Initial status for the task's history item (e.g., "active" for child tasks) */ - initialStatus?: "active" | "delegated" | "completed" + initialStatus?: "active" | "delegated" | "completed" | "interrupted" rateLimitClock?: RateLimitClock + diffFuzzyThreshold?: number } export class Task extends EventEmitter implements TaskLike { @@ -355,6 +356,29 @@ export class Task extends EventEmitter implements TaskLike { */ assistantMessageSavedToHistory = false + /** + * Fire-and-forget wrapper around `presentAssistantMessage` that swallows the + * expected cancellation rejection (the presenter throws when `this.abort` is set) + * and logs any other failure. Keeping it non-blocking preserves the streaming + * presenter's self-locking semantics while preventing unhandled promise rejections + * from crashing the extension host. + */ + private presentAssistantMessageSafe(): void { + void presentAssistantMessage(this).catch((error) => { + // Discriminate on the error message rather than `this.abort` state, + // which can flip between the throw and the catch microtask running: + // a real failure followed by an abort flip would otherwise be + // silently swallowed, and a stale abort error logged as a failure. + // The abort throw site in presentAssistantMessage emits a message + // ending in "aborted" (matching the other abort-throw contracts in + // this file), so we suppress exactly that. + if (error instanceof Error && error.message.endsWith("aborted")) { + return + } + console.error(`[Task#presentAssistantMessage] task ${this.taskId}.${this.instanceId} failed:`, error) + }) + } + /** * Push a tool_result block to userMessageContent, preventing duplicates. * Duplicate tool_use_ids cause API errors. @@ -407,7 +431,7 @@ export class Task extends EventEmitter implements TaskLike { private cloudSyncedMessageTimestamps: Set = new Set() // Initial status for the task's history item (set at creation time to avoid race conditions) - private readonly initialStatus?: "active" | "delegated" | "completed" + private readonly initialStatus?: "active" | "delegated" | "completed" | "interrupted" // MessageManager for high-level message operations (lazy initialized) private _messageManager?: MessageManager @@ -432,6 +456,7 @@ export class Task extends EventEmitter implements TaskLike { workspacePath, initialStatus, rateLimitClock, + diffFuzzyThreshold, }: TaskOptions) { super() @@ -520,7 +545,15 @@ export class Task extends EventEmitter implements TaskLike { this.messageQueueStateChangedHandler = () => { this.emit(RooCodeEventName.TaskUserMessage, this.taskId) this.emit(RooCodeEventName.QueuedMessagesUpdated, this.taskId, this.messageQueueService.messages) - this.providerRef.deref()?.postStateToWebviewWithoutTaskHistory() + void this.providerRef + .deref() + ?.postStateToWebviewWithoutTaskHistory() + .catch((error) => { + console.error( + "[Task#messageQueueStateChangedHandler] postStateToWebviewWithoutTaskHistory failed:", + error, + ) + }) } this.messageQueueService.on("stateChanged", this.messageQueueStateChangedHandler) @@ -529,7 +562,7 @@ export class Task extends EventEmitter implements TaskLike { this.setupProviderProfileChangeListener(provider) // Set up diff strategy - this.diffStrategy = new MultiSearchReplaceDiffStrategy() + this.diffStrategy = new MultiSearchReplaceDiffStrategy(diffFuzzyThreshold) this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit) @@ -565,9 +598,13 @@ export class Task extends EventEmitter implements TaskLike { if (startTask) { this._started = true if (task || images) { - this.startTask(task, images) + void this.startTask(task, images).catch((error) => { + console.error("[Task#constructor] startTask failed:", error) + }) } else if (historyItem) { - this.resumeTaskFromHistory() + void this.resumeTaskFromHistory().catch((error) => { + console.error("[Task#constructor] resumeTaskFromHistory failed:", error) + }) } else { throw new Error("Either historyItem or task/images must be provided") } @@ -1090,7 +1127,9 @@ export class Task extends EventEmitter implements TaskLike { // - Final state is emitted when updates stop (trailing: true) this.debouncedEmitTokenUsage(tokenUsage, this.toolUsage) - await this.providerRef.deref()?.updateTaskHistory(historyItem) + const provider = this.providerRef.deref() + const existingStatus = provider?.taskHistoryStore.get(this.taskId)?.status + await provider?.updateTaskHistory(existingStatus ? { ...historyItem, status: existingStatus } : historyItem) return true } catch (error) { console.error("Failed to save Roo messages:", error) @@ -1160,7 +1199,13 @@ export class Task extends EventEmitter implements TaskLike { // data or one whole message at a time so ignore partial for // saves, and only post parts of partial message instead of // whole array in new listener. - this.updateClineMessage(lastMessage) + // Fire-and-forget: the webview post is internally guarded, but + // the `RooCodeEventName.Message` emit can synchronously throw + // if any consumer-attached listener does, which would surface + // here as an unhandled rejection. Log it instead. + this.updateClineMessage(lastMessage).catch((error) => { + console.error("[Task#ask] updateClineMessage failed:", error) + }) // console.log("Task#ask: current ask promise was ignored (#1)") throw new AskIgnoredError("updating existing partial") } else { @@ -1201,7 +1246,11 @@ export class Task extends EventEmitter implements TaskLike { lastMessage.isAnswered = true } await this.saveClineMessages() - this.updateClineMessage(lastMessage) + // Fire-and-forget: see updateClineMessage call above for the + // rationale on the .catch arm. + this.updateClineMessage(lastMessage).catch((error) => { + console.error("[Task#ask] updateClineMessage failed:", error) + }) } else { // This is a new and complete message, so add it like normal. this.askResponse = undefined @@ -1272,7 +1321,10 @@ export class Task extends EventEmitter implements TaskLike { if (message) { this.interactiveAsk = message this.emit(RooCodeEventName.TaskInteractive, this.taskId) - provider?.postMessageToWebview({ type: "interactionRequired" }) + /* v8 ignore next 3 -- fires inside 2s timer after ask() resolves; not reachable in unit tests */ + void provider?.postMessageToWebview({ type: "interactionRequired" }).catch((error) => { + console.error("[Task#ask] postMessageToWebview interactionRequired failed:", error) + }) } }, statusMutationTimeout), ) @@ -1412,7 +1464,9 @@ export class Task extends EventEmitter implements TaskLike { ) if (lastToolAskIndex !== -1) { this.clineMessages[lastToolAskIndex].isAnswered = true - void this.updateClineMessage(this.clineMessages[lastToolAskIndex]) + void this.updateClineMessage(this.clineMessages[lastToolAskIndex]).catch((error) => { + console.error("[Task#handleWebviewAskResponse] updateClineMessage failed:", error) + }) this.saveClineMessages().catch((error) => { console.error("Failed to save answered tool-ask state:", error) }) @@ -1555,6 +1609,11 @@ export class Task extends EventEmitter implements TaskLike { const metadata: ApiHandlerCreateMessageMetadata = { mode, taskId: this.taskId, + ...(this.currentRequestAbortController?.signal + ? { + abortSignal: this.currentRequestAbortController.signal, + } + : {}), ...(allTools.length > 0 ? { tools: allTools, @@ -1655,7 +1714,13 @@ export class Task extends EventEmitter implements TaskLike { lastMessage.images = images lastMessage.partial = partial lastMessage.progressStatus = progressStatus - this.updateClineMessage(lastMessage) + // Fire-and-forget: webview post is internally guarded, but the + // `RooCodeEventName.Message` emit can synchronously throw via a + // consumer-attached listener. Surface that as a log, not an + // unhandled rejection. + this.updateClineMessage(lastMessage).catch((error) => { + console.error("[Task#say] updateClineMessage failed:", error) + }) } else { // This is a new partial message, so add it with partial state. const sayTs = Date.now() @@ -1694,7 +1759,11 @@ export class Task extends EventEmitter implements TaskLike { await this.saveClineMessages() // More performant than an entire `postStateToWebview`. - this.updateClineMessage(lastMessage) + // Fire-and-forget: see updateClineMessage call above for the + // rationale on the .catch arm. + this.updateClineMessage(lastMessage).catch((error) => { + console.error("[Task#say] updateClineMessage failed:", error) + }) } else { // This is a new and complete message, so add it like normal. const sayTs = Date.now() @@ -1786,13 +1855,10 @@ export class Task extends EventEmitter implements TaskLike { /** * Manually start a **new** task when it was created with `startTask: false`. * - * This fires `startTask` as a background async operation for the - * `task/images` code-path only. It does **not** handle the - * `historyItem` resume path (use the constructor with `startTask: true` - * for that). The primary use-case is in the delegation flow where the - * parent's metadata must be persisted to globalState **before** the - * child task begins writing its own history (avoiding a read-modify-write - * race on globalState). + * This fires task startup as a background async operation after the provider + * has installed the task in the stack and wired listeners. The primary + * use-case is delegation/rehydration flow where metadata and stack state + * must be in place before the task begins writing history or emitting asks. */ public start(): void { if (this._started) { @@ -1803,7 +1869,9 @@ export class Task extends EventEmitter implements TaskLike { const { task, images } = this.metadata if (task || images) { - this.startTask(task ?? undefined, images ?? undefined) + void this.startTask(task ?? undefined, images ?? undefined).catch((error) => { + console.error("[Task#start] startTask failed:", error) + }) } } @@ -2344,7 +2412,11 @@ export class Task extends EventEmitter implements TaskLike { private async initiateTaskLoop(userContent: Anthropic.Messages.ContentBlockParam[]): Promise { // Kicks off the checkpoints initialization process in the background. - getCheckpointService(this) + // `getCheckpointService` wraps its full body in a try/catch and returns + // `undefined` on failure (see src/core/checkpoints/index.ts), so the + // returned promise cannot reject. `void` is sufficient — no `.catch` + // arm needed. + void getCheckpointService(this) let nextUserContent = userContent let includeFileDetails = true @@ -2681,9 +2753,13 @@ export class Task extends EventEmitter implements TaskLike { if (signal.aborted) { reject(new Error("Request cancelled by user")) } else { - signal.addEventListener("abort", () => { - reject(new Error("Request cancelled by user")) - }) + signal.addEventListener( + "abort", + () => { + reject(new Error("Request cancelled by user")) + }, + { once: true }, + ) } }) return await Promise.race([nextPromise, abortPromise]) @@ -2787,7 +2863,8 @@ export class Task extends EventEmitter implements TaskLike { // Add to content and present this.assistantMessageContent.push(partialToolUse) this.userMessageContentReady = false - presentAssistantMessage(this) + /* v8 ignore next -- streaming presenter; .catch lives in presentAssistantMessageSafe (covered) */ + this.presentAssistantMessageSafe() } else if (event.type === "tool_call_delta") { // Process chunk using streaming JSON parser const partialToolUse = NativeToolCallParser.processStreamingChunk( @@ -2806,7 +2883,8 @@ export class Task extends EventEmitter implements TaskLike { this.assistantMessageContent[toolUseIndex] = partialToolUse // Present updated tool use - presentAssistantMessage(this) + /* v8 ignore next -- streaming presenter; .catch lives in presentAssistantMessageSafe (covered) */ + this.presentAssistantMessageSafe() } } } else if (event.type === "tool_call_end") { @@ -2832,7 +2910,8 @@ export class Task extends EventEmitter implements TaskLike { this.userMessageContentReady = false // Present the finalized tool call - presentAssistantMessage(this) + /* v8 ignore next -- streaming presenter; .catch lives in presentAssistantMessageSafe (covered) */ + this.presentAssistantMessageSafe() } else if (toolUseIndex !== undefined) { // finalizeStreamingToolCall returned null (malformed JSON or missing args) // Mark the tool as non-partial so it's presented as complete, but execution @@ -2851,7 +2930,8 @@ export class Task extends EventEmitter implements TaskLike { this.userMessageContentReady = false // Present the tool call - validation will handle missing params - presentAssistantMessage(this) + /* v8 ignore next -- streaming presenter; .catch lives in presentAssistantMessageSafe (covered) */ + this.presentAssistantMessageSafe() } } } @@ -2884,7 +2964,8 @@ export class Task extends EventEmitter implements TaskLike { // Present the tool call to user - presentAssistantMessage will execute // tools sequentially and accumulate all results in userMessageContent - presentAssistantMessage(this) + /* v8 ignore next -- streaming presenter; .catch lives in presentAssistantMessageSafe (covered) */ + this.presentAssistantMessageSafe() break } case "text": { @@ -2903,7 +2984,8 @@ export class Task extends EventEmitter implements TaskLike { }) this.userMessageContentReady = false } - presentAssistantMessage(this) + /* v8 ignore next -- streaming presenter; .catch lives in presentAssistantMessageSafe (covered) */ + this.presentAssistantMessageSafe() break } } @@ -3230,7 +3312,8 @@ export class Task extends EventEmitter implements TaskLike { this.userMessageContentReady = false // Present the finalized tool call - presentAssistantMessage(this) + /* v8 ignore next -- streaming presenter; .catch lives in presentAssistantMessageSafe (covered) */ + this.presentAssistantMessageSafe() } else if (toolUseIndex !== undefined) { // finalizeStreamingToolCall returned null (malformed JSON or missing args) // We still need to mark the tool as non-partial so it gets executed @@ -3249,7 +3332,8 @@ export class Task extends EventEmitter implements TaskLike { this.userMessageContentReady = false // Present the tool call - validation will handle missing params - presentAssistantMessage(this) + /* v8 ignore next -- streaming presenter; .catch lives in presentAssistantMessageSafe (covered) */ + this.presentAssistantMessageSafe() } } } @@ -3448,7 +3532,8 @@ export class Task extends EventEmitter implements TaskLike { // If there is content to update then it will complete and // update `this.userMessageContentReady` to true, which we // `pWaitFor` before making the next request. - presentAssistantMessage(this) + /* v8 ignore next -- streaming presenter; .catch lives in presentAssistantMessageSafe (covered) */ + this.presentAssistantMessageSafe() } if (hasTextContent || hasToolUses) { @@ -3727,7 +3812,10 @@ export class Task extends EventEmitter implements TaskLike { settings: this.apiConfiguration, }) - const contextWindow = modelInfo.contextWindow + // vscode-lm condenses against its static-table maxInputTokens (not the inflated live window); + // only it implements getCondenseContextWindow, so others fall back to the full contextWindow. + const contextWindow = this.api.getCondenseContextWindow?.() ?? modelInfo.contextWindow + const useAvailableInputForContextPercent = typeof this.api.getCondenseContextWindow === "function" // Get the current profile ID using the helper method const currentProfileId = this.getCurrentProfileId(state) @@ -3763,6 +3851,11 @@ export class Task extends EventEmitter implements TaskLike { const metadata: ApiHandlerCreateMessageMetadata = { mode, taskId: this.taskId, + ...(this.currentRequestAbortController?.signal + ? { + abortSignal: this.currentRequestAbortController.signal, + } + : {}), ...(allTools.length > 0 ? { tools: allTools, @@ -3791,6 +3884,7 @@ export class Task extends EventEmitter implements TaskLike { currentProfileId, metadata, environmentDetails, + useAvailableInputForContextPercent, }) if (truncateResult.messages !== this.apiConversationHistory) { @@ -3918,7 +4012,10 @@ export class Task extends EventEmitter implements TaskLike { settings: this.apiConfiguration, }) - const contextWindow = modelInfo.contextWindow + // vscode-lm condenses against its static-table maxInputTokens (not the inflated live window); + // only it implements getCondenseContextWindow, so others fall back to the full contextWindow. + const contextWindow = this.api.getCondenseContextWindow?.() ?? modelInfo.contextWindow + const useAvailableInputForContextPercent = typeof this.api.getCondenseContextWindow === "function" // Get the current profile ID using the helper method const currentProfileId = this.getCurrentProfileId(state) @@ -3943,6 +4040,7 @@ export class Task extends EventEmitter implements TaskLike { profileThresholds, currentProfileId, lastMessageTokens, + useAvailableInputForContextPercent, }) // Send condenseTaskContextStarted BEFORE manageContext to show in-progress indicator @@ -3979,6 +4077,11 @@ export class Task extends EventEmitter implements TaskLike { const contextMgmtMetadata: ApiHandlerCreateMessageMetadata = { mode, taskId: this.taskId, + ...(this.currentRequestAbortController?.signal + ? { + abortSignal: this.currentRequestAbortController.signal, + } + : {}), ...(contextMgmtTools.length > 0 ? { tools: contextMgmtTools, @@ -4020,6 +4123,7 @@ export class Task extends EventEmitter implements TaskLike { filesReadByRoo: contextMgmtFilesReadByRoo, cwd: this.cwd, rooIgnoreController: this.rooIgnoreController, + useAvailableInputForContextPercent, }) if (truncateResult.messages !== this.apiConversationHistory) { await this.overwriteApiConversationHistory(truncateResult.messages) @@ -4141,10 +4245,15 @@ export class Task extends EventEmitter implements TaskLike { const shouldIncludeTools = allTools.length > 0 + // Create an AbortController to allow cancelling the request mid-stream + this.currentRequestAbortController = new AbortController() + const abortSignal = this.currentRequestAbortController.signal + const metadata: ApiHandlerCreateMessageMetadata = { mode: mode, taskId: this.taskId, suppressPreviousResponseId: this.skipPrevResponseIdOnce, + abortSignal, // Include tools whenever they are present. ...(shouldIncludeTools ? { @@ -4157,10 +4266,6 @@ export class Task extends EventEmitter implements TaskLike { } : {}), } - - // Create an AbortController to allow cancelling the request mid-stream - this.currentRequestAbortController = new AbortController() - const abortSignal = this.currentRequestAbortController.signal // Reset the flag after using it this.skipPrevResponseIdOnce = false @@ -4173,10 +4278,14 @@ export class Task extends EventEmitter implements TaskLike { const iterator = stream[Symbol.asyncIterator]() // Set up abort handling - when the signal is aborted, clean up the controller reference - abortSignal.addEventListener("abort", () => { - console.log(`[Task#${this.taskId}.${this.instanceId}] AbortSignal triggered for current request`) - this.currentRequestAbortController = undefined - }) + abortSignal.addEventListener( + "abort", + () => { + console.log(`[Task#${this.taskId}.${this.instanceId}] AbortSignal triggered for current request`) + this.currentRequestAbortController = undefined + }, + { once: true }, + ) try { // Awaiting first chunk to see if it will throw an error. @@ -4188,9 +4297,13 @@ export class Task extends EventEmitter implements TaskLike { if (abortSignal.aborted) { reject(new Error("Request cancelled by user")) } else { - abortSignal.addEventListener("abort", () => { - reject(new Error("Request cancelled by user")) - }) + abortSignal.addEventListener( + "abort", + () => { + reject(new Error("Request cancelled by user")) + }, + { once: true }, + ) } }) @@ -4199,9 +4312,12 @@ export class Task extends EventEmitter implements TaskLike { this.isWaitingForFirstChunk = false } catch (error) { this.isWaitingForFirstChunk = false - this.currentRequestAbortController = undefined const isContextWindowExceededError = checkContextWindowExceededError(error) + if (!isContextWindowExceededError) { + this.currentRequestAbortController = undefined + } + // If it's a context window error and we haven't exceeded max retries for this error type if (isContextWindowExceededError && retryAttempt < MAX_CONTEXT_WINDOW_RETRIES) { console.warn( diff --git a/src/core/task/__tests__/Task.dispose.test.ts b/src/core/task/__tests__/Task.dispose.test.ts index 16bf3c91c2..9d61cd67c1 100644 --- a/src/core/task/__tests__/Task.dispose.test.ts +++ b/src/core/task/__tests__/Task.dispose.test.ts @@ -10,6 +10,18 @@ vi.mock("../../../integrations/terminal/TerminalRegistry", () => ({ releaseTerminalsForTask: vi.fn(), }, })) +// dispose() fires an UNawaited getTaskDirectoryPath -> OutputInterceptor.cleanup chain. +// Mock both so it resolves immediately with no real fs and no late console.error, +// otherwise that dangling promise logs after the test ends and trips Vitest's +// "Closing rpc while onUserConsoleLog was pending" teardown race. +vi.mock("../../../utils/storage", () => ({ + getTaskDirectoryPath: vi.fn().mockResolvedValue("/test/path/tasks/test-task"), +})) +vi.mock("../../../integrations/terminal/OutputInterceptor", () => ({ + OutputInterceptor: { + cleanup: vi.fn().mockResolvedValue(undefined), + }, +})) vi.mock("../../ignore/RooIgnoreController") vi.mock("../../protect/RooProtectedController") vi.mock("../../context-tracking/FileContextTracker") diff --git a/src/core/task/__tests__/Task.persistence.spec.ts b/src/core/task/__tests__/Task.persistence.spec.ts index 17c0bd9acc..1761db5bc3 100644 --- a/src/core/task/__tests__/Task.persistence.spec.ts +++ b/src/core/task/__tests__/Task.persistence.spec.ts @@ -73,26 +73,30 @@ vi.mock("p-wait-for", () => ({ default: mockPWaitFor, })) -vi.mock("../../task-persistence", () => ({ - saveApiMessages: mockSaveApiMessages, - saveTaskMessages: mockSaveTaskMessages, - readApiMessages: mockReadApiMessages, - readTaskMessages: mockReadTaskMessages, - taskMetadata: mockTaskMetadata, - TaskHistoryStore: vi.fn().mockImplementation(function () { - return { - initialize: vi.fn().mockResolvedValue(undefined), - dispose: vi.fn(), - get: vi.fn(), - getAll: vi.fn().mockReturnValue([]), - upsert: vi.fn().mockResolvedValue([]), - delete: vi.fn().mockResolvedValue(undefined), - deleteMany: vi.fn().mockResolvedValue(undefined), - reconcile: vi.fn().mockResolvedValue(undefined), - initialized: Promise.resolve(), - } - }), -})) +vi.mock("../../task-persistence", async (importOriginal) => { + const mod = await importOriginal() + return { + ...mod, + saveApiMessages: mockSaveApiMessages, + saveTaskMessages: mockSaveTaskMessages, + readApiMessages: mockReadApiMessages, + readTaskMessages: mockReadTaskMessages, + taskMetadata: mockTaskMetadata, + TaskHistoryStore: vi.fn().mockImplementation(function () { + return { + initialize: vi.fn().mockResolvedValue(undefined), + dispose: vi.fn(), + get: vi.fn(), + getAll: vi.fn().mockReturnValue([]), + upsert: vi.fn().mockResolvedValue([]), + delete: vi.fn().mockResolvedValue(undefined), + deleteMany: vi.fn().mockResolvedValue(undefined), + reconcile: vi.fn().mockResolvedValue(undefined), + initialized: Promise.resolve(), + } + }), + } +}) vi.mock("vscode", () => { const mockDisposable = { dispose: vi.fn() } @@ -260,6 +264,7 @@ describe("Task persistence", () => { mockProvider.postStateToWebview = vi.fn().mockResolvedValue(undefined) mockProvider.postStateToWebviewWithoutTaskHistory = vi.fn().mockResolvedValue(undefined) mockProvider.updateTaskHistory = vi.fn().mockResolvedValue(undefined) + mockProvider.log = vi.fn() }) // ── saveApiConversationHistory (via retrySaveApiConversationHistory) ── @@ -418,6 +423,51 @@ describe("Task persistence", () => { // But the content should be the same expect(callArgs.messages).toEqual(task.clineMessages) }) + + it("preserves an existing lifecycle status during metadata saves", async () => { + mockSaveTaskMessages.mockResolvedValueOnce(undefined) + mockTaskMetadata.mockResolvedValueOnce({ + historyItem: { + id: "task-with-advanced-status", + ts: Date.now(), + task: "test", + status: "interrupted", + tokensIn: 10, + }, + tokenUsage: { + totalTokensIn: 10, + totalTokensOut: 0, + totalCacheWrites: 0, + totalCacheReads: 0, + totalCost: 0, + contextTokens: 0, + }, + }) + + const updateTaskHistory = vi.fn().mockResolvedValue([]) + const taskHistoryStore = { + get: vi.fn().mockReturnValue({ id: "task-with-advanced-status", status: "completed" }), + } + const provider = { ...mockProvider, updateTaskHistory, taskHistoryStore } + const task = new Task({ + provider: provider as any, + apiConfiguration: mockApiConfig, + taskId: "task-with-advanced-status", + task: "test task", + startTask: false, + initialStatus: "interrupted", + }) + + await (task as Record).saveClineMessages() + + expect(updateTaskHistory).toHaveBeenCalledWith( + expect.objectContaining({ + id: "task-with-advanced-status", + status: "completed", + tokensIn: 10, + }), + ) + }) }) // ── flushPendingToolResultsToHistory — save failure/success ─────────── diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index dd63135802..27ba5ce8ff 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -11,6 +11,7 @@ import { TelemetryService } from "@roo-code/telemetry" import { Task } from "../Task" import { createRateLimitClock } from "../RateLimitClock" +import { summarizeConversation } from "../../condense" import { ClineProvider } from "../../webview/ClineProvider" import { ApiStreamChunk } from "../../../api/transform/stream" import { ContextProxy } from "../../config/ContextProxy" @@ -1478,7 +1479,7 @@ describe("Cline", () => { ] // Call submitUserMessage - task.submitUserMessage("test message", ["image1.png"]) + await task.submitUserMessage("test message", ["image1.png"]) // Verify handleWebviewAskResponse was called directly (not webview) expect(handleResponseSpy).toHaveBeenCalledWith("messageResponse", "test message", ["image1.png"]) @@ -1498,13 +1499,13 @@ describe("Cline", () => { const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse") // Call with empty text and no images - task.submitUserMessage("", []) + await task.submitUserMessage("", []) // Should not call handleWebviewAskResponse for empty messages expect(handleResponseSpy).not.toHaveBeenCalled() // Call with whitespace only - task.submitUserMessage(" ", []) + await task.submitUserMessage(" ", []) expect(handleResponseSpy).not.toHaveBeenCalled() }) @@ -1521,7 +1522,7 @@ describe("Cline", () => { // Test with no messages (new task scenario) task.clineMessages = [] - task.submitUserMessage("new task", ["image1.png"]) + await task.submitUserMessage("new task", ["image1.png"]) expect(handleResponseSpy).toHaveBeenCalledWith("messageResponse", "new task", ["image1.png"]) @@ -1537,7 +1538,7 @@ describe("Cline", () => { text: "Initial message", }, ] - task.submitUserMessage("follow-up message", ["image2.png"]) + await task.submitUserMessage("follow-up message", ["image2.png"]) expect(handleResponseSpy).toHaveBeenCalledWith("messageResponse", "follow-up message", ["image2.png"]) }) @@ -1564,7 +1565,7 @@ describe("Cline", () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) // Should log error but not throw - task.submitUserMessage("test message") + await task.submitUserMessage("test message") expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#submitUserMessage] Provider reference lost") expect(handleResponseSpy).not.toHaveBeenCalled() @@ -1795,6 +1796,516 @@ describe("Cline", () => { // Verify cancelCurrentRequest was called expect(cancelSpy).toHaveBeenCalled() }) + describe("abortSignal", () => { + it("should pass AbortController signal to condenseContext metadata when a current request exists", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + task.currentRequestAbortController = new AbortController() + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + + await task.condenseContext() + + expect(summarizeConversation).toHaveBeenCalled() + const [options] = vi.mocked(summarizeConversation).mock.calls.at(-1)! + expect(options.metadata?.abortSignal).toBeInstanceOf(AbortSignal) + }) + + it("should omit abortSignal from condenseContext metadata when no current request exists", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + + await task.condenseContext() + + expect(summarizeConversation).toHaveBeenCalled() + const [options] = vi.mocked(summarizeConversation).mock.calls.at(-1)! + expect(options.metadata).toBeDefined() + expect("abortSignal" in (options.metadata ?? {})).toBe(false) + }) + + it("should pass AbortController signal to createMessage metadata", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Mock required methods for attemptApiRequest to work without hanging + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200000, + maxTokens: 4096, + inputPrice: 0.3, + outputPrice: 1.5, + } as ModelInfo, + }) + + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration: mockApiConfig, + autoApprovalEnabled: true, + requestDelaySeconds: 0, + }) + + // Mock the API stream response + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { type: "text", text: "response" } + }, + async next() { + return { done: true, value: { type: "text", text: "response" } } + }, + async return() { + return { done: true, value: undefined } + }, + async throw(e: any) { + throw e + }, + [Symbol.asyncDispose]: async () => {}, + } as AsyncGenerator + + const createMessageSpy = vi.spyOn(task.api, "createMessage").mockReturnValue(mockStream) + + task.apiConversationHistory = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "test message" }], + ts: Date.now(), + }, + ] as any + + const iterator = task.attemptApiRequest(0) + await iterator.next() + + // Verify createMessage was called with metadata containing abortSignal + expect(createMessageSpy).toHaveBeenCalled() + const [, , metadata] = createMessageSpy.mock.calls[0]! + + expect(metadata).toBeDefined() + expect(metadata!.abortSignal).toBeInstanceOf(AbortSignal) + }) + + it("should invoke abort on currentRequestAbortController during first-chunk wait", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + const abortSpy = vi.fn() + task.currentRequestAbortController = { + abort: abortSpy, + signal: new AbortController().signal, + } as AbortController + + task.cancelCurrentRequest() + + expect(abortSpy).toHaveBeenCalledTimes(1) + expect(task.currentRequestAbortController).toBeUndefined() + }) + + it("should reject streaming consumption when aborted between chunks", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200000, + maxTokens: 4096, + inputPrice: 0.3, + outputPrice: 1.5, + } as ModelInfo, + }) + + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration: mockApiConfig, + autoApprovalEnabled: true, + requestDelaySeconds: 0, + }) + + const createMessageSpy = vi.fn((_systemPrompt, _messages, metadata) => { + let callCount = 0 + return { + [Symbol.asyncIterator]() { + return this + }, + next: () => { + callCount++ + if (callCount === 1) { + return Promise.resolve({ + done: false, + value: { type: "text", text: "first chunk" }, + }) + } + return new Promise>((resolve, reject) => { + if (metadata?.abortSignal?.aborted) { + return reject(new Error("Request cancelled by user")) + } + metadata?.abortSignal?.addEventListener("abort", () => { + reject(new Error("Request cancelled by user")) + }) + }) + }, + async return() { + return { done: true, value: undefined } + }, + async throw(e: any) { + throw e + }, + [Symbol.asyncDispose]: async () => {}, + } as AsyncGenerator + }) + vi.spyOn(task.api, "createMessage").mockImplementation(createMessageSpy) + + task.apiConversationHistory = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "test message" }], + ts: Date.now(), + }, + ] as any + + const streamIterator = task.attemptApiRequest(0) + await expect(streamIterator.next()).resolves.toMatchObject({ + done: false, + value: { type: "text", text: "first chunk" }, + }) + + task.cancelCurrentRequest() + + await expect(streamIterator.next()).rejects.toThrow("Request cancelled by user") + expect(createMessageSpy).toHaveBeenCalledTimes(1) + }) + + it("should use the same AbortController signal as currentRequestAbortController", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Mock required methods for attemptApiRequest to work without hanging + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200000, + maxTokens: 4096, + inputPrice: 0.3, + outputPrice: 1.5, + } as ModelInfo, + }) + + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration: mockApiConfig, + autoApprovalEnabled: true, + requestDelaySeconds: 0, + }) + + // Mock the API stream response + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { type: "text", text: "response" } + }, + async next() { + return { done: true, value: { type: "text", text: "response" } } + }, + async return() { + return { done: true, value: undefined } + }, + async throw(e: any) { + throw e + }, + [Symbol.asyncDispose]: async () => {}, + } as AsyncGenerator + + const createMessageSpy = vi.spyOn(task.api, "createMessage").mockReturnValue(mockStream) + + task.apiConversationHistory = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "test message" }], + ts: Date.now(), + }, + ] as any + + const iterator = task.attemptApiRequest(0) + await iterator.next() + + // Get the signal from metadata + const [, , metadata] = createMessageSpy.mock.calls[0]! + const metadataSignal = metadata!.abortSignal + + // The signal in metadata should be the same as the one from currentRequestAbortController + expect(metadataSignal).toBe(task.currentRequestAbortController!.signal) + }) + + it("should omit createMessage abortSignal metadata when no current request exists before condense metadata checks", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200000, + maxTokens: 4096, + inputPrice: 0.3, + outputPrice: 1.5, + } as ModelInfo, + }) + + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration: mockApiConfig, + autoApprovalEnabled: true, + requestDelaySeconds: 0, + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { type: "text", text: "response" } + }, + async next() { + return { done: true, value: { type: "text", text: "response" } } + }, + async return() { + return { done: true, value: undefined } + }, + async throw(e: any) { + throw e + }, + [Symbol.asyncDispose]: async () => {}, + } as AsyncGenerator + + const createMessageSpy = vi.spyOn(task.api, "createMessage").mockReturnValue(mockStream) + task.apiConversationHistory = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "test message" }], + ts: Date.now(), + }, + ] as any + + expect(task.currentRequestAbortController).toBeUndefined() + + const iterator = task.attemptApiRequest(0) + await iterator.next() + + const [, , metadata] = createMessageSpy.mock.calls[0]! + expect(metadata).toBeDefined() + expect("abortSignal" in metadata!).toBe(true) + expect(metadata!.abortSignal).toBeInstanceOf(AbortSignal) + }) + + it("should keep createMessage abortSignal metadata unaborted before cancellation", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200000, + maxTokens: 4096, + inputPrice: 0.3, + outputPrice: 1.5, + } as ModelInfo, + }) + + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration: mockApiConfig, + autoApprovalEnabled: true, + requestDelaySeconds: 0, + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { type: "text", text: "response" } + }, + async next() { + return { done: false, value: { type: "text", text: "response" } } + }, + async return() { + return { done: true, value: undefined } + }, + async throw(e: any) { + throw e + }, + [Symbol.asyncDispose]: async () => {}, + } as AsyncGenerator + + const createMessageSpy = vi.spyOn(task.api, "createMessage").mockReturnValue(mockStream) + task.apiConversationHistory = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "test message" }], + ts: Date.now(), + }, + ] as any + + const iterator = task.attemptApiRequest(0) + await iterator.next() + + const [, , metadata] = createMessageSpy.mock.calls[0]! + expect(metadata?.abortSignal).toBeInstanceOf(AbortSignal) + expect(metadata?.abortSignal?.aborted).toBe(false) + }) + }) + + it("should propagate AbortController signal through attemptApiRequest context-window retry path", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task, "getTokenUsage").mockReturnValue({ + totalCost: 0, + totalTokensIn: 0, + totalTokensOut: 0, + contextTokens: 120000, + }) + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 1000, + maxTokens: 4096, + inputPrice: 0.3, + outputPrice: 1.5, + } as ModelInfo, + }) + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration: mockApiConfig, + mode: "code", + autoCondenseContext: true, + autoCondenseContextPercent: 80, + requestDelaySeconds: 0, + customModes: [], + experiments: {}, + disabledTools: [], + customSupportPrompts: {}, + autoApprovalEnabled: true, + profileThresholds: {}, + currentApiConfigName: "default", + }) + + task.apiConversationHistory = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "test message" }], + ts: Date.now(), + }, + ] as any + + let firstCall = true + const retryStream = { + async *[Symbol.asyncIterator]() { + yield { type: "text", text: "retried response" } + }, + async next() { + return { done: false, value: { type: "text", text: "retried response" } } + }, + async return() { + return { done: true, value: undefined } + }, + async throw(e: any) { + throw e + }, + [Symbol.asyncDispose]: async () => {}, + } as AsyncGenerator + + const contextWindowErrorStream = { + [Symbol.asyncIterator]() { + return this + }, + async next() { + throw { status: 400, message: "context length exceeded" } + }, + async return() { + return { done: true, value: undefined } + }, + async throw(e: any) { + throw e + }, + [Symbol.asyncDispose]: async () => {}, + } as AsyncGenerator + + vi.spyOn(task.api, "createMessage").mockImplementation(() => { + if (firstCall) { + firstCall = false + return contextWindowErrorStream + } + return retryStream + }) + + const iterator = task.attemptApiRequest(0) + await expect(iterator.next()).resolves.toMatchObject({ + done: false, + value: { type: "text", text: "retried response" }, + }) + + expect(summarizeConversation).toHaveBeenCalled() + const [options] = vi.mocked(summarizeConversation).mock.calls.at(-1)! + expect(options.metadata?.taskId).toBe(task.taskId) + expect(options.metadata?.abortSignal).toBeInstanceOf(AbortSignal) + expect(options.metadata?.abortSignal?.aborted).toBe(false) + }) }) }) @@ -1840,6 +2351,373 @@ describe("Cline", () => { startTaskSpy.mockRestore() }) }) + + describe("unhandled-rejection guards on void async calls", () => { + // PR #253 wired `.catch(...)` onto every fire-and-forget async call that + // Copilot flagged as a potential unhandled-rejection source. These specs + // pin that behavior so a future refactor cannot silently drop the + // handler and reintroduce the crash risk on the extension host. + + const flushMicrotasks = () => new Promise((resolve) => setImmediate(resolve)) + + let consoleErrorSpy: ReturnType + + beforeEach(() => { + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + }) + + afterEach(() => { + consoleErrorSpy.mockRestore() + vi.restoreAllMocks() + }) + + it("logs (instead of crashing) when startTask rejects from the constructor", async () => { + const boom = new Error("startTask boom") + const startTaskSpy = vi.spyOn(Task.prototype as any, "startTask").mockImplementation(async () => { + throw boom + }) + + new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: true, + }) + + expect(startTaskSpy).toHaveBeenCalledTimes(1) + await flushMicrotasks() + + expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#constructor] startTask failed:", boom) + startTaskSpy.mockRestore() + }) + + it("logs (instead of crashing) when resumeTaskFromHistory rejects from the constructor", async () => { + const boom = new Error("resume boom") + const resumeSpy = vi.spyOn(Task.prototype as any, "resumeTaskFromHistory").mockImplementation(async () => { + throw boom + }) + + new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + historyItem: { + id: "123", + number: 0, + ts: Date.now(), + task: "historical task", + tokensIn: 100, + tokensOut: 200, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.001, + }, + startTask: true, + }) + + expect(resumeSpy).toHaveBeenCalledTimes(1) + await flushMicrotasks() + + expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#constructor] resumeTaskFromHistory failed:", boom) + resumeSpy.mockRestore() + }) + + it("logs (instead of crashing) when postStateToWebviewWithoutTaskHistory rejects from the queue handler", async () => { + const boom = new Error("postState boom") + mockProvider.postStateToWebviewWithoutTaskHistory = vi.fn().mockRejectedValue(boom) + + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Triggers messageQueueStateChangedHandler -> void postStateToWebviewWithoutTaskHistory() + task.messageQueueService.addMessage("queued text") + await flushMicrotasks() + + expect(mockProvider.postStateToWebviewWithoutTaskHistory).toHaveBeenCalled() + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[Task#messageQueueStateChangedHandler] postStateToWebviewWithoutTaskHistory failed:", + boom, + ) + }) + + it("logs (instead of crashing) when startTask rejects from start()", async () => { + const boom = new Error("start() boom") + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + vi.spyOn(task as any, "startTask").mockImplementation(async () => { + throw boom + }) + + task.start() + await flushMicrotasks() + + expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#start] startTask failed:", boom) + }) + + it("swallows the expected abort rejection from presentAssistantMessageSafe", async () => { + const assistantMessageModule = await import("../../assistant-message") + const presentSpy = vi + .spyOn(assistantMessageModule, "presentAssistantMessage") + .mockRejectedValue(new Error("[Task#presentAssistantMessage] task t.i aborted")) + + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Drain any unrelated console.error noise emitted by async constructor side effects + // (CloudService/getState complaints in the test harness) so we only assert on the + // abort-path behavior under test. + await flushMicrotasks() + consoleErrorSpy.mockClear() + + task.abort = true + ;(task as any).presentAssistantMessageSafe() + await flushMicrotasks() + + expect(presentSpy).toHaveBeenCalledTimes(1) + const presentErrors = consoleErrorSpy.mock.calls.filter( + (call: unknown[]) => typeof call[0] === "string" && call[0].includes("[Task#presentAssistantMessage]"), + ) + expect(presentErrors).toHaveLength(0) + }) + + it("logs non-abort rejections from presentAssistantMessageSafe", async () => { + const assistantMessageModule = await import("../../assistant-message") + const boom = new Error("present boom") + const presentSpy = vi.spyOn(assistantMessageModule, "presentAssistantMessage").mockRejectedValue(boom) + + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + expect(task.abort).toBeFalsy() + ;(task as any).presentAssistantMessageSafe() + await flushMicrotasks() + + expect(presentSpy).toHaveBeenCalledTimes(1) + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("[Task#presentAssistantMessage] task"), + boom, + ) + }) + + it("logs a non-abort error even when this.abort flips true after the throw", async () => { + // Pins that the message-based discriminator is load-bearing, not the + // state check. Under the previous `if (this.abort) return` guard this + // case (a genuine downstream failure racing with an abort flip between + // the throw and the catch microtask) would silently swallow the error. + const assistantMessageModule = await import("../../assistant-message") + const realError = new Error("genuine downstream failure") + const presentSpy = vi.spyOn(assistantMessageModule, "presentAssistantMessage").mockRejectedValue(realError) + + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + await flushMicrotasks() + consoleErrorSpy.mockClear() + + // Simulate the TOCTOU race: abort flips between throw and catch. + task.abort = true + ;(task as any).presentAssistantMessageSafe() + await flushMicrotasks() + + expect(presentSpy).toHaveBeenCalledTimes(1) + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("[Task#presentAssistantMessage] task"), + realError, + ) + }) + + it("suppresses an abort-pattern error by message match even when this.abort is false", async () => { + // Pins the inverse: message wins over state. A stale abort rejection + // arriving before `this.abort` has been observed as true must still be + // suppressed, so the catch handler never logs the expected + // cancellation rejection as a real failure. + const assistantMessageModule = await import("../../assistant-message") + const abortError = new Error("[Task#presentAssistantMessage] task t.i aborted") + const presentSpy = vi.spyOn(assistantMessageModule, "presentAssistantMessage").mockRejectedValue(abortError) + + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + await flushMicrotasks() + consoleErrorSpy.mockClear() + + expect(task.abort).toBeFalsy() + ;(task as any).presentAssistantMessageSafe() + await flushMicrotasks() + + expect(presentSpy).toHaveBeenCalledTimes(1) + const presentErrors = consoleErrorSpy.mock.calls.filter( + (call: unknown[]) => typeof call[0] === "string" && call[0].includes("[Task#presentAssistantMessage]"), + ) + expect(presentErrors).toHaveLength(0) + }) + + it("logs (instead of crashing) when updateClineMessage rejects from the say() partial-update path", async () => { + // Pins the symmetric .catch arm on the fire-and-forget + // updateClineMessage call in say(). The callee's webview post is + // internally guarded, but its synchronous emit can throw via a + // consumer-attached listener — that path must surface as a log, + // not an unhandled rejection. + const boom = new Error("updateClineMessage boom") + const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { + throw boom + }) + + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Seed a prior partial "say" so the partial-update branch fires. + task.clineMessages.push({ + ts: Date.now() - 1, + type: "say", + say: "text", + text: "partial", + partial: true, + }) + + await task.say("text", "updated partial", undefined, true) + await flushMicrotasks() + + expect(updateSpy).toHaveBeenCalled() + expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#say] updateClineMessage failed:", boom) + updateSpy.mockRestore() + }) + + it("logs (instead of crashing) when updateClineMessage rejects from the ask() complete-partial path", async () => { + // Pins the symmetric .catch arm on the fire-and-forget + // updateClineMessage call in ask() when finalizing a partial. + const boom = new Error("updateClineMessage boom") + const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { + throw boom + }) + const saveSpy = vi.spyOn(Task.prototype as any, "saveClineMessages").mockResolvedValue(true) + + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Seed a prior partial "ask" of type "tool" so the complete-partial + // branch fires when ask("tool", ..., false) is called. + task.clineMessages.push({ + ts: Date.now() - 1, + type: "ask", + ask: "tool", + text: "partial", + partial: true, + }) + + // ask() resolves only after a response — fire-and-forget so the + // promise the suite awaits stays bounded. The .catch on the + // pending ask handles the never-resolved promise. + void task.ask("tool", "complete", false).catch(() => {}) + await flushMicrotasks() + + expect(updateSpy).toHaveBeenCalled() + expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#ask] updateClineMessage failed:", boom) + updateSpy.mockRestore() + saveSpy.mockRestore() + }) + + it("logs (instead of crashing) when updateClineMessage rejects from the ask() ignore-partial path", async () => { + // Pins the .catch arm on the fire-and-forget updateClineMessage call + // in ask() when a new partial ask arrives while the previous partial + // is still pending (AskIgnoredError path). + const boom = new Error("updateClineMessage boom") + const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { + throw boom + }) + + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Seed a prior partial ask so the isUpdatingPreviousPartial branch fires. + task.clineMessages.push({ + ts: Date.now() - 1, + type: "ask", + ask: "tool", + text: "partial", + partial: true, + }) + + // Sending a new partial of the same type triggers updateClineMessage + // then throws AskIgnoredError — catch it so the test doesn't fail. + await task.ask("tool", "updated partial", true).catch(() => {}) + await flushMicrotasks() + + expect(updateSpy).toHaveBeenCalled() + expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#ask] updateClineMessage failed:", boom) + }) + + it("logs (instead of crashing) when updateClineMessage rejects from handleWebviewAskResponse", async () => { + // Pins the .catch arm on the fire-and-forget updateClineMessage call + // in handleWebviewAskResponse when marking a tool ask as answered. + const boom = new Error("updateClineMessage boom") + const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { + throw boom + }) + vi.spyOn(Task.prototype as any, "saveClineMessages").mockResolvedValue(undefined) + + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Seed an unanswered tool ask so the lastToolAskIndex branch fires. + task.clineMessages.push({ + ts: Date.now() - 1, + type: "ask", + ask: "tool", + text: "tool call", + partial: false, + }) + + task.handleWebviewAskResponse("yesButtonClicked") + await flushMicrotasks() + + expect(updateSpy).toHaveBeenCalled() + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[Task#handleWebviewAskResponse] updateClineMessage failed:", + boom, + ) + }) + }) }) describe("Queued message processing after condense", () => { diff --git a/src/core/task/__tests__/Task.throttle.test.ts b/src/core/task/__tests__/Task.throttle.test.ts index c9d78dc291..34d78a4ef9 100644 --- a/src/core/task/__tests__/Task.throttle.test.ts +++ b/src/core/task/__tests__/Task.throttle.test.ts @@ -33,7 +33,8 @@ vi.mock("@roo-code/telemetry", () => ({ })) // Mock task persistence to avoid disk writes -vi.mock("../../task-persistence", () => ({ +vi.mock("../../task-persistence", async (importOriginal) => ({ + ...(await importOriginal()), readApiMessages: vi.fn().mockResolvedValue([]), saveApiMessages: vi.fn().mockResolvedValue(undefined), readTaskMessages: vi.fn().mockResolvedValue([]), diff --git a/src/core/tools/AskFollowupQuestionTool.ts b/src/core/tools/AskFollowupQuestionTool.ts index 857ee7c152..3baea7ed52 100644 --- a/src/core/tools/AskFollowupQuestionTool.ts +++ b/src/core/tools/AskFollowupQuestionTool.ts @@ -12,6 +12,9 @@ interface Suggestion { interface AskFollowupQuestionParams { question: string + // follow_up is typed as an array, but at runtime the value may arrive as a + // non-array (object/string/number) due to incremental JSON parsing, so the + // runtime validation in execute() guards against that explicitly. follow_up: Suggestion[] } @@ -29,17 +32,37 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> { pushToolResult(await task.sayAndCreateMissingParamError("ask_followup_question", paramName)) } + const recordValidationError = async (message: string): Promise => { + task.consecutiveMistakeCount++ + task.recordToolError("ask_followup_question") + task.didToolFailInCurrentTurn = true + await task.say("error", message) + pushToolResult(formatResponse.toolError(message)) + } + try { if (!question) { await recordMissingParamError("question") return } - if (!follow_up || !Array.isArray(follow_up)) { + // Truly missing follow_up (null/undefined) -> report as a missing parameter. + if (follow_up === undefined || follow_up === null) { await recordMissingParamError("follow_up") return } + // Present-but-wrong-type follow_up (object/string/number) -> report a clear + // type/shape error rather than the misleading "Missing value" message, so the + // model can correct it instead of looping with the same payload. + if (!Array.isArray(follow_up)) { + await recordValidationError( + "The 'follow_up' parameter must be an array of suggestion objects, each shaped like { text: string, mode?: string }. " + + "Retry with 'follow_up' as a JSON array.", + ) + return + } + // Transform follow_up suggestions to the format expected by task.ask const follow_up_json = { question, diff --git a/src/core/tools/AttemptCompletionTool.ts b/src/core/tools/AttemptCompletionTool.ts index c6c9bc908e..39feae0d9b 100644 --- a/src/core/tools/AttemptCompletionTool.ts +++ b/src/core/tools/AttemptCompletionTool.ts @@ -97,7 +97,7 @@ export class AttemptCompletionTool extends BaseTool<"attempt_completion"> { // Fall through to normal completion ask flow below (outside this if block) // This shows the user the completion result and waits for acceptance // without injecting another tool_result to the parent - } else if (status === "active") { + } else if (status === "active" || status === "interrupted") { historyLookupTaskId = task.parentTaskId const { historyItem: parentHistory } = await provider.getTaskWithId(task.parentTaskId) @@ -132,7 +132,7 @@ export class AttemptCompletionTool extends BaseTool<"attempt_completion"> { // "delegated" would mean this child has its own grandchild pending (shouldn't reach attempt_completion) provider.log( `[AttemptCompletionTool] Unexpected child task status "${status}" for task ${task.taskId}. ` + - `Expected "active" or "completed". Skipping delegation to prevent data corruption.`, + `Expected "active", "interrupted", or "completed". Skipping delegation to prevent data corruption.`, ) // Fall through to normal completion ask flow } diff --git a/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts index 9bac90067b..4a035447b1 100644 --- a/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts +++ b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts @@ -83,7 +83,7 @@ describe("AskFollowupQuestionTool", () => { expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("ask_followup_question", "follow_up") }) - it("should handle follow_up that is not an array", async () => { + it("should report a type error when follow_up is a string", async () => { const params = { question: "What?", follow_up: "not-an-array" as any } await tool.execute(params, mockTask, mockCallbacks) @@ -91,7 +91,35 @@ describe("AskFollowupQuestionTool", () => { expect(mockTask.consecutiveMistakeCount).toBe(1) expect(mockTask.recordToolError).toHaveBeenCalledWith("ask_followup_question") expect(mockTask.didToolFailInCurrentTurn).toBe(true) - expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("ask_followup_question", "follow_up") + // A present-but-non-array value is a type error. + expect(mockTask.sayAndCreateMissingParamError).not.toHaveBeenCalled() + const pushed = (mockCallbacks.pushToolResult as any).mock.calls[0][0] + expect(pushed).toContain("must be an array") + expect(pushed).not.toContain("Missing value") + }) + + it("should report a type error when follow_up is an object", async () => { + // Reproduces the issue: follow_up arrives as a keyed object instead of an array. + const params = { + question: "How should I proceed?", + follow_up: { + "0": { mode: null, text: "Keep the guard" }, + "1": { mode: null, text: "Remove the guard" }, + } as any, + } + + await tool.execute(params, mockTask, mockCallbacks) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("ask_followup_question") + expect(mockTask.didToolFailInCurrentTurn).toBe(true) + expect(mockTask.sayAndCreateMissingParamError).not.toHaveBeenCalled() + expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("must be an array")) + const pushed = (mockCallbacks.pushToolResult as any).mock.calls[0][0] + expect(pushed).toContain("must be an array") + expect(pushed).not.toContain("Missing value") + // The tool must not proceed to ask the user with an invalid payload. + expect(mockTask.ask).not.toHaveBeenCalled() }) // ===== Happy path tests ===== @@ -513,5 +541,31 @@ describe("AskFollowupQuestionTool", () => { }) } }) + + it("should finalize and forward a non-array follow_up so the tool can report it", () => { + NativeToolCallParser.startStreamingToolCall("call_789", "ask_followup_question") + + // follow_up arrives as a keyed object instead of an array (the bug repro). + const completeJson = + '{"question":"How should I proceed?","follow_up":{"0":{"mode":null,"text":"Keep"},"1":{"mode":null,"text":"Remove"}}}' + NativeToolCallParser.processStreamingChunk("call_789", completeJson) + + const result = NativeToolCallParser.finalizeStreamingToolCall("call_789") + + // The call must NOT be dropped (null) - it should reach the tool with the raw + // value so the tool can emit a precise "must be an array" error. + expect(result).not.toBeNull() + expect(result?.type).toBe("tool_use") + expect(result?.name).toBe("ask_followup_question") + if (result?.type === "tool_use") { + const nativeArgs = result.nativeArgs as { question: string; follow_up: unknown } + expect(nativeArgs.question).toBe("How should I proceed?") + expect(Array.isArray(nativeArgs.follow_up)).toBe(false) + expect(nativeArgs.follow_up).toEqual({ + "0": { mode: null, text: "Keep" }, + "1": { mode: null, text: "Remove" }, + }) + } + }) }) }) diff --git a/src/core/tools/__tests__/attemptCompletionTool.spec.ts b/src/core/tools/__tests__/attemptCompletionTool.spec.ts index cf21eeee3e..86ff112585 100644 --- a/src/core/tools/__tests__/attemptCompletionTool.spec.ts +++ b/src/core/tools/__tests__/attemptCompletionTool.spec.ts @@ -636,6 +636,57 @@ describe("attemptCompletionTool", () => { expect(mockCaptureTaskCompleted).toHaveBeenCalledWith("child-1") }) + it("delegates an interrupted subtask completion when the parent is still delegated and awaiting that child", async () => { + const block: AttemptCompletionToolUse = { + type: "tool_use", + name: "attempt_completion", + params: { result: "9" }, + nativeArgs: { result: "9" }, + partial: false, + } + const mockProvider = { + log: vi.fn(), + getTaskWithId: vi.fn().mockImplementation((id: string) => { + if (id === "child-1") { + return Promise.resolve({ historyItem: { id, status: "interrupted" } }) + } + if (id === "parent-1") { + return Promise.resolve({ + historyItem: { id, status: "delegated", awaitingChildId: "child-1" }, + }) + } + throw new Error(`unexpected task id ${id}`) + }), + reopenParentFromDelegation: vi.fn().mockResolvedValue(true), + } + + Object.assign(mockTask, { + taskId: "child-1", + parentTaskId: "parent-1", + providerRef: { deref: () => mockProvider }, + }) + mockAskFinishSubTaskApproval.mockResolvedValue(true) + + const callbacks: AttemptCompletionCallbacks = { + askApproval: mockAskApproval, + handleError: mockHandleError, + pushToolResult: mockPushToolResult, + askFinishSubTaskApproval: mockAskFinishSubTaskApproval, + toolDescription: mockToolDescription, + } + + await attemptCompletionTool.handle(mockTask as Task, block, callbacks) + + expect(mockAskFinishSubTaskApproval).toHaveBeenCalled() + expect(mockProvider.reopenParentFromDelegation).toHaveBeenCalledWith({ + parentTaskId: "parent-1", + childTaskId: "child-1", + completionResultSummary: "9", + }) + expect(mockTask.ask).not.toHaveBeenCalled() + expect(mockPushToolResult).toHaveBeenCalledWith("") + }) + it("does not resume the parent when the parent is active but awaiting a different child", async () => { const block: AttemptCompletionToolUse = { type: "tool_use", diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index e4c7424a0c..6c32ab70a1 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -41,6 +41,10 @@ import { requestyDefaultModelId, openRouterDefaultModelId, DEFAULT_WRITE_DELAY_MS, + DEFAULT_DIFF_FUZZY_THRESHOLD, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, ORGANIZATION_ALLOW_ALL, DEFAULT_MODES, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, @@ -98,7 +102,13 @@ import { Task } from "../task/Task" import { webviewMessageHandler } from "./webviewMessageHandler" import type { ClineMessage, TodoItem } from "@roo-code/types" -import { readApiMessages, saveApiMessages, saveTaskMessages, TaskHistoryStore } from "../task-persistence" +import { + readApiMessages, + saveApiMessages, + saveTaskMessages, + TaskHistoryStore, + assertValidTransition, +} from "../task-persistence" import { readTaskMessages } from "../task-persistence/taskMessages" import { getNonce } from "./getNonce" import { getUri } from "./getUri" @@ -157,6 +167,14 @@ export class ClineProvider private clineStack: Task[] = [] private delegationTransitionLocks?: Map> private cancelledDelegationChildIds = new Set() + // Marks a child whose cancellation is currently in flight, from the moment cancelTask() + // is invoked until its "interrupted" status write lands (or the cancel path bails out). + // removeClineFromStack()'s delegation repair must not run against a stale "active" read + // while this is set — otherwise a concurrent navigation (e.g. showTaskWithId(parentTaskId) + // from the user clicking "back to parent" right after hitting Stop) can win the race + // against cancelTask()'s own runDelegationTransition call and repair the parent to + // "active" before "interrupted" is ever persisted, permanently severing the delegation link. + private cancellingDelegationChildIds = new Set() private codeIndexStatusSubscription?: vscode.Disposable private codeIndexManager?: CodeIndexManager private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class @@ -195,7 +213,7 @@ export class ClineProvider public isViewLaunched = false public settingsImportedAt?: number - public readonly latestAnnouncementId = "jun-2026-v3.62.0-glm52-opencodego-toolwriter" // v3.62.0 GLM-5.2, OpenCode-Go native model params & routing, tool-writer mode + public readonly latestAnnouncementId = "jul-2026-v3.68.0-friendli-ollama-anthropic-apimodelid" // v3.68.0 Friendli GLM-5.2 support, native Ollama thinking/reasoning, Anthropic custom apiModelId fix public readonly providerSettingsManager: ProviderSettingsManager public readonly customModesManager: CustomModesManager @@ -523,10 +541,35 @@ export class ClineProvider const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId) if (parentHistory?.status === "delegated" && parentHistory?.awaitingChildId === childTaskId) { + // If the child is "interrupted", cancelTask already persisted that + // status and intentionally left the parent delegated so the child + // can resume and report back. Do not auto-repair in that case. + if (this.taskHistoryStore.get(childTaskId)?.status === "interrupted") { + this.log( + `[ClineProvider#removeClineFromStack] Skipping parent repair: child ${childTaskId} is interrupted`, + ) + return + } + + // A cancellation for this child may be in flight (cancelTask() has + // marked it synchronously but its "interrupted" write hasn't landed + // yet, since both paths serialize on the same per-parent transition + // lock and this call won the race). Repairing here would clear + // awaitingChildId based on a stale "active" read and permanently + // sever the delegation link. Defer to cancelTask()'s own write instead. + if (this.cancellingDelegationChildIds.has(childTaskId)) { + this.log( + `[ClineProvider#removeClineFromStack] Skipping parent repair: cancellation for child ${childTaskId} is in flight`, + ) + return + } + + assertValidTransition(parentHistory.status, "active") await this.updateTaskHistory({ ...parentHistory, status: "active", awaitingChildId: undefined, + delegatedToId: undefined, }) const repairMsg = `[ClineProvider#removeClineFromStack] Repaired parent ${parentTaskId} metadata: delegated → active (child ${childTaskId} removed). ` + @@ -1052,10 +1095,12 @@ export class ClineProvider if (profile?.name) { try { - await this.activateProviderProfile( - { name: profile.name }, - { persistModeConfig: false, persistTaskHistory: false }, - ) + if (profile.apiProvider) { + await this.activateProviderProfile( + { name: profile.name }, + { persistModeConfig: false, persistTaskHistory: false }, + ) + } } catch (error) { // Log the error but continue with task restoration. this.log( @@ -1076,8 +1121,15 @@ export class ClineProvider ) } - const { apiConfiguration, enableCheckpoints, checkpointTimeout, experiments, cloudUserInfo, taskSyncEnabled } = - await this.getState() + const { + apiConfiguration, + enableCheckpoints, + checkpointTimeout, + experiments, + cloudUserInfo, + taskSyncEnabled, + diffFuzzyThreshold, + } = await this.getState() const task = new Task({ provider: this, @@ -1096,6 +1148,7 @@ export class ClineProvider // Preserve the status from the history item to avoid overwriting it when the task saves messages initialStatus: historyItem.status, rateLimitClock: this.rateLimitClock, + diffFuzzyThreshold, }) if (isRehydratingCurrentTask) { @@ -2222,6 +2275,7 @@ export class ClineProvider taskHistory, soundVolume, writeDelayMs, + diffFuzzyThreshold, terminalShellIntegrationTimeout, terminalShellIntegrationDisabled, terminalCommandDelay, @@ -2278,6 +2332,9 @@ export class ClineProvider openRouterImageApiKey, openRouterImageGenerationSelectedModel, lockApiConfigAcrossModes, + autoCloseZooOpenedFiles, + autoCloseZooOpenedFilesAfterUserEdited, + autoCloseZooOpenedNewFiles, } = await this.getState() let cloudOrganizations: CloudOrganizationMembership[] = [] @@ -2325,9 +2382,8 @@ export class ClineProvider } try { - const { isZooCodeAuthenticated, getCachedZooCodeUserInfo, getZooCodeBaseUrl } = await import( - "../../services/zoo-code-auth" - ) + const { isZooCodeAuthenticated, getCachedZooCodeUserInfo, getZooCodeBaseUrl } = + await import("../../services/zoo-code-auth") const userInfo = getCachedZooCodeUserInfo() zooCodeState = { zooCodeIsAuthenticated: await isZooCodeAuthenticated(), @@ -2376,6 +2432,7 @@ export class ClineProvider deniedCommands: mergedDeniedCommands, soundVolume: soundVolume ?? 0.5, writeDelayMs: writeDelayMs ?? DEFAULT_WRITE_DELAY_MS, + diffFuzzyThreshold: diffFuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD, terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout, terminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? true, terminalCommandDelay: terminalCommandDelay ?? 0, @@ -2490,6 +2547,10 @@ export class ClineProvider imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, + autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + autoCloseZooOpenedFilesAfterUserEdited: + autoCloseZooOpenedFilesAfterUserEdited ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, openAiCodexIsAuthenticated: await (async () => { try { const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth") @@ -2615,6 +2676,7 @@ export class ClineProvider checkpointTimeout: stateValues.checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, soundVolume: stateValues.soundVolume, writeDelayMs: stateValues.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS, + diffFuzzyThreshold: stateValues.diffFuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD, terminalShellIntegrationTimeout: stateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout, terminalShellIntegrationDisabled: stateValues.terminalShellIntegrationDisabled ?? true, @@ -2690,6 +2752,9 @@ export class ClineProvider imageGenerationProvider: stateValues.imageGenerationProvider, openRouterImageApiKey: stateValues.openRouterImageApiKey, openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel, + autoCloseZooOpenedFiles: stateValues.autoCloseZooOpenedFiles, + autoCloseZooOpenedFilesAfterUserEdited: stateValues.autoCloseZooOpenedFilesAfterUserEdited, + autoCloseZooOpenedNewFiles: stateValues.autoCloseZooOpenedNewFiles, } } @@ -3066,8 +3131,14 @@ export class ClineProvider } } - const { apiConfiguration, organizationAllowList, enableCheckpoints, checkpointTimeout, experiments } = - await this.getState() + const { + apiConfiguration, + enableCheckpoints, + checkpointTimeout, + experiments, + organizationAllowList, + diffFuzzyThreshold, + } = await this.getState() // Single-open-task invariant: always enforce for user-initiated top-level tasks if (!parentTask) { @@ -3099,6 +3170,7 @@ export class ClineProvider // Ensure this task is present in clineStack before startTask() emits // its initial state update, so state.currentTaskId is available ASAP. startTask: false, + diffFuzzyThreshold, ...options, rateLimitClock: this.rateLimitClock, }) @@ -3124,6 +3196,23 @@ export class ClineProvider console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`) + // Mark this child as "cancellation in flight" synchronously, before any await, so a + // concurrent removeClineFromStack() (e.g. from the user navigating back to the parent + // right after clicking Stop) cannot win the race against this function's own + // runDelegationTransition call below and repair the parent from a stale "active" read + // before "interrupted" is persisted (see cancellingDelegationChildIds doc comment). + if (task.parentTaskId) { + this.cancellingDelegationChildIds.add(task.taskId) + } + + try { + await this.cancelTaskInternal(task) + } finally { + this.cancellingDelegationChildIds.delete(task.taskId) + } + } + + private async cancelTaskInternal(task: Task): Promise { let historyItem: HistoryItem | undefined try { const history = await this.getTaskWithId(task.taskId) @@ -3153,8 +3242,11 @@ export class ClineProvider // This ensures the stream fails quickly rather than waiting for network timeout task.cancelCurrentRequest() - // Begin abort (non-blocking) - task.abortTask() + // Kick off abort (sets abort flag synchronously; stream exit and final saveClineMessages + // happen asynchronously). We capture the promise so we can await its completion below — + // this ensures task.initialStatus ("active") cannot overwrite "interrupted" after we + // persist it (issue #560). + const abortPromise = task.abortTask() // Immediately mark the original instance as abandoned to prevent any residual activity task.abandoned = true @@ -3175,6 +3267,10 @@ export class ClineProvider console.error("Failed to abort task") }) + // Wait for abortTask to fully settle (including its final saveClineMessages write) + // before we persist "interrupted", so our write is always the last one. + await abortPromise.catch(() => {}) + // Defensive safeguard: if current instance already changed, skip rehydrate const current = this.getCurrentTask() if (current && current.instanceId !== originalInstanceId) { @@ -3205,25 +3301,21 @@ export class ClineProvider const { historyItem: parentHistory } = await this.getTaskWithId(task.parentTaskId!) if (parentHistory?.status === "delegated" && parentHistory?.awaitingChildId === task.taskId) { - await this.updateTaskHistory({ - ...parentHistory, - status: "active", - awaitingChildId: undefined, - }) - + // Mark the child interrupted and leave parent delegated with awaitingChildId + // intact — the user can resume this child later and it will report back. + historyItem = { ...historyItem!, status: "interrupted" } + await this.updateTaskHistory(historyItem) + // Clear any stale fail-closed entry from a prior failed cancel attempt so + // reopenParentFromDelegation is not incorrectly blocked on resume. + this.cancelledDelegationChildIds.delete(task.taskId) this.log( - `[cancelTask] Detached delegated parent ${task.parentTaskId}: delegated → active (child ${task.taskId} cancelled)`, + `[cancelTask] Marked child ${task.taskId} interrupted; parent ${task.parentTaskId} stays delegated`, ) - parentTask = undefined - rootTask = undefined - // Clear any stale fail-closed entry from a prior failed cancel attempt. - this.cancelledDelegationChildIds.delete(task.taskId) } }) } catch (error) { - // Fail closed: if we cannot prove the parent was detached, make the - // rehydrated child standalone so later completions cannot reopen a - // stale delegated parent, even after a provider reload. + // Fail closed: if we cannot persist the interrupted status, sever the link + // so later completions don't reopen a stale delegated parent. parentTask = undefined rootTask = undefined this.cancelledDelegationChildIds.add(task.taskId) @@ -3236,14 +3328,14 @@ export class ClineProvider await this.updateTaskHistory(historyItem) } catch (historyError) { this.log( - `[cancelTask] Failed to persist standalone child state for ${task.taskId}: ${ + `[cancelTask] Failed to persist interrupted child state for ${task.taskId}: ${ historyError instanceof Error ? historyError.message : String(historyError) }`, ) throw historyError } this.log( - `[cancelTask] Failed to detach delegated parent for ${task.taskId}: ${ + `[cancelTask] Failed to mark child interrupted for ${task.taskId}: ${ error instanceof Error ? error.message : String(error) }`, ) @@ -3515,17 +3607,29 @@ export class ClineProvider }) // 5) Persist parent delegation metadata BEFORE the child starts writing. + // atomicReadAndUpdate reads from the in-memory cache and writes back within a + // single lock acquisition — no concurrent writer can slip between the read and + // write, and the pure updater cannot re-enter the lock (no deadlock). + // Broadcast and cache invalidation happen outside the lock after it releases. try { - const { historyItem } = await this.getTaskWithId(parentTaskId) - const childIds = Array.from(new Set([...(historyItem.childIds ?? []), child.taskId])) - const updatedHistory: typeof historyItem = { - ...historyItem, - status: "delegated", - delegatedToId: child.taskId, - awaitingChildId: child.taskId, - childIds, - } - await this.updateTaskHistory(updatedHistory) + await this.taskHistoryStore.atomicReadAndUpdate(parentTaskId, (historyItem) => { + assertValidTransition(historyItem.status, "delegated") + const childIds = Array.from(new Set([...(historyItem.childIds ?? []), child.taskId])) + return { + ...historyItem, + status: "delegated", + delegatedToId: child.taskId, + awaitingChildId: child.taskId, + childIds, + } + }) + this.recentTasksCache = undefined + if (this.isViewLaunched) { + const updatedItem = this.taskHistoryStore.get(parentTaskId) + if (updatedItem) { + await this.postMessageToWebview({ type: "taskHistoryItemUpdated", taskHistoryItem: updatedItem }) + } + } } catch (err) { this.log( `[delegateParentAndOpenChild] Failed to persist parent metadata for ${parentTaskId} -> ${child.taskId}: ${ @@ -3533,7 +3637,11 @@ export class ClineProvider }`, ) try { - await this.removeClineFromStack({ skipDelegationRepair: true }) + // Only pop the stack if the child we just created is still on top. + // A concurrent delegation could have pushed another child since we created ours. + if (this.getCurrentTask()?.taskId === child.taskId) { + await this.removeClineFromStack({ skipDelegationRepair: true }) + } } catch (cleanupError) { this.log( `[delegateParentAndOpenChild] Failed to close paused child ${child.taskId} during rollback: ${ @@ -3736,44 +3844,58 @@ export class ClineProvider await saveApiMessages({ messages: parentApiMessages as any, taskId: parentTaskId, globalStoragePath }) - // 3) Persist parent metadata before closing the child. If persistence fails, - // the delegated child remains active and can retry completion. - const childIds = Array.from(new Set([...(historyItem.childIds ?? []), childTaskId])) - const updatedHistory: typeof historyItem = { - ...historyItem, - status: "active", - completedByChildId: childTaskId, - completionResultSummary, - awaitingChildId: undefined, - childIds, - } - await this.updateTaskHistory(updatedHistory) - // 4) Close child instance if still open (single-open-task invariant). - // This MUST happen BEFORE updating the child's status to "completed" because + // This MUST happen BEFORE marking the child "completed" because // removeClineFromStack() → abortTask(true) → saveClineMessages() writes // the historyItem with initialStatus (typically "active"), which would - // overwrite a "completed" status set earlier. + // overwrite a "completed" status set later. const current = this.getCurrentTask() if (current?.taskId === childTaskId) { await this.removeClineFromStack({ skipDelegationRepair: true }) } - // 5) Update child metadata to "completed" status. - // This runs after the abort so it overwrites the stale "active" status - // that saveClineMessages() may have written during step 4. - try { - const { historyItem: childHistory } = await this.getTaskWithId(childTaskId) - await this.updateTaskHistory({ - ...childHistory, - status: "completed", - }) - } catch (err) { - this.log( - `[reopenParentFromDelegation] Failed to persist child completed status for ${childTaskId}: ${ - (err as Error)?.message ?? String(err) - }`, - ) + // 3+5) Atomically mark child completed and parent active in one lock acquisition. + // No intermediate state is ever persisted — no sentinel needed. + // Build the parent update inside the updater from the locked snapshot so + // any concurrent write that landed between step 1 and the lock acquisition + // is preserved rather than silently overwritten. + let updatedHistory!: typeof historyItem + await this.taskHistoryStore.atomicUpdatePair( + childTaskId, + parentTaskId, + (child) => { + assertValidTransition(child.status, "completed") + return { ...child, status: "completed" as const, completionResultSummary } + }, + (parent) => { + if (parent.status !== "active") { + assertValidTransition(parent.status, "active") + } + const childIds = Array.from(new Set([...(parent.childIds ?? []), childTaskId])) + updatedHistory = { + ...parent, + status: "active" as const, + completedByChildId: childTaskId, + completionResultSummary, + awaitingChildId: undefined, + delegatedToId: undefined, + childIds, + } + return updatedHistory + }, + ) + this.recentTasksCache = undefined + + // Notify the webview of both updated items so its in-memory history stays current. + if (this.isViewLaunched) { + const updatedChild = this.taskHistoryStore.get(childTaskId) + const updatedParent = this.taskHistoryStore.get(parentTaskId) + if (updatedChild) { + await this.postMessageToWebview({ type: "taskHistoryItemUpdated", taskHistoryItem: updatedChild }) + } + if (updatedParent) { + await this.postMessageToWebview({ type: "taskHistoryItemUpdated", taskHistoryItem: updatedParent }) + } } // 6) Emit TaskDelegationCompleted (provider-level) diff --git a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts index 2710136978..555253c345 100644 --- a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest" import * as vscode from "vscode" import { ClineProvider } from "../ClineProvider" @@ -44,20 +44,35 @@ vi.mock("vscode", () => { } }) -vi.mock("../../task/Task") -vi.mock("../../config/ContextProxy") +vi.mock("../../task/Task", () => ({ + Task: vi.fn().mockImplementation(function () { + return { + taskId: "mock-task-id", + instanceId: "mock-instance-id", + abortTask: vi.fn().mockResolvedValue(undefined), + emit: vi.fn(), + on: vi.fn(), + off: vi.fn(), + } + }), +})) vi.mock("../../../services/mcp/McpServerManager", () => ({ McpServerManager: { getInstance: vi.fn().mockResolvedValue({ registerClient: vi.fn(), + unregisterClient: vi.fn(), }), unregisterProvider: vi.fn(), }, })) -vi.mock("../../../services/marketplace") -vi.mock("../../../integrations/workspace/WorkspaceTracker") -vi.mock("../../config/ProviderSettingsManager") -vi.mock("../../config/CustomModesManager") +vi.mock("../../../integrations/workspace/WorkspaceTracker", () => ({ + default: vi.fn().mockImplementation(function () { + return { + initializeFilePaths: vi.fn(), + dispose: vi.fn(), + } + }), +})) vi.mock("../../../utils/path", () => ({ getWorkspacePath: vi.fn().mockReturnValue("/test/workspace"), })) @@ -87,18 +102,183 @@ vi.mock("../../../shared/embeddingModels", () => ({ EMBEDDING_MODEL_PROFILES: [], })) +vi.mock("../../../shared/modes", () => ({ + modes: [{ slug: "code", name: "Code Mode", roleDefinition: "You are a code assistant", groups: ["read", "edit"] }], + getModeBySlug: vi.fn().mockReturnValue({ + slug: "code", + name: "Code Mode", + roleDefinition: "You are a code assistant", + groups: ["read", "edit"], + }), + getGroupName: vi.fn().mockReturnValue("General Tools"), + defaultModeSlug: "code", +})) + +vi.mock("p-wait-for", () => ({ + __esModule: true, + default: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("fs/promises", () => ({ + mkdir: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockResolvedValue(""), + readdir: vi.fn().mockResolvedValue([]), + unlink: vi.fn().mockResolvedValue(undefined), + rmdir: vi.fn().mockResolvedValue(undefined), + access: vi.fn().mockResolvedValue(undefined), + rm: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("axios", () => ({ + default: { get: vi.fn().mockResolvedValue({ data: { data: [] } }), post: vi.fn() }, + get: vi.fn().mockResolvedValue({ data: { data: [] } }), + post: vi.fn(), +})) + +vi.mock("delay", () => { + const delayFn = (_ms: number) => Promise.resolve() + delayFn.createDelay = () => delayFn + delayFn.reject = () => Promise.reject(new Error("Delay rejected")) + delayFn.range = () => Promise.resolve() + return { default: delayFn } +}) + +vi.mock("../../../utils/storage", () => ({ + getSettingsDirectoryPath: vi.fn().mockResolvedValue("/test/settings/path"), + getTaskDirectoryPath: vi.fn().mockResolvedValue("/test/task/path"), + getGlobalStoragePath: vi.fn().mockResolvedValue("/test/storage/path"), + getStorageBasePath: vi.fn().mockImplementation((defaultPath: string) => defaultPath), +})) + +vi.mock("../../../utils/safeWriteJson", () => ({ + safeWriteJson: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../../utils/tts", () => ({ + setTtsEnabled: vi.fn(), + setTtsSpeed: vi.fn(), +})) + +vi.mock("../../../api", () => ({ + buildApiHandler: vi.fn().mockReturnValue({ + getModel: vi.fn().mockReturnValue({ id: "claude-3-sonnet" }), + }), +})) + +vi.mock("../../prompts/system", () => ({ + SYSTEM_PROMPT: vi.fn().mockImplementation(async () => "mocked system prompt"), + codeMode: "code", +})) + +vi.mock("../../prompts/sections/custom-instructions") + +vi.mock("../../../api/providers/fetchers/modelCache", () => ({ + getModels: vi.fn().mockResolvedValue({}), + flushModels: vi.fn(), + getModelsFromCache: vi.fn().mockReturnValue(undefined), +})) + +vi.mock("../../../integrations/misc/extract-text", () => ({ + extractTextFromFile: vi.fn().mockResolvedValue("file content"), +})) + +vi.mock("../diff/strategies/multi-search-replace", () => ({ + MultiSearchReplaceDiffStrategy: vi.fn().mockImplementation(function () { + return { getName: () => "test-strategy", applyDiff: vi.fn() } + }), +})) + +vi.mock("@modelcontextprotocol/sdk/types.js", () => ({ + CallToolResultSchema: {}, + ListResourcesResultSchema: {}, + ListResourceTemplatesResultSchema: {}, + ListToolsResultSchema: {}, + ReadResourceResultSchema: {}, + ErrorCode: { InvalidRequest: "InvalidRequest", MethodNotFound: "MethodNotFound", InternalError: "InternalError" }, + McpError: class McpError extends Error { + code: string + constructor(code: string, message: string) { + super(message) + this.code = code + this.name = "McpError" + } + }, +})) + +vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({ + Client: vi.fn().mockImplementation(function () { + return { + connect: vi.fn().mockResolvedValue(undefined), + close: vi.fn().mockResolvedValue(undefined), + listTools: vi.fn().mockResolvedValue({ tools: [] }), + callTool: vi.fn().mockResolvedValue({ content: [] }), + } + }), +})) + +vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({ + StdioClientTransport: vi.fn().mockImplementation(function () { + return { connect: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined) } + }), +})) + +vi.mock("../../../services/skills/SkillsManager", () => ({ + SkillsManager: vi.fn().mockImplementation(function () { + return { + initialize: vi.fn().mockResolvedValue(undefined), + dispose: vi.fn().mockResolvedValue(undefined), + } + }), +})) + +vi.mock("../../task-persistence", async (importOriginal) => { + const mod = await importOriginal() + return { + ...mod, + TaskHistoryStore: vi.fn().mockImplementation(function () { + return { + initialize: vi.fn().mockResolvedValue(undefined), + dispose: vi.fn(), + initialized: Promise.resolve(), + get: vi.fn().mockReturnValue(undefined), + getAll: vi.fn().mockReturnValue([]), + upsert: vi.fn().mockResolvedValue([]), + delete: vi.fn().mockResolvedValue(undefined), + deleteMany: vi.fn().mockResolvedValue(undefined), + migrateFromGlobalState: vi.fn().mockResolvedValue(undefined), + } + }), + readApiMessages: vi.fn().mockResolvedValue([]), + saveApiMessages: vi.fn().mockResolvedValue(undefined), + saveTaskMessages: vi.fn().mockResolvedValue(undefined), + } +}) + describe("ClineProvider flicker-free cancel", () => { let provider: ClineProvider let mockContext: any let mockOutputChannel: any let mockTask1: any let mockTask2: any + let consoleLogSpy: ReturnType + let consoleErrorSpy: ReturnType const mockApiConfig: ProviderSettings = { apiProvider: "anthropic", apiKey: "test-key", } as ProviderSettings + beforeAll(() => { + consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}) + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + }) + + afterAll(() => { + consoleLogSpy.mockRestore() + consoleErrorSpy.mockRestore() + }) + beforeEach(() => { vi.clearAllMocks() @@ -195,6 +375,10 @@ describe("ClineProvider flicker-free cancel", () => { }) }) + afterEach(async () => { + await provider.dispose() + }) + it("should not remove current task from stack when rehydrating same taskId", async () => { // Setup: Add a task to the stack first ;(provider as any).clineStack = [mockTask1] @@ -242,7 +426,9 @@ describe("ClineProvider flicker-free cancel", () => { ;(provider as any).clineStack = [mockTask1] // Spy on removeClineFromStack to verify it IS called - const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack").mockResolvedValue(undefined) + const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack").mockImplementation(async () => { + ;(provider as any).clineStack.pop() + }) // Create history item with different taskId const historyItem: HistoryItem = { @@ -268,7 +454,9 @@ describe("ClineProvider flicker-free cancel", () => { ;(provider as any).clineStack = [] // Spy on removeClineFromStack - const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack").mockResolvedValue(undefined) + const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack").mockImplementation(async () => { + ;(provider as any).clineStack.pop() + }) // Create history item const historyItem: HistoryItem = { @@ -321,7 +509,7 @@ describe("ClineProvider flicker-free cancel", () => { expect((provider as any).clineStack[1]).toBe(mockTask2) }) - it("detaches runtime parent links for a cancelled delegated child while preserving history lineage", async () => { + it("marks a cancelled delegated child as interrupted and keeps parent delegated (preserving resume path)", async () => { const mockRootTask = { taskId: "root-1" } const mockParentTask = { taskId: "parent-1" } const childHistory: HistoryItem = { @@ -335,6 +523,7 @@ describe("ClineProvider flicker-free cancel", () => { workspace: "/test/workspace", parentTaskId: "parent-1", rootTaskId: "root-1", + status: "active", } const parentHistory: HistoryItem = { id: "parent-1", @@ -357,7 +546,7 @@ describe("ClineProvider flicker-free cancel", () => { parentTask: mockParentTask, parentTaskId: "parent-1", cancelCurrentRequest: vi.fn(), - abortTask: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), abandoned: false, isStreaming: false, didFinishAbortingStream: true, @@ -381,20 +570,21 @@ describe("ClineProvider flicker-free cancel", () => { await provider.cancelTask() + // Child is marked interrupted, not detached expect(updateTaskHistorySpy).toHaveBeenCalledWith( expect.objectContaining({ - id: "parent-1", - status: "active", - awaitingChildId: undefined, + id: "child-1", + status: "interrupted", }), ) + // Parent is NOT transitioned to active — it stays delegated + expect(updateTaskHistorySpy).not.toHaveBeenCalledWith(expect.objectContaining({ id: "parent-1" })) + // Rehydrated child keeps its parent link so it can resume and report back expect(createTaskWithHistoryItemSpy).toHaveBeenCalledWith( expect.objectContaining({ id: "child-1", parentTaskId: "parent-1", rootTaskId: "root-1", - parentTask: undefined, - rootTask: undefined, }), ) }) @@ -422,7 +612,7 @@ describe("ClineProvider flicker-free cancel", () => { parentTask: mockParentTask, parentTaskId: "parent-1", cancelCurrentRequest: vi.fn(), - abortTask: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), abandoned: false, isStreaming: false, didFinishAbortingStream: true, @@ -447,7 +637,7 @@ describe("ClineProvider flicker-free cancel", () => { await provider.cancelTask() expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( - expect.stringContaining("[cancelTask] Failed to detach delegated parent for child-1: parent lookup failed"), + expect.stringContaining("[cancelTask] Failed to mark child interrupted for child-1: parent lookup failed"), ) expect(updateTaskHistorySpy).toHaveBeenCalledWith( expect.objectContaining({ @@ -487,7 +677,7 @@ describe("ClineProvider flicker-free cancel", () => { instanceId: "instance-child", parentTaskId: "parent-1", cancelCurrentRequest: vi.fn(), - abortTask: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), abandoned: false, isStreaming: false, didFinishAbortingStream: true, @@ -513,4 +703,190 @@ describe("ClineProvider flicker-free cancel", () => { expect(createTaskWithHistoryItemSpy).not.toHaveBeenCalled() expect((provider as any).cancelledDelegationChildIds.has("child-1")).toBe(true) }) + + it("marks a cancelled delegated child as 'interrupted' and keeps parent delegated", async () => { + const childHistory: HistoryItem = { + id: "child-1", + number: 2, + task: "child task", + ts: Date.now(), + tokensIn: 10, + tokensOut: 20, + totalCost: 0.001, + workspace: "/test/workspace", + parentTaskId: "parent-1", + rootTaskId: "root-1", + status: "active", + } + const parentHistory: HistoryItem = { + id: "parent-1", + number: 1, + task: "parent task", + ts: Date.now(), + tokensIn: 10, + tokensOut: 20, + totalCost: 0.001, + workspace: "/test/workspace", + status: "delegated", + awaitingChildId: "child-1", + delegatedToId: "child-1", + } + + Object.assign(mockTask1, { + taskId: "child-1", + instanceId: "instance-child", + rootTask: { taskId: "root-1" }, + parentTask: { taskId: "parent-1" }, + parentTaskId: "parent-1", + cancelCurrentRequest: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + abandoned: false, + isStreaming: false, + didFinishAbortingStream: true, + isWaitingForFirstChunk: false, + }) + ;(provider as any).clineStack = [mockTask1] + provider.getTaskWithId = vi.fn().mockImplementation((id) => { + if (id === "child-1") return Promise.resolve({ historyItem: childHistory }) + if (id === "parent-1") return Promise.resolve({ historyItem: parentHistory }) + throw new Error(`unexpected task lookup: ${id}`) + }) as any + + const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) + const createTaskWithHistoryItemSpy = vi + .spyOn(provider, "createTaskWithHistoryItem") + .mockResolvedValue(undefined as any) + + await provider.cancelTask() + + // Child should be marked interrupted, not have its parent link severed + expect(updateTaskHistorySpy).toHaveBeenCalledWith( + expect.objectContaining({ + id: "child-1", + status: "interrupted", + }), + ) + + // Parent should remain delegated with awaitingChildId intact + expect(updateTaskHistorySpy).not.toHaveBeenCalledWith(expect.objectContaining({ id: "parent-1" })) + + // Rehydrated child retains parent link + expect(createTaskWithHistoryItemSpy).toHaveBeenCalledWith( + expect.objectContaining({ + id: "child-1", + parentTaskId: "parent-1", + rootTaskId: "root-1", + }), + ) + }) + + it("removeClineFromStack does not repair parent when child is interrupted", async () => { + const parentHistory: HistoryItem = { + id: "parent-1", + number: 1, + task: "parent task", + ts: Date.now(), + tokensIn: 10, + tokensOut: 20, + totalCost: 0.001, + workspace: "/test/workspace", + status: "delegated", + awaitingChildId: "child-1", + delegatedToId: "child-1", + } + + const childTask = { + taskId: "child-1", + instanceId: "inst-child", + parentTaskId: "parent-1", + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + ;(provider as any).clineStack = [childTask] + ;(provider as any).taskEventListeners = new Map() + // Seed the in-memory store so taskHistoryStore.get("child-1") returns interrupted + vi.spyOn((provider as any).taskHistoryStore, "get").mockImplementation((id: unknown) => + id === "child-1" ? { status: "interrupted" } : undefined, + ) + + provider.getTaskWithId = vi.fn().mockImplementation((id) => { + if (id === "parent-1") return Promise.resolve({ historyItem: parentHistory }) + throw new Error(`unexpected task lookup: ${id}`) + }) as any + + const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) + + await (provider as any).removeClineFromStack() + + // Parent must NOT be transitioned to active — it stays delegated + expect(updateTaskHistorySpy).not.toHaveBeenCalledWith( + expect.objectContaining({ id: "parent-1", status: "active" }), + ) + }) + + // Regression test for the race where a user clicks Stop on a freshly-delegated + // child and immediately navigates back to the parent (showTaskWithId), before + // cancelTask()'s own persistence of childHistory.status = "interrupted" has + // landed. Both cancelTask() and removeClineFromStack() serialize their parent + // writes through runDelegationTransition(parentTaskId, ...), but removeClineFromStack + // only skips its repair when taskHistoryStore.get(childTaskId)?.status === "interrupted". + // If removeClineFromStack's transition wins the race and runs while the store still + // reports "active" (the write from cancelTask() hasn't landed yet), it incorrectly + // repairs the parent to "active" and clears awaitingChildId, permanently severing + // the delegation link before the child ever gets a chance to report back. + it("removeClineFromStack does not repair parent when a cancellation for the child is in flight", async () => { + const parentHistory: HistoryItem = { + id: "parent-1", + number: 1, + task: "parent task", + ts: Date.now(), + tokensIn: 10, + tokensOut: 20, + totalCost: 0.001, + workspace: "/test/workspace", + status: "delegated", + awaitingChildId: "child-1", + delegatedToId: "child-1", + } + + const childTask = { + taskId: "child-1", + instanceId: "inst-child", + parentTaskId: "parent-1", + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + ;(provider as any).clineStack = [childTask] + ;(provider as any).taskEventListeners = new Map() + + // The store still reports "active" — cancelTask()'s write to "interrupted" + // has not landed yet. This is the pre-write window of the race. + vi.spyOn((provider as any).taskHistoryStore, "get").mockImplementation((id: unknown) => + id === "child-1" ? { status: "active" } : undefined, + ) + + provider.getTaskWithId = vi.fn().mockImplementation((id) => { + if (id === "parent-1") return Promise.resolve({ historyItem: parentHistory }) + throw new Error(`unexpected task lookup: ${id}`) + }) as any + + const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) + + // Simulate cancelTask() having already synchronously marked this child as + // "being cancelled" before its own await chain reaches the history write. + ;(provider as any).cancellingDelegationChildIds.add("child-1") + + await (provider as any).removeClineFromStack() + + // Parent must NOT be transitioned to active while the child's cancellation + // is still in flight — repairing here would clear awaitingChildId and + // permanently sever the delegation link before "interrupted" is persisted. + expect(updateTaskHistorySpy).not.toHaveBeenCalledWith( + expect.objectContaining({ id: "parent-1", status: "active" }), + ) + }) + + afterAll(() => { + vi.restoreAllMocks() + }) }) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index e1ddd881b8..1904b46bd4 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -13,6 +13,8 @@ import { type ExtensionState, ORGANIZATION_ALLOW_ALL, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + DEFAULT_DIFF_FUZZY_THRESHOLD, + DEFAULT_WRITE_DELAY_MS, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -673,6 +675,7 @@ describe("ClineProvider", () => { openRouterImageGenerationSelectedModel: undefined, taskSyncEnabled: false, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, + diffFuzzyThreshold: DEFAULT_DIFF_FUZZY_THRESHOLD, } const message: ExtensionMessage = { @@ -930,6 +933,48 @@ describe("ClineProvider", () => { expect(state.writeDelayMs).toBe(1000) }) + test("getState applies fallback defaults for write, diff, and terminal settings", async () => { + ;(mockContext.globalState.get as any).mockImplementation((key: string) => { + if ( + [ + "writeDelayMs", + "diffFuzzyThreshold", + "terminalShellIntegrationTimeout", + "terminalShellIntegrationDisabled", + "terminalCommandDelay", + ].includes(key) + ) { + return undefined + } + + return null + }) + + const state = await provider.getState() + + expect(state.writeDelayMs).toBe(DEFAULT_WRITE_DELAY_MS) + expect(state.diffFuzzyThreshold).toBe(DEFAULT_DIFF_FUZZY_THRESHOLD) + expect(state.terminalShellIntegrationTimeout).toBe(Terminal.defaultShellIntegrationTimeout) + expect(state.terminalShellIntegrationDisabled).toBe(true) + expect(state.terminalCommandDelay).toBe(0) + }) + + test("getState passes through defined write/diff/terminal values instead of defaults", async () => { + await provider.contextProxy.setValue("writeDelayMs", 500) + await provider.contextProxy.setValue("diffFuzzyThreshold", 0.5) + await provider.contextProxy.setValue("terminalShellIntegrationTimeout", 99999) + await provider.contextProxy.setValue("terminalShellIntegrationDisabled", false) + await provider.contextProxy.setValue("terminalCommandDelay", 1234) + + const state = await provider.getState() + + expect(state.writeDelayMs).toBe(500) + expect(state.diffFuzzyThreshold).toBe(0.5) + expect(state.terminalShellIntegrationTimeout).toBe(99999) + expect(state.terminalShellIntegrationDisabled).toBe(false) + expect(state.terminalCommandDelay).toBe(1234) + }) + test("handles writeDelayMs message", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] @@ -1010,6 +1055,65 @@ describe("ClineProvider", () => { expect(mockPostMessage).toHaveBeenCalled() }) + describe("auto-close settings are included in posted state", () => { + it("getStateToPostToWebview returns saved autoCloseZooOpenedFiles value", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Simulate the updateSettings handler storing the value. + await provider.contextProxy.setValue("autoCloseZooOpenedFiles", false) + await provider.contextProxy.setValue("autoCloseZooOpenedFilesAfterUserEdited", true) + await provider.contextProxy.setValue("autoCloseZooOpenedNewFiles", true) + + const state = await provider.getStateToPostToWebview() + + // The saved values must be present in the state posted to the webview. + expect(state.autoCloseZooOpenedFiles).toBe(false) + expect(state.autoCloseZooOpenedFilesAfterUserEdited).toBe(true) + expect(state.autoCloseZooOpenedNewFiles).toBe(true) + }) + + it("getStateToPostToWebview defaults autoCloseZooOpenedFiles to false when unset", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Ensure the settings are not set. + await provider.contextProxy.setValue("autoCloseZooOpenedFiles", undefined) + await provider.contextProxy.setValue("autoCloseZooOpenedFilesAfterUserEdited", undefined) + await provider.contextProxy.setValue("autoCloseZooOpenedNewFiles", undefined) + + const state = await provider.getStateToPostToWebview() + + // Unset values should default to their documented defaults (opt-in). + expect(state.autoCloseZooOpenedFiles).toBe(false) + expect(state.autoCloseZooOpenedFilesAfterUserEdited).toBe(false) + expect(state.autoCloseZooOpenedNewFiles).toBe(false) + }) + + it("getState returns saved autoCloseZooOpenedFiles value for DiffViewProvider", async () => { + await provider.resolveWebviewView(mockWebviewView) + + await provider.contextProxy.setValue("autoCloseZooOpenedFiles", false) + await provider.contextProxy.setValue("autoCloseZooOpenedFilesAfterUserEdited", true) + await provider.contextProxy.setValue("autoCloseZooOpenedNewFiles", true) + + const state = await provider.getState() + + // DiffViewProvider reads from getState(); all three fields must be present + // so a regression that drops any of them is caught. + expect(state.autoCloseZooOpenedFiles).toBe(false) + expect(state.autoCloseZooOpenedFilesAfterUserEdited).toBe(true) + expect(state.autoCloseZooOpenedNewFiles).toBe(true) + }) + }) + + it("getStateToPostToWebview passes through defined diffFuzzyThreshold value", async () => { + await provider.resolveWebviewView(mockWebviewView) + await provider.contextProxy.setValue("diffFuzzyThreshold", 0.5) + + const state = await provider.getStateToPostToWebview() + + expect(state.diffFuzzyThreshold).toBe(0.5) + }) + it("loads saved API config when switching modes", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] diff --git a/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts index 4121b42a40..c982cf53c0 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts @@ -526,6 +526,36 @@ describe("ClineProvider - Sticky Provider Profile", () => { ) }) + it("should not restore an empty task apiConfigName profile from history", async () => { + await provider.resolveWebviewView(mockWebviewView) + + const historyItem: HistoryItem = { + id: "test-task-id", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 200, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.001, + mode: "ask", + apiConfigName: "default", + } + + const activateProviderProfileSpy = vi + .spyOn(provider, "activateProviderProfile") + .mockResolvedValue(undefined) + + vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([ + { name: "default", id: "default-id" }, + ]) + + await provider.createTaskWithHistoryItem(historyItem) + + expect(activateProviderProfileSpy).not.toHaveBeenCalled() + }) + it("should skip restoring task apiConfigName from history in CLI runtime", async () => { await provider.resolveWebviewView(mockWebviewView) process.env.ROO_CLI_RUNTIME = "1" diff --git a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts index 98773feb6c..6640c2ef07 100644 --- a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts +++ b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts @@ -5,7 +5,8 @@ import pWaitFor from "p-wait-for" import * as vscode from "vscode" // Mock dependencies -vi.mock("../../task-persistence", () => ({ +vi.mock("../../task-persistence", async (importOriginal) => ({ + ...(await importOriginal()), saveTaskMessages: vi.fn(), })) vi.mock("p-wait-for") diff --git a/src/core/webview/__tests__/rulesMessageHandler.spec.ts b/src/core/webview/__tests__/rulesMessageHandler.spec.ts new file mode 100644 index 0000000000..ec8f14a007 --- /dev/null +++ b/src/core/webview/__tests__/rulesMessageHandler.spec.ts @@ -0,0 +1,291 @@ +import type { RuleMetadata, WebviewMessage } from "@roo-code/types" +import type { ClineProvider } from "../ClineProvider" + +vi.mock("vscode", () => ({ + window: { + showErrorMessage: vi.fn(), + showWarningMessage: vi.fn(), + }, +})) + +vi.mock("../../../integrations/misc/open-file", () => ({ + openFile: vi.fn(), +})) + +vi.mock("../../../services/rules/rules", () => ({ + getRules: vi.fn(), + createRule: vi.fn(), + deleteRule: vi.fn(), + resolveRuleFile: vi.fn(), + getRulesDirectoryPath: vi.fn(), +})) + +import * as vscode from "vscode" +import { openFile } from "../../../integrations/misc/open-file" +import { createRule, deleteRule, getRules, getRulesDirectoryPath, resolveRuleFile } from "../../../services/rules/rules" +import { + handleCreateRule, + handleDeleteRule, + handleOpenRuleFile, + handleOpenRulesDirectory, + handleRequestRules, +} from "../rulesMessageHandler" + +const mockRules: RuleMetadata[] = [ + { + id: "global:generic:generic:rule.md", + name: "rule.md", + scope: "global", + kind: "generic", + filePath: "/home/.roo/rules/rule.md", + relativePath: "rule.md", + directoryPath: "/home/.roo/rules", + }, +] + +describe("rulesMessageHandler", () => { + const mockLog = vi.fn() + const mockPostMessageToWebview = vi.fn() + const mockGetModes = vi.fn() + + const createMockProvider = (): ClineProvider => + ({ + log: mockLog, + postMessageToWebview: mockPostMessageToWebview, + getModes: mockGetModes, + }) as unknown as ClineProvider + + beforeEach(() => { + vi.clearAllMocks() + mockGetModes.mockResolvedValue([{ slug: "code", name: "Code" }]) + vi.mocked(getRules).mockResolvedValue(mockRules) + }) + + it("handleRequestRules posts rules", async () => { + const provider = createMockProvider() + + const result = await handleRequestRules(provider, "/workspace") + + expect(result).toEqual(mockRules) + expect(getRules).toHaveBeenCalledWith("/workspace", { modes: [{ slug: "code", name: "Code" }] }) + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "rules", rules: mockRules }) + }) + + it("handleRequestRules posts an empty list on failure", async () => { + const provider = createMockProvider() + vi.mocked(getRules).mockRejectedValue(new Error("scan failed")) + + const result = await handleRequestRules(provider, "/workspace") + + expect(result).toEqual([]) + expect(mockLog).toHaveBeenCalled() + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "rules", rules: [] }) + }) + + it("handleCreateRule creates, opens, and posts refreshed rules", async () => { + const provider = createMockProvider() + vi.mocked(createRule).mockResolvedValue("/workspace/.roo/rules/new.md") + + const result = await handleCreateRule(provider, "/workspace", { + type: "createRule", + values: { scope: "project", kind: "generic", fileName: "new.md" }, + } as WebviewMessage) + + expect(result).toEqual(mockRules) + expect(createRule).toHaveBeenCalledWith("/workspace", { scope: "project", kind: "generic", fileName: "new.md" }) + expect(openFile).toHaveBeenCalledWith("/workspace/.roo/rules/new.md") + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "rules", rules: mockRules }) + }) + + it("handleDeleteRule deletes and posts refreshed rules", async () => { + const provider = createMockProvider() + + const result = await handleDeleteRule(provider, "/workspace", { + type: "deleteRule", + values: { scope: "global", kind: "generic", relativePath: "rule.md" }, + } as WebviewMessage) + + expect(result).toEqual(mockRules) + expect(deleteRule).toHaveBeenCalledWith("/workspace", { + scope: "global", + kind: "generic", + relativePath: "rule.md", + }) + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "rules", rules: mockRules }) + }) + + it("handleOpenRuleFile safely resolves and opens the rule file", async () => { + const provider = createMockProvider() + vi.mocked(resolveRuleFile).mockResolvedValue("/workspace/.roo/rules/rule.md") + + await handleOpenRuleFile(provider, "/workspace", { + type: "openRuleFile", + values: { scope: "project", kind: "generic", relativePath: "rule.md" }, + } as WebviewMessage) + + expect(resolveRuleFile).toHaveBeenCalledWith("/workspace", { + scope: "project", + kind: "generic", + relativePath: "rule.md", + }) + expect(openFile).toHaveBeenCalledWith("/workspace/.roo/rules/rule.md") + }) + + it("handleDeleteRule shows an error when required delete values are missing", async () => { + const provider = createMockProvider() + + const result = await handleDeleteRule(provider, "/workspace", { + type: "deleteRule", + values: { scope: "global", kind: "generic" }, + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(deleteRule).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to delete rule: Missing required fields: scope, kind, or relativePath", + ) + }) + + it("handleOpenRuleFile shows an error when the resolved rule file is missing", async () => { + const provider = createMockProvider() + vi.mocked(resolveRuleFile).mockResolvedValue(undefined) + + await handleOpenRuleFile(provider, "/workspace", { + type: "openRuleFile", + values: { scope: "project", kind: "generic", relativePath: "missing.md" }, + } as WebviewMessage) + + expect(openFile).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to open rule file: Rule file not found") + }) + + it("handleOpenRulesDirectory shows an error when directory input is invalid", async () => { + const provider = createMockProvider() + vi.mocked(getRulesDirectoryPath).mockImplementation(() => { + throw new Error("Invalid rule scope") + }) + + await handleOpenRulesDirectory(provider, "/workspace", { + type: "openRulesDirectory", + values: { scope: "team", kind: "generic" }, + } as WebviewMessage) + + expect(openFile).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to open rules directory: Invalid rule scope", + ) + }) + + it("handleCreateRule shows an error for missing required values before creating", async () => { + const provider = createMockProvider() + + const result = await handleCreateRule(provider, "/workspace", { + type: "createRule", + values: { scope: "project", kind: "generic" }, + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(createRule).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to create rule: Missing required fields: scope, kind, or fileName", + ) + }) + + it("handleCreateRule validates scope and kind before creating", async () => { + const provider = createMockProvider() + + const result = await handleCreateRule(provider, "/workspace", { + type: "createRule", + values: { scope: "team", kind: "workspace", fileName: "new.md" }, + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(createRule).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to create rule: Missing required fields: scope, kind, or fileName", + ) + }) + + it("handleCreateRule shows an error for missing workspace project rules and does not refresh", async () => { + const provider = createMockProvider() + vi.mocked(createRule).mockRejectedValue(new Error("Workspace rules require an open workspace")) + + const result = await handleCreateRule(provider, "", { + type: "createRule", + values: { scope: "project", kind: "generic", fileName: "new.md" }, + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Failed to create rule: Workspace rules require an open workspace", + ) + expect(mockPostMessageToWebview).not.toHaveBeenCalled() + }) + it("handleRequestRules safely handles nullish errors", async () => { + const provider = createMockProvider() + vi.mocked(getRules).mockRejectedValue(null) + + const result = await handleRequestRules(provider, "/workspace") + + expect(result).toEqual([]) + expect(mockLog).toHaveBeenCalledWith("Error fetching rules: null") + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "rules", rules: [] }) + }) + + it("handleCreateRule warns when refresh fails after creation succeeds", async () => { + const provider = createMockProvider() + vi.mocked(createRule).mockResolvedValue("/workspace/.roo/rules/new.md") + vi.mocked(getRules).mockRejectedValue(new Error("refresh failed")) + + const result = await handleCreateRule(provider, "/workspace", { + type: "createRule", + values: { scope: "project", kind: "generic", fileName: "new.md" }, + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(createRule).toHaveBeenCalledWith("/workspace", { scope: "project", kind: "generic", fileName: "new.md" }) + expect(openFile).toHaveBeenCalledWith("/workspace/.roo/rules/new.md") + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( + "Rule created, but refreshing the rules list failed.", + ) + }) + + it("handleDeleteRule warns when refresh fails after deletion succeeds", async () => { + const provider = createMockProvider() + vi.mocked(getRules).mockRejectedValue(new Error("refresh failed")) + + const result = await handleDeleteRule(provider, "/workspace", { + type: "deleteRule", + values: { scope: "global", kind: "generic", relativePath: "rule.md" }, + } as WebviewMessage) + + expect(result).toBeUndefined() + expect(deleteRule).toHaveBeenCalledWith("/workspace", { + scope: "global", + kind: "generic", + relativePath: "rule.md", + }) + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( + "Rule deleted, but refreshing the rules list failed.", + ) + }) + + it("handleOpenRulesDirectory resolves and opens the requested rules directory", async () => { + const provider = createMockProvider() + vi.mocked(getRulesDirectoryPath).mockReturnValue("/workspace/.roo/rules-code") + + await handleOpenRulesDirectory(provider, "/workspace", { + type: "openRulesDirectory", + values: { scope: "project", kind: "mode", modeSlug: "code" }, + } as WebviewMessage) + + expect(getRulesDirectoryPath).toHaveBeenCalledWith("/workspace", { + scope: "project", + kind: "mode", + modeSlug: "code", + }) + expect(openFile).toHaveBeenCalledWith("/workspace/.roo/rules-code") + }) +}) diff --git a/src/core/webview/__tests__/webviewMessageHandler.checkpoint.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.checkpoint.spec.ts index 2b2b0f78b1..71a58d6f8d 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.checkpoint.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.checkpoint.spec.ts @@ -1,12 +1,23 @@ import { describe, it, expect, vi, beforeEach } from "vitest" +import pWaitFor from "p-wait-for" import { webviewMessageHandler } from "../webviewMessageHandler" import { saveTaskMessages } from "../../task-persistence" import { handleCheckpointRestoreOperation } from "../checkpointRestoreHandler" import { MessageManager } from "../../message-manager" // Mock dependencies -vi.mock("../../task-persistence") +vi.mock("../../task-persistence", async (importOriginal) => ({ + ...(await importOriginal()), + saveTaskMessages: vi.fn(), +})) vi.mock("../checkpointRestoreHandler") +vi.mock("p-wait-for", () => ({ + default: vi.fn(async (condition: () => boolean) => { + if (!condition()) { + throw new Error("condition not met") + } + }), +})) vi.mock("vscode", () => ({ window: { showErrorMessage: vi.fn(), @@ -26,6 +37,7 @@ describe("webviewMessageHandler - checkpoint operations", () => { // Setup mock Cline instance mockCline = { taskId: "test-task-123", + isInitialized: true, clineMessages: [ { ts: 1, type: "user", say: "user", text: "First message" }, { ts: 2, type: "assistant", say: "checkpoint_saved", text: "abc123" }, @@ -37,6 +49,7 @@ describe("webviewMessageHandler - checkpoint operations", () => { { ts: 3, role: "user", content: [{ type: "text", text: "Message to delete" }] }, { ts: 4, role: "assistant", content: [{ type: "text", text: "After message" }] }, ], + checkpointDiff: vi.fn(), checkpointRestore: vi.fn(), overwriteClineMessages: vi.fn(), overwriteApiConversationHistory: vi.fn(), @@ -52,6 +65,7 @@ describe("webviewMessageHandler - checkpoint operations", () => { })), createTaskWithHistoryItem: vi.fn(), setPendingEditOperation: vi.fn(), + cancelTask: vi.fn(), contextProxy: { globalStorageUri: { fsPath: "/test/storage" }, }, @@ -134,4 +148,109 @@ describe("webviewMessageHandler - checkpoint operations", () => { }) }) }) + + describe("completion checkpoint actions", () => { + beforeEach(() => { + mockCline.clineMessages = [ + { ts: 1, type: "say", say: "text", text: "Initial task" }, + { ts: 2, type: "say", say: "checkpoint_saved", text: "initial-checkpoint" }, + { ts: 3, type: "say", say: "user_feedback", text: "Latest prompt" }, + { ts: 4, type: "say", say: "checkpoint_saved", text: "latest-prompt-checkpoint" }, + { ts: 5, type: "say", say: "completion_result", text: "Task complete" }, + { ts: 6, type: "ask", ask: "completion_result", text: "", partial: false }, + ] + }) + + it("diffs changes from the checkpoint created after the latest prompt", async () => { + await webviewMessageHandler(mockProvider, { type: "completionCheckpointDiff" }) + + expect(mockCline.checkpointDiff).toHaveBeenCalledWith({ + ts: 4, + commitHash: "latest-prompt-checkpoint", + mode: "to-current", + }) + }) + + it("restores files and task state to the checkpoint created after the latest prompt", async () => { + const callOrder: string[] = [] + mockProvider.cancelTask.mockImplementation(async () => callOrder.push("cancelTask")) + mockCline.checkpointRestore.mockImplementation(async () => callOrder.push("checkpointRestore")) + + await webviewMessageHandler(mockProvider, { type: "completionCheckpointRestore" }) + + expect(mockProvider.cancelTask).toHaveBeenCalled() + expect(mockCline.checkpointRestore).toHaveBeenCalledWith({ + ts: 4, + commitHash: "latest-prompt-checkpoint", + mode: "restore", + }) + expect(callOrder).toEqual(["cancelTask", "checkpointRestore"]) + }) + + it("does not diff or restore when no latest-prompt checkpoint exists", async () => { + mockCline.clineMessages = [ + { ts: 1, type: "say", say: "text", text: "Initial task" }, + { ts: 2, type: "say", say: "user_feedback", text: "Latest prompt" }, + { ts: 3, type: "ask", ask: "completion_result", text: "", partial: false }, + ] + + await webviewMessageHandler(mockProvider, { type: "completionCheckpointDiff" }) + await webviewMessageHandler(mockProvider, { type: "completionCheckpointRestore" }) + + expect(mockCline.checkpointDiff).not.toHaveBeenCalled() + expect(mockCline.checkpointRestore).not.toHaveBeenCalled() + expect(mockProvider.cancelTask).not.toHaveBeenCalled() + }) + + it("resolves the latest completion checkpoint in the extension host", async () => { + await webviewMessageHandler(mockProvider, { type: "completionCheckpointDiff" }) + + expect(mockCline.checkpointDiff).toHaveBeenCalledWith({ + ts: 4, + commitHash: "latest-prompt-checkpoint", + mode: "to-current", + }) + }) + + it("does not restore when task re-initialization times out", async () => { + ;(pWaitFor as any).mockRejectedValueOnce(new Error("timed out")) + + await webviewMessageHandler(mockProvider, { type: "completionCheckpointRestore" }) + + expect(mockProvider.cancelTask).toHaveBeenCalled() + expect(mockCline.checkpointRestore).not.toHaveBeenCalled() + const vscode = await import("vscode") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.checkpoint_timeout") + }) + + it("shows an error when completion checkpoint restore fails", async () => { + const restoreError = new Error("restore failed") + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + mockCline.checkpointRestore.mockRejectedValueOnce(restoreError) + + await webviewMessageHandler(mockProvider, { type: "completionCheckpointRestore" }) + + const vscode = await import("vscode") + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[completionCheckpointRestore] checkpointRestore failed:", + restoreError, + ) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.checkpoint_failed") + consoleErrorSpy.mockRestore() + }) + + it("does not restore when task identity changes during cancellation", async () => { + mockProvider.getCurrentTask.mockReturnValueOnce(mockCline).mockReturnValue({ + ...mockCline, + taskId: "different-task-id", + }) + + await webviewMessageHandler(mockProvider, { type: "completionCheckpointRestore" }) + + expect(mockProvider.cancelTask).toHaveBeenCalled() + expect(mockCline.checkpointRestore).not.toHaveBeenCalled() + const vscode = await import("vscode") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.checkpoint_failed") + }) + }) }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts index 1af6b43bc1..ef2bee3f6d 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts @@ -5,7 +5,8 @@ import { ClineProvider } from "../ClineProvider" import { MessageManager } from "../../message-manager" // Mock the saveTaskMessages function -vi.mock("../../task-persistence", () => ({ +vi.mock("../../task-persistence", async (importOriginal) => ({ + ...(await importOriginal()), saveTaskMessages: vi.fn(), })) diff --git a/src/core/webview/__tests__/webviewMessageHandler.edit.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.edit.spec.ts index 2f11281d2b..523f03e1c2 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.edit.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.edit.spec.ts @@ -22,7 +22,8 @@ vi.mock("vscode", () => ({ }, })) -vi.mock("../../task-persistence", () => ({ +vi.mock("../../task-persistence", async (importOriginal) => ({ + ...(await importOriginal()), saveTaskMessages: vi.fn(), })) diff --git a/src/core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts new file mode 100644 index 0000000000..df85ff1df4 --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts @@ -0,0 +1,257 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string) => key), + changeLanguage: vi.fn(), +})) + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +vi.mock("vscode", () => ({ + window: { + showErrorMessage: vi.fn(), + showWarningMessage: vi.fn(), + showInformationMessage: vi.fn(), + }, + workspace: { + workspaceFolders: undefined, + getConfiguration: vi.fn(() => ({ + get: vi.fn(), + update: vi.fn(), + })), + }, + env: { + clipboard: { writeText: vi.fn() }, + openExternal: vi.fn(), + }, + commands: { + executeCommand: vi.fn(), + }, + Uri: { + parse: vi.fn((s: string) => ({ toString: () => s })), + file: vi.fn((p: string) => ({ fsPath: p })), + }, + ConfigurationTarget: { + Global: 1, + Workspace: 2, + WorkspaceFolder: 3, + }, +})) + +const importRooTaskHistoryMock = vi.fn() +vi.mock("../../task-persistence/importRooTaskHistory", () => ({ + importRooTaskHistory: (...args: any[]) => importRooTaskHistoryMock(...args), +})) + +import * as vscode from "vscode" + +describe("webviewMessageHandler - importRooHistory", () => { + let mockProvider: ClineProvider & { + contextProxy: any + taskHistoryStore: { + invalidateAll: ReturnType + reconcile: ReturnType + flushIndex: ReturnType + } + postMessageToWebview: ReturnType + postStateToWebview: ReturnType + log: ReturnType + } + + beforeEach(() => { + vi.clearAllMocks() + + mockProvider = { + contextProxy: { + getValue: vi.fn(), + setValue: vi.fn(), + globalStorageUri: { fsPath: "/mock/storage" }, + }, + taskHistoryStore: { + invalidateAll: vi.fn(), + reconcile: vi.fn().mockResolvedValue(undefined), + flushIndex: vi.fn().mockResolvedValue(undefined), + }, + postMessageToWebview: vi.fn().mockResolvedValue(undefined), + postStateToWebview: vi.fn().mockResolvedValue(undefined), + log: vi.fn(), + } as any + }) + + it("refreshes task history, streams progress, and shows a success message after importing Roo history", async () => { + importRooTaskHistoryMock.mockImplementation(async (_globalStoragePath, onProgress) => { + await onProgress?.({ + copiedFileCount: 2, + totalFileCount: 4, + importedTaskCount: 1, + totalTaskCount: 2, + currentTaskId: "task-1", + currentFileName: "ui_messages.json", + }) + + return { + rooExtensionDomain: "RooVeterinaryInc.roo-cline", + zooExtensionDomain: "ZooCodeOrganization.zoo-code", + rooStorageRoots: ["/mock/roo-storage"], + zooStorageRoot: "/mock/storage", + foundTaskCount: 2, + importedTaskCount: 2, + importedFileCount: 4, + } + }) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + expect(importRooTaskHistoryMock).toHaveBeenCalledWith("/mock/storage", expect.any(Function)) + expect(mockProvider.taskHistoryStore.invalidateAll).toHaveBeenCalledTimes(1) + expect(mockProvider.taskHistoryStore.reconcile).toHaveBeenCalledTimes(1) + expect(mockProvider.taskHistoryStore.flushIndex).toHaveBeenCalledTimes(1) + expect(mockProvider.postStateToWebview).toHaveBeenCalledTimes(1) + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(1, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "starting", + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + }, + }) + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "copying", + copiedFileCount: 2, + totalFileCount: 4, + importedTaskCount: 1, + totalTaskCount: 2, + currentTaskId: "task-1", + currentFileName: "ui_messages.json", + }, + }) + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(3, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + copiedFileCount: 4, + totalFileCount: 4, + importedTaskCount: 2, + totalTaskCount: 2, + currentTaskId: "task-1", + currentFileName: "ui_messages.json", + }, + }) + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("common:info.rooHistoryImport.success") + expect(vscode.window.showWarningMessage).not.toHaveBeenCalled() + }) + + it("uses the singular success message when one Roo task history is imported", async () => { + importRooTaskHistoryMock.mockResolvedValue({ + rooExtensionDomain: "RooVeterinaryInc.roo-cline", + zooExtensionDomain: "ZooCodeOrganization.zoo-code", + rooStorageRoots: ["/mock/roo-storage"], + zooStorageRoot: "/mock/storage", + foundTaskCount: 1, + importedTaskCount: 1, + importedFileCount: 2, + }) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("common:info.rooHistoryImport.success") + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + copiedFileCount: 2, + totalFileCount: 2, + importedTaskCount: 1, + totalTaskCount: 1, + }, + }) + expect(vscode.window.showWarningMessage).not.toHaveBeenCalled() + }) + + it("shows a 'not found' warning when no Roo history exists at all", async () => { + importRooTaskHistoryMock.mockResolvedValue({ + rooExtensionDomain: "RooVeterinaryInc.roo-cline", + zooExtensionDomain: "ZooCodeOrganization.zoo-code", + rooStorageRoots: ["/mock/roo-storage"], + zooStorageRoot: "/mock/storage", + foundTaskCount: 0, + importedTaskCount: 0, + importedFileCount: 0, + }) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + expect(importRooTaskHistoryMock).toHaveBeenCalledWith("/mock/storage", expect.any(Function)) + expect(mockProvider.taskHistoryStore.invalidateAll).not.toHaveBeenCalled() + expect(mockProvider.taskHistoryStore.reconcile).not.toHaveBeenCalled() + expect(mockProvider.taskHistoryStore.flushIndex).not.toHaveBeenCalled() + expect(mockProvider.postStateToWebview).not.toHaveBeenCalled() + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + }, + }) + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("common:warnings.rooHistoryImport.nothingFound") + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled() + }) + + it("shows an 'already imported' warning when all Roo tasks are already in Zoo", async () => { + importRooTaskHistoryMock.mockResolvedValue({ + rooExtensionDomain: "RooVeterinaryInc.roo-cline", + zooExtensionDomain: "ZooCodeOrganization.zoo-code", + rooStorageRoots: ["/mock/roo-storage"], + zooStorageRoot: "/mock/storage", + foundTaskCount: 3, + importedTaskCount: 0, + importedFileCount: 0, + }) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + // History is refreshed even when nothing new was imported, so a retry + // after a partial-copy failure still reconciles the store. + expect(mockProvider.taskHistoryStore.invalidateAll).toHaveBeenCalledTimes(1) + expect(mockProvider.taskHistoryStore.reconcile).toHaveBeenCalledTimes(1) + expect(mockProvider.taskHistoryStore.flushIndex).toHaveBeenCalledTimes(1) + expect(mockProvider.postStateToWebview).toHaveBeenCalledTimes(1) + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( + "common:warnings.rooHistoryImport.alreadyImported", + ) + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled() + }) + + it("shows an error without refreshing task history when the import throws", async () => { + importRooTaskHistoryMock.mockRejectedValue(new Error("permission denied")) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + expect(mockProvider.taskHistoryStore.invalidateAll).not.toHaveBeenCalled() + expect(mockProvider.taskHistoryStore.reconcile).not.toHaveBeenCalled() + expect(mockProvider.taskHistoryStore.flushIndex).not.toHaveBeenCalled() + expect(mockProvider.postStateToWebview).not.toHaveBeenCalled() + expect(mockProvider.log).toHaveBeenCalledWith("[importRooHistory] failed: permission denied") + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "failed", + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + }, + }) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.rooHistoryImport") + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled() + expect(vscode.window.showWarningMessage).not.toHaveBeenCalled() + }) +}) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 9704a7229d..335911a7c7 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -43,17 +43,33 @@ vi.mock("../diagnosticsHandler", () => ({ generateErrorDiagnostics: vi.fn().mockResolvedValue({ success: true, filePath: "/tmp/diagnostics.json" }), })) +vi.mock("../rulesMessageHandler", () => ({ + handleRequestRules: vi.fn(), + handleCreateRule: vi.fn(), + handleDeleteRule: vi.fn(), + handleOpenRuleFile: vi.fn(), + handleOpenRulesDirectory: vi.fn(), +})) + import type { ModelRecord } from "@roo-code/types" import { webviewMessageHandler } from "../webviewMessageHandler" import type { ClineProvider } from "../ClineProvider" -import { getModels } from "../../../api/providers/fetchers/modelCache" +import { flushModels, getModels } from "../../../api/providers/fetchers/modelCache" import { getLMStudioModels } from "../../../api/providers/fetchers/lmstudio" import { getCommands } from "../../../services/command/commands" +import { + handleCreateRule, + handleDeleteRule, + handleOpenRuleFile, + handleOpenRulesDirectory, + handleRequestRules, +} from "../rulesMessageHandler" const { openAiCodexOAuthManager } = await import("../../../integrations/openai-codex/oauth") const { fetchOpenAiCodexRateLimitInfo } = await import("../../../integrations/openai-codex/rate-limits") const mockGetModels = getModels as Mock +const mockFlushModels = flushModels as Mock const mockGetLMStudioModels = getLMStudioModels as Mock const mockGetCommands = vi.mocked(getCommands) const mockGetAccessToken = vi.mocked(openAiCodexOAuthManager.getAccessToken) @@ -418,6 +434,40 @@ describe("webviewMessageHandler - requestRouterModels", () => { expect(routerModelsCall?.[0].routerModels["opencode-go"]).toEqual(mockModels) }) + it("flushes and fetches Opencode Go models when an explicit API key is supplied", async () => { + mockClineProvider.getState = vi.fn().mockResolvedValue({ + apiConfiguration: {}, + }) + mockGetModels.mockResolvedValue({ + "opencode/model": { + maxTokens: 4096, + contextWindow: 8192, + supportsPromptCache: false, + description: "Opencode model", + }, + }) + + await webviewMessageHandler(mockClineProvider, { + type: "requestRouterModels", + values: { + provider: "opencode-go", + opencodeGoApiKey: "fresh-key", + }, + }) + + expect(mockFlushModels).toHaveBeenCalledWith({ provider: "opencode-go", apiKey: "fresh-key" }, true) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "opencode-go", apiKey: "fresh-key" }) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "routerModels", + routerModels: { + "opencode-go": { + "opencode/model": expect.objectContaining({ description: "Opencode model" }), + }, + }, + values: { provider: "opencode-go" }, + }) + }) + it("handles LiteLLM models with values from message when config is missing", async () => { mockClineProvider.getState = vi.fn().mockResolvedValue({ apiConfiguration: { @@ -629,7 +679,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) }) - it("prefers config values over message values for LiteLLM", async () => { + it("prefers message values over config values for LiteLLM", async () => { const mockModels: ModelRecord = {} mockGetModels.mockResolvedValue(mockModels) @@ -641,11 +691,11 @@ describe("webviewMessageHandler - requestRouterModels", () => { }, }) - // Verify config values are used over message values + // Verify message values take precedence over saved config (current unsaved field state wins) expect(mockGetModels).toHaveBeenCalledWith({ provider: "litellm", - apiKey: "litellm-key", // From config - baseUrl: "http://localhost:4000", // From config + apiKey: "message-key", // From message.values + baseUrl: "http://message-url", // From message.values }) }) }) @@ -1208,6 +1258,45 @@ describe("webviewMessageHandler - requestCommands", () => { }) }) +describe("webviewMessageHandler - rules", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue(undefined) + ;(mockClineProvider as any).cwd = "/mock/workspace" + }) + + it("routes rules management messages with the current workspace", async () => { + const messages = [ + { type: "requestRules" }, + { type: "createRule", values: { scope: "project", kind: "generic", fileName: "new.md" } }, + { type: "deleteRule", values: { scope: "project", kind: "generic", relativePath: "old.md" } }, + { type: "openRuleFile", values: { scope: "global", kind: "generic", relativePath: "global.md" } }, + { type: "openRulesDirectory", values: { scope: "project", kind: "mode", modeSlug: "code" } }, + ] as const + + for (const message of messages) { + await webviewMessageHandler(mockClineProvider, message as any) + } + + expect(handleRequestRules).toHaveBeenCalledWith(mockClineProvider, "/mock/workspace") + expect(handleCreateRule).toHaveBeenCalledWith(mockClineProvider, "/mock/workspace", messages[1]) + expect(handleDeleteRule).toHaveBeenCalledWith(mockClineProvider, "/mock/workspace", messages[2]) + expect(handleOpenRuleFile).toHaveBeenCalledWith(mockClineProvider, "/mock/workspace", messages[3]) + expect(handleOpenRulesDirectory).toHaveBeenCalledWith(mockClineProvider, "/mock/workspace", messages[4]) + }) + + it("uses the active task cwd when routing rule messages", async () => { + vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue({ + cwd: "/mock/task-workspace", + } as unknown as ReturnType) + + const message = { type: "requestRules" } as const + await webviewMessageHandler(mockClineProvider, message as any) + + expect(handleRequestRules).toHaveBeenCalledWith(mockClineProvider, "/mock/task-workspace") + }) +}) + describe("webviewMessageHandler - downloadErrorDiagnostics", () => { beforeEach(() => { vi.clearAllMocks() diff --git a/src/core/webview/rulesMessageHandler.ts b/src/core/webview/rulesMessageHandler.ts new file mode 100644 index 0000000000..343adf8738 --- /dev/null +++ b/src/core/webview/rulesMessageHandler.ts @@ -0,0 +1,170 @@ +import * as vscode from "vscode" + +import type { + CreateRuleInput, + DeleteRuleInput, + RuleKind, + RuleMetadata, + RuleScope, + WebviewMessage, +} from "@roo-code/types" + +import type { ClineProvider } from "./ClineProvider" +import { openFile } from "../../integrations/misc/open-file" +import { createRule, deleteRule, getRules, getRulesDirectoryPath, resolveRuleFile } from "../../services/rules/rules" + +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +export async function handleRequestRules(provider: ClineProvider, cwd: string): Promise { + try { + const modes = await provider.getModes() + const rules = await getRules(cwd, { modes }) + await provider.postMessageToWebview({ type: "rules", rules }) + return rules + } catch (error) { + provider.log(`Error fetching rules: ${getErrorMessage(error)}`) + await provider.postMessageToWebview({ type: "rules", rules: [] }) + return [] + } +} + +export async function handleCreateRule( + provider: ClineProvider, + cwd: string, + message: WebviewMessage, +): Promise { + try { + const input = parseCreateRuleInput(message) + const createdPath = await createRule(cwd, input) + openFile(createdPath) + } catch (error) { + const errorMessage = getErrorMessage(error) + provider.log(`Error creating rule: ${errorMessage}`) + vscode.window.showErrorMessage(`Failed to create rule: ${errorMessage}`) + return undefined + } + + try { + return await refreshRules(provider, cwd) + } catch (error) { + const errorMessage = getErrorMessage(error) + provider.log(`Rule created but failed to refresh rules: ${errorMessage}`) + vscode.window.showWarningMessage("Rule created, but refreshing the rules list failed.") + return undefined + } +} + +export async function handleDeleteRule( + provider: ClineProvider, + cwd: string, + message: WebviewMessage, +): Promise { + try { + const input = parseDeleteRuleInput(message) + await deleteRule(cwd, input) + } catch (error) { + const errorMessage = getErrorMessage(error) + provider.log(`Error deleting rule: ${errorMessage}`) + vscode.window.showErrorMessage(`Failed to delete rule: ${errorMessage}`) + return undefined + } + + try { + return await refreshRules(provider, cwd) + } catch (error) { + const errorMessage = getErrorMessage(error) + provider.log(`Rule deleted but failed to refresh rules: ${errorMessage}`) + vscode.window.showWarningMessage("Rule deleted, but refreshing the rules list failed.") + return undefined + } +} + +export async function handleOpenRuleFile(provider: ClineProvider, cwd: string, message: WebviewMessage): Promise { + try { + const input = parseDeleteRuleInput(message) + const filePath = await resolveRuleFile(cwd, input) + if (!filePath) { + throw new Error("Rule file not found") + } + + openFile(filePath) + } catch (error) { + const errorMessage = getErrorMessage(error) + provider.log(`Error opening rule file: ${errorMessage}`) + vscode.window.showErrorMessage(`Failed to open rule file: ${errorMessage}`) + } +} + +export async function handleOpenRulesDirectory( + provider: ClineProvider, + cwd: string, + message: WebviewMessage, +): Promise { + try { + const values = message.values ?? {} + const directoryPath = getRulesDirectoryPath(cwd, { + scope: values.scope, + kind: values.kind, + modeSlug: values.modeSlug, + } as CreateRuleInput) + openFile(directoryPath) + } catch (error) { + const errorMessage = getErrorMessage(error) + provider.log(`Error opening rules directory: ${errorMessage}`) + vscode.window.showErrorMessage(`Failed to open rules directory: ${errorMessage}`) + } +} + +async function refreshRules(provider: ClineProvider, cwd: string): Promise { + const modes = await provider.getModes() + const rules = await getRules(cwd, { modes }) + await provider.postMessageToWebview({ type: "rules", rules }) + return rules +} + +function parseCreateRuleInput(message: WebviewMessage): CreateRuleInput { + const values = message.values ?? {} + const scope = parseRuleScope(values.scope) + const kind = parseRuleKind(values.kind) + const fileName = values.fileName ?? message.text + + if (!scope || !kind || !fileName) { + throw new Error("Missing required fields: scope, kind, or fileName") + } + + return { + scope, + kind, + modeSlug: typeof values.modeSlug === "string" ? values.modeSlug : undefined, + fileName, + } +} + +function parseDeleteRuleInput(message: WebviewMessage): DeleteRuleInput { + const values = message.values ?? {} + const scope = parseRuleScope(values.scope) + const kind = parseRuleKind(values.kind) + const relativePath = values.relativePath ?? message.text + + if (!scope || !kind || !relativePath) { + throw new Error("Missing required fields: scope, kind, or relativePath") + } + + return { + id: typeof values.id === "string" ? values.id : undefined, + scope, + kind, + modeSlug: typeof values.modeSlug === "string" ? values.modeSlug : undefined, + relativePath, + } +} + +function parseRuleScope(value: unknown): RuleScope | undefined { + return value === "global" || value === "project" ? value : undefined +} + +function parseRuleKind(value: unknown): RuleKind | undefined { + return value === "generic" || value === "mode" ? value : undefined +} diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 2ba2ba379e..fdc33d75c4 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -21,6 +21,7 @@ import { ExperimentId, checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, + getCompletionCheckpoint, } from "@roo-code/types" import { customToolRegistry } from "@roo-code/core" import { CloudService } from "@roo-code/cloud" @@ -28,6 +29,7 @@ import { TelemetryService } from "@roo-code/telemetry" import { type ApiMessage } from "../task-persistence/apiMessages" import { saveTaskMessages } from "../task-persistence" +import { importRooTaskHistory } from "../task-persistence/importRooTaskHistory" import { ClineProvider } from "./ClineProvider" import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler" @@ -40,6 +42,13 @@ import { handleUpdateSkillModes, handleOpenSkillFile, } from "./skillsMessageHandler" +import { + handleRequestRules, + handleCreateRule, + handleDeleteRule, + handleOpenRuleFile, + handleOpenRulesDirectory, +} from "./rulesMessageHandler" import { changeLanguage, t } from "../../i18n" import { Package } from "../../shared/package" import { type RouterName, toRouterName } from "../../shared/api" @@ -112,6 +121,10 @@ export const webviewMessageHandler = async ( vscode.window.showInformationMessage(getRouterUnavailableSignInMessage()) } + const resolveCompletionCheckpoint = (currentCline: { clineMessages: ClineMessage[] }) => { + return getCompletionCheckpoint(currentCline.clineMessages) + } + const getCurrentMode = async (): Promise => { const currentTask = provider.getCurrentTask() @@ -903,6 +916,92 @@ export const webviewMessageHandler = async ( break } + case "importRooHistory": { + let latestProgress = { + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + } + + try { + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "starting", + ...latestProgress, + }, + }) + + const result = await importRooTaskHistory( + provider.contextProxy.globalStorageUri.fsPath, + async (progress) => { + latestProgress = progress + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "copying", + ...progress, + }, + }) + }, + ) + + if (result.foundTaskCount === 0) { + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + ...latestProgress, + }, + }) + vscode.window.showWarningMessage( + t("common:warnings.rooHistoryImport.nothingFound", { domain: result.rooExtensionDomain }), + ) + break + } + + // Refresh history whenever Roo tasks were found — even if all already existed — + // so a retry after a partial-copy failure still reconciles the store. + provider.taskHistoryStore.invalidateAll() + await provider.taskHistoryStore.reconcile() + await provider.taskHistoryStore.flushIndex() + await provider.postStateToWebview() + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + ...latestProgress, + copiedFileCount: result.importedFileCount, + totalFileCount: latestProgress.totalFileCount || result.importedFileCount, + importedTaskCount: result.importedTaskCount, + totalTaskCount: latestProgress.totalTaskCount || result.importedTaskCount, + }, + }) + + if (result.importedTaskCount === 0) { + vscode.window.showWarningMessage( + t("common:warnings.rooHistoryImport.alreadyImported", { count: result.foundTaskCount }), + ) + } else { + vscode.window.showInformationMessage( + t("common:info.rooHistoryImport.success", { count: result.importedTaskCount }), + ) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + provider.log(`[importRooHistory] failed: ${message}`) + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "failed", + ...latestProgress, + }, + }) + vscode.window.showErrorMessage(t("common:errors.rooHistoryImport", { error: message })) + } + break + } case "exportSettings": await exportSettings({ providerSettingsManager: provider.providerSettingsManager, @@ -919,7 +1018,7 @@ export const webviewMessageHandler = async ( // For providers that need credentials, use their specific handlers await flushModels({ provider: routerNameFlush } as GetModelsOptions, true) break - case "requestRouterModels": + case "requestRouterModels": { const { apiConfiguration } = await provider.getState() // Optional single provider filter from webview @@ -987,9 +1086,11 @@ export const webviewMessageHandler = async ( }, ] - // LiteLLM is conditional on baseUrl+apiKey - const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey - const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl + // LiteLLM is conditional on baseUrl+apiKey. + // Prefer explicit values from message (current unsaved field state) over saved config, + // matching the pattern used for DeepSeek and other credential-carrying providers. + const litellmApiKey = message?.values?.litellmApiKey ?? apiConfiguration.litellmApiKey + const litellmBaseUrl = message?.values?.litellmBaseUrl ?? apiConfiguration.litellmBaseUrl if (litellmApiKey && litellmBaseUrl) { // If explicit credentials are provided in message.values (from Refresh Models button), @@ -1098,6 +1199,7 @@ export const webviewMessageHandler = async ( values: providerFilter ? { provider: requestedProvider } : undefined, }) break + } case "requestOllamaModels": { // Specific handler for Ollama models only. const { apiConfiguration: ollamaApiConfig } = await provider.getState() @@ -1280,6 +1382,7 @@ export const webviewMessageHandler = async ( await pWaitFor(() => provider.getCurrentTask()?.isInitialized === true, { timeout: 3_000 }) } catch (error) { vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout")) + return } try { @@ -1291,6 +1394,56 @@ export const webviewMessageHandler = async ( break } + case "completionCheckpointDiff": { + const currentCline = provider.getCurrentTask() + const checkpoint = currentCline ? resolveCompletionCheckpoint(currentCline) : undefined + + if (currentCline && checkpoint) { + await currentCline.checkpointDiff({ + ts: checkpoint.ts, + commitHash: checkpoint.commitHash, + mode: "to-current", + }) + } + + break + } + case "completionCheckpointRestore": { + const currentCline = provider.getCurrentTask() + const checkpoint = currentCline ? resolveCompletionCheckpoint(currentCline) : undefined + + if (currentCline && checkpoint) { + const originalTaskId = currentCline.taskId + await provider.cancelTask() + + try { + await pWaitFor(() => provider.getCurrentTask()?.isInitialized === true, { timeout: 3_000 }) + } catch (error) { + vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout")) + return + } + + try { + const restoredTask = provider.getCurrentTask() + + if (!restoredTask || restoredTask.taskId !== originalTaskId) { + vscode.window.showErrorMessage(t("common:errors.checkpoint_failed")) + return + } + + await restoredTask.checkpointRestore({ + ts: checkpoint.ts, + commitHash: checkpoint.commitHash, + mode: "restore", + }) + } catch (error) { + console.error("[completionCheckpointRestore] checkpointRestore failed:", error) + vscode.window.showErrorMessage(t("common:errors.checkpoint_failed")) + } + } + + break + } case "cancelTask": await provider.cancelTask() break @@ -3148,6 +3301,26 @@ export const webviewMessageHandler = async ( await handleOpenSkillFile(provider, message) break } + case "requestRules": { + await handleRequestRules(provider, getCurrentCwd()) + break + } + case "createRule": { + await handleCreateRule(provider, getCurrentCwd(), message) + break + } + case "deleteRule": { + await handleDeleteRule(provider, getCurrentCwd(), message) + break + } + case "openRuleFile": { + await handleOpenRuleFile(provider, getCurrentCwd(), message) + break + } + case "openRulesDirectory": { + await handleOpenRulesDirectory(provider, getCurrentCwd(), message) + break + } case "openCommandFile": { try { if (message.text) { diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index c488d77f25..5c866ef941 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -33,7 +33,7 @@ export default [ { // Ratchet: enforce no-floating-promises directory by directory. Each // directory is added here once its floating promises are resolved. - files: ["activate/**/*.ts"], + files: ["activate/**/*.ts", "core/task/**/*.ts"], languageOptions: { parserOptions: { project: true, diff --git a/src/extension/__tests__/api-task-conversation-history-length.spec.ts b/src/extension/__tests__/api-task-conversation-history-length.spec.ts new file mode 100644 index 0000000000..4cfd9bbe4b --- /dev/null +++ b/src/extension/__tests__/api-task-conversation-history-length.spec.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import * as vscode from "vscode" + +import { API } from "../api" +import { ClineProvider } from "../../core/webview/ClineProvider" + +vi.mock("vscode") +vi.mock("../../core/webview/ClineProvider") + +describe("API#getTaskApiConversationHistoryLength", () => { + let api: API + let mockOutputChannel: vscode.OutputChannel + let mockProvider: ClineProvider + let mockGetTaskWithId: ReturnType + + beforeEach(() => { + mockOutputChannel = { + appendLine: vi.fn(), + } as unknown as vscode.OutputChannel + + mockGetTaskWithId = vi.fn() + + mockProvider = { + context: {} as vscode.ExtensionContext, + getTaskWithId: mockGetTaskWithId, + on: vi.fn(), + } as unknown as ClineProvider + + api = new API(mockOutputChannel, mockProvider, undefined, true) + }) + + it("returns the persisted api conversation history length", async () => { + mockGetTaskWithId.mockResolvedValue({ + apiConversationHistory: [{ role: "user" }, { role: "assistant" }], + }) + + await expect(api.getTaskApiConversationHistoryLength("task-1")).resolves.toBe(2) + }) + + it("returns 0 instead of throwing when the task is unavailable", async () => { + mockGetTaskWithId.mockRejectedValue(new Error("Task not found")) + + await expect(api.getTaskApiConversationHistoryLength("missing-task")).resolves.toBe(0) + }) +}) diff --git a/src/extension/api.ts b/src/extension/api.ts index 0f056f16da..c140ec8ec0 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -235,6 +235,20 @@ export class API extends EventEmitter implements RooCodeAPI { } } + public async getTaskHistoryItem(taskId: string) { + const item = this.sidebarProvider.taskHistoryStore.get(taskId) + return item ? structuredClone(item) : undefined + } + + public async getTaskApiConversationHistoryLength(taskId: string): Promise { + try { + const { apiConversationHistory } = await this.sidebarProvider.getTaskWithId(taskId) + return apiConversationHistory.length + } catch { + return 0 + } + } + public getCurrentTaskStack() { return this.sidebarProvider.getCurrentTaskStack() } @@ -418,6 +432,17 @@ export class API extends EventEmitter implements RooCodeAPI { this.emit(RooCodeEventName.TaskCreated, task.taskId) }) + + // Delegation events are emitted by the provider, not by individual task instances. + provider.on(RooCodeEventName.TaskDelegated, (parentTaskId, childTaskId) => { + ;(this.emit as any)(RooCodeEventName.TaskDelegated, parentTaskId, childTaskId) + }) + provider.on(RooCodeEventName.TaskDelegationCompleted, (parentTaskId, childTaskId, summary) => { + ;(this.emit as any)(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, summary) + }) + provider.on(RooCodeEventName.TaskDelegationResumed, (parentTaskId, childTaskId) => { + ;(this.emit as any)(RooCodeEventName.TaskDelegationResumed, parentTaskId, childTaskId) + }) } // Logging diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 0a1c99f1f8..f988fa0c37 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -145,12 +145,18 @@ "manual_url_missing_params": "URL de callback no vàlida: falten paràmetres requerits (code i state)", "manual_url_auth_failed": "Autenticació manual per URL ha fallat", "manual_url_auth_error": "Autenticació fallida", - "mode_import_failed": "Ha fallat la importació del mode: {{error}}" + "mode_import_failed": "Ha fallat la importació del mode: {{error}}", + "rooHistoryImport": "No s'ha pogut importar l'historial de Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "No s'ha seleccionat contingut de terminal", "missing_task_files": "Els fitxers d'aquesta tasca falten. Vols eliminar-la de la llista de tasques?", - "auto_import_failed": "Ha fallat la importació automàtica de la configuració de ZooCode: {{error}}" + "auto_import_failed": "Ha fallat la importació automàtica de la configuració de ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "No s'ha trobat historial de tasques de Roo Code per importar de {{domain}}.", + "alreadyImported_one": "Tot l'historial de tasca de Roo Code ({{count}}) ja s'ha importat a Zoo Code.", + "alreadyImported_other": "Tots els {{count}} historials de tasques de Roo Code ja s'han importat a Zoo Code." + } }, "info": { "no_changes": "No s'han trobat canvis.", @@ -169,6 +175,10 @@ "mode_imported": "Mode importat correctament", "roo": { "signInUnavailable": "L'inici de sessió de Roo Code Cloud no està disponible en aquest moment. Configura un altre proveïdor per continuar." + }, + "rooHistoryImport": { + "success_one": "S'ha importat {{count}} historial de tasca de Roo Code a Zoo Code.", + "success_other": "S'han importat {{count}} historials de tasques de Roo Code a Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/ca/embeddings.json b/src/i18n/locales/ca/embeddings.json index 9ceec7d05c..3075773504 100644 --- a/src/i18n/locales/ca/embeddings.json +++ b/src/i18n/locales/ca/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indexació requereix una carpeta de workspace oberta", "indexingStopped": "Indexació aturada per l'usuari.", "indexingStoppedPartial": "Indexació aturada. Dades d'índex parcials conservades." + }, + "semble": { + "downloadingBinary": "Descarregant el binari de semble...", + "ready": "Semble {{version}} està llest. Les cerques s'indexen sobre la marxa.", + "unsupportedPlatform": "Semble no és compatible amb aquesta plataforma ({{platform}}-{{arch}}).", + "downloadFailed": "Error en descarregar semble: {{errorMessage}}", + "checkFailed": "Comprovació de semble fallida: {{errorMessage}}", + "providerReset": "Proveïdor de Semble restablert. La memòria cau al disc es conserva fins a la propera reconstrucció." } } diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 02f27925a5..03dc94ca22 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -141,12 +141,18 @@ "manual_url_no_query": "Ungültige Callback-URL: Query-Parameter fehlen", "manual_url_missing_params": "Ungültige Callback-URL: erforderliche Parameter (code und state) fehlen", "manual_url_auth_failed": "Manuelle URL-Authentifizierung fehlgeschlagen", - "manual_url_auth_error": "Authentifizierung fehlgeschlagen" + "manual_url_auth_error": "Authentifizierung fehlgeschlagen", + "rooHistoryImport": "Fehler beim Importieren des Roo Code-Verlaufs: {{error}}" }, "warnings": { "no_terminal_content": "Kein Terminal-Inhalt ausgewählt", "missing_task_files": "Die Dateien dieser Aufgabe fehlen. Möchtest du sie aus der Aufgabenliste entfernen?", - "auto_import_failed": "Fehler beim automatischen Import der ZooCode-Einstellungen: {{error}}" + "auto_import_failed": "Fehler beim automatischen Import der ZooCode-Einstellungen: {{error}}", + "rooHistoryImport": { + "nothingFound": "Es wurde kein Roo Code-Aufgabenverlauf zum Importieren von {{domain}} gefunden.", + "alreadyImported_one": "Der {{count}} Roo Code-Aufgabenverlauf wurde bereits in Zoo Code importiert.", + "alreadyImported_other": "Alle {{count}} Roo Code-Aufgabenverläufe wurden bereits in Zoo Code importiert." + } }, "info": { "no_changes": "Keine Änderungen gefunden.", @@ -165,6 +171,10 @@ "mode_imported": "Modus erfolgreich importiert", "roo": { "signInUnavailable": "Die Anmeldung bei Roo Code Cloud ist derzeit nicht verfügbar. Konfiguriere einen anderen Anbieter, um fortzufahren." + }, + "rooHistoryImport": { + "success_one": "{{count}} Roo Code-Aufgabenverlauf in Zoo Code importiert.", + "success_other": "{{count}} Roo Code-Aufgabenverläufe in Zoo Code importiert." } }, "answers": { diff --git a/src/i18n/locales/de/embeddings.json b/src/i18n/locales/de/embeddings.json index 766d31d5ba..4209c80a16 100644 --- a/src/i18n/locales/de/embeddings.json +++ b/src/i18n/locales/de/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indexierung erfordert einen offenen Workspace-Ordner", "indexingStopped": "Indexierung vom Benutzer gestoppt.", "indexingStoppedPartial": "Indexierung gestoppt. Teilweise Indexdaten beibehalten." + }, + "semble": { + "downloadingBinary": "Semble-Binary wird heruntergeladen...", + "ready": "Semble {{version}} ist bereit. Suchen erfolgen spontan.", + "unsupportedPlatform": "Semble wird auf dieser Plattform nicht unterstützt ({{platform}}-{{arch}}).", + "downloadFailed": "Fehler beim Herunterladen von Semble: {{errorMessage}}", + "checkFailed": "Semble-Überprüfung fehlgeschlagen: {{errorMessage}}", + "providerReset": "Semble-Anbieter zurückgesetzt. Der Cache auf der Festplatte bleibt bis zum nächsten Neuaufbau erhalten." } } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 5abed998a5..190af9180a 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -78,6 +78,7 @@ "share_not_enabled": "Task sharing is not enabled for this organization.", "share_task_not_found": "Task not found or access denied.", "mode_import_failed": "Failed to import mode: {{error}}", + "rooHistoryImport": "Failed to import Roo Code history: {{error}}", "delete_rules_folder_failed": "Failed to delete rules folder: {{rulesFolderPath}}. Error: {{error}}", "command_not_found": "Command '{{name}}' not found", "open_command_file": "Failed to open command file", @@ -146,7 +147,12 @@ "warnings": { "no_terminal_content": "No terminal content selected", "missing_task_files": "This task's files are missing. Would you like to remove it from the task list?", - "auto_import_failed": "Failed to auto-import ZooCode settings: {{error}}" + "auto_import_failed": "Failed to auto-import ZooCode settings: {{error}}", + "rooHistoryImport": { + "nothingFound": "No Roo Code task history was found to import from {{domain}}.", + "alreadyImported_one": "All {{count}} Roo Code task history has already been imported into Zoo Code.", + "alreadyImported_other": "All {{count}} Roo Code task histories have already been imported into Zoo Code." + } }, "info": { "no_changes": "No changes found.", @@ -163,6 +169,10 @@ "image_saved": "Image saved to {{path}}", "mode_exported": "Mode '{{mode}}' exported successfully", "mode_imported": "Mode imported successfully", + "rooHistoryImport": { + "success_one": "Imported {{count}} Roo Code task history into Zoo Code.", + "success_other": "Imported {{count}} Roo Code task histories into Zoo Code." + }, "roo": { "signInUnavailable": "Roo Code Cloud sign-in is currently unavailable. Configure another provider to continue." } diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index 7777af9027..36b31a1272 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indexing requires an open workspace folder", "indexingStopped": "Indexing stopped by user.", "indexingStoppedPartial": "Indexing stopped. Partial index data preserved." + }, + "semble": { + "downloadingBinary": "Downloading semble binary...", + "ready": "Semble {{version}} is ready. Searches index on-the-fly.", + "unsupportedPlatform": "Semble is not supported on this platform ({{platform}}-{{arch}}).", + "downloadFailed": "Failed to download semble: {{errorMessage}}", + "checkFailed": "Semble check failed: {{errorMessage}}", + "providerReset": "Semble provider reset. On-disk cache remains until next rebuild." } } diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 3c5d6ea754..4fcbd82a85 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Error al importar el historial de Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "No hay contenido de terminal seleccionado", "missing_task_files": "Los archivos de esta tarea faltan. ¿Deseas eliminarla de la lista de tareas?", - "auto_import_failed": "Error al importar automáticamente la configuración de ZooCode: {{error}}" + "auto_import_failed": "Error al importar automáticamente la configuración de ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "No se encontró historial de tareas de Roo Code para importar desde {{domain}}.", + "alreadyImported_one": "El historial de {{count}} tarea de Roo Code ya ha sido importado en Zoo Code.", + "alreadyImported_other": "Los {{count}} historiales de tareas de Roo Code ya han sido importados en Zoo Code." + } }, "info": { "no_changes": "No se encontraron cambios.", @@ -165,6 +171,10 @@ "mode_imported": "Modo importado correctamente", "roo": { "signInUnavailable": "El inicio de sesión de Roo Code Cloud no está disponible en este momento. Configura otro proveedor para continuar." + }, + "rooHistoryImport": { + "success_one": "Se importó {{count}} historial de tarea de Roo Code en Zoo Code.", + "success_other": "Se importaron {{count}} historiales de tareas de Roo Code en Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/es/embeddings.json b/src/i18n/locales/es/embeddings.json index 930404de1f..8589169f52 100644 --- a/src/i18n/locales/es/embeddings.json +++ b/src/i18n/locales/es/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "La indexación requiere una carpeta de workspace abierta", "indexingStopped": "Indexación detenida por el usuario.", "indexingStoppedPartial": "Indexación detenida. Datos de índice parciales conservados." + }, + "semble": { + "downloadingBinary": "Descargando el binario de semble...", + "ready": "Semble {{version}} está listo. Las búsquedas se indexan sobre la marcha.", + "unsupportedPlatform": "Semble no es compatible con esta plataforma ({{platform}}-{{arch}}).", + "downloadFailed": "Error al descargar semble: {{errorMessage}}", + "checkFailed": "Verificación de semble fallida: {{errorMessage}}", + "providerReset": "Proveedor de Semble restablecido. La caché en disco se conserva hasta la próxima reconstrucción." } } diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 7f9e466f04..ece55c9b11 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Échec de l'importation de l'historique Roo Code : {{error}}" }, "warnings": { "no_terminal_content": "Aucun contenu de terminal sélectionné", "missing_task_files": "Les fichiers de cette tâche sont introuvables. Souhaitez-vous la supprimer de la liste des tâches ?", - "auto_import_failed": "Échec de l'importation automatique des paramètres ZooCode : {{error}}" + "auto_import_failed": "Échec de l'importation automatique des paramètres ZooCode : {{error}}", + "rooHistoryImport": { + "nothingFound": "Aucun historique de tâches Roo Code trouvé à importer depuis {{domain}}.", + "alreadyImported_one": "L'historique de {{count}} tâche Roo Code a déjà été importé dans Zoo Code.", + "alreadyImported_other": "Les {{count}} historiques de tâches Roo Code ont déjà été importés dans Zoo Code." + } }, "info": { "no_changes": "Aucun changement trouvé.", @@ -165,6 +171,10 @@ "mode_imported": "Mode importé avec succès", "roo": { "signInUnavailable": "La connexion à Roo Code Cloud n'est pas disponible pour le moment. Configure un autre fournisseur pour continuer." + }, + "rooHistoryImport": { + "success_one": "{{count}} historique de tâche Roo Code importé dans Zoo Code.", + "success_other": "{{count}} historiques de tâches Roo Code importés dans Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/fr/embeddings.json b/src/i18n/locales/fr/embeddings.json index 7de086307e..33f3d5fbde 100644 --- a/src/i18n/locales/fr/embeddings.json +++ b/src/i18n/locales/fr/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "L'indexation nécessite l'ouverture d'un dossier workspace", "indexingStopped": "Indexation arrêtée par l'utilisateur.", "indexingStoppedPartial": "Indexation arrêtée. Données d'index partielles conservées." + }, + "semble": { + "downloadingBinary": "Téléchargement du binaire semble...", + "ready": "Semble {{version}} est prêt. Les recherches s'indexent à la volée.", + "unsupportedPlatform": "Semble n'est pas pris en charge sur cette plateforme ({{platform}}-{{arch}}).", + "downloadFailed": "Échec du téléchargement de semble : {{errorMessage}}", + "checkFailed": "Vérification de semble échouée : {{errorMessage}}", + "providerReset": "Fournisseur Semble réinitialisé. Le cache sur disque reste jusqu'à la prochaine reconstruction." } } diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 3c567dac08..42e46f0e9e 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Roo Code इतिहास आयात करने में विफल: {{error}}" }, "warnings": { "no_terminal_content": "कोई टर्मिनल सामग्री चयनित नहीं", "missing_task_files": "इस टास्क की फाइलें गायब हैं। क्या आप इसे टास्क सूची से हटाना चाहते हैं?", - "auto_import_failed": "ZooCode सेटिंग्स का स्वचालित आयात विफल: {{error}}" + "auto_import_failed": "ZooCode सेटिंग्स का स्वचालित आयात विफल: {{error}}", + "rooHistoryImport": { + "nothingFound": "{{domain}} से आयात करने के लिए कोई Roo Code कार्य इतिहास नहीं मिला।", + "alreadyImported_one": "सभी {{count}} Roo Code कार्य इतिहास पहले से ही Zoo Code में आयात किया जा चुका है।", + "alreadyImported_other": "सभी {{count}} Roo Code कार्य इतिहास पहले से ही Zoo Code में आयात किए जा चुके हैं।" + } }, "info": { "no_changes": "कोई परिवर्तन नहीं मिला।", @@ -165,6 +171,10 @@ "mode_imported": "मोड सफलतापूर्वक आयात किया गया", "roo": { "signInUnavailable": "Roo Code Cloud साइन-इन अभी उपलब्ध नहीं है। जारी रखने के लिए कोई दूसरा प्रदाता कॉन्फ़िगर करें।" + }, + "rooHistoryImport": { + "success_one": "{{count}} Roo Code कार्य इतिहास Zoo Code में आयात किया।", + "success_other": "{{count}} Roo Code कार्य इतिहास Zoo Code में आयात किए।" } }, "answers": { diff --git a/src/i18n/locales/hi/embeddings.json b/src/i18n/locales/hi/embeddings.json index 9c7f9ca50a..e1fc603113 100644 --- a/src/i18n/locales/hi/embeddings.json +++ b/src/i18n/locales/hi/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "इंडेक्सिंग के लिए एक खुला वर्कस्पेस फ़ोल्डर आवश्यक है", "indexingStopped": "उपयोगकर्ता द्वारा इंडेक्सिंग रोकी गई।", "indexingStoppedPartial": "इंडेक्सिंग रोकी गई। आंशिक इंडेक्स डेटा संरक्षित।" + }, + "semble": { + "downloadingBinary": "semble बाइनरी डाउनलोड हो रहा है...", + "ready": "Semble {{version}} तैयार है। खोजें ऑन-द-फ्लाई इंडेक्स होती हैं।", + "unsupportedPlatform": "इस प्लेटफ़ॉर्म पर Semble समर्थित नहीं है ({{platform}}-{{arch}})।", + "downloadFailed": "semble डाउनलोड करने में विफल: {{errorMessage}}", + "checkFailed": "Semble जाँच विफल: {{errorMessage}}", + "providerReset": "Semble प्रदाता रीसेट किया गया। डिस्क कैश अगले रीबिल्ड तक बना रहता है।" } } diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 7208ea9507..92dbf24171 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Gagal mengimpor riwayat Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Tidak ada konten terminal yang dipilih", "missing_task_files": "File tugas ini hilang. Apakah kamu ingin menghapusnya dari daftar tugas?", - "auto_import_failed": "Gagal mengimpor pengaturan ZooCode secara otomatis: {{error}}" + "auto_import_failed": "Gagal mengimpor pengaturan ZooCode secara otomatis: {{error}}", + "rooHistoryImport": { + "nothingFound": "Tidak ada riwayat tugas Roo Code yang ditemukan untuk diimpor dari {{domain}}.", + "alreadyImported_one": "Semua {{count}} riwayat tugas Roo Code sudah diimpor ke Zoo Code.", + "alreadyImported_other": "Semua {{count}} riwayat tugas Roo Code sudah diimpor ke Zoo Code." + } }, "info": { "no_changes": "Tidak ada perubahan ditemukan.", @@ -165,6 +171,10 @@ "mode_imported": "Mode berhasil diimpor", "roo": { "signInUnavailable": "Masuk ke Roo Code Cloud saat ini tidak tersedia. Konfigurasikan penyedia lain untuk melanjutkan." + }, + "rooHistoryImport": { + "success_one": "{{count}} riwayat tugas Roo Code berhasil diimpor ke Zoo Code.", + "success_other": "{{count}} riwayat tugas Roo Code berhasil diimpor ke Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/id/embeddings.json b/src/i18n/locales/id/embeddings.json index 955a039eff..3163e4ff48 100644 --- a/src/i18n/locales/id/embeddings.json +++ b/src/i18n/locales/id/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Pengindeksan memerlukan folder workspace yang terbuka", "indexingStopped": "Pengindeksan dihentikan oleh pengguna.", "indexingStoppedPartial": "Pengindeksan dihentikan. Data indeks parsial dipertahankan." + }, + "semble": { + "downloadingBinary": "Mengunduh biner semble...", + "ready": "Semble {{version}} siap. Pencarian diindeks secara langsung.", + "unsupportedPlatform": "Semble tidak didukung di platform ini ({{platform}}-{{arch}}).", + "downloadFailed": "Gagal mengunduh semble: {{errorMessage}}", + "checkFailed": "Pemeriksaan semble gagal: {{errorMessage}}", + "providerReset": "Penyedia Semble direset. Cache di disk tetap ada hingga pembangunan ulang berikutnya." } } diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index a69a96092c..fd739f7d8d 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Impossibile importare la cronologia di Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Nessun contenuto del terminale selezionato", "missing_task_files": "I file di questa attività sono mancanti. Vuoi rimuoverla dall'elenco delle attività?", - "auto_import_failed": "Importazione automatica delle impostazioni ZooCode fallita: {{error}}" + "auto_import_failed": "Importazione automatica delle impostazioni ZooCode fallita: {{error}}", + "rooHistoryImport": { + "nothingFound": "Nessuna cronologia di attività Roo Code trovata da importare da {{domain}}.", + "alreadyImported_one": "La cronologia di {{count}} attività Roo Code è già stata importata in Zoo Code.", + "alreadyImported_other": "Tutte le {{count}} cronologie di attività Roo Code sono già state importate in Zoo Code." + } }, "info": { "no_changes": "Nessuna modifica trovata.", @@ -165,6 +171,10 @@ "mode_imported": "Modalità importata con successo", "roo": { "signInUnavailable": "L'accesso a Roo Code Cloud non è attualmente disponibile. Configura un altro provider per continuare." + }, + "rooHistoryImport": { + "success_one": "Importata {{count}} cronologia di attività Roo Code in Zoo Code.", + "success_other": "Importate {{count}} cronologie di attività Roo Code in Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/it/embeddings.json b/src/i18n/locales/it/embeddings.json index b7314c244d..f49eabb978 100644 --- a/src/i18n/locales/it/embeddings.json +++ b/src/i18n/locales/it/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "L'indicizzazione richiede una cartella di workspace aperta", "indexingStopped": "Indicizzazione interrotta dall'utente.", "indexingStoppedPartial": "Indicizzazione interrotta. Dati di indice parziali conservati." + }, + "semble": { + "downloadingBinary": "Download del binario semble in corso...", + "ready": "Semble {{version}} è pronto. Le ricerche vengono indicizzate al volo.", + "unsupportedPlatform": "Semble non è supportato su questa piattaforma ({{platform}}-{{arch}}).", + "downloadFailed": "Download di semble fallito: {{errorMessage}}", + "checkFailed": "Verifica di semble fallita: {{errorMessage}}", + "providerReset": "Provider Semble ripristinato. La cache su disco rimane fino alla prossima ricostruzione." } } diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index bea4db707c..06c8fd4a05 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Roo Codeの履歴のインポートに失敗しました: {{error}}" }, "warnings": { "no_terminal_content": "選択されたターミナルコンテンツがありません", "missing_task_files": "このタスクのファイルが見つかりません。タスクリストから削除しますか?", - "auto_import_failed": "ZooCode設定の自動インポートに失敗しました:{{error}}" + "auto_import_failed": "ZooCode設定の自動インポートに失敗しました:{{error}}", + "rooHistoryImport": { + "nothingFound": "{{domain}}からインポートするRoo Codeのタスク履歴が見つかりませんでした。", + "alreadyImported_one": "{{count}}件のRoo Codeタスク履歴はすでにZoo Codeにインポート済みです。", + "alreadyImported_other": "{{count}}件のRoo Codeタスク履歴はすでにZoo Codeにインポート済みです。" + } }, "info": { "no_changes": "変更は見つかりませんでした。", @@ -165,6 +171,10 @@ "mode_imported": "モードが正常にインポートされました", "roo": { "signInUnavailable": "Roo Code Cloud へのサインインは現在利用できません。続行するには別のプロバイダーを設定してください。" + }, + "rooHistoryImport": { + "success_one": "{{count}}件のRoo CodeタスクをZoo Codeにインポートしました。", + "success_other": "{{count}}件のRoo CodeタスクをZoo Codeにインポートしました。" } }, "answers": { diff --git a/src/i18n/locales/ja/embeddings.json b/src/i18n/locales/ja/embeddings.json index ce7150cf1c..38526a8ad6 100644 --- a/src/i18n/locales/ja/embeddings.json +++ b/src/i18n/locales/ja/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "インデックス作成には、開かれたワークスペースフォルダーが必要です", "indexingStopped": "ユーザーによりインデックス作成が停止されました。", "indexingStoppedPartial": "インデックス作成が停止されました。部分的なインデックスデータは保持されています。" + }, + "semble": { + "downloadingBinary": "sembleバイナリをダウンロード中...", + "ready": "Semble {{version}}の準備ができました。検索はその場でインデックス化されます。", + "unsupportedPlatform": "このプラットフォームではSembleはサポートされていません ({{platform}}-{{arch}})。", + "downloadFailed": "sembleのダウンロードに失敗しました: {{errorMessage}}", + "checkFailed": "Sembleの確認に失敗しました: {{errorMessage}}", + "providerReset": "Sembleプロバイダーをリセットしました。次回の再構築までディスクキャッシュは残ります。" } } diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index daf9f69e4c..4b2944cfd9 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Roo Code 기록 가져오기 실패: {{error}}" }, "warnings": { "no_terminal_content": "선택된 터미널 내용이 없습니다", "missing_task_files": "이 작업의 파일이 누락되었습니다. 작업 목록에서 제거하시겠습니까?", - "auto_import_failed": "ZooCode 설정 자동 가져오기 실패: {{error}}" + "auto_import_failed": "ZooCode 설정 자동 가져오기 실패: {{error}}", + "rooHistoryImport": { + "nothingFound": "{{domain}}에서 가져올 Roo Code 작업 기록을 찾을 수 없습니다.", + "alreadyImported_one": "{{count}}개의 Roo Code 작업 기록이 이미 Zoo Code로 가져와졌습니다.", + "alreadyImported_other": "{{count}}개의 Roo Code 작업 기록이 이미 Zoo Code로 가져와졌습니다." + } }, "info": { "no_changes": "변경 사항이 없습니다.", @@ -165,6 +171,10 @@ "mode_imported": "모드를 성공적으로 가져왔습니다", "roo": { "signInUnavailable": "Roo Code Cloud 로그인은 현재 사용할 수 없습니다. 계속하려면 다른 제공업체를 구성하세요." + }, + "rooHistoryImport": { + "success_one": "Roo Code 작업 기록 {{count}}개를 Zoo Code로 가져왔습니다.", + "success_other": "Roo Code 작업 기록 {{count}}개를 Zoo Code로 가져왔습니다." } }, "answers": { diff --git a/src/i18n/locales/ko/embeddings.json b/src/i18n/locales/ko/embeddings.json index 436fa985c0..94ac0ace73 100644 --- a/src/i18n/locales/ko/embeddings.json +++ b/src/i18n/locales/ko/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "인덱싱에는 열린 워크스페이스 폴더가 필요합니다", "indexingStopped": "사용자에 의해 인덱싱이 중지되었습니다.", "indexingStoppedPartial": "인덱싱이 중지되었습니다. 부분 인덱스 데이터가 보존되었습니다." + }, + "semble": { + "downloadingBinary": "semble 바이너리 다운로드 중...", + "ready": "Semble {{version}}가 준비되었습니다. 검색은 즉시 인덱싱됩니다.", + "unsupportedPlatform": "이 플랫폼에서는 Semble가 지원되지 않습니다 ({{platform}}-{{arch}}).", + "downloadFailed": "semble 다운로드 실패: {{errorMessage}}", + "checkFailed": "Semble 확인 실패: {{errorMessage}}", + "providerReset": "Semble 공급자가 재설정되었습니다. 디스크 캐시는 다음 재구축까지 유지됩니다." } } diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 3f4c1e984d..96a921c6c1 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Importeren van Roo Code-geschiedenis mislukt: {{error}}" }, "warnings": { "no_terminal_content": "Geen terminalinhoud geselecteerd", "missing_task_files": "De bestanden van deze taak ontbreken. Wil je deze uit de takenlijst verwijderen?", - "auto_import_failed": "Automatisch importeren van ZooCode-instellingen mislukt: {{error}}" + "auto_import_failed": "Automatisch importeren van ZooCode-instellingen mislukt: {{error}}", + "rooHistoryImport": { + "nothingFound": "Geen Roo Code-taakgeschiedenis gevonden om te importeren van {{domain}}.", + "alreadyImported_one": "De {{count}} Roo Code-taakgeschiedenis is al geïmporteerd in Zoo Code.", + "alreadyImported_other": "Alle {{count}} Roo Code-taakgeschiedenissen zijn al geïmporteerd in Zoo Code." + } }, "info": { "no_changes": "Geen wijzigingen gevonden.", @@ -165,6 +171,10 @@ "mode_imported": "Modus succesvol geïmporteerd", "roo": { "signInUnavailable": "Inloggen bij Roo Code Cloud is momenteel niet beschikbaar. Configureer een andere provider om door te gaan." + }, + "rooHistoryImport": { + "success_one": "{{count}} Roo Code-taakgeschiedenis geïmporteerd in Zoo Code.", + "success_other": "{{count}} Roo Code-taakgeschiedenissen geïmporteerd in Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/nl/embeddings.json b/src/i18n/locales/nl/embeddings.json index 01e68683d3..da68323b5f 100644 --- a/src/i18n/locales/nl/embeddings.json +++ b/src/i18n/locales/nl/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indexering vereist een geopende workspace map", "indexingStopped": "Indexering gestopt door gebruiker.", "indexingStoppedPartial": "Indexering gestopt. Gedeeltelijke indexgegevens bewaard." + }, + "semble": { + "downloadingBinary": "Semble-binary downloaden...", + "ready": "Semble {{version}} is klaar. Zoekopdrachten worden direct geïndexeerd.", + "unsupportedPlatform": "Semble wordt niet ondersteund op dit platform ({{platform}}-{{arch}}).", + "downloadFailed": "Downloaden van semble mislukt: {{errorMessage}}", + "checkFailed": "Semble-controle mislukt: {{errorMessage}}", + "providerReset": "Semble-provider gereset. Schijfcache blijft behouden tot de volgende heropbouw." } } diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index c88aabcaa5..f3e789e842 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Nie udało się zaimportować historii Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Nie wybrano zawartości terminala", "missing_task_files": "Pliki tego zadania są brakujące. Czy chcesz usunąć je z listy zadań?", - "auto_import_failed": "Nie udało się automatycznie zaimportować ustawień ZooCode: {{error}}" + "auto_import_failed": "Nie udało się automatycznie zaimportować ustawień ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "Nie znaleziono historii zadań Roo Code do zaimportowania z {{domain}}.", + "alreadyImported_one": "Cała {{count}} historia zadań Roo Code została już zaimportowana do Zoo Code.", + "alreadyImported_other": "Wszystkie {{count}} historii zadań Roo Code zostały już zaimportowane do Zoo Code." + } }, "info": { "no_changes": "Nie znaleziono zmian.", @@ -165,6 +171,10 @@ "mode_imported": "Tryb pomyślnie zaimportowany", "roo": { "signInUnavailable": "Logowanie do Roo Code Cloud jest obecnie niedostępne. Skonfiguruj innego dostawcę, aby kontynuować." + }, + "rooHistoryImport": { + "success_one": "Zaimportowano {{count}} historię zadania Roo Code do Zoo Code.", + "success_other": "Zaimportowano {{count}} historii zadań Roo Code do Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/pl/embeddings.json b/src/i18n/locales/pl/embeddings.json index 0ef846b2cc..73715fea52 100644 --- a/src/i18n/locales/pl/embeddings.json +++ b/src/i18n/locales/pl/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Indeksowanie wymaga otwartego folderu workspace", "indexingStopped": "Indeksowanie zatrzymane przez użytkownika.", "indexingStoppedPartial": "Indeksowanie zatrzymane. Częściowe dane indeksu zachowane." + }, + "semble": { + "downloadingBinary": "Pobieranie binariów semble...", + "ready": "Semble {{version}} jest gotowy. Wyszukiwania są indeksowane w locie.", + "unsupportedPlatform": "Semble nie jest obsługiwany na tej platformie ({{platform}}-{{arch}}).", + "downloadFailed": "Nie udało się pobrać semble: {{errorMessage}}", + "checkFailed": "Sprawdzenie semble nie powiodło się: {{errorMessage}}", + "providerReset": "Dostawca Semble zresetowany. Pamięć podręczna na dysku pozostaje do kolejnej przebudowy." } } diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index ce4c51e08d..ac5ec44f6b 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -145,12 +145,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Falha ao importar o histórico do Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Nenhum conteúdo do terminal selecionado", "missing_task_files": "Os arquivos desta tarefa estão faltando. Deseja removê-la da lista de tarefas?", - "auto_import_failed": "Falha ao importar automaticamente as configurações do ZooCode: {{error}}" + "auto_import_failed": "Falha ao importar automaticamente as configurações do ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "Nenhum histórico de tarefas do Roo Code encontrado para importar de {{domain}}.", + "alreadyImported_one": "O histórico de {{count}} tarefa do Roo Code já foi importado para o Zoo Code.", + "alreadyImported_other": "Todos os {{count}} históricos de tarefas do Roo Code já foram importados para o Zoo Code." + } }, "info": { "no_changes": "Nenhuma alteração encontrada.", @@ -169,6 +175,10 @@ "mode_imported": "Modo importado com sucesso", "roo": { "signInUnavailable": "O login do Roo Code Cloud não está disponível no momento. Configure outro provedor para continuar." + }, + "rooHistoryImport": { + "success_one": "{{count}} histórico de tarefa do Roo Code importado para o Zoo Code.", + "success_other": "{{count}} históricos de tarefas do Roo Code importados para o Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/pt-BR/embeddings.json b/src/i18n/locales/pt-BR/embeddings.json index 9cdf775e76..2f4f7eb8d1 100644 --- a/src/i18n/locales/pt-BR/embeddings.json +++ b/src/i18n/locales/pt-BR/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "A indexação requer uma pasta de workspace aberta", "indexingStopped": "Indexação interrompida pelo usuário.", "indexingStoppedPartial": "Indexação interrompida. Dados de índice parciais preservados." + }, + "semble": { + "downloadingBinary": "Baixando o binário do semble...", + "ready": "Semble {{version}} está pronto. As buscas são indexadas em tempo real.", + "unsupportedPlatform": "Semble não é suportado nesta plataforma ({{platform}}-{{arch}}).", + "downloadFailed": "Falha ao baixar semble: {{errorMessage}}", + "checkFailed": "Verificação do semble falhou: {{errorMessage}}", + "providerReset": "Provedor Semble redefinido. O cache em disco permanece até a próxima reconstrução." } } diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 9d096fb346..26d4b2032f 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Ошибка импорта истории Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Не выбрано содержимое терминала", "missing_task_files": "Файлы этой задачи отсутствуют. Хотите удалить её из списка задач?", - "auto_import_failed": "Не удалось автоматически импортировать настройки ZooCode: {{error}}" + "auto_import_failed": "Не удалось автоматически импортировать настройки ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "История задач Roo Code для импорта из {{domain}} не найдена.", + "alreadyImported_one": "{{count}} история задач Roo Code уже импортирована в Zoo Code.", + "alreadyImported_other": "Все {{count}} истории задач Roo Code уже импортированы в Zoo Code." + } }, "info": { "no_changes": "Изменения не найдены.", @@ -165,6 +171,10 @@ "mode_imported": "Режим успешно импортирован", "roo": { "signInUnavailable": "Вход в Roo Code Cloud сейчас недоступен. Настрой другого провайдера, чтобы продолжить." + }, + "rooHistoryImport": { + "success_one": "{{count}} история задач Roo Code импортирована в Zoo Code.", + "success_other": "{{count}} историй задач Roo Code импортировано в Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/ru/embeddings.json b/src/i18n/locales/ru/embeddings.json index 873b1c0630..de86730af7 100644 --- a/src/i18n/locales/ru/embeddings.json +++ b/src/i18n/locales/ru/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Для индексации требуется открытая папка рабочего пространства", "indexingStopped": "Индексация остановлена пользователем.", "indexingStoppedPartial": "Индексация остановлена. Частичные данные индекса сохранены." + }, + "semble": { + "downloadingBinary": "Загрузка бинарного файла semble...", + "ready": "Semble {{version}} готов. Поиск индексируется на лету.", + "unsupportedPlatform": "Semble не поддерживается на этой платформе ({{platform}}-{{arch}}).", + "downloadFailed": "Не удалось загрузить semble: {{errorMessage}}", + "checkFailed": "Проверка semble не удалась: {{errorMessage}}", + "providerReset": "Провайдер Semble сброшен. Дисковый кеш сохраняется до следующей пересборки." } } diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 3d83d28c32..bde000783a 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Roo Code geçmişi içe aktarılamadı: {{error}}" }, "warnings": { "no_terminal_content": "Seçili terminal içeriği yok", "missing_task_files": "Bu görevin dosyaları eksik. Görev listesinden kaldırmak istiyor musunuz?", - "auto_import_failed": "ZooCode ayarları otomatik olarak içe aktarılamadı: {{error}}" + "auto_import_failed": "ZooCode ayarları otomatik olarak içe aktarılamadı: {{error}}", + "rooHistoryImport": { + "nothingFound": "{{domain}} kaynağından içe aktarılacak Roo Code görev geçmişi bulunamadı.", + "alreadyImported_one": "{{count}} Roo Code görev geçmişinin tamamı zaten Zoo Code'a aktarılmış.", + "alreadyImported_other": "{{count}} Roo Code görev geçmişinin tamamı zaten Zoo Code'a aktarılmış." + } }, "info": { "no_changes": "Değişiklik bulunamadı.", @@ -165,6 +171,10 @@ "mode_imported": "Mod başarıyla içe aktarıldı", "roo": { "signInUnavailable": "Roo Code Cloud girişi şu anda kullanılamıyor. Devam etmek için farklı bir sağlayıcı yapılandırın." + }, + "rooHistoryImport": { + "success_one": "{{count}} Roo Code görev geçmişi Zoo Code'a aktarıldı.", + "success_other": "{{count}} Roo Code görev geçmişi Zoo Code'a aktarıldı." } }, "answers": { diff --git a/src/i18n/locales/tr/embeddings.json b/src/i18n/locales/tr/embeddings.json index 30b703a93f..37d0595aef 100644 --- a/src/i18n/locales/tr/embeddings.json +++ b/src/i18n/locales/tr/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "İndeksleme açık bir workspace klasörü gerektirir", "indexingStopped": "İndeksleme kullanıcı tarafından durduruldu.", "indexingStoppedPartial": "İndeksleme durduruldu. Kısmi indeks verileri korundu." + }, + "semble": { + "downloadingBinary": "semble ikili dosyası indiriliyor...", + "ready": "Semble {{version}} hazır. Aramalar anında indekslenir.", + "unsupportedPlatform": "Semble bu platformda desteklenmiyor ({{platform}}-{{arch}}).", + "downloadFailed": "semble indirilemedi: {{errorMessage}}", + "checkFailed": "Semble kontrolü başarısız: {{errorMessage}}", + "providerReset": "Semble sağlayıcısı sıfırlandı. Diskteki önbellek bir sonraki yeniden yapılandırmaya kadar korunur." } } diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index d111ade94b..2a9433c506 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Không thể nhập lịch sử Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Không có nội dung terminal được chọn", "missing_task_files": "Các tệp của nhiệm vụ này bị thiếu. Bạn có muốn xóa nó khỏi danh sách nhiệm vụ không?", - "auto_import_failed": "Không thể tự động nhập cài đặt ZooCode: {{error}}" + "auto_import_failed": "Không thể tự động nhập cài đặt ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "Không tìm thấy lịch sử nhiệm vụ Roo Code nào để nhập từ {{domain}}.", + "alreadyImported_one": "{{count}} lịch sử nhiệm vụ Roo Code đã được nhập vào Zoo Code.", + "alreadyImported_other": "Tất cả {{count}} lịch sử nhiệm vụ Roo Code đã được nhập vào Zoo Code." + } }, "info": { "no_changes": "Không tìm thấy thay đổi nào.", @@ -165,6 +171,10 @@ "mode_imported": "Chế độ đã được nhập thành công", "roo": { "signInUnavailable": "Đăng nhập Roo Code Cloud hiện không khả dụng. Hãy cấu hình nhà cung cấp khác để tiếp tục." + }, + "rooHistoryImport": { + "success_one": "Đã nhập {{count}} lịch sử nhiệm vụ Roo Code vào Zoo Code.", + "success_other": "Đã nhập {{count}} lịch sử nhiệm vụ Roo Code vào Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/vi/embeddings.json b/src/i18n/locales/vi/embeddings.json index c92ebba276..0f494674e5 100644 --- a/src/i18n/locales/vi/embeddings.json +++ b/src/i18n/locales/vi/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "Lập chỉ mục yêu cầu một thư mục workspace đang mở", "indexingStopped": "Lập chỉ mục đã bị dừng bởi người dùng.", "indexingStoppedPartial": "Lập chỉ mục đã dừng. Dữ liệu chỉ mục một phần được bảo toàn." + }, + "semble": { + "downloadingBinary": "Đang tải xuống tệp nhị phân semble...", + "ready": "Semble {{version}} đã sẵn sàng. Tìm kiếm được lập chỉ mục tức thì.", + "unsupportedPlatform": "Semble không được hỗ trợ trên nền tảng này ({{platform}}-{{arch}}).", + "downloadFailed": "Tải xuống semble thất bại: {{errorMessage}}", + "checkFailed": "Kiểm tra semble thất bại: {{errorMessage}}", + "providerReset": "Nhà cung cấp Semble đã đặt lại. Bộ nhớ đệm trên đĩa vẫn còn cho đến lần xây dựng lại tiếp theo." } } diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 1ff8d330cf..a1d0b06e89 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -146,12 +146,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "导入 Roo Code 历史记录失败:{{error}}" }, "warnings": { "no_terminal_content": "没有选择终端内容", "missing_task_files": "此任务的文件丢失。您想从任务列表中删除它吗?", - "auto_import_failed": "自动导入 ZooCode 设置失败:{{error}}" + "auto_import_failed": "自动导入 ZooCode 设置失败:{{error}}", + "rooHistoryImport": { + "nothingFound": "未找到可从 {{domain}} 导入的 Roo Code 任务历史记录。", + "alreadyImported_one": "全部 {{count}} 条 Roo Code 任务历史记录已导入 Zoo Code。", + "alreadyImported_other": "全部 {{count}} 条 Roo Code 任务历史记录已导入 Zoo Code。" + } }, "info": { "no_changes": "未找到更改。", @@ -170,6 +176,10 @@ "mode_imported": "模式已成功导入", "roo": { "signInUnavailable": "Roo Code Cloud 登录当前不可用。请配置其他提供商以继续。" + }, + "rooHistoryImport": { + "success_one": "已将 {{count}} 条 Roo Code 任务历史记录导入 Zoo Code。", + "success_other": "已将 {{count}} 条 Roo Code 任务历史记录导入 Zoo Code。" } }, "answers": { diff --git a/src/i18n/locales/zh-CN/embeddings.json b/src/i18n/locales/zh-CN/embeddings.json index b4f4eaad1d..484dd7a8e4 100644 --- a/src/i18n/locales/zh-CN/embeddings.json +++ b/src/i18n/locales/zh-CN/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "索引需要打开的工作区文件夹", "indexingStopped": "用户已停止索引。", "indexingStoppedPartial": "索引已停止。部分索引数据已保留。" + }, + "semble": { + "downloadingBinary": "正在下载 semble 二进制文件...", + "ready": "Semble {{version}} 已就绪。搜索即时建立索引。", + "unsupportedPlatform": "此平台不支持 Semble ({{platform}}-{{arch}})。", + "downloadFailed": "下载 semble 失败:{{errorMessage}}", + "checkFailed": "Semble 检查失败:{{errorMessage}}", + "providerReset": "Semble 提供程序已重置。磁盘缓存将保留至下次重建。" } } diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 74e661e280..92435e42ff 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "匯入 Roo Code 歷史記錄失敗:{{error}}" }, "warnings": { "no_terminal_content": "沒有選擇終端機內容", "missing_task_files": "此工作的檔案遺失。您想從工作列表中刪除它嗎?", - "auto_import_failed": "自動匯入 ZooCode 設定失敗:{{error}}" + "auto_import_failed": "自動匯入 ZooCode 設定失敗:{{error}}", + "rooHistoryImport": { + "nothingFound": "未找到可從 {{domain}} 匯入的 Roo Code 工作歷史記錄。", + "alreadyImported_one": "全部 {{count}} 筆 Roo Code 工作歷史記錄已匯入 Zoo Code。", + "alreadyImported_other": "全部 {{count}} 筆 Roo Code 工作歷史記錄已匯入 Zoo Code。" + } }, "info": { "no_changes": "沒有找到更改。", @@ -165,6 +171,10 @@ "mode_imported": "模式已成功匯入", "roo": { "signInUnavailable": "Roo Code Cloud 登入目前無法使用。請設定其他提供者以繼續。" + }, + "rooHistoryImport": { + "success_one": "已將 {{count}} 筆 Roo Code 工作歷史記錄匯入 Zoo Code。", + "success_other": "已將 {{count}} 筆 Roo Code 工作歷史記錄匯入 Zoo Code。" } }, "answers": { diff --git a/src/i18n/locales/zh-TW/embeddings.json b/src/i18n/locales/zh-TW/embeddings.json index 26845ed948..66d6328d48 100644 --- a/src/i18n/locales/zh-TW/embeddings.json +++ b/src/i18n/locales/zh-TW/embeddings.json @@ -72,5 +72,13 @@ "indexingRequiresWorkspace": "索引需要開啟的工作區資料夾", "indexingStopped": "使用者已停止索引。", "indexingStoppedPartial": "索引已停止。部分索引資料已保留。" + }, + "semble": { + "downloadingBinary": "正在下載 semble 二進位檔...", + "ready": "Semble {{version}} 已就緒。搜尋即時建立索引。", + "unsupportedPlatform": "此平台不支援 Semble ({{platform}}-{{arch}})。", + "downloadFailed": "下載 semble 失敗:{{errorMessage}}", + "checkFailed": "Semble 檢查失敗:{{errorMessage}}", + "providerReset": "Semble 提供者已重設。磁碟快取將保留至下次重建。" } } diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index ba2ae4fc04..bb3368f063 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -5,7 +5,13 @@ import * as diff from "diff" import stripBom from "strip-bom" import delay from "delay" -import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { + type ClineSayTool, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, + DEFAULT_WRITE_DELAY_MS, +} from "@roo-code/types" import { createDirectoriesForFile } from "../../utils/fs" import { arePathsEqual, getReadablePath } from "../../utils/path" @@ -352,9 +358,9 @@ export class DiffViewProvider { await this.keepOrCloseEditedFile( absolutePath, this.userTouchedDiffEditor, - saveState?.autoCloseZooOpenedFiles ?? true, - saveState?.autoCloseZooOpenedFilesAfterUserEdited ?? false, - saveState?.autoCloseZooOpenedNewFiles ?? false, + saveState?.autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + saveState?.autoCloseZooOpenedFilesAfterUserEdited ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + saveState?.autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, ) // Restore any preview tabs the diff evicted, reconstructing the user's @@ -563,9 +569,10 @@ export class DiffViewProvider { await this.keepOrCloseEditedFile( absolutePath, false, - revertState?.autoCloseZooOpenedFiles ?? true, - revertState?.autoCloseZooOpenedFilesAfterUserEdited ?? false, - revertState?.autoCloseZooOpenedNewFiles ?? false, + revertState?.autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + revertState?.autoCloseZooOpenedFilesAfterUserEdited ?? + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + revertState?.autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, ) } @@ -647,16 +654,20 @@ export class DiffViewProvider { // refinement of the base auto-close, so it has no effect when the base // setting is off. // 4. autoCloseZooOpenedFiles=false -> keep the transiently-opened tab. - // 5. Default -> close the transiently-opened tab (current behavior preserved). + // 5. autoCloseZooOpenedFiles=true -> close the transiently-opened tab. + // + // The default value of autoCloseZooOpenedFiles is false (opt-in), so by default + // branch 4 applies and the edited file stays open. See DEFAULT_AUTO_CLOSE_* in + // @roo-code/types for the single source of truth for these defaults. // // keepIfTouchedDiff is passed as true from saveChanges() when the user clicked // or typed inside the diff editor itself. private async keepOrCloseEditedFile( absolutePath: string, keepIfTouchedDiff = false, - autoCloseZooOpenedFiles = true, - autoCloseZooOpenedFilesAfterUserEdited = false, - autoCloseZooOpenedNewFiles = false, + autoCloseZooOpenedFiles = DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + autoCloseZooOpenedFilesAfterUserEdited = DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + autoCloseZooOpenedNewFiles = DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, ): Promise { // Files the user already had open are never auto-closed. if (this.documentWasOpen) { @@ -682,7 +693,8 @@ export class DiffViewProvider { return } - // Transient tab opened by Zoo: close by default, keep only when opted out. + // Transient tab opened by Zoo: close only when auto-close is enabled (opt-in); + // keep and re-show it otherwise (the default). if (autoCloseZooOpenedFiles) { await this.closeFileTab(absolutePath) } else { diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index f7bf0d708d..d56338b6b5 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -154,6 +154,11 @@ describe("DiffViewProvider", () => { getState: vi.fn().mockResolvedValue({ includeDiagnosticMessages: true, maxDiagnosticMessages: 50, + // Auto-closing edited tabs is opt-in by default; the legacy + // "close/keep behavior" suite below asserts the close path, so + // enable it here. The opt-in default itself is covered by the + // dedicated "auto-close settings decision table" suite. + autoCloseZooOpenedFiles: true, }), }), }, @@ -1711,7 +1716,24 @@ describe("DiffViewProvider", () => { expect(vscode.window.showTextDocument).toHaveBeenCalled() }) - it("transient tab is closed when autoCloseZooOpenedFiles is true (default)", async () => { + it("transient tab is kept by default when autoCloseZooOpenedFiles is unset (opt-in)", async () => { + // Empty state -> autoCloseZooOpenedFiles is undefined and falls back to the + // centralized default (false), so an untouched transient tab is kept. + const provider = setupProvider({}) + const closeFileTab = vi.fn().mockResolvedValue(undefined) + ;(provider as any).closeFileTab = closeFileTab + ;(provider as any).documentWasOpen = false + ;(provider as any).userTouchedDocument = false + ;(provider as any).userTouchedDiffEditor = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + + await provider.saveChanges(false) + + expect(closeFileTab).not.toHaveBeenCalled() + expect(vscode.window.showTextDocument).toHaveBeenCalled() + }) + + it("transient tab is closed when autoCloseZooOpenedFiles is true", async () => { const provider = setupProvider({ autoCloseZooOpenedFiles: true }) const closeFileTab = vi.fn().mockResolvedValue(undefined) ;(provider as any).closeFileTab = closeFileTab @@ -1739,7 +1761,12 @@ describe("DiffViewProvider", () => { }) it("touched tab is closed when autoCloseZooOpenedFilesAfterUserEdited is true", async () => { - const provider = setupProvider({ autoCloseZooOpenedFilesAfterUserEdited: true }) + // The after-edit override only closes when the base auto-close is also + // enabled, so set both (the base default is now opt-in/false). + const provider = setupProvider({ + autoCloseZooOpenedFiles: true, + autoCloseZooOpenedFilesAfterUserEdited: true, + }) const closeFileTab = vi.fn().mockResolvedValue(undefined) ;(provider as any).closeFileTab = closeFileTab ;(provider as any).documentWasOpen = false @@ -1807,18 +1834,21 @@ describe("DiffViewProvider", () => { expect(vscode.window.showTextDocument).toHaveBeenCalled() }) - it("defaults preserve existing behavior when all settings are unset", async () => { - // No auto-close settings in state: transient tab should be closed (existing default). + it("defaults keep the transient tab open when all settings are unset", async () => { + // No auto-close settings in state: auto-closing is opt-in, so an + // untouched transient tab is kept and re-shown (long-standing behavior). const provider = setupProvider({}) const closeFileTab = vi.fn().mockResolvedValue(undefined) ;(provider as any).closeFileTab = closeFileTab ;(provider as any).documentWasOpen = false ;(provider as any).userTouchedDocument = false ;(provider as any).userTouchedDiffEditor = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) await provider.saveChanges(false) - expect(closeFileTab).toHaveBeenCalledWith(mockTargetPath) + expect(closeFileTab).not.toHaveBeenCalled() + expect(vscode.window.showTextDocument).toHaveBeenCalled() }) }) }) diff --git a/src/integrations/misc/__tests__/export-markdown.spec.ts b/src/integrations/misc/__tests__/export-markdown.spec.ts index fd4c30c3d2..081fb26158 100644 --- a/src/integrations/misc/__tests__/export-markdown.spec.ts +++ b/src/integrations/misc/__tests__/export-markdown.spec.ts @@ -72,5 +72,31 @@ describe("export-markdown", () => { const block = { type: "unknown_type" as const } as any expect(formatContentBlockToMarkdown(block)).toBe("[Unexpected content type: unknown_type]") }) + + it("should format document blocks", () => { + const block = { + type: "document", + source: { type: "base64", media_type: "application/pdf", data: "abc" } as const, + } satisfies ExtendedContentBlock + expect(formatContentBlockToMarkdown(block)).toBe("[Document]") + }) + + it("should format search_result blocks", () => { + const block = { + type: "search_result", + source: "https://example.com", + title: "Example", + content: [{ type: "text", text: "result text" }], + } satisfies ExtendedContentBlock + expect(formatContentBlockToMarkdown(block)).toBe("[Search Result]") + }) + + it("should format tool_reference blocks", () => { + const block = { + type: "tool_reference", + tool_name: "read_file", + } satisfies ExtendedContentBlock + expect(formatContentBlockToMarkdown(block)).toBe("[Tool Reference]") + }) }) }) diff --git a/src/integrations/misc/export-markdown.ts b/src/integrations/misc/export-markdown.ts index d65bb3200e..0636a9e7b8 100644 --- a/src/integrations/misc/export-markdown.ts +++ b/src/integrations/misc/export-markdown.ts @@ -13,7 +13,11 @@ interface ThoughtSignatureBlock { type: "thoughtSignature" } -export type ExtendedContentBlock = Anthropic.Messages.ContentBlockParam | ReasoningBlock | ThoughtSignatureBlock +export type ExtendedContentBlock = + | Anthropic.Messages.ContentBlockParam + | Anthropic.Messages.ToolReferenceBlockParam + | ReasoningBlock + | ThoughtSignatureBlock export function getTaskFileName(dateTs: number): string { const date = new Date(dateTs) @@ -69,6 +73,12 @@ export function formatContentBlockToMarkdown(block: ExtendedContentBlock): strin return block.text case "image": return `[Image]` + case "document": + return `[Document]` + case "search_result": + return `[Search Result]` + case "tool_reference": + return `[Tool Reference]` case "tool_use": { let input: string if (typeof block.input === "object" && block.input !== null) { diff --git a/src/package.json b/src/package.json index 88427f5b5a..181f30122e 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "ZooCodeOrganization", - "version": "3.62.0", + "version": "3.68.0", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", @@ -457,29 +457,20 @@ "clean": "rimraf README.md CHANGELOG.md LICENSE dist logs mock .turbo" }, "dependencies": { - "@ai-sdk/amazon-bedrock": "^4.0.51", - "@ai-sdk/baseten": "^1.0.31", - "@ai-sdk/deepseek": "^2.0.18", - "@ai-sdk/fireworks": "^2.0.32", - "@ai-sdk/google": "^3.0.22", - "@ai-sdk/google-vertex": "^4.0.45", - "@ai-sdk/mistral": "^3.0.19", - "@ai-sdk/xai": "^3.0.48", - "@anthropic-ai/sdk": "^0.37.0", - "@anthropic-ai/vertex-sdk": "^0.7.0", + "@anthropic-ai/sdk": "^0.106.0", + "@anthropic-ai/vertex-sdk": "^0.18.0", "@aws-sdk/client-bedrock-runtime": "^3.922.0", "@aws-sdk/credential-providers": "^3.922.0", "@google/genai": "^1.29.1", "@lmstudio/sdk": "^1.1.1", "@mistralai/mistralai": "^1.9.18", - "@modelcontextprotocol/sdk": "1.26.0", + "@modelcontextprotocol/sdk": "1.29.0", "@qdrant/js-client-rest": "^1.14.0", "@roo-code/cloud": "workspace:^", "@roo-code/core": "workspace:^", "@roo-code/ipc": "workspace:^", "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", - "@vscode/codicons": "^0.0.36", "ai-sdk-provider-poe": "2.0.18", "async-mutex": "^0.5.0", "axios": "^1.12.0", @@ -503,7 +494,7 @@ "mammoth": "^1.9.1", "monaco-vscode-textmate-theme-converter": "^0.1.7", "node-cache": "^5.1.2", - "ollama": "^0.5.17", + "ollama": "^0.6.0", "openai": "^5.12.2", "os-name": "^6.0.0", "p-limit": "^6.2.0", @@ -515,7 +506,6 @@ "ps-tree": "^1.2.0", "reconnecting-eventsource": "^1.6.4", "safe-stable-stringify": "^2.5.0", - "sambanova-ai-provider": "^1.2.2", "sanitize-filename": "^1.6.3", "say": "^0.16.0", "semver-compare": "^1.0.0", @@ -531,34 +521,32 @@ "web-tree-sitter": "^0.25.6", "workerpool": "^9.2.0", "yaml": "^2.8.0", - "zhipu-ai-provider": "^0.2.2", "zod": "3.25.76" }, "devDependencies": { - "@ai-sdk/openai-compatible": "2.0.28", + "@ai-sdk/openai-compatible": "2.0.51", "@roo-code/build": "workspace:^", "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", "@types/clone-deep": "4.0.4", "@types/diff": "5.2.3", "@types/lodash.debounce": "4.0.9", - "@types/mocha": "10.0.10", - "@types/node": "20.19.41", + "@types/node": "20.19.43", "@types/node-cache": "4.2.5", "@types/proper-lockfile": "4.1.4", "@types/ps-tree": "1.1.6", "@types/semver-compare": "1.0.3", "@types/shell-quote": "1.7.5", "@types/vscode": "1.100.0", - "@vitest/coverage-v8": "4.1.0", - "@vscode/vsce": "3.3.2", - "ai": "6.0.77", + "@vitest/coverage-v8": "4.1.9", + "@vscode/vsce": "3.9.2", + "ai": "6.0.209", "esbuild-wasm": "0.25.12", - "execa": "9.5.3", + "execa": "9.6.1", "mkdirp": "3.0.1", - "nock": "14.0.10", - "ovsx": "0.10.4", + "nock": "14.0.15", + "ovsx": "0.10.12", "rimraf": "6.0.1", - "vitest": "4.1.0" + "vitest": "4.1.9" } } diff --git a/src/services/code-index/embedders/openai-compatible.ts b/src/services/code-index/embedders/openai-compatible.ts index 6eaf2b6c2c..e4e36a0272 100644 --- a/src/services/code-index/embedders/openai-compatible.ts +++ b/src/services/code-index/embedders/openai-compatible.ts @@ -12,7 +12,7 @@ import { withValidationErrorHandling, HttpError, formatEmbeddingError } from ".. import { TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Mutex } from "async-mutex" -import { handleOpenAIError } from "../../../api/providers/utils/openai-error-handler" +import { handleOpenAIError } from "../../../api/providers/utils/error-handler" interface EmbeddingItem { embedding: string | number[] diff --git a/src/services/code-index/embedders/openai.ts b/src/services/code-index/embedders/openai.ts index b993e280d9..4ff8f27e47 100644 --- a/src/services/code-index/embedders/openai.ts +++ b/src/services/code-index/embedders/openai.ts @@ -13,7 +13,7 @@ import { t } from "../../../i18n" import { withValidationErrorHandling, formatEmbeddingError, HttpError } from "../shared/validation-helpers" import { TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { handleOpenAIError } from "../../../api/providers/utils/openai-error-handler" +import { handleOpenAIError } from "../../../api/providers/utils/error-handler" /** * OpenAI implementation of the embedder interface with batching and rate limiting diff --git a/src/services/code-index/embedders/openrouter.ts b/src/services/code-index/embedders/openrouter.ts index d98aaeeeb5..d483d531ca 100644 --- a/src/services/code-index/embedders/openrouter.ts +++ b/src/services/code-index/embedders/openrouter.ts @@ -12,7 +12,7 @@ import { withValidationErrorHandling, HttpError, formatEmbeddingError } from ".. import { TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Mutex } from "async-mutex" -import { handleOpenAIError } from "../../../api/providers/utils/openai-error-handler" +import { handleOpenAIError } from "../../../api/providers/utils/error-handler" // Default provider name when no specific provider is selected export const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]" diff --git a/src/services/code-index/semble/__tests__/provider.spec.ts b/src/services/code-index/semble/__tests__/provider.spec.ts index 5de0d6c5e7..ce09035a28 100644 --- a/src/services/code-index/semble/__tests__/provider.spec.ts +++ b/src/services/code-index/semble/__tests__/provider.spec.ts @@ -21,6 +21,7 @@ vi.mock("../semble-cli", () => ({ vi.mock("../semble-downloader", () => ({ isSembleSupportedPlatform: vi.fn().mockReturnValue(true), downloadSemble: vi.fn().mockResolvedValue("/mock/storage/semble/semble"), + SEMBLE_VERSION: "v0.4.1", })) // Mock TelemetryService @@ -37,6 +38,29 @@ vi.mock("vscode", () => ({ ExtensionContext: vi.fn(), })) +// Mock i18n — semble provider state messages are internationalized via t(). +// Return the English strings so existing message-content assertions stay stable. +vi.mock("../../../../i18n", () => ({ + t: (key: string, params?: any) => { + switch (key) { + case "embeddings:semble.downloadingBinary": + return "Downloading semble binary..." + case "embeddings:semble.ready": + return `Semble ${params?.version ?? ""} is ready. Searches index on-the-fly.` + case "embeddings:semble.unsupportedPlatform": + return `Semble is not supported on this platform (${params?.platform ?? ""}-${params?.arch ?? ""}).` + case "embeddings:semble.downloadFailed": + return `Failed to download semble: ${params?.errorMessage ?? ""}` + case "embeddings:semble.checkFailed": + return `Semble check failed: ${params?.errorMessage ?? ""}` + case "embeddings:semble.providerReset": + return "Semble provider reset. On-disk cache remains until next rebuild." + default: + return key + } + }, +})) + import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" import { isSembleSupportedPlatform, downloadSemble } from "../semble-downloader" @@ -90,7 +114,7 @@ describe("SembleProvider", () => { expect(provider.state).toBe("Indexed") expect(mockStateManager.setSystemState).toHaveBeenCalledWith( "Indexed", - "Semble is ready. Searches index on-the-fly.", + "Semble v0.4.1 is ready. Searches index on-the-fly.", ) }) @@ -141,6 +165,16 @@ describe("SembleProvider", () => { expect(mockCli.checkInstalled).toHaveBeenCalledTimes(1) }) + + it("should include the semble version in the ready status message", async () => { + mockCli.checkInstalled.mockResolvedValue({ installed: true }) + + await provider.initialize() + + // The ready message interpolates the active SEMBLE_VERSION so the UI + // (CodeIndexPopover) surfaces which release is installed. + expect(mockStateManager.setSystemState).toHaveBeenCalledWith("Indexed", expect.stringContaining("v0.4.1")) + }) }) describe("startIndexing", () => { @@ -194,25 +228,17 @@ describe("SembleProvider", () => { it("should search using CLI and convert results", async () => { const mockResults = [ { - chunk: { - content: "function authenticate() {}", - file_path: "src/auth.ts", - start_line: 10, - end_line: 25, - language: "typescript", - location: "src/auth.ts:10-25", - }, + content: "function authenticate() {}", + file_path: "src/auth.ts", + start_line: 10, + end_line: 25, score: 0.92, }, { - chunk: { - content: "export function login() {}", - file_path: "src/login.ts", - start_line: 5, - end_line: 15, - language: "typescript", - location: "src/login.ts:5-15", - }, + content: "export function login() {}", + file_path: "src/login.ts", + start_line: 5, + end_line: 15, score: 0.78, }, ] @@ -252,36 +278,24 @@ describe("SembleProvider", () => { it("should filter out results with missing file_path", async () => { const mockResults = [ { - chunk: { - content: "good result", - file_path: "src/good.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "src/good.ts:1-10", - }, + content: "good result", + file_path: "src/good.ts", + start_line: 1, + end_line: 10, score: 0.8, }, { - chunk: { - content: "no file path result", - file_path: "", - start_line: 1, - end_line: 5, - language: "typescript", - location: "", - }, + content: "no file path result", + file_path: "", + start_line: 1, + end_line: 5, score: 0.5, }, { - chunk: { - content: "null file path result", - file_path: null, - start_line: 1, - end_line: 5, - language: null, - location: "", - }, + content: "null file path result", + file_path: null, + start_line: 1, + end_line: 5, score: 0.3, }, ] @@ -321,36 +335,24 @@ describe("SembleProvider", () => { it("should filter results by directoryPrefix when provided", async () => { const mockResults = [ { - chunk: { - content: "code in src/auth", - file_path: "src/auth/login.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "src/auth/login.ts:1-10", - }, + content: "code in src/auth", + file_path: "src/auth/login.ts", + start_line: 1, + end_line: 10, score: 0.95, }, { - chunk: { - content: "code in src/utils", - file_path: "src/utils/helper.ts", - start_line: 5, - end_line: 15, - language: "typescript", - location: "src/utils/helper.ts:5-15", - }, + content: "code in src/utils", + file_path: "src/utils/helper.ts", + start_line: 5, + end_line: 15, score: 0.8, }, { - chunk: { - content: "code in root", - file_path: "README.md", - start_line: 1, - end_line: 5, - language: "markdown", - location: "README.md:1-5", - }, + content: "code in root", + file_path: "README.md", + start_line: 1, + end_line: 5, score: 0.6, }, ] @@ -367,25 +369,17 @@ describe("SembleProvider", () => { it("should not filter results when no directoryPrefix is provided", async () => { const mockResults = [ { - chunk: { - content: "code in src/auth", - file_path: "src/auth/login.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "src/auth/login.ts:1-10", - }, + content: "code in src/auth", + file_path: "src/auth/login.ts", + start_line: 1, + end_line: 10, score: 0.95, }, { - chunk: { - content: "code in src/utils", - file_path: "src/utils/helper.ts", - start_line: 5, - end_line: 15, - language: "typescript", - location: "src/utils/helper.ts:5-15", - }, + content: "code in src/utils", + file_path: "src/utils/helper.ts", + start_line: 5, + end_line: 15, score: 0.8, }, ] @@ -412,6 +406,24 @@ describe("SembleProvider", () => { ) }) + it("should capture stack only when error is an Error instance", async () => { + // A non-Error rejection exercises the `error instanceof Error` false + // branch of the telemetry payload (stack: undefined). + mockCli.search.mockRejectedValue("string error") + + const results = await provider.searchIndex("test") + + expect(results).toEqual([]) + expect(TelemetryService.instance.captureEvent).toHaveBeenCalledWith( + TelemetryEventName.CODE_INDEX_ERROR, + expect.objectContaining({ + error: "string error", + stack: undefined, + location: "SembleProvider.searchIndex", + }), + ) + }) + it("should return empty array when in Error state", async () => { ;(isSembleSupportedPlatform as any).mockReturnValue(false) const errorProvider = new SembleProvider("/workspace", mockContext, mockStateManager) @@ -459,14 +471,15 @@ describe("SembleProvider", () => { it("should handle results with null content using empty string fallback", async () => { const mockResults = [ { - chunk: { - content: null, - file_path: "src/file.ts", - start_line: null, - end_line: null, - language: null, - location: "", - }, + content: null, + file_path: "src/file.ts", + // Non-null line numbers exercise the flat-field access path + // (r.start_line / r.end_line). With null/undefined values the old + // nested `r.chunk?.start_line ?? 0` shape would coalesce to 0 + // identically, so a regression to the wrapped schema would slip + // past unnoticed. Real values must round-trip through unchanged. + start_line: 5, + end_line: 20, score: 0.6, }, ] @@ -477,21 +490,17 @@ describe("SembleProvider", () => { expect(results).toHaveLength(1) expect(results[0].payload?.codeChunk).toBe("") - expect(results[0].payload?.startLine).toBe(0) - expect(results[0].payload?.endLine).toBe(0) + expect(results[0].payload?.startLine).toBe(5) + expect(results[0].payload?.endLine).toBe(20) }) it("should handle results with undefined content fields", async () => { const mockResults = [ { - chunk: { - content: undefined, - file_path: "src/file.ts", - start_line: undefined, - end_line: undefined, - language: undefined, - location: "", - }, + content: undefined, + file_path: "src/file.ts", + start_line: undefined, + end_line: undefined, score: 0.5, }, ] @@ -509,14 +518,10 @@ describe("SembleProvider", () => { it("should normalize backslashes in file paths", async () => { const mockResults = [ { - chunk: { - content: "code", - file_path: "src\\nested\\file.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "", - }, + content: "code", + file_path: "src\\nested\\file.ts", + start_line: 1, + end_line: 10, score: 0.8, }, ] @@ -530,17 +535,43 @@ describe("SembleProvider", () => { expect(results[0].payload?.filePath).toContain("/") }) + it("should reject file paths that escape the workspace via traversal", async () => { + // A path-traversal payload (../../etc/passwd) resolves outside the + // workspace base. The guard at provider.ts must exclude it so semble + // results can never surface files outside the indexed workspace. + const mockResults = [ + { + content: "secret", + file_path: "../../etc/passwd", + start_line: 1, + end_line: 10, + score: 0.9, + }, + { + content: "safe", + file_path: "src/file.ts", + start_line: 1, + end_line: 10, + score: 0.8, + }, + ] + + mockCli.search.mockResolvedValue(mockResults) + + const results = await provider.searchIndex("test") + + // Only the in-workspace result survives; the traversal entry is dropped. + expect(results).toHaveLength(1) + expect(results[0].payload?.filePath).toBe("/workspace/src/file.ts") + }) + it("should always join file paths against workspace root, even with directoryPrefix", async () => { const mockResults = [ { - chunk: { - content: "code", - file_path: "src/file.ts", - start_line: 1, - end_line: 5, - language: "typescript", - location: "", - }, + content: "code", + file_path: "src/file.ts", + start_line: 1, + end_line: 5, score: 0.9, }, ] @@ -556,36 +587,24 @@ describe("SembleProvider", () => { it("should assign sequential semble-N IDs to results", async () => { const mockResults = [ { - chunk: { - content: "a", - file_path: "a.ts", - start_line: 1, - end_line: 2, - language: "ts", - location: "", - }, + content: "a", + file_path: "a.ts", + start_line: 1, + end_line: 2, score: 0.9, }, { - chunk: { - content: "b", - file_path: "b.ts", - start_line: 1, - end_line: 2, - language: "ts", - location: "", - }, + content: "b", + file_path: "b.ts", + start_line: 1, + end_line: 2, score: 0.8, }, { - chunk: { - content: "c", - file_path: "c.ts", - start_line: 1, - end_line: 2, - language: "ts", - location: "", - }, + content: "c", + file_path: "c.ts", + start_line: 1, + end_line: 2, score: 0.7, }, ] diff --git a/src/services/code-index/semble/__tests__/semble-cli.spec.ts b/src/services/code-index/semble/__tests__/semble-cli.spec.ts index 5f692969f7..e9b7fd4594 100644 --- a/src/services/code-index/semble/__tests__/semble-cli.spec.ts +++ b/src/services/code-index/semble/__tests__/semble-cli.spec.ts @@ -219,14 +219,10 @@ describe("SembleCLI", () => { query: "related", results: [ { - chunk: { - content: "related code", - file_path: "src/related.ts", - start_line: 1, - end_line: 10, - language: "typescript", - location: "src/related.ts:1-10", - }, + content: "related code", + file_path: "src/related.ts", + start_line: 1, + end_line: 10, score: 0.85, }, ], @@ -236,36 +232,28 @@ describe("SembleCLI", () => { const results = await cli.findRelated("src/auth.ts", 42, "/repo") expect(results).toHaveLength(1) - expect(results[0].chunk.file_path).toBe("src/related.ts") + expect(results[0].file_path).toBe("src/related.ts") expect(results[0].score).toBe(0.85) }) }) describe("_parseOutput (via search)", () => { - it("should parse v0.3.0+ JSON format with nested chunk", async () => { + it("should parse v0.4.0+ flat JSON format (no chunk wrapper)", async () => { const jsonResponse = { query: "authentication", results: [ { - chunk: { - content: "function authenticate() {}", - file_path: "src/auth.ts", - start_line: 10, - end_line: 25, - language: "typescript", - location: "src/auth.ts:10-25", - }, + content: "function authenticate() {}", + file_path: "src/auth.ts", + start_line: 10, + end_line: 25, score: 0.92, }, { - chunk: { - content: "export function login() {}", - file_path: "src/login.ts", - start_line: 5, - end_line: 15, - language: "typescript", - location: "src/login.ts:5-15", - }, + content: "export function login() {}", + file_path: "src/login.ts", + start_line: 5, + end_line: 15, score: 0.78, }, ], @@ -276,12 +264,12 @@ describe("SembleCLI", () => { const results = await cli.search("authentication", "/repo") expect(results).toHaveLength(2) - expect(results[0].chunk.file_path).toBe("src/auth.ts") - expect(results[0].chunk.start_line).toBe(10) - expect(results[0].chunk.end_line).toBe(25) - expect(results[0].chunk.content).toBe("function authenticate() {}") + expect(results[0].file_path).toBe("src/auth.ts") + expect(results[0].start_line).toBe(10) + expect(results[0].end_line).toBe(25) + expect(results[0].content).toBe("function authenticate() {}") expect(results[0].score).toBe(0.92) - expect(results[1].chunk.file_path).toBe("src/login.ts") + expect(results[1].file_path).toBe("src/login.ts") expect(results[1].score).toBe(0.78) }) @@ -325,17 +313,13 @@ describe("SembleCLI", () => { expect(results).toEqual([]) }) - it("should handle flat array format (older semble format)", async () => { + it("should handle flat array format (bare array of result entries)", async () => { const flatArray = [ { - chunk: { - content: "old format result", - file_path: "src/old.ts", - start_line: 1, - end_line: 5, - language: "typescript", - location: "src/old.ts:1-5", - }, + content: "flat array result", + file_path: "src/old.ts", + start_line: 1, + end_line: 5, score: 0.7, }, ] @@ -344,7 +328,7 @@ describe("SembleCLI", () => { const results = await cli.search("test", "/repo") expect(results).toHaveLength(1) - expect(results[0].chunk.file_path).toBe("src/old.ts") + expect(results[0].file_path).toBe("src/old.ts") expect(results[0].score).toBe(0.7) }) diff --git a/src/services/code-index/semble/__tests__/semble-downloader.spec.ts b/src/services/code-index/semble/__tests__/semble-downloader.spec.ts index 210abf3e32..7a3eee9a70 100644 --- a/src/services/code-index/semble/__tests__/semble-downloader.spec.ts +++ b/src/services/code-index/semble/__tests__/semble-downloader.spec.ts @@ -7,10 +7,10 @@ import { EventEmitter } from "events" // and computes a SHA-256. We make digest() dynamically return the expected checksum // for the current process.platform/arch so verification always passes in unit tests. const CHECKSUMS: Record = { - "linux-x64": "2bd4117dbd1ff7a26ed5ef44dad8d43162a4b9f431ec0bcc9dd2f9c6f5952e28", - "linux-arm64": "177d14f41d3272594844a2635d59d97ad20400868a874a59169fd26a868c32a5", - "darwin-arm64": "9130f447ff2c21803853a9aee58268f0e05134326384ac23d8b74ed22905e118", - "win32-x64": "c8ae86f3703675e356824e08cf79c8a20c41c602296d2a5bff15bf35d762a46b", + "linux-x64": "33a6c8ae78d750e917b291524d788747c62de795274def5c6b07b7a6d1671493", + "linux-arm64": "a4a3fbca363f5a894a57594679c787ff6b4ac1332ebf0edcb36cc89f348c7aba", + "darwin-arm64": "f8b5718e2264c9addbf61ac52f0106f1ebb6717980bf25ecfe135d12f164ed30", + "win32-x64": "2a8734d486db1feaa3bd3cf111d1ac17c805102d758be8f5295fbc862ee00bb3", } vi.mock("crypto", () => ({ createHash: vi.fn(() => ({ @@ -29,6 +29,7 @@ vi.mock("fs/promises", () => ({ readFile: vi.fn(), writeFile: vi.fn().mockResolvedValue(undefined), rename: vi.fn().mockResolvedValue(undefined), + readdir: vi.fn().mockResolvedValue([]), })) // Mock fs (createWriteStream and createReadStream for checksum verification) @@ -84,10 +85,21 @@ import { getSembleSupportedPlatforms, downloadSemble, getSembleBinaryPath, + SEMBLE_SHA256, } from "../semble-downloader" import * as https from "https" import { spawn } from "child_process" +// Guards against drift: the local CHECKSUMS table that drives the crypto mock +// must stay in lock-step with the production SEMBLE_SHA256 constant. A future +// version bump that updates SEMBLE_SHA256 but misses this copy would otherwise +// silently mock the wrong hash and let checksum verification pass spuriously. +describe("SEMBLE_SHA256 checksum fixture", () => { + it("the local CHECKSUMS table matches the exported SEMBLE_SHA256 constant", () => { + expect(CHECKSUMS).toEqual(SEMBLE_SHA256) + }) +}) + describe("semble-downloader", () => { beforeEach(() => { vi.clearAllMocks() @@ -182,7 +194,7 @@ describe("semble-downloader", () => { // fs.access resolves => file exists ;(fs.access as any).mockResolvedValue(undefined) // Version file matches current version - ;(fs.readFile as any).mockResolvedValue("v0.3.1") + ;(fs.readFile as any).mockResolvedValue("v0.4.1") try { const result = await downloadSemble("/storage") @@ -222,6 +234,10 @@ describe("semble-downloader", () => { const result = await downloadSemble("/storage") expect(result).toBe(path.join("/storage", "semble", "semble")) + // The release URL must target the current SEMBLE_VERSION tag — a typo + // in SEMBLE_VERSION would otherwise fetch the wrong release while still + // matching the unversioned asset filename. + expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.4.1"), expect.any(Function)) expect(https.get).toHaveBeenCalledWith( expect.stringContaining("semble-linux-x64-fast.tar.gz"), expect.any(Function), @@ -231,7 +247,7 @@ describe("semble-downloader", () => { "tar", [ "-xzf", - path.join("/storage", "semble-linux-x64-fast.tar.gz"), + path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz"), "-C", path.join("/storage", "semble.new"), "--no-same-owner", @@ -248,11 +264,11 @@ describe("semble-downloader", () => { // Version file should be written expect(fs.writeFile).toHaveBeenCalledWith( path.join("/storage", "semble", ".semble-version"), - "v0.3.1", + "v0.4.1", "utf-8", ) - // Archive should be cleaned up - expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz")) + // Archive should be cleaned up (version-prefixed local cache path) + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz")) } finally { if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform) if (originalArch) Object.defineProperty(process, "arch", originalArch) @@ -269,7 +285,7 @@ describe("semble-downloader", () => { // fs.access resolves => file exists ;(fs.access as any).mockResolvedValue(undefined) // Version file matches - ;(fs.readFile as any).mockResolvedValue("v0.3.1") + ;(fs.readFile as any).mockResolvedValue("v0.4.1") try { const result = await downloadSemble("/storage") @@ -309,7 +325,7 @@ describe("semble-downloader", () => { try { await expect(downloadSemble("/storage")).rejects.toThrow("Failed to download semble") - expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-arm64-fast.tar.gz")) + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.1-semble-linux-arm64-fast.tar.gz")) // Should clean up staging directory, not the original expect(fs.rm).toHaveBeenCalledWith(path.join("/storage", "semble.new"), { recursive: true, @@ -622,11 +638,82 @@ describe("semble-downloader", () => { path.join("/storage", "semble"), ) // Should download the new version - expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.3.1"), expect.any(Function)) + expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.4.1"), expect.any(Function)) // Should write the new version file expect(fs.writeFile).toHaveBeenCalledWith( path.join("/storage", "semble", ".semble-version"), - "v0.3.1", + "v0.4.1", + "utf-8", + ) + } finally { + if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform) + if (originalArch) Object.defineProperty(process, "arch", originalArch) + } + }) + + it("should download a fresh package immediately after a version upgrade (version-prefixed archive path)", async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + const originalArch = Object.getOwnPropertyDescriptor(process, "arch") + + Object.defineProperty(process, "platform", { value: "linux", configurable: true }) + Object.defineProperty(process, "arch", { value: "x64", configurable: true }) + + // Old version recorded on disk → must trigger a fresh download + ;(fs.readFile as any).mockResolvedValue("v0.4.0") + // Simulate a prior-version archive (v0.4.0) and a legacy unversioned + // archive (pre-v0.4.0 cache layout) left over in the storage dir. + // cleanupStaleArchives must sweep both during the upgrade. + ;(fs.readdir as any).mockResolvedValue([ + "v0.4.0-semble-linux-x64-fast.tar.gz", + "semble-linux-x64-fast.tar.gz", + "v0.4.1-semble-linux-x64-fast.tar.gz", + "unrelated-file.txt", + ]) + // fs.access resolves — only called for staged binary verification + ;(fs.access as any).mockResolvedValue(undefined) + + // Simulate successful download + mockWriteStream.on.mockImplementation((event: string, cb: () => void) => { + if (event === "finish") { + setImmediate(cb) + } + }) + + try { + const result = await downloadSemble("/storage") + + expect(result).toBe(path.join("/storage", "semble", "semble")) + const versionedArchive = path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz") + + // A fresh download must happen after the version upgrade + expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.4.1"), expect.any(Function)) + // The release URL keeps the unversioned asset name + expect(https.get).toHaveBeenCalledWith( + expect.stringContaining("semble-linux-x64-fast.tar.gz"), + expect.any(Function), + ) + // Extraction reads from the version-prefixed local cache path + expect(spawn).toHaveBeenCalledWith( + "tar", + expect.arrayContaining(["-xzf", versionedArchive]), + expect.any(Object), + ) + // The stale archive is removed before the fresh download to guarantee + // a clean package is verified against the new checksum. + expect(fs.unlink).toHaveBeenCalledWith(versionedArchive) + // The prior-version archive (v0.4.0-*) is swept by cleanupStaleArchives + // after a successful install, so a version upgrade doesn't accumulate + // orphaned packages on disk. + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.0-semble-linux-x64-fast.tar.gz")) + // The legacy unversioned archive (pre-v0.4.0 cache layout) is also + // swept, covering the v0.3.1 → v0.4.1 upgrade path. + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz")) + // Unrelated files in the storage dir must not be touched. + expect(fs.unlink).not.toHaveBeenCalledWith(path.join("/storage", "unrelated-file.txt")) + // The new version file is recorded + expect(fs.writeFile).toHaveBeenCalledWith( + path.join("/storage", "semble", ".semble-version"), + "v0.4.1", "utf-8", ) } finally { @@ -643,7 +730,7 @@ describe("semble-downloader", () => { Object.defineProperty(process, "arch", { value: "x64", configurable: true }) // Version matches - ;(fs.readFile as any).mockResolvedValue("v0.3.1") + ;(fs.readFile as any).mockResolvedValue("v0.4.1") // Binary exists ;(fs.access as any).mockResolvedValue(undefined) @@ -669,7 +756,7 @@ describe("semble-downloader", () => { Object.defineProperty(process, "arch", { value: "x64", configurable: true }) // Version matches - ;(fs.readFile as any).mockResolvedValue("v0.3.1") + ;(fs.readFile as any).mockResolvedValue("v0.4.1") // But binary is missing let accessCallCount = 0 ;(fs.access as any).mockImplementation(() => { @@ -692,8 +779,9 @@ describe("semble-downloader", () => { const result = await downloadSemble("/storage") expect(result).toBe(path.join("/storage", "semble", "semble")) - // Should download since binary was missing - expect(https.get).toHaveBeenCalled() + // Should download since binary was missing — pin the version tag so a + // wrong SEMBLE_VERSION can't fetch a stale release unnoticed. + expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.4.1"), expect.any(Function)) // Should rename staging to final expect(fs.rename).toHaveBeenCalledWith( path.join("/storage", "semble.new"), @@ -702,7 +790,7 @@ describe("semble-downloader", () => { // Should write version file again expect(fs.writeFile).toHaveBeenCalledWith( path.join("/storage", "semble", ".semble-version"), - "v0.3.1", + "v0.4.1", "utf-8", ) } finally { @@ -734,7 +822,8 @@ describe("semble-downloader", () => { const result = await downloadSemble("/storage") expect(result).toBe(path.join("/storage", "semble", "semble")) - expect(https.get).toHaveBeenCalled() + // First-install path: the download URL must target the current version. + expect(https.get).toHaveBeenCalledWith(expect.stringContaining("v0.4.1"), expect.any(Function)) // Should rename staging to final expect(fs.rename).toHaveBeenCalledWith( path.join("/storage", "semble.new"), @@ -743,7 +832,7 @@ describe("semble-downloader", () => { // Should write version file expect(fs.writeFile).toHaveBeenCalledWith( path.join("/storage", "semble", ".semble-version"), - "v0.3.1", + "v0.4.1", "utf-8", ) } finally { @@ -752,4 +841,84 @@ describe("semble-downloader", () => { } }) }) + + describe("downloadSemble - stale archive cleanup", () => { + it("should ignore readdir failures during stale-archive cleanup", async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + const originalArch = Object.getOwnPropertyDescriptor(process, "arch") + + Object.defineProperty(process, "platform", { value: "linux", configurable: true }) + Object.defineProperty(process, "arch", { value: "x64", configurable: true }) + + // First install (no version file) → triggers a fresh download + ;(fs.readFile as any).mockRejectedValue(new Error("ENOENT")) + ;(fs.access as any).mockResolvedValue(undefined) + // readdir rejects — exercises the catch block in cleanupStaleArchives + ;(fs.readdir as any).mockRejectedValue(new Error("EACCES")) + + mockWriteStream.on.mockImplementation((event: string, cb: () => void) => { + if (event === "finish") { + setImmediate(cb) + } + }) + + try { + const result = await downloadSemble("/storage") + + // Should still succeed — cleanup failure is swallowed + expect(result).toBe(path.join("/storage", "semble", "semble")) + expect(fs.readdir).toHaveBeenCalledWith("/storage") + } finally { + if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform) + if (originalArch) Object.defineProperty(process, "arch", originalArch) + } + }) + + it("should preserve the current archive and unrelated files during cleanup", async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + const originalArch = Object.getOwnPropertyDescriptor(process, "arch") + + Object.defineProperty(process, "platform", { value: "linux", configurable: true }) + Object.defineProperty(process, "arch", { value: "x64", configurable: true }) + + // First install (no version file) → triggers a fresh download + ;(fs.readFile as any).mockRejectedValue(new Error("ENOENT")) + ;(fs.access as any).mockResolvedValue(undefined) + // Storage dir contains the current archive plus unrelated files + ;(fs.readdir as any).mockResolvedValue([ + "v0.4.1-semble-linux-x64-fast.tar.gz", + "v0.4.0-semble-linux-x64-fast.tar.gz", + "semble-linux-x64-fast.tar.gz", + "unrelated.txt", + ]) + + mockWriteStream.on.mockImplementation((event: string, cb: () => void) => { + if (event === "finish") { + setImmediate(cb) + } + }) + + try { + await downloadSemble("/storage") + + const currentArchive = path.join("/storage", "v0.4.1-semble-linux-x64-fast.tar.gz") + // Stale versioned + legacy unversioned archives are swept + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "v0.4.0-semble-linux-x64-fast.tar.gz")) + expect(fs.unlink).toHaveBeenCalledWith(path.join("/storage", "semble-linux-x64-fast.tar.gz")) + // The current archive is never swept by cleanupStaleArchives (it is + // excluded by the currentArchivePath guard). It is unlinked only by + // the pre-download partial-archive cleanup and the post-install + // archive cleanup steps. unrelated.txt is never touched. + expect(fs.unlink).not.toHaveBeenCalledWith(path.join("/storage", "unrelated.txt")) + // Sanity: the current archive path is never passed to the stale sweep. + // It is unlinked exactly twice (pre-download cleanup + post-install + // archive cleanup), never via cleanupStaleArchives. + const currentUnlinks = (fs.unlink as any).mock.calls.filter((c: any[]) => c[0] === currentArchive) + expect(currentUnlinks.length).toBe(2) + } finally { + if (originalPlatform) Object.defineProperty(process, "platform", originalPlatform) + if (originalArch) Object.defineProperty(process, "arch", originalArch) + } + }) + }) }) diff --git a/src/services/code-index/semble/index.ts b/src/services/code-index/semble/index.ts index e63115e076..607af2c191 100644 --- a/src/services/code-index/semble/index.ts +++ b/src/services/code-index/semble/index.ts @@ -5,13 +5,7 @@ export { getSembleSupportedPlatforms, downloadSemble, getSembleBinaryPath, + SEMBLE_VERSION, } from "./semble-downloader" -export type { - ISembleProvider, - SembleSearchResult, - SembleChunk, - SembleCheckResult, - SembleConfig, - SembleContentType, -} from "./types" +export type { ISembleProvider, SembleSearchResult, SembleCheckResult, SembleConfig, SembleContentType } from "./types" export { SEMBLE_DEFAULTS } from "./types" diff --git a/src/services/code-index/semble/provider.ts b/src/services/code-index/semble/provider.ts index 176169814a..1600c03fda 100644 --- a/src/services/code-index/semble/provider.ts +++ b/src/services/code-index/semble/provider.ts @@ -5,10 +5,11 @@ import { IndexingState } from "../interfaces/manager" import { VectorStoreSearchResult } from "../interfaces/vector-store" import { CodeIndexStateManager } from "../state-manager" import { SembleCLI } from "./semble-cli" -import { downloadSemble, isSembleSupportedPlatform } from "./semble-downloader" +import { downloadSemble, isSembleSupportedPlatform, SEMBLE_VERSION } from "./semble-downloader" import { ISembleProvider, SembleConfig, SembleContentType, SembleSearchResult, SEMBLE_DEFAULTS } from "./types" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" +import { t } from "../../../i18n" /** * Orchestrates code search via the semble CLI. @@ -82,7 +83,7 @@ export class SembleProvider implements ISembleProvider { this._state = "Error" this.stateManager.setSystemState( "Error", - `Semble is not supported on this platform (${process.platform}-${process.arch}).`, + t("embeddings:semble.unsupportedPlatform", { platform: process.platform, arch: process.arch }), ) console.error(`[SembleProvider] Unsupported platform: ${process.platform}-${process.arch}`) return @@ -90,7 +91,7 @@ export class SembleProvider implements ISembleProvider { // Download semble binary try { - this.stateManager.setSystemState("Indexing", "Downloading semble binary...") + this.stateManager.setSystemState("Indexing", t("embeddings:semble.downloadingBinary")) const storageDir = this.context.globalStorageUri.fsPath const binaryPath = await downloadSemble(storageDir) if (!binaryPath) { @@ -99,7 +100,10 @@ export class SembleProvider implements ISembleProvider { this.cli = new SembleCLI(binaryPath) } catch (error: any) { this._state = "Error" - this.stateManager.setSystemState("Error", `Failed to download semble: ${error?.message || error}`) + this.stateManager.setSystemState( + "Error", + t("embeddings:semble.downloadFailed", { errorMessage: error?.message || error }), + ) console.error("[SembleProvider] Download failed:", error?.message || error) return } @@ -110,16 +114,18 @@ export class SembleProvider implements ISembleProvider { if (!checkResult.installed) { const errorMsg = checkResult.error || "Semble binary is not functional" this._state = "Error" - this.stateManager.setSystemState("Error", `Semble check failed: ${errorMsg}`) + this.stateManager.setSystemState("Error", t("embeddings:semble.checkFailed", { errorMessage: errorMsg })) console.error("[SembleProvider] Semble check failed:", errorMsg) return } console.log("[SembleProvider] Semble found and ready.") - // Semble indexes on-the-fly, so we mark as "Indexed" (ready for search) + // Semble indexes on-the-fly, so we mark as "Indexed" (ready for search). + // The version is included in the status message so the UI (CodeIndexPopover) + // surfaces which semble release is active. this._state = "Indexed" - this.stateManager.setSystemState("Indexed", "Semble is ready. Searches index on-the-fly.") + this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready", { version: SEMBLE_VERSION })) this._isInitialized = true } @@ -140,7 +146,7 @@ export class SembleProvider implements ISembleProvider { // Semble indexes on-the-fly — no separate indexing step needed. // Mark as indexed/ready. this._state = "Indexed" - this.stateManager.setSystemState("Indexed", "Semble is ready. Searches index on-the-fly.") + this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready", { version: SEMBLE_VERSION })) } /** @@ -220,7 +226,7 @@ export class SembleProvider implements ISembleProvider { */ async clearIndexData(): Promise { this._state = "Standby" - this.stateManager.setSystemState("Standby", "Semble provider reset. On-disk cache remains until next rebuild.") + this.stateManager.setSystemState("Standby", t("embeddings:semble.providerReset")) } /** @@ -235,8 +241,8 @@ export class SembleProvider implements ISembleProvider { /** * Converts Semble CLI results to Zoo's VectorStoreSearchResult format. * - * Semble v0.3.0+ returns results in the format: - * { chunk: { content, file_path, start_line, end_line, language, location }, score } + * Semble v0.4.0+ returns results in a flat format (no `chunk` wrapper): + * { file_path, start_line, end_line, score, content } * * Note: semble returns file paths relative to the path it was invoked with. * We join against `basePath` (the actual path passed to semble) to produce @@ -250,15 +256,15 @@ export class SembleProvider implements ISembleProvider { const converted: VectorStoreSearchResult[] = [] for (const [index, r] of results.entries()) { - if (!r.chunk?.file_path) { + if (!r.file_path) { continue } // Use path.join for the displayed path (preserves basePath format). - const filePath = path.join(basePath, r.chunk.file_path).replace(/\\/g, "/") + const filePath = path.join(basePath, r.file_path).replace(/\\/g, "/") // Use path.resolve to normalize any ../ for the security check. - const resolvedFilePath = path.resolve(basePath, r.chunk.file_path).replace(/\\/g, "/") + const resolvedFilePath = path.resolve(basePath, r.file_path).replace(/\\/g, "/") // Guard against path traversal: reject file paths that resolve outside the workspace if (!resolvedFilePath.startsWith(resolvedBase + "/") && resolvedFilePath !== resolvedBase) { @@ -270,9 +276,9 @@ export class SembleProvider implements ISembleProvider { score: r.score, payload: { filePath, - codeChunk: r.chunk?.content ?? "", - startLine: r.chunk?.start_line ?? 0, - endLine: r.chunk?.end_line ?? 0, + codeChunk: r.content ?? "", + startLine: r.start_line ?? 0, + endLine: r.end_line ?? 0, }, }) } diff --git a/src/services/code-index/semble/semble-cli.ts b/src/services/code-index/semble/semble-cli.ts index 51b9f2e348..572e8b1581 100644 --- a/src/services/code-index/semble/semble-cli.ts +++ b/src/services/code-index/semble/semble-cli.ts @@ -10,15 +10,17 @@ import { SembleSearchResult, SembleCheckResult, SembleContentType, SEMBLE_DEFAUL * All methods spawn the semble process via child_process.spawn with array * arguments (no shell) to prevent shell injection. * - * Semble CLI (v0.3.0+) subcommands: + * Semble CLI (v0.4.0+) subcommands: * search [path] — search a codebase * find-related [path] — find similar code - * init — write sub-agent file + * clear [all|index|savings] — clear cached indexes/savings + * install / uninstall — configure coding-agent integrations * savings — show token stats * * Common flags: * -k, --top-k N — number of results (default: 5) * --content TYPE [TYPE ...] — content types: code, docs, config, all + * --max-snippet-lines N — lines of source per result (default: full chunk) */ export class SembleCLI { private readonly semblePath: string @@ -167,8 +169,9 @@ export class SembleCLI { /** * Parses semble CLI JSON output into structured results. * - * Semble v0.3.0+ outputs JSON by default with format: - * { "query": "...", "results": [{ "chunk": { "content": "...", "file_path": "...", "start_line": N, "end_line": M, "language": "...", "location": "..." }, "score": X }] } + * Semble v0.4.0+ outputs JSON by default with a flat format (no `chunk` + * wrapper — the chunk fields are top-level on each result entry): + * { "query": "...", "results": [{ "file_path": "...", "start_line": N, "end_line": M, "score": X, "content": "..." }] } * * If the query returns no results, semble outputs: * { "error": "No results found." } @@ -187,19 +190,19 @@ export class SembleCLI { return [] } - // Handle successful response: {query, results: [{chunk, score}]} + // Handle successful response: {query, results: [{file_path, start_line, end_line, score, content}]} if (parsed.results && Array.isArray(parsed.results)) { return parsed.results as SembleSearchResult[] } - // Fallback: if it's a flat array (older format) + // Fallback: if it's a flat array of result entries if (Array.isArray(parsed)) { return parsed as SembleSearchResult[] } return [] } catch { - // Not JSON — this shouldn't happen with v0.3.0+ but handle gracefully + // Not JSON — this shouldn't happen with v0.4.0+ but handle gracefully console.warn("[SembleCLI] Unexpected non-JSON output from semble") return [] } diff --git a/src/services/code-index/semble/semble-downloader.ts b/src/services/code-index/semble/semble-downloader.ts index a8b08abee5..fc8a8e2a33 100644 --- a/src/services/code-index/semble/semble-downloader.ts +++ b/src/services/code-index/semble/semble-downloader.ts @@ -20,7 +20,11 @@ const SEMBLE_ARCHIVES: Record = { "win32-x64": { archive: "semble-windows-x64-fast.zip", binary: "semble.exe" }, } -const SEMBLE_VERSION = "v0.3.1" +/** + * The bundled semble version. Surfaced to the UI via the provider's + * system-state message so users can see which version is active. + */ +export const SEMBLE_VERSION = "v0.4.1" const DOWNLOAD_BASE_URL = `https://github.com/Zoo-Code-Org/sembleexec/releases/download/${SEMBLE_VERSION}` const VERSION_FILE = ".semble-version" @@ -31,11 +35,11 @@ const VERSION_FILE = ".semble-version" * * To regenerate: `shasum -a 256 ` */ -const SEMBLE_SHA256: Record = { - "linux-x64": "2bd4117dbd1ff7a26ed5ef44dad8d43162a4b9f431ec0bcc9dd2f9c6f5952e28", - "linux-arm64": "177d14f41d3272594844a2635d59d97ad20400868a874a59169fd26a868c32a5", - "darwin-arm64": "9130f447ff2c21803853a9aee58268f0e05134326384ac23d8b74ed22905e118", - "win32-x64": "c8ae86f3703675e356824e08cf79c8a20c41c602296d2a5bff15bf35d762a46b", +export const SEMBLE_SHA256: Record = { + "linux-x64": "33a6c8ae78d750e917b291524d788747c62de795274def5c6b07b7a6d1671493", + "linux-arm64": "a4a3fbca363f5a894a57594679c787ff6b4ac1332ebf0edcb36cc89f348c7aba", + "darwin-arm64": "f8b5718e2264c9addbf61ac52f0106f1ebb6717980bf25ecfe135d12f164ed30", + "win32-x64": "2a8734d486db1feaa3bd3cf111d1ac17c805102d758be8f5295fbc862ee00bb3", } /** @@ -105,6 +109,42 @@ async function writeInstalledVersion(storageDir: string, version: string): Promi await fs.writeFile(versionPath, version, "utf-8") } +/** + * Best-effort removal of archive files left over from previous semble versions. + * + * Because the local archive cache path is version-prefixed (see `downloadSemble`), + * upgrading SEMBLE_VERSION leaves the prior version's archive orphaned on disk. + * This sweeps those stale packages so a version upgrade doesn't accumulate them. + * + * Matches both the version-prefixed cache names (`${version}-${archiveName}`, + * used since v0.4.0) and the legacy unversioned cache name (`${archiveName}`, + * used before v0.4.0), so a v0.3.1 → v0.4.1 upgrade also clears the legacy file. + * The current archive path is always preserved. + * + * Errors are swallowed since this is purely cosmetic cleanup. + */ +async function cleanupStaleArchives( + storageDir: string, + archiveName: string, + currentArchivePath: string, +): Promise { + try { + const entries = await fs.readdir(storageDir) + const suffix = `-${archiveName}` + await Promise.all( + entries + .filter( + (name) => + (name === archiveName || name.endsWith(suffix)) && + path.join(storageDir, name) !== currentArchivePath, + ) + .map((name) => fs.unlink(path.join(storageDir, name)).catch(() => {})), + ) + } catch { + // ignore — storage dir may not be listable yet + } +} + /** * Downloads and extracts the semble archive for the current platform. * @@ -152,7 +192,12 @@ export async function downloadSemble(storageDir: string): Promise ({ - default: { - access: vi.fn().mockResolvedValue(undefined), - writeFile: vi.fn().mockResolvedValue(undefined), - readFile: vi.fn().mockResolvedValue("{}"), - unlink: vi.fn().mockResolvedValue(undefined), - rename: vi.fn().mockResolvedValue(undefined), - lstat: vi.fn().mockImplementation(() => { - return Promise.resolve({ - isDirectory: () => true, - }) - }), - mkdir: vi.fn().mockResolvedValue(undefined), - }, - access: vi.fn().mockResolvedValue(undefined), - writeFile: vi.fn().mockResolvedValue(undefined), - readFile: vi.fn().mockResolvedValue("{}"), - unlink: vi.fn().mockResolvedValue(undefined), - rename: vi.fn().mockResolvedValue(undefined), - lstat: vi.fn().mockImplementation(() => { - return Promise.resolve({ - isDirectory: () => true, - }) - }), - mkdir: vi.fn().mockResolvedValue(undefined), -})) +// Mock fs/promises before importing anything that uses it. +// Named exports and the default export must share the same vi.fn() instances so that +// `import * as fs` (used by McpHub.ts) and `import fs` (default) both see the same mocks. +vi.mock("fs/promises", () => { + const access = vi.fn().mockResolvedValue(undefined) + const writeFile = vi.fn().mockResolvedValue(undefined) + const readFile = vi.fn().mockResolvedValue("{}") + const unlink = vi.fn().mockResolvedValue(undefined) + const rename = vi.fn().mockResolvedValue(undefined) + const lstat = vi.fn().mockImplementation(() => Promise.resolve({ isDirectory: () => true })) + const mkdir = vi.fn().mockResolvedValue(undefined) + const shared = { access, writeFile, readFile, unlink, rename, lstat, mkdir } + return { default: shared, ...shared } +}) // Import safeWriteJson to use in mocks import { safeWriteJson } from "../../../utils/safeWriteJson" @@ -93,7 +80,6 @@ vi.mock("vscode", () => ({ from: vi.fn(), }, })) -vi.mock("fs/promises") vi.mock("../../../core/webview/ClineProvider") // Mock the MCP SDK modules @@ -124,7 +110,7 @@ describe("McpHub", () => { const originalConsoleError = console.error const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks() // Mock console.error to suppress error messages during tests @@ -194,6 +180,7 @@ describe("McpHub", () => { ) mcpHub = new McpHub(mockProvider as ClineProvider) + await mcpHub.waitUntilReady() }) afterEach(() => { @@ -426,7 +413,7 @@ describe("McpHub", () => { ) const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Verify watcher was created expect(chokidar.watch).toHaveBeenCalledWith(["/path/to/watch"], expect.any(Object)) @@ -503,7 +490,7 @@ describe("McpHub", () => { ) const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Verify watchers were created expect(chokidar.watch).toHaveBeenCalled() @@ -537,7 +524,7 @@ describe("McpHub", () => { vi.mocked(chokidar.watch).mockClear() const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Verify no watcher was created for disabled server expect(chokidar.watch).not.toHaveBeenCalled() @@ -561,7 +548,7 @@ describe("McpHub", () => { ) const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Find the connection const connection = mcpHub.connections.find((conn) => conn.server.name === "mcp-disabled-server") @@ -587,7 +574,7 @@ describe("McpHub", () => { ) const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Find the connection const connection = mcpHub.connections.find((conn) => conn.server.name === "server-disabled-server") @@ -614,7 +601,7 @@ describe("McpHub", () => { ) const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Find the connection const connection = mcpHub.connections.find((conn) => conn.server.name === "both-reasons-server") @@ -645,7 +632,7 @@ describe("McpHub", () => { const mcpHub = new McpHub(mockProvider as ClineProvider) // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // The server should be created as a disconnected connection with null client/transport const connection = mcpHub.connections.find((conn) => conn.server.name === "null-safety-server") @@ -715,7 +702,7 @@ describe("McpHub", () => { ) const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Get the connection const connection = mcpHub.connections.find((conn) => conn.server.name === "type-check-server") @@ -733,7 +720,7 @@ describe("McpHub", () => { it("should handle missing connections safely", async () => { const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Try operations on non-existent server await expect(mcpHub.callTool("non-existent-server", "test-tool", {})).rejects.toThrow( @@ -791,7 +778,7 @@ describe("McpHub", () => { ) const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Delete the connection await mcpHub.deleteConnection("delete-safety-server") @@ -1414,7 +1401,7 @@ describe("McpHub", () => { const mcpHub = new McpHub(mockProvider as ClineProvider) // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // The server should be created as a disconnected connection const connection = mcpHub.connections.find((conn) => conn.server.name === "disabled-server") @@ -1445,7 +1432,7 @@ describe("McpHub", () => { const mcpHub = new McpHub(mockProvider as ClineProvider) // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // The server should be created as a disconnected connection const connection = mcpHub.connections.find((conn) => conn.server.name === "disabled-server") @@ -1831,7 +1818,7 @@ describe("McpHub", () => { // Create McpHub and let it initialize with MCP enabled const mcpHub = new McpHub(mockProvider as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Verify server is connected const connectedServer = mcpHub.connections.find((conn) => conn.server.name === "toggle-test-server") @@ -1889,7 +1876,7 @@ describe("McpHub", () => { const mcpHub = new McpHub(disabledMockProvider as unknown as ClineProvider) // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Find the disabled-test-server const disabledServer = mcpHub.connections.find((conn) => conn.server.name === "disabled-test-server") @@ -1961,7 +1948,7 @@ describe("McpHub", () => { const mcpHub = new McpHub(enabledMockProvider as unknown as ClineProvider) // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Find the enabled-test-server const enabledServer = mcpHub.connections.find((conn) => conn.server.name === "enabled-test-server") @@ -2000,7 +1987,7 @@ describe("McpHub", () => { // Create McpHub with disabled MCP const mcpHub = new McpHub(disabledMockProvider as unknown as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Clear previous calls vi.clearAllMocks() @@ -2047,7 +2034,7 @@ describe("McpHub", () => { // Create McpHub with disabled MCP const mcpHub = new McpHub(disabledMockProvider as unknown as ClineProvider) - await new Promise((resolve) => setTimeout(resolve, 100)) + await mcpHub.waitUntilReady() // Set isConnecting to false to ensure it's properly reset mcpHub.isConnecting = false @@ -2125,9 +2112,6 @@ describe("McpHub", () => { } }) - // Create a new McpHub instance - const mcpHub = new McpHub(mockProvider as ClineProvider) - // Mock the config file read vi.mocked(fs.readFile).mockResolvedValue( JSON.stringify({ @@ -2140,8 +2124,11 @@ describe("McpHub", () => { }), ) - // Initialize servers (this will trigger connectToServer) - await mcpHub["initializeGlobalMcpServers"]() + // Create a new McpHub instance + const mcpHub = new McpHub(mockProvider as ClineProvider) + + // Wait for initialization + await mcpHub.waitUntilReady() // Verify StdioClientTransport was called with wrapped command expect(StdioClientTransport).toHaveBeenCalledWith( @@ -2189,9 +2176,6 @@ describe("McpHub", () => { } }) - // Create a new McpHub instance - const mcpHub = new McpHub(mockProvider as ClineProvider) - // Mock the config file read vi.mocked(fs.readFile).mockResolvedValue( JSON.stringify({ @@ -2204,8 +2188,11 @@ describe("McpHub", () => { }), ) - // Initialize servers (this will trigger connectToServer) - await mcpHub["initializeGlobalMcpServers"]() + // Create a new McpHub instance + const mcpHub = new McpHub(mockProvider as ClineProvider) + + // Wait for initialization + await mcpHub.waitUntilReady() // Verify StdioClientTransport was called without wrapping expect(StdioClientTransport).toHaveBeenCalledWith( @@ -2253,9 +2240,6 @@ describe("McpHub", () => { } }) - // Create a new McpHub instance - const mcpHub = new McpHub(mockProvider as ClineProvider) - // Mock the config file read with cmd.exe already as command vi.mocked(fs.readFile).mockResolvedValue( JSON.stringify({ @@ -2268,8 +2252,11 @@ describe("McpHub", () => { }), ) - // Initialize servers (this will trigger connectToServer) - await mcpHub["initializeGlobalMcpServers"]() + // Create a new McpHub instance + const mcpHub = new McpHub(mockProvider as ClineProvider) + + // Wait for initialization + await mcpHub.waitUntilReady() // Verify StdioClientTransport was called without double-wrapping expect(StdioClientTransport).toHaveBeenCalledWith( @@ -2324,9 +2311,6 @@ describe("McpHub", () => { } }) - // Create a new McpHub instance - const mcpHub = new McpHub(mockProvider as ClineProvider) - // Mock the config file read - simulating fnm/nvm-windows scenario vi.mocked(fs.readFile).mockResolvedValue( JSON.stringify({ @@ -2345,8 +2329,11 @@ describe("McpHub", () => { }), ) - // Initialize servers (this will trigger connectToServer) - await mcpHub["initializeGlobalMcpServers"]() + // Create a new McpHub instance + const mcpHub = new McpHub(mockProvider as ClineProvider) + + // Wait for initialization + await mcpHub.waitUntilReady() // Verify that the command was wrapped with cmd.exe expect(StdioClientTransport).toHaveBeenCalledWith( @@ -2399,9 +2386,6 @@ describe("McpHub", () => { } }) - // Create a new McpHub instance - const mcpHub = new McpHub(mockProvider as ClineProvider) - // Mock the config file read with CMD (uppercase) as command vi.mocked(fs.readFile).mockResolvedValue( JSON.stringify({ @@ -2414,8 +2398,11 @@ describe("McpHub", () => { }), ) - // Initialize servers (this will trigger connectToServer) - await mcpHub["initializeGlobalMcpServers"]() + // Create a new McpHub instance + const mcpHub = new McpHub(mockProvider as ClineProvider) + + // Wait for initialization + await mcpHub.waitUntilReady() // Verify StdioClientTransport was called without double-wrapping expect(StdioClientTransport).toHaveBeenCalledWith( @@ -2462,7 +2449,7 @@ describe("McpHub", () => { mockSecretStorage = { getOAuthData: vi.fn().mockResolvedValue(null), - onDidChange: vi.fn().mockReturnValue(vi.fn()), + onDidChange: vi.fn().mockImplementation(() => vi.fn()), } ;(mcpHub as any).secretStorage = mockSecretStorage @@ -2608,11 +2595,9 @@ describe("McpHub", () => { mockConnection, ) - // _initiateOAuthFlow awaits getOAuthData() before calling withProgress. - // Two ticks: tick 1 resolves getOAuthData, tick 2 runs the continuation - // that calls withProgress, setting capturedCancellationToken. - await Promise.resolve() - await Promise.resolve() + // Wait for withProgress to be invoked: getOAuthData must resolve first, + // then the continuation calls withProgress synchronously, setting capturedCancellationToken. + await vi.waitFor(() => expect(capturedCancellationToken).toBeDefined()) capturedCancellationToken._fire() await flowPromise @@ -2940,9 +2925,9 @@ describe("McpHub", () => { mockConnection, ) - // Two ticks: getOAuthData resolves, then withProgress registers the watcher - await Promise.resolve() - await Promise.resolve() + // Flush microtasks: getOAuthData resolves, then withProgress fires synchronously + // (registering the watcher) — no tick-counting needed. + await vi.advanceTimersByTimeAsync(0) expect((mcpHub as any)._oauthWatchers.size).toBe(1) const firstEntry = (mcpHub as any)._oauthWatchers.get(`${serverName}:${source}`) @@ -2956,10 +2941,11 @@ describe("McpHub", () => { mockConnection, ) - await Promise.resolve() - await Promise.resolve() + await vi.advanceTimersByTimeAsync(0) // Still exactly one watcher for this server key expect((mcpHub as any)._oauthWatchers.size).toBe(1) + // The first watcher's unsubscribe must have been called before replacement + expect(firstEntry.unsubscribe).toHaveBeenCalledOnce() // Watcher entry was replaced (second flow's entry, not first) const secondEntry = (mcpHub as any)._oauthWatchers.get(`${serverName}:${source}`) expect(secondEntry).not.toBe(firstEntry) diff --git a/src/services/rules/__tests__/rules.spec.ts b/src/services/rules/__tests__/rules.spec.ts new file mode 100644 index 0000000000..9bf557156e --- /dev/null +++ b/src/services/rules/__tests__/rules.spec.ts @@ -0,0 +1,314 @@ +import fs from "fs/promises" +import * as path from "path" +import { tmpdir } from "node:os" + +const mockHome = vi.hoisted(() => ({ path: "" })) + +vi.mock("os", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + homedir: () => mockHome.path, + } +}) + +import { createRule, deleteRule, getRules, resolveRuleFile, shouldIncludeRuleFile } from "../rules" + +describe("rules service", () => { + let tempDir: string + let homeDir: string + let cwd: string + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(tmpdir(), "zoo-rules-")) + homeDir = path.join(tempDir, "home") + cwd = path.join(tempDir, "workspace") + mockHome.path = homeDir + await fs.mkdir(cwd, { recursive: true }) + }) + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }) + }) + + it("returns global and workspace generic rules with deterministic metadata", async () => { + await fs.mkdir(path.join(homeDir, ".roo", "rules"), { recursive: true }) + await fs.mkdir(path.join(cwd, ".roo", "rules"), { recursive: true }) + await fs.writeFile(path.join(homeDir, ".roo", "rules", "global-rule.md"), "# Global") + await fs.writeFile(path.join(cwd, ".roo", "rules", "workspace-rule.md"), "# Workspace") + + const rules = await getRules(cwd, { modes: [] }) + + expect(rules).toEqual([ + expect.objectContaining({ + id: "global:generic:generic:global-rule.md", + name: "global-rule.md", + scope: "global", + kind: "generic", + relativePath: "global-rule.md", + }), + expect.objectContaining({ + id: "project:generic:generic:workspace-rule.md", + name: "workspace-rule.md", + scope: "project", + kind: "generic", + relativePath: "workspace-rule.md", + }), + ]) + }) + + it("returns mode-specific rules for provided modes", async () => { + await fs.mkdir(path.join(cwd, ".roo", "rules-code"), { recursive: true }) + await fs.writeFile(path.join(cwd, ".roo", "rules-code", "code-rule.md"), "# Code") + + const rules = await getRules(cwd, { modes: [{ slug: "code", name: "Code" }] }) + + expect(rules).toEqual([ + expect.objectContaining({ + id: "project:mode:code:code-rule.md", + kind: "mode", + modeSlug: "code", + modeName: "Code", + relativePath: "code-rule.md", + }), + ]) + }) + + it("creates global generic and workspace mode rules", async () => { + const globalPath = await createRule(cwd, { + scope: "global", + kind: "generic", + fileName: "global-new", + }) + const projectPath = await createRule(cwd, { + scope: "project", + kind: "mode", + modeSlug: "code", + fileName: "workspace-new.md", + }) + + expect(globalPath).toBe(path.join(homeDir, ".roo", "rules", "global-new.md")) + expect(projectPath).toBe(path.join(cwd, ".roo", "rules-code", "workspace-new.md")) + expect(await fs.readFile(globalPath, "utf-8")).toContain("# global-new") + expect(await fs.readFile(projectPath, "utf-8")).toContain("for code mode") + }) + + it("rejects path traversal, absolute paths, and invalid file names", async () => { + await expect(createRule(cwd, { scope: "global", kind: "generic", fileName: "../bad" })).rejects.toThrow( + "not a path", + ) + await expect( + createRule(cwd, { scope: "global", kind: "generic", fileName: path.join(tempDir, "bad.md") }), + ).rejects.toThrow("not a path") + await expect(createRule(cwd, { scope: "global", kind: "generic", fileName: "Bad Name" })).rejects.toThrow( + "lowercase letters", + ) + }) + + it("deletes only resolved files inside an allowed rules directory", async () => { + const rulePath = await createRule(cwd, { scope: "project", kind: "generic", fileName: "delete-me" }) + + await deleteRule(cwd, { + scope: "project", + kind: "generic", + relativePath: "delete-me.md", + }) + + await expect(fs.stat(rulePath)).rejects.toMatchObject({ code: "ENOENT" }) + await expect( + resolveRuleFile(cwd, { scope: "project", kind: "generic", relativePath: "../outside.md" }), + ).rejects.toThrow("Invalid rule path") + }) + + it("ignores non-markdown, cache, and system files", async () => { + await fs.mkdir(path.join(cwd, ".roo", "rules"), { recursive: true }) + await fs.writeFile(path.join(cwd, ".roo", "rules", "good.md"), "# Good") + await fs.writeFile(path.join(cwd, ".roo", "rules", "debug.log"), "log") + await fs.writeFile(path.join(cwd, ".roo", "rules", ".DS_Store"), "store") + await fs.writeFile(path.join(cwd, ".roo", "rules", "notes.txt"), "notes") + + const rules = await getRules(cwd, { modes: [] }) + + expect(rules.map((rule) => rule.name)).toEqual(["good.md"]) + expect(shouldIncludeRuleFile("debug.log")).toBe(false) + expect(shouldIncludeRuleFile("notes.txt")).toBe(false) + expect(shouldIncludeRuleFile("good.md")).toBe(true) + expect(shouldIncludeRuleFile("backup.md.bak")).toBe(false) + expect(shouldIncludeRuleFile("Thumbs.db")).toBe(false) + }) + + it("skips non-directory rule paths", async () => { + await fs.mkdir(path.join(cwd, ".roo"), { recursive: true }) + await fs.writeFile(path.join(cwd, ".roo", "rules"), "not a directory") + + await expect(getRules(cwd, { modes: [] })).resolves.toEqual([]) + }) + + it("skips symlinked directory rules outside the rules directory", async () => { + const projectRulesDir = path.join(cwd, ".roo", "rules") + const targetRulesDir = path.join(tempDir, "target-rules") + const targetRulePath = path.join(targetRulesDir, "nested", "symlinked.md") + await fs.mkdir(path.dirname(targetRulePath), { recursive: true }) + await fs.mkdir(projectRulesDir, { recursive: true }) + await fs.writeFile(targetRulePath, "# Symlinked") + await fs.symlink(targetRulesDir, path.join(projectRulesDir, "linked"), "dir") + + const rules = await getRules(cwd, { modes: [] }) + + expect(rules).toEqual([]) + await expect( + resolveRuleFile(cwd, { + scope: "project", + kind: "generic", + relativePath: path.join("linked", "nested", "symlinked.md"), + }), + ).rejects.toThrow("Rule path must stay inside the rules directory") + await expect( + deleteRule(cwd, { + scope: "project", + kind: "generic", + relativePath: path.join("linked", "nested", "symlinked.md"), + }), + ).rejects.toThrow("Rule path must stay inside the rules directory") + await expect(fs.stat(targetRulePath)).resolves.toBeDefined() + }) + + it("round-trips symlinked directory rules that stay inside the rules directory", async () => { + const projectRulesDir = path.join(cwd, ".roo", "rules") + const targetRulesDir = path.join(projectRulesDir, "target-rules") + const targetRulePath = path.join(targetRulesDir, "nested", "symlinked.md") + await fs.mkdir(path.dirname(targetRulePath), { recursive: true }) + await fs.writeFile(targetRulePath, "# Symlinked") + await fs.symlink(targetRulesDir, path.join(projectRulesDir, "linked"), "dir") + + const rules = await getRules(cwd, { modes: [] }) + const symlinkedRule = rules.find((rule) => rule.name === "symlinked.md") + + expect(symlinkedRule).toEqual( + expect.objectContaining({ + isSymlink: true, + relativePath: path.join("linked", "nested", "symlinked.md"), + filePath: targetRulePath, + }), + ) + expect(symlinkedRule!.relativePath).not.toContain("..") + + const resolvedPath = await resolveRuleFile(cwd, { + scope: "project", + kind: "generic", + relativePath: symlinkedRule!.relativePath, + }) + expect(resolvedPath).toBe(path.join(projectRulesDir, "linked", "nested", "symlinked.md")) + + await deleteRule(cwd, { + scope: "project", + kind: "generic", + relativePath: symlinkedRule!.relativePath, + }) + await expect(fs.stat(targetRulePath)).rejects.toMatchObject({ code: "ENOENT" }) + }) + + it("skips symlinked rule file targets outside the rules directory", async () => { + const projectRulesDir = path.join(cwd, ".roo", "rules") + const targetRulePath = path.join(tempDir, "linked-rule.md") + await fs.mkdir(projectRulesDir, { recursive: true }) + await fs.writeFile(targetRulePath, "# Linked") + await fs.symlink(targetRulePath, path.join(projectRulesDir, "linked-file.md"), "file") + + const rules = await getRules(cwd, { modes: [] }) + + expect(rules).toEqual([]) + await expect( + resolveRuleFile(cwd, { scope: "project", kind: "generic", relativePath: "linked-file.md" }), + ).rejects.toThrow("Rule path must stay inside the rules directory") + }) + + it("discovers a symlinked rule file target inside the rules directory", async () => { + const projectRulesDir = path.join(cwd, ".roo", "rules") + const targetRulePath = path.join(projectRulesDir, "target-rule.md") + await fs.mkdir(projectRulesDir, { recursive: true }) + await fs.writeFile(targetRulePath, "# Linked") + await fs.symlink(targetRulePath, path.join(projectRulesDir, "linked-file.md"), "file") + + const rules = await getRules(cwd, { modes: [] }) + + expect(rules).toEqual([ + expect.objectContaining({ + name: "linked-file.md", + filePath: targetRulePath, + isSymlink: true, + }), + expect.objectContaining({ + name: "target-rule.md", + filePath: targetRulePath, + }), + ]) + }) + + it("skips broken symlinks while scanning rules", async () => { + const projectRulesDir = path.join(cwd, ".roo", "rules") + await fs.mkdir(projectRulesDir, { recursive: true }) + await fs.symlink(path.join(tempDir, "missing-target"), path.join(projectRulesDir, "broken.md"), "file") + + await expect(getRules(cwd, { modes: [] })).resolves.toEqual([]) + }) + + it("handles duplicate creation and invalid target rule directory inputs", async () => { + await createRule(cwd, { scope: "global", kind: "generic", fileName: "duplicate" }) + + await expect(createRule(cwd, { scope: "global", kind: "generic", fileName: "duplicate.md" })).rejects.toThrow( + "Rule file already exists: duplicate.md", + ) + await expect(createRule(cwd, { scope: "team" as any, kind: "generic", fileName: "bad" })).rejects.toThrow( + "Invalid rule scope", + ) + await expect(createRule(cwd, { scope: "global", kind: "team" as any, fileName: "bad" })).rejects.toThrow( + "Invalid rule kind", + ) + await expect(createRule(cwd, { scope: "global", kind: "mode", fileName: "bad" })).rejects.toThrow( + "Mode-specific rules require a mode", + ) + await expect( + createRule(cwd, { scope: "global", kind: "generic", modeSlug: "code", fileName: "bad" }), + ).rejects.toThrow("Generic rules cannot specify a mode") + await expect(createRule("", { scope: "project", kind: "generic", fileName: "bad" })).rejects.toThrow( + "Workspace rules require an open workspace", + ) + await expect( + createRule(cwd, { scope: "global", kind: "mode", modeSlug: "../bad", fileName: "bad" }), + ).rejects.toThrow("Invalid mode slug") + }) + + it("validates additional invalid rule names and paths", async () => { + await expect(createRule(cwd, { scope: "global", kind: "generic", fileName: " " })).rejects.toThrow( + "Rule name is required", + ) + await expect( + createRule(cwd, { scope: "global", kind: "generic", fileName: `${"a".repeat(65)}.md` }), + ).rejects.toThrow("Rule name must be 64 characters or less (excluding the .md suffix)") + await expect( + resolveRuleFile(cwd, { scope: "project", kind: "generic", relativePath: "notes.txt" }), + ).rejects.toThrow("Invalid rule file") + await expect(resolveRuleFile(cwd, { scope: "project", kind: "generic", relativePath: " " })).rejects.toThrow( + "Rule path is required", + ) + }) + + it("returns undefined when resolving missing paths and directories", async () => { + await fs.mkdir(path.join(cwd, ".roo", "rules", "nested"), { recursive: true }) + + await expect( + resolveRuleFile(cwd, { scope: "project", kind: "generic", relativePath: "missing.md" }), + ).resolves.toBeUndefined() + await expect( + resolveRuleFile(cwd, { scope: "project", kind: "generic", relativePath: "nested" }), + ).rejects.toThrow("Invalid rule file") + await expect( + resolveRuleFile(cwd, { scope: "project", kind: "generic", relativePath: "nested/missing.md" }), + ).resolves.toBeUndefined() + }) + + it("handles missing directories as empty lists", async () => { + await expect(getRules(cwd, { modes: [] })).resolves.toEqual([]) + }) +}) diff --git a/src/services/rules/rules.ts b/src/services/rules/rules.ts new file mode 100644 index 0000000000..c69aae12ab --- /dev/null +++ b/src/services/rules/rules.ts @@ -0,0 +1,408 @@ +import fs from "fs/promises" +import * as path from "path" +import { Dirent } from "fs" + +import type { + CreateRuleInput, + DeleteRuleInput, + RuleKind, + RuleLookupInput, + RuleMetadata, + RuleScope, +} from "@roo-code/types" +import { DEFAULT_MODES, type ModeConfig } from "@roo-code/types" + +import { getGlobalRooDirectory, getProjectRooDirectoryForCwd } from "../roo-config" + +const MAX_DEPTH = 5 +const VALID_RULE_FILENAME_PATTERN = /^[a-z0-9_-]+(?:\.md)?$/ + +interface RuleFileInfo { + originalPath: string + resolvedPath: string + isSymlink: boolean +} + +interface RuleDirectoryInfo { + scope: RuleScope + kind: RuleKind + modeSlug?: string + modeName?: string + directoryPath: string +} + +export type RulesMode = Pick + +export function shouldIncludeRuleFile(filename: string): boolean { + const basename = path.basename(filename) + if (!basename.toLowerCase().endsWith(".md")) { + return false + } + + const cachePatterns = [ + "*.DS_Store", + "*.bak", + "*.cache", + "*.crdownload", + "*.db", + "*.dmp", + "*.dump", + "*.eslintcache", + "*.lock", + "*.log", + "*.old", + "*.part", + "*.partial", + "*.pyc", + "*.pyo", + "*.stackdump", + "*.swo", + "*.swp", + "*.temp", + "*.tmp", + "Thumbs.db", + ] + + return !cachePatterns.some((pattern) => { + if (pattern.startsWith("*.")) { + const extension = pattern.slice(1) + return basename.endsWith(extension) + } + + return basename === pattern + }) +} + +export async function getRules(cwd: string, options: { modes?: readonly RulesMode[] } = {}): Promise { + const directories = getRuleDirectories(cwd, options.modes) + const rulesById = new Map() + + for (const directory of directories) { + const rules = await scanRuleDirectory(directory) + for (const rule of rules) { + rulesById.set(rule.id, rule) + } + } + + return Array.from(rulesById.values()).sort(compareRules) +} + +export async function createRule(cwd: string, input: CreateRuleInput): Promise { + const directoryPath = getTargetRuleDirectory(cwd, input) + const fileName = normalizeRuleFileName(input.fileName) + const filePath = path.join(directoryPath, fileName) + + assertPathInsideDirectory(filePath, directoryPath) + + try { + await fs.mkdir(directoryPath, { recursive: true }) + await fs.writeFile(filePath, createRuleTemplate(fileName, input), { encoding: "utf-8", flag: "wx" }) + return filePath + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") { + throw new Error(`Rule file already exists: ${fileName}`) + } + + throw error + } +} + +export async function deleteRule(cwd: string, input: DeleteRuleInput): Promise { + const filePath = await resolveRuleFile(cwd, input) + if (!filePath) { + throw new Error("Rule file not found") + } + + await fs.rm(filePath, { force: true }) +} + +export async function resolveRuleFile(cwd: string, input: RuleLookupInput): Promise { + const directoryPath = getTargetRuleDirectory(cwd, input) + const relativePath = normalizeRelativeRulePath(input.relativePath) + const filePath = path.resolve(directoryPath, relativePath) + + assertPathInsideDirectory(filePath, directoryPath) + + try { + const stats = await fs.lstat(filePath) + if (stats.isFile() || stats.isSymbolicLink()) { + await assertRealPathInsideDirectory(filePath, directoryPath) + return filePath + } + } catch (error) { + if (["ENOENT", "ENOTDIR"].includes((error as NodeJS.ErrnoException).code ?? "")) { + return undefined + } + + throw error + } + + return undefined +} + +export function getRulesDirectoryPath( + cwd: string, + input: Pick, +): string { + return getTargetRuleDirectory(cwd, input) +} + +function getRuleDirectories(cwd: string, modes: readonly RulesMode[] = DEFAULT_MODES): RuleDirectoryInfo[] { + const globalRooDirectory = getGlobalRooDirectory() + const projectRooDirectory = getProjectRooDirectoryForCwd(cwd) + const bases: Array<{ scope: RuleScope; basePath: string }> = [{ scope: "global", basePath: globalRooDirectory }] + if (cwd) { + bases.push({ scope: "project", basePath: projectRooDirectory }) + } + + return bases.flatMap(({ scope, basePath }) => [ + { + scope, + kind: "generic" as const, + directoryPath: path.join(basePath, "rules"), + }, + ...modes.map((mode) => ({ + scope, + kind: "mode" as const, + modeSlug: mode.slug, + modeName: mode.name, + directoryPath: path.join(basePath, `rules-${mode.slug}`), + })), + ]) +} + +async function scanRuleDirectory(directory: RuleDirectoryInfo): Promise { + try { + const stats = await fs.stat(directory.directoryPath) + if (!stats.isDirectory()) { + return [] + } + + const entries = await fs.readdir(directory.directoryPath, { withFileTypes: true, recursive: true }) + const fileInfo: RuleFileInfo[] = [] + + await Promise.all( + entries.map((entry) => + resolveRuleDirectoryEntry(entry, directory.directoryPath, fileInfo, 0, directory.directoryPath), + ), + ) + + return fileInfo + .filter(({ originalPath }) => shouldIncludeRuleFile(originalPath)) + .sort((a, b) => a.originalPath.toLowerCase().localeCompare(b.originalPath.toLowerCase())) + .map(({ originalPath, resolvedPath, isSymlink }) => { + const relativePath = path.relative(directory.directoryPath, originalPath) + const name = path.basename(originalPath) + return { + id: createRuleId(directory.scope, directory.kind, directory.modeSlug, relativePath), + name, + scope: directory.scope, + kind: directory.kind, + modeSlug: directory.modeSlug, + modeName: directory.modeName, + filePath: resolvedPath, + relativePath, + directoryPath: directory.directoryPath, + isSymlink, + } + }) + } catch (error) { + if (["ENOENT", "ENOTDIR"].includes((error as NodeJS.ErrnoException).code ?? "")) { + return [] + } + + throw error + } +} + +async function resolveRuleDirectoryEntry( + entry: Dirent, + dirPath: string, + fileInfo: RuleFileInfo[], + depth: number, + rulesDirectoryPath: string, + originalDirPath = dirPath, +): Promise { + if (depth > MAX_DEPTH) { + return + } + + const fullPath = path.resolve(entry.parentPath || dirPath, entry.name) + const originalPath = path.join(originalDirPath, path.relative(dirPath, fullPath)) + if (entry.isFile()) { + if (await isRealPathInsideDirectory(fullPath, rulesDirectoryPath)) { + fileInfo.push({ originalPath, resolvedPath: fullPath, isSymlink: originalPath !== fullPath }) + } + } else if (entry.isSymbolicLink()) { + await resolveRuleSymlink(fullPath, fileInfo, depth + 1, rulesDirectoryPath, originalPath) + } +} + +async function resolveRuleSymlink( + symlinkPath: string, + fileInfo: RuleFileInfo[], + depth: number, + rulesDirectoryPath: string, + originalSymlinkPath = symlinkPath, +): Promise { + if (depth > MAX_DEPTH) { + return + } + + try { + const linkTarget = await fs.readlink(symlinkPath) + let realSymlinkDir: string + try { + realSymlinkDir = await fs.realpath(path.dirname(symlinkPath)) + } catch { + realSymlinkDir = path.dirname(symlinkPath) + } + const resolvedTarget = path.resolve(realSymlinkDir, linkTarget) + const stats = await fs.stat(resolvedTarget) + + if (!(await isRealPathInsideDirectory(resolvedTarget, rulesDirectoryPath))) { + return + } + + if (stats.isFile()) { + fileInfo.push({ originalPath: originalSymlinkPath, resolvedPath: resolvedTarget, isSymlink: true }) + } else if (stats.isDirectory()) { + const entries = await fs.readdir(resolvedTarget, { withFileTypes: true, recursive: true }) + await Promise.all( + entries.map((entry) => + resolveRuleDirectoryEntry( + entry, + resolvedTarget, + fileInfo, + depth + 1, + rulesDirectoryPath, + originalSymlinkPath, + ), + ), + ) + } else if (stats.isSymbolicLink()) { + await resolveRuleSymlink(resolvedTarget, fileInfo, depth + 1, rulesDirectoryPath, originalSymlinkPath) + } + } catch { + // Skip invalid symlinks. + } +} + +function getTargetRuleDirectory(cwd: string, input: Pick): string { + if (input.scope !== "global" && input.scope !== "project") { + throw new Error("Invalid rule scope") + } + + if (input.kind !== "generic" && input.kind !== "mode") { + throw new Error("Invalid rule kind") + } + + if (input.kind === "mode" && !input.modeSlug) { + throw new Error("Mode-specific rules require a mode") + } + + if (input.kind === "generic" && input.modeSlug) { + throw new Error("Generic rules cannot specify a mode") + } + + if (input.scope === "project" && !cwd) { + throw new Error("Workspace rules require an open workspace") + } + + const basePath = input.scope === "global" ? getGlobalRooDirectory() : getProjectRooDirectoryForCwd(cwd) + return path.join(basePath, input.kind === "generic" ? "rules" : `rules-${validateModeSlug(input.modeSlug!)}`) +} + +function normalizeRuleFileName(fileName: string): string { + const trimmed = fileName.trim() + + if (!trimmed) { + throw new Error("Rule name is required") + } + + if (path.isAbsolute(trimmed) || trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) { + throw new Error("Rule name must be a file name, not a path") + } + + const ruleName = trimmed.endsWith(".md") ? trimmed.slice(0, -".md".length) : trimmed + if (ruleName.length > 64) { + throw new Error("Rule name must be 64 characters or less (excluding the .md suffix)") + } + + if (!VALID_RULE_FILENAME_PATTERN.test(trimmed)) { + throw new Error("Rule name must contain only lowercase letters, numbers, hyphens, and underscores") + } + + return trimmed.endsWith(".md") ? trimmed : `${trimmed}.md` +} + +function normalizeRelativeRulePath(relativePath: string): string { + const trimmed = relativePath.trim() + + if (!trimmed) { + throw new Error("Rule path is required") + } + + if (path.isAbsolute(trimmed) || trimmed.split(/[\\/]+/).includes("..")) { + throw new Error("Invalid rule path") + } + + if (!shouldIncludeRuleFile(trimmed)) { + throw new Error("Invalid rule file") + } + + return path.normalize(trimmed) +} + +function validateModeSlug(modeSlug: string): string { + if (!/^[a-zA-Z0-9_-]+$/.test(modeSlug)) { + throw new Error("Invalid mode slug") + } + + return modeSlug +} + +function assertPathInsideDirectory(filePath: string, directoryPath: string): void { + if (!isPathInsideDirectory(path.resolve(filePath), path.resolve(directoryPath))) { + throw new Error("Rule path must stay inside the rules directory") + } +} + +async function assertRealPathInsideDirectory(filePath: string, directoryPath: string): Promise { + if (!(await isRealPathInsideDirectory(filePath, directoryPath))) { + throw new Error("Rule path must stay inside the rules directory") + } +} + +async function isRealPathInsideDirectory(filePath: string, directoryPath: string): Promise { + const [realFilePath, realDirectoryPath] = await Promise.all([fs.realpath(filePath), fs.realpath(directoryPath)]) + return isPathInsideDirectory(realFilePath, realDirectoryPath) +} + +function isPathInsideDirectory(filePath: string, directoryPath: string): boolean { + const relativePath = path.relative(directoryPath, filePath) + return relativePath !== "" && !relativePath.startsWith("..") && !path.isAbsolute(relativePath) +} + +function createRuleId(scope: RuleScope, kind: RuleKind, modeSlug: string | undefined, relativePath: string): string { + return [scope, kind, modeSlug ?? "generic", relativePath].join(":") +} + +function createRuleTemplate(fileName: string, input: CreateRuleInput): string { + const title = path.basename(fileName, ".md") + const modeLine = input.kind === "mode" ? ` for ${input.modeSlug} mode` : "" + + return `# ${title}\n\nAdd Zoo Code rule guidance${modeLine} here.\n` +} + +function compareRules(a: RuleMetadata, b: RuleMetadata): number { + const scopeOrder = { global: 0, project: 1 } satisfies Record + const kindOrder = { mode: 0, generic: 1 } satisfies Record + + return ( + scopeOrder[a.scope] - scopeOrder[b.scope] || + kindOrder[a.kind] - kindOrder[b.kind] || + (a.modeName ?? "").localeCompare(b.modeName ?? "") || + a.relativePath.toLowerCase().localeCompare(b.relativePath.toLowerCase()) + ) +} diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 7246a90177..bb56718a7d 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -63,6 +63,7 @@ export class ProfileValidator { case "xai": case "sambanova": case "fireworks": + case "friendli": return profile.apiModelId case "litellm": return profile.litellmModelId diff --git a/src/shared/__tests__/ProfileValidator.spec.ts b/src/shared/__tests__/ProfileValidator.spec.ts index 9bf913cdc2..efeb7a4820 100644 --- a/src/shared/__tests__/ProfileValidator.spec.ts +++ b/src/shared/__tests__/ProfileValidator.spec.ts @@ -178,6 +178,7 @@ describe("ProfileValidator", () => { "xai", "sambanova", "fireworks", + "friendli", ] apiModelProviders.forEach((provider) => { diff --git a/src/shared/__tests__/api.spec.ts b/src/shared/__tests__/api.spec.ts index 9954d5cbf7..f4fb8330bb 100644 --- a/src/shared/__tests__/api.spec.ts +++ b/src/shared/__tests__/api.spec.ts @@ -162,6 +162,33 @@ describe("getModelMaxOutputTokens", () => { ).toBe(32_768) }) + test("should preserve Anthropic hybrid token handling for Claude Sonnet 5", () => { + const model: ModelInfo = { + contextWindow: 1_000_000, + supportsPromptCache: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + maxTokens: 128_000, + } + + expect( + getModelMaxOutputTokens({ + modelId: "claude-sonnet-5", + model, + settings: { apiProvider: "anthropic", enableReasoningEffort: false }, + }), + ).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS) + + expect( + getModelMaxOutputTokens({ + modelId: "claude-sonnet-5", + model, + settings: { apiProvider: "anthropic", enableReasoningEffort: true, modelMaxTokens: 32_768 }, + }), + ).toBe(32_768) + }) + test("should return model.maxTokens for non-Anthropic models that support reasoning budget but aren't using it", () => { const geminiModelId = "gemini-2.5-flash-preview-04-17" const model: ModelInfo = { diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 92a7d7604f..f2261a5c09 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -21,6 +21,7 @@ describe("experiments", () => { imageGeneration: false, runSlashCommand: false, customTools: false, + parallelToolExecution: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) }) @@ -31,6 +32,7 @@ describe("experiments", () => { imageGeneration: false, runSlashCommand: false, customTools: false, + parallelToolExecution: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(true) }) @@ -41,8 +43,27 @@ describe("experiments", () => { imageGeneration: false, runSlashCommand: false, customTools: false, + parallelToolExecution: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) }) }) + + describe("PARALLEL_TOOL_EXECUTION", () => { + it("is configured correctly", () => { + expect(EXPERIMENT_IDS.PARALLEL_TOOL_EXECUTION).toBe("parallelToolExecution") + expect(experimentConfigsMap.PARALLEL_TOOL_EXECUTION).toMatchObject({ + enabled: false, + showInSettings: false, + }) + }) + + it("returns false by default", () => { + expect(Experiments.isEnabled({}, "parallelToolExecution")).toBe(false) + }) + + it("returns true when enabled", () => { + expect(Experiments.isEnabled({ parallelToolExecution: true }, "parallelToolExecution")).toBe(true) + }) + }) }) diff --git a/src/shared/api.ts b/src/shared/api.ts index 1212cf010d..84131f9f4c 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -183,7 +183,7 @@ const dynamicProviderExtras = { openrouter: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type "vercel-ai-gateway": {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type "zoo-gateway": {} as { apiKey?: string; baseUrl?: string }, - litellm: {} as { apiKey: string; baseUrl: string }, + litellm: {} as { apiKey?: string; baseUrl: string }, requesty: {} as { apiKey?: string; baseUrl?: string }, unbound: {} as { apiKey?: string }, ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index e189f99e23..ae538b9138 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -5,6 +5,7 @@ export const EXPERIMENT_IDS = { IMAGE_GENERATION: "imageGeneration", RUN_SLASH_COMMAND: "runSlashCommand", CUSTOM_TOOLS: "customTools", + PARALLEL_TOOL_EXECUTION: "parallelToolExecution", } as const satisfies Record type _AssertExperimentIds = AssertEqual>> @@ -13,6 +14,8 @@ type ExperimentKey = Keys interface ExperimentConfig { enabled: boolean + /** Defaults to true; set to false to hide from the Settings panel. */ + showInSettings?: boolean } export const experimentConfigsMap: Record = { @@ -20,6 +23,8 @@ export const experimentConfigsMap: Record = { IMAGE_GENERATION: { enabled: false }, RUN_SLASH_COMMAND: { enabled: false }, CUSTOM_TOOLS: { enabled: false }, + // TODO: add i18n keys (settings:experimental.PARALLEL_TOOL_EXECUTION.name/.description) in the same PR that sets showInSettings: true + PARALLEL_TOOL_EXECUTION: { enabled: false, showInSettings: false }, } export const experimentDefault = Object.fromEntries( diff --git a/src/utils/TaskSemaphore.ts b/src/utils/TaskSemaphore.ts new file mode 100644 index 0000000000..15db11c2e1 --- /dev/null +++ b/src/utils/TaskSemaphore.ts @@ -0,0 +1,65 @@ +import { Semaphore } from "async-mutex" + +/** + * A thin wrapper around `async-mutex`'s `Semaphore` that adds observable + * queue-depth (`waiting`) and safe bulk-cancellation (`cancel()`). + * + * **Why not use `Semaphore` directly?** + * `Semaphore` has no way to inspect how many callers are blocked waiting for a + * permit. `TaskSemaphore` tracks that count so callers can make scheduling + * decisions (e.g. "don't enqueue more work when the queue is already deep"). + * + * **`_waiting`** is incremented before `sem.acquire()` is awaited (only when + * the semaphore is already locked, i.e. the caller will actually block) and + * decremented once the permit is granted or the acquire is rejected. + * + * **`_generation`** is a monotonically-increasing counter bumped on every + * `cancel()` call. Each in-flight `acquire()` captures the generation at + * enqueue time; when the acquire settles it only adjusts `_waiting` if the + * generation hasn't changed, preventing stale decrements after a cancel has + * already reset the counter to 0. + */ +export class TaskSemaphore { + private sem: Semaphore + private _waiting = 0 + private _generation = 0 + + constructor(permits: number) { + this.sem = new Semaphore(permits) + } + + get available(): number { + return this.sem.getValue() + } + + get waiting(): number { + return this._waiting + } + + async acquire(): Promise<() => void> { + // Only count as waiting if the permit won't be granted immediately. + const willQueue = this.sem.isLocked() + const gen = this._generation + if (willQueue) this._waiting++ + try { + const [, release] = await this.sem.acquire() + if (willQueue && gen === this._generation) this._waiting-- + return release + } catch (e) { + if (willQueue && gen === this._generation) this._waiting-- + throw e + } + } + + /** + * Rejects all queued waiters and resets the waiting count to 0. + * Does NOT release or alter any held permits — callers that already + * received a release function must still call it. + * The semaphore remains usable after cancellation. + */ + cancel(): void { + this._waiting = 0 + this._generation++ + this.sem.cancel() + } +} diff --git a/src/utils/__tests__/TaskSemaphore.spec.ts b/src/utils/__tests__/TaskSemaphore.spec.ts new file mode 100644 index 0000000000..8c8873b025 --- /dev/null +++ b/src/utils/__tests__/TaskSemaphore.spec.ts @@ -0,0 +1,152 @@ +import { TaskSemaphore } from "../TaskSemaphore" + +describe("TaskSemaphore", () => { + it("acquire() resolves immediately when permits are available", async () => { + const sem = new TaskSemaphore(2) + const release = await sem.acquire() + expect(sem.available).toBe(1) + expect(sem.waiting).toBe(0) + release() + }) + + it("second acquire() queues when no permits remain; resolves after release", async () => { + const sem = new TaskSemaphore(1) + const release1 = await sem.acquire() + expect(sem.available).toBe(0) + + let acquired = false + const p = sem.acquire().then((r) => { + acquired = true + return r + }) + + await Promise.resolve() + expect(sem.waiting).toBe(1) + expect(acquired).toBe(false) + + release1() + const release2 = await p + expect(acquired).toBe(true) + expect(sem.waiting).toBe(0) + release2() + }) + + it("release restores exactly one permit and unblocks one waiter", async () => { + const sem = new TaskSemaphore(1) + const release1 = await sem.acquire() + + const results: number[] = [] + const p1 = sem.acquire().then((r) => { + results.push(1) + return r + }) + const p2 = sem.acquire().then((r) => { + results.push(2) + return r + }) + + await Promise.resolve() + expect(sem.waiting).toBe(2) + + release1() + const r1 = await p1 + expect(results).toEqual([1]) + expect(sem.waiting).toBe(1) + + r1() + const r2 = await p2 + expect(results).toEqual([1, 2]) + expect(sem.waiting).toBe(0) + r2() + }) + + it("available and waiting return correct values at each step", async () => { + const sem = new TaskSemaphore(2) + expect(sem.available).toBe(2) + expect(sem.waiting).toBe(0) + + const r1 = await sem.acquire() + expect(sem.available).toBe(1) + expect(sem.waiting).toBe(0) + + const r2 = await sem.acquire() + expect(sem.available).toBe(0) + expect(sem.waiting).toBe(0) + + const p = sem.acquire() + await Promise.resolve() + expect(sem.waiting).toBe(1) + + r1() + await p.then((r) => r()) + expect(sem.available).toBe(1) + expect(sem.waiting).toBe(0) + + r2() + expect(sem.available).toBe(2) + }) + + it("cancel() rejects all queued waiters", async () => { + const sem = new TaskSemaphore(1) + const release = await sem.acquire() + + const errors: unknown[] = [] + const p1 = sem.acquire().catch((e) => errors.push(e)) + const p2 = sem.acquire().catch((e) => errors.push(e)) + + await Promise.resolve() + expect(sem.waiting).toBe(2) + + sem.cancel() + await Promise.all([p1, p2]) + + expect(errors).toHaveLength(2) + release() + }) + + it("waiting is 0 while an immediate acquire is in flight (permit available)", async () => { + const sem = new TaskSemaphore(2) + // Do NOT await — capture the promise before it settles. + const p = sem.acquire() + // Permit was available so nothing should be queued. + expect(sem.waiting).toBe(0) + const release = await p + expect(sem.waiting).toBe(0) + release() + }) + + it("cancel() resets waiting count to 0 synchronously", async () => { + const sem = new TaskSemaphore(1) + const release = await sem.acquire() + + const p1 = sem.acquire().catch(() => {}) + const p2 = sem.acquire().catch(() => {}) + + await Promise.resolve() + expect(sem.waiting).toBe(2) + + sem.cancel() + // Synchronous check — waiting must be 0 before any promise callbacks run. + expect(sem.waiting).toBe(0) + await Promise.all([p1, p2]) + + expect(sem.waiting).toBe(0) + release() + }) + + it("acquire() works after cancel() with permits still available", async () => { + const sem = new TaskSemaphore(1) + sem.cancel() // no waiters, no holders + const release = await sem.acquire() + expect(sem.available).toBe(0) + release() + expect(sem.available).toBe(1) + }) + + it("cancel() on an idle semaphore is a safe no-op", () => { + const sem = new TaskSemaphore(2) + expect(() => sem.cancel()).not.toThrow() + expect(sem.waiting).toBe(0) + expect(sem.available).toBe(2) + }) +}) diff --git a/src/utils/__tests__/shell.spec.ts b/src/utils/__tests__/shell.spec.ts index 5ddffc6239..4c45837680 100644 --- a/src/utils/__tests__/shell.spec.ts +++ b/src/utils/__tests__/shell.spec.ts @@ -370,6 +370,293 @@ describe("Shell Detection Tests", () => { }) }) + // -------------------------------------------------------------------------- + // getTerminalConfig Behavior (tested via getShell) + // -------------------------------------------------------------------------- + describe("getTerminalConfig", () => { + it("returns defaultProfileName and matching profile for Windows", () => { + Object.defineProperty(process, "platform", { value: "win32" }) + mockVsCodeConfig("windows", "Command Prompt", { + "Command Prompt": { path: "C:\\Windows\\System32\\cmd.exe" }, + }) + expect(getShell()).toBe("C:\\Windows\\System32\\cmd.exe") + }) + + it("returns defaultProfileName and matching profile for macOS", () => { + Object.defineProperty(process, "platform", { value: "darwin" }) + mockVsCodeConfig("osx", "fish", { + fish: { path: "/usr/local/bin/fish" }, + }) + expect(getShell()).toBe("/usr/local/bin/fish") + }) + + it("returns defaultProfileName and matching profile for Linux", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + mockVsCodeConfig("linux", "zsh", { + zsh: { path: "/usr/bin/zsh" }, + }) + expect(getShell()).toBe("/usr/bin/zsh") + }) + + it("returns null defaultProfileName when config value is undefined", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.linux") return undefined + if (key === "profiles.linux") return { bash: { path: "/bin/bash" } } + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + expect(getShell()).toBe("/bin/bash") + }) + + it("returns empty profiles when profiles config is null", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.linux") return "bash" + if (key === "profiles.linux") return null + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + expect(getShell()).toBe("/bin/bash") + }) + + it("returns fallback when getConfiguration throws", () => { + Object.defineProperty(process, "platform", { value: "darwin" }) + vi.mocked(vscode.workspace.getConfiguration).mockImplementation(() => { + throw new Error("config error") + }) + expect(getShell()).toBe("/bin/zsh") + }) + + it("returns fallback when config.get throws", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: () => { + throw new Error("get error") + }, + } as any) + expect(getShell()).toBe("/bin/bash") + }) + }) + + // -------------------------------------------------------------------------- + // Non-string defaultProfileName Handling + // -------------------------------------------------------------------------- + describe("Non-string defaultProfileName handling", () => { + it("Windows: handles numeric defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "win32" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.windows") return 1 + if (key === "profiles.windows") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + vi.mocked(existsSync).mockReturnValue(false) + + expect(getShell()).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe") + }) + + it("Windows: handles boolean defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "win32" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.windows") return true + if (key === "profiles.windows") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + vi.mocked(existsSync).mockReturnValue(false) + + expect(getShell()).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe") + }) + + it("Windows: handles array defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "win32" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.windows") return ["PowerShell"] + if (key === "profiles.windows") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + vi.mocked(existsSync).mockReturnValue(false) + + expect(getShell()).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe") + }) + + it("Windows: handles object defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "win32" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.windows") return { name: "PowerShell" } + if (key === "profiles.windows") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + vi.mocked(existsSync).mockReturnValue(false) + + expect(getShell()).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe") + }) + + it("macOS: handles numeric defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "darwin" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.osx") return 1 + if (key === "profiles.osx") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + expect(getShell()).toBe("/bin/zsh") + }) + + it("macOS: handles boolean defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "darwin" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.osx") return true + if (key === "profiles.osx") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + expect(getShell()).toBe("/bin/zsh") + }) + + it("macOS: handles array defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "darwin" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.osx") return ["zsh"] + if (key === "profiles.osx") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + expect(getShell()).toBe("/bin/zsh") + }) + + it("macOS: handles object defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "darwin" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.osx") return {} + if (key === "profiles.osx") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + expect(getShell()).toBe("/bin/zsh") + }) + + // Mutation-resistant: without the typeof guard, profiles[1] === profiles["1"] in JS, + // so a numeric key that matches a real profile would return its path instead of falling back. + it("macOS: ignores numeric defaultProfileName even when it matches a profile key", () => { + Object.defineProperty(process, "platform", { value: "darwin" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.osx") return 1 + // Profile keyed as "1" — would be reached by profiles[1] if the guard were absent + if (key === "profiles.osx") return { "1": { path: "/usr/local/bin/zsh" } } + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + // Guard treats 1 as null → getMacShellFromVSCode returns null → fallback to /bin/zsh + // Without the guard it would return /usr/local/bin/zsh + expect(getShell()).toBe("/bin/zsh") + }) + + it("Linux: handles numeric defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.linux") return 1 + if (key === "profiles.linux") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + expect(getShell()).toBe("/bin/bash") + }) + + it("Linux: handles boolean defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.linux") return true + if (key === "profiles.linux") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + expect(getShell()).toBe("/bin/bash") + }) + + it("Linux: handles array defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.linux") return ["bash"] + if (key === "profiles.linux") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + expect(getShell()).toBe("/bin/bash") + }) + + it("Linux: handles object defaultProfileName without TypeError", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.linux") return {} + if (key === "profiles.linux") return {} + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + expect(getShell()).toBe("/bin/bash") + }) + + // Mutation-resistant: same pattern as macOS — numeric key matches profile "1" only if unguarded. + it("Linux: ignores numeric defaultProfileName even when it matches a profile key", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + const mockConfig = { + get: vi.fn((key: string) => { + if (key === "defaultProfile.linux") return 1 + // Profile keyed as "1" — would be reached by profiles[1] if the guard were absent + if (key === "profiles.linux") return { "1": { path: "/usr/bin/fish" } } + return undefined + }), + } + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any) + + // Guard treats 1 as null → getLinuxShellFromVSCode returns null → fallback to /bin/bash + // Without the guard it would return /usr/bin/fish + expect(getShell()).toBe("/bin/bash") + }) + }) + // -------------------------------------------------------------------------- // Shell Validation Tests // -------------------------------------------------------------------------- diff --git a/src/utils/__tests__/tag-matcher.spec.ts b/src/utils/__tests__/tag-matcher.spec.ts new file mode 100644 index 0000000000..114eaccce7 --- /dev/null +++ b/src/utils/__tests__/tag-matcher.spec.ts @@ -0,0 +1,154 @@ +// npx vitest utils/__tests__/tag-matcher.spec.ts + +import { TagMatcher } from "../tag-matcher" + +describe("TagMatcher", () => { + describe("collect() chunk merging (line 52)", () => { + it("merges consecutive same-type chars into one chunk within a single call", () => { + // Two text chars in one update() → both hit collect() with matched=false + // second char finds last chunk same type → last.data += char (line 52) + const matcher = new TagMatcher("think") + const result = matcher.update("ab") + expect(result).toEqual([{ matched: false, data: "ab" }]) + }) + + it("merges consecutive reasoning chars within a single call", () => { + const matcher = new TagMatcher("think") + matcher.update("") + const result = matcher.update("ab") + expect(result).toEqual([{ matched: true, data: "ab" }]) + }) + }) + + describe("final() with a chunk argument (line 131)", () => { + it("processes a chunk passed directly to final()", () => { + // Call final() with a chunk instead of update() — exercises line 131 + const matcher = new TagMatcher("think") + const result = matcher.final("hello") + expect(result).toEqual([{ matched: false, data: "hello" }]) + }) + + it("processes a closing tag passed to final()", () => { + const matcher = new TagMatcher("think") + // Don't use update() — keeps reasoning in the buffer so final() flushes it + const result = matcher.final("reasoning") + expect(result.some((r) => r.matched && r.data === "reasoning")).toBe(true) + }) + }) + + describe("space handling in TAG_OPEN (lines 93-97)", () => { + it("tolerates a space before tag name has started (line 95: all candidates at index 0)", () => { + // "< think>" — space arrives when all candidates are at index 0 + // hits line 95 (continue), candidates survive, 't' then matches normally + const matcher = new TagMatcher("think") + const result = matcher.final("< think>content") + expect(result.some((r) => r.matched && r.data === "content")).toBe(true) + }) + + it("drops mid-match candidates on a space (line 97)", () => { + // "" — space arrives mid-match (index > 0, index < name.length) + // those candidates are dropped, tag is not opened + const matcher = new TagMatcher("think") + const result = matcher.final("content") + expect(result.every((r) => !r.matched)).toBe(true) + }) + }) + + describe("multi-tag constructor (string[])", () => { + it("opens and closes when constructed with array", () => { + const matcher = new TagMatcher(["think", "thought"]) + const result = matcher.final("deep reasoningdone") + expect(result.some((r) => r.matched && r.data === "deep reasoning")).toBe(true) + expect(result.some((r) => !r.matched && r.data === "done")).toBe(true) + }) + + it("opens and closes when constructed with array", () => { + const matcher = new TagMatcher(["think", "thought"]) + const result = matcher.final("thinkingdone") + expect(result.some((r) => r.matched && r.data === "thinking")).toBe(true) + expect(result.some((r) => !r.matched && r.data === "done")).toBe(true) + }) + + it(" open is not closed by (cross-tag isolation)", () => { + const matcher = new TagMatcher(["think", "thought"]) + const result = matcher.final("reasoningstill reasoningdone") + // must be treated as text since active tag is + expect(result.some((r) => r.matched && r.data.includes(""))).toBe(true) + expect(result.some((r) => !r.matched && r.data === "done")).toBe(true) + }) + + it(" open is not closed by (inverse cross-tag isolation)", () => { + const matcher = new TagMatcher(["think", "thought"]) + const result = matcher.final("reasoningstill reasoningdone") + // must be treated as text since active tag is + expect(result.some((r) => r.matched && r.data.includes(""))).toBe(true) + expect(result.some((r) => !r.matched && r.data === "done")).toBe(true) + }) + }) + + describe("chunk split at mid-tag-name boundary", () => { + it("correctly opens tag split across two update() calls", () => { + const matcher = new TagMatcher("think") + const first = matcher.update("content") + expect(second.some((r) => r.matched && r.data === "content")).toBe(true) + }) + }) + + describe("unmatched > in TAG_OPEN falls back to TEXT", () => { + it("treats as plain text when xyz is not a configured tag name", () => { + const matcher = new TagMatcher("think") + const result = matcher.final("content") + expect(result.every((r) => !r.matched)).toBe(true) + }) + + it("treats stray closing tag as plain text when no tag is open", () => { + const matcher = new TagMatcher(["think", "thought"]) + const result = matcher.final("finaltext") + expect(result).toEqual([{ matched: false, data: "finaltext" }]) + }) + + it("treats extra closing tag after a closed block as plain text", () => { + const matcher = new TagMatcher(["think", "thought"]) + const result = matcher.final("thinkingfinaltext") + expect(result.some((r) => r.matched && r.data === "thinking")).toBe(true) + expect(result.some((r) => !r.matched && r.data === "finaltext")).toBe(true) + }) + }) + + describe("nested tags", () => { + it("treats inner as text when outer is active", () => { + const matcher = new TagMatcher(["think", "thought"]) + const result = matcher.final("outerinner middlefinal") + expect(result.some((r) => r.matched && r.data.includes("inner"))).toBe(true) + expect(result.some((r) => !r.matched && r.data === "final")).toBe(true) + }) + + it("correctly unwinds nested same-name tags", () => { + const matcher = new TagMatcher(["think", "thought"]) + const result = matcher.final("outerinner middlefinal") + expect(result.some((r) => r.matched && r.data.includes("inner"))).toBe(true) + expect(result.some((r) => !r.matched && r.data === "final")).toBe(true) + }) + }) + + describe("space handling in TAG_CLOSE (line 119)", () => { + it("tolerates a trailing space before > in closing tag ()", () => { + // space at index === tagName.length hits line 119 (continue) + const matcher = new TagMatcher("think") + const result = matcher.final("reasoningafter") + expect(result.some((r) => r.matched && r.data === "reasoning")).toBe(true) + expect(result.some((r) => !r.matched && r.data === "after")).toBe(true) + }) + + it("tolerates a leading space after )", () => { + // space at index === 0 hits line 119 (continue) + const matcher = new TagMatcher("think") + const result = matcher.final("reasoningafter") + expect(result.some((r) => r.matched && r.data === "reasoning")).toBe(true) + expect(result.some((r) => !r.matched && r.data === "after")).toBe(true) + }) + }) +}) diff --git a/src/utils/shell.ts b/src/utils/shell.ts index 49c512910a..31aa0b0fa1 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -136,36 +136,36 @@ type LinuxTerminalProfiles = Record // 1) VS Code Terminal Configuration Helpers // ----------------------------------------------------- -function getWindowsTerminalConfig() { - try { - const config = vscode.workspace.getConfiguration("terminal.integrated") - const defaultProfileName = config.get("defaultProfile.windows") - const profiles = config.get("profiles.windows") || {} - return { defaultProfileName, profiles } - } catch { - return { defaultProfileName: null, profiles: {} as WindowsTerminalProfiles } - } -} - -function getMacTerminalConfig() { - try { - const config = vscode.workspace.getConfiguration("terminal.integrated") - const defaultProfileName = config.get("defaultProfile.osx") - const profiles = config.get("profiles.osx") || {} - return { defaultProfileName, profiles } - } catch { - return { defaultProfileName: null, profiles: {} as MacTerminalProfiles } - } +type PlatformProfilesMap = { + windows: WindowsTerminalProfiles + osx: MacTerminalProfiles + linux: LinuxTerminalProfiles } -function getLinuxTerminalConfig() { +/** + * Reads the VS Code terminal profile configuration for the given platform. + * + * The key must be one of `"windows"`, `"osx"`, or `"linux"` — the exact strings + * VS Code uses in `terminal.integrated.defaultProfile.` and + * `terminal.integrated.profiles.`. Passing any other value is a compile-time + * error, which prevents silent mismatches such as `"darwin"` returning empty config. + * + * The return type (`defaultProfileName` and `profiles`) is inferred from `K` via + * `PlatformProfilesMap`, so callers don't need an explicit type parameter. + * + * Returns `{ defaultProfileName: null, profiles: {} }` on any VS Code API error. + */ +function getTerminalConfig( + platformKey: K, +): { defaultProfileName: string | null; profiles: PlatformProfilesMap[K] } { try { const config = vscode.workspace.getConfiguration("terminal.integrated") - const defaultProfileName = config.get("defaultProfile.linux") - const profiles = config.get("profiles.linux") || {} + const rawProfileName = config.get(`defaultProfile.${platformKey}`) + const defaultProfileName = typeof rawProfileName === "string" ? rawProfileName : null + const profiles = config.get(`profiles.${platformKey}`) ?? ({} as PlatformProfilesMap[K]) return { defaultProfileName, profiles } } catch { - return { defaultProfileName: null, profiles: {} as LinuxTerminalProfiles } + return { defaultProfileName: null, profiles: {} as PlatformProfilesMap[K] } } } @@ -187,7 +187,7 @@ function normalizeShellPath(path: string | string[] | undefined): string | null /** Attempts to retrieve a shell path from VS Code config on Windows. */ function getWindowsShellFromVSCode(): string | null { - const { defaultProfileName, profiles } = getWindowsTerminalConfig() + const { defaultProfileName, profiles } = getTerminalConfig("windows") if (!defaultProfileName) { // No explicit Windows terminal profile is configured. VS Code auto-detects // the default on modern Windows and prefers PowerShell 7 (pwsh.exe) when it @@ -232,7 +232,7 @@ function getWindowsShellFromVSCode(): string | null { /** Attempts to retrieve a shell path from VS Code config on macOS. */ function getMacShellFromVSCode(): string | null { - const { defaultProfileName, profiles } = getMacTerminalConfig() + const { defaultProfileName, profiles } = getTerminalConfig("osx") if (!defaultProfileName) { return null } @@ -243,7 +243,7 @@ function getMacShellFromVSCode(): string | null { /** Attempts to retrieve a shell path from VS Code config on Linux. */ function getLinuxShellFromVSCode(): string | null { - const { defaultProfileName, profiles } = getLinuxTerminalConfig() + const { defaultProfileName, profiles } = getTerminalConfig("linux") if (!defaultProfileName) { return null } diff --git a/src/utils/tag-matcher.ts b/src/utils/tag-matcher.ts index 38d99a2904..f10bf2f1ee 100644 --- a/src/utils/tag-matcher.ts +++ b/src/utils/tag-matcher.ts @@ -17,11 +17,17 @@ export class TagMatcher { state: "TEXT" | "TAG_OPEN" | "TAG_CLOSE" = "TEXT" depth = 0 pointer = 0 + private readonly tagNames: string[] + private activeTagNames: string[] = [] + private candidates: { name: string; index: number }[] = [] + constructor( - readonly tagName: string, + tagName: string | [string, ...string[]], readonly transform?: (chunks: TagMatcherResult) => Result, readonly position = 0, - ) {} + ) { + this.tagNames = Array.isArray(tagName) ? tagName : [tagName] + } private collect() { if (!this.cached.length) { return @@ -56,39 +62,64 @@ export class TagMatcher { if (this.state === "TEXT") { if (char === "<" && (this.pointer <= this.position + 1 || this.matched)) { this.state = "TAG_OPEN" - this.index = 0 + if (this.depth === 0) { + this.candidates = this.tagNames.map((name) => ({ name, index: 0 })) + } else { + const active = this.activeTagNames.at(-1) + this.candidates = active ? [{ name: active, index: 0 }] : [] + } } else { this.collect() } } else if (this.state === "TAG_OPEN") { - if (char === ">" && this.index === this.tagName.length) { - this.state = "TEXT" - if (!this.matched) { - this.cached = [] + if (char === ">") { + const matched = this.candidates.find((c) => c.index === c.name.length) + if (matched) { + this.state = "TEXT" + this.activeTagNames.push(matched.name) + if (!this.matched) { + this.cached = [] + } + this.depth++ + this.matched = true + continue + } else { + this.state = "TEXT" + this.collect() } - this.depth++ - this.matched = true - } else if (this.index === 0 && char === "/") { + } else if (this.candidates.every((c) => c.index === 0) && char === "/") { this.state = "TAG_CLOSE" - } else if (char === " " && (this.index === 0 || this.index === this.tagName.length)) { + this.index = 0 continue - } else if (this.tagName[this.index] === char) { - this.index++ + } else if (char === " ") { + const remaining = this.candidates.filter((c) => c.index === 0 || c.index === c.name.length) + if (remaining.length === this.candidates.length) { + continue + } + this.candidates = remaining } else { - this.state = "TEXT" - this.collect() + this.candidates = this.candidates.filter((c) => c.name[c.index] === char) + for (const c of this.candidates) { + c.index++ + } + if (this.candidates.length === 0) { + this.state = "TEXT" + this.collect() + } } } else if (this.state === "TAG_CLOSE") { - if (char === ">" && this.index === this.tagName.length) { + const tagName = this.activeTagNames.at(-1) ?? this.tagNames[0] + if (char === ">" && this.index === tagName.length) { this.state = "TEXT" this.depth-- + this.activeTagNames.pop() this.matched = this.depth > 0 if (!this.matched) { this.cached = [] } - } else if (char === " " && (this.index === 0 || this.index === this.tagName.length)) { + } else if (char === " " && (this.index === 0 || this.index === tagName.length)) { continue - } else if (this.tagName[this.index] === char) { + } else if (tagName[this.index] === char) { this.index++ } else { this.state = "TEXT" @@ -102,10 +133,15 @@ export class TagMatcher { this._update(chunk) } this.collect() + this.candidates = [] + this.activeTagNames = [] return this.pop() } update(chunk: string) { this._update(chunk) + if (this.state === "TEXT") { + this.collect() + } return this.pop() } } diff --git a/webview-ui/package.json b/webview-ui/package.json index 4a68b0d9bf..6a90a04c0f 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -64,7 +64,6 @@ "react-textarea-autosize": "^8.5.3", "react-use": "^17.5.1", "react-virtuoso": "^4.7.13", - "rehype-highlight": "^7.0.0", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", @@ -86,24 +85,24 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", - "@testing-library/jest-dom": "6.6.3", - "@testing-library/react": "16.3.0", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", "@types/diff": "5.2.3", "@types/jest": "29.5.14", "@types/katex": "0.16.7", - "@types/node": "20.19.41", - "@types/react": "18.3.23", + "@types/node": "20.19.43", + "@types/react": "18.3.31", "@types/react-dom": "18.3.7", "@types/shell-quote": "1.7.5", "@types/stacktrace-js": "2.0.3", "@types/vscode-webview": "1.57.5", "@vitejs/plugin-react": "5.2.0", - "@vitest/coverage-v8": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", "babel-plugin-react-compiler": "1.0.0", "jsdom": "26.1.0", - "vite": "8.0.16", - "vitest": "4.1.0" + "vite": "8.1.0", + "vitest": "4.1.9" } } diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 3914751039..af40af4ecf 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -11,6 +11,7 @@ import type { ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool, + CompletionCheckpoint, } from "@roo-code/types" import { Mode } from "@roo/modes" @@ -75,6 +76,7 @@ import { import { cn } from "@/lib/utils" import { PathTooltip } from "../ui/PathTooltip" import { OpenMarkdownPreviewButton } from "./OpenMarkdownPreviewButton" +import { SeeNewChangesButtons } from "./SeeNewChangesButtons" // Helper function to get previous todos before a specific message function getPreviousTodos(messages: ClineMessage[], currentMessageTs: number): any[] { @@ -124,6 +126,7 @@ interface ChatRowProps { isFollowUpAutoApprovalPaused?: boolean editable?: boolean hasCheckpoint?: boolean + completionCheckpoint?: CompletionCheckpoint onJumpToPreviousCheckpoint?: () => void } @@ -178,12 +181,21 @@ export const ChatRowContent = ({ onBatchFileResponse, isFollowUpAnswered, isFollowUpAutoApprovalPaused, + completionCheckpoint, onJumpToPreviousCheckpoint, }: ChatRowContentProps) => { const { t, i18n } = useTranslation() - const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages, currentTaskItem } = - useExtensionState() + const { + mcpServers, + alwaysAllowMcp, + currentCheckpoint, + mode, + apiConfiguration, + clineMessages, + currentTaskItem, + enableCheckpoints, + } = useExtensionState() const { info: model } = useSelectedModel(apiConfiguration) const [isEditing, setIsEditing] = useState(false) const [editedContent, setEditedContent] = useState("") @@ -1331,6 +1343,9 @@ export const ChatRowContent = ({
+ {!message.partial && enableCheckpoints !== false && completionCheckpoint ? ( + + ) : null}
) @@ -1661,6 +1676,9 @@ export const ChatRowContent = ({
+ {!message.partial && enableCheckpoints !== false && completionCheckpoint ? ( + + ) : null}
) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 2e3808309d..01c4796c16 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -11,7 +11,7 @@ import { getCostBreakdownIfNeeded } from "@src/utils/costFormatting" import { batchConsecutive } from "@src/utils/batchConsecutive" import type { ClineAsk, ClineSayTool, ClineMessage, ExtensionMessage, AudioType, SuggestionItem } from "@roo-code/types" -import { getSuggestionMode, isRetiredProvider } from "@roo-code/types" +import { getCompletionCheckpoint, getSuggestionMode, isRetiredProvider } from "@roo-code/types" import { findLast } from "@roo/array" import { combineApiRequests } from "@roo/combineApiRequests" @@ -129,6 +129,25 @@ const ChatViewComponent: React.ForwardRefRenderFunction combineApiRequests(combineCommandSequences(messages.slice(1))), [messages]) + const completionCheckpoint = useMemo(() => getCompletionCheckpoint(messages), [messages]) + const completionResultTs = useMemo(() => { + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i] + + if (message?.type === "say" && message.say === "completion_result") { + return message.ts + } + + // Zero-text ask completion rows are hidden by visibleMessages below, so attach + // actions to the latest renderable completion row while the extension host + // still derives the checkpoint target from authoritative task state. + if (message?.type === "ask" && message.ask === "completion_result" && (message.text ?? "") !== "") { + return message.ts + } + } + + return undefined + }, [messages]) // Has to be after api_req_finished are all reduced into api_req_started messages. const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages]) @@ -374,6 +393,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction ) @@ -1485,6 +1508,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + const { t } = useTranslation() + const [restoringChanges, setRestoringChanges] = useState(false) + + const seeNewChangesCallback = useCallback(() => { + vscode.postMessage({ type: "completionCheckpointDiff" }) + }, []) + + const restoreChangesCallback = useCallback(() => setRestoringChanges(true), []) + + const confirmRestoreChangesCallback = useCallback(() => { + vscode.postMessage({ type: "completionCheckpointRestore" }) + }, []) + + const cancelRestoreChangesCallback = useCallback(() => setRestoringChanges(false), []) + + return ( +
+ {restoringChanges ? ( + <> + + {t("chat:checkpoint.menu.confirm")} {t("chat:restoreChanges.title")} + + + {t("chat:checkpoint.menu.cancel")} + + + ) : ( + <> + + {t("chat:seeNewChanges.title")} + + + {t("chat:restoreChanges.title")} + + + )} +
+ ) +} diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 4ddf5ef35c..927d3d057d 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -76,7 +76,8 @@ const TaskHeader = ({ : 0, [model, modelId, apiConfiguration], ) - const reservedForOutput = maxTokens || 0 + // vscode-lm reports maxTokens: -1 (unlimited); a negative reserve must not distort the window math. + const reservedForOutput = maxTokens && maxTokens > 0 ? maxTokens : 0 const condenseButton = ( } vi.mock("@src/utils/vscode", () => ({ @@ -31,8 +32,26 @@ vi.mock("use-sound", () => ({ })) vi.mock("../ChatRow", () => ({ - default: function MockChatRow({ message }: { message: ClineMessage }) { - return
{JSON.stringify(message)}
+ default: function MockChatRow({ + message, + completionCheckpoint, + }: { + message: ClineMessage + completionCheckpoint?: { ts: number; commitHash: string } + }) { + return ( +
+ {JSON.stringify(message)} + {((message.type === "say" && message.say === "completion_result") || + (message.type === "ask" && message.ask === "completion_result" && message.text)) && + completionCheckpoint ? ( +
+ + +
+ ) : null} +
+ ) }, })) @@ -90,6 +109,9 @@ vi.mock("react-i18next", () => ({ const RUN_BUTTON_LABEL = "chat:runCommand.title" const DENY_BUTTON_LABEL = "chat:reject.title" +const START_NEW_TASK_BUTTON_LABEL = "chat:startNewTask.title" +const SEE_NEW_CHANGES_BUTTON_LABEL = "chat:seeNewChanges.title" +const RESTORE_CHANGES_BUTTON_LABEL = "chat:restoreChanges.title" const hydrateState = (clineMessages: ClineMessage[]) => { window.postMessage( @@ -137,6 +159,36 @@ const autoApprovedCommandAsk = (): ClineMessage[] => [ { type: "ask", ask: "command", ts: 2, text: "echo hi", partial: false, isAnswered: true }, ] +const completionAskWithoutCheckpoint = (): ClineMessage[] => [ + { type: "say", say: "task", ts: 1, text: "Initial task" }, + { type: "ask", ask: "completion_result", ts: 2, text: "Task complete", partial: false }, +] + +const completionAskWithCheckpoint = (): ClineMessage[] => [ + { type: "say", say: "task", ts: 1, text: "Initial task" }, + { + type: "say", + say: "checkpoint_saved", + ts: 2, + text: "checkpoint-after-user-prompt", + checkpoint: { suppressMessage: true }, + }, + { type: "say", say: "completion_result", ts: 3, text: "Task complete" }, + { type: "ask", ask: "completion_result", ts: 4, text: "", partial: false }, +] + +const askOnlyCompletionWithCheckpoint = (): ClineMessage[] => [ + { type: "say", say: "task", ts: 1, text: "Initial task" }, + { + type: "say", + say: "checkpoint_saved", + ts: 2, + text: "checkpoint-after-user-prompt", + checkpoint: { suppressMessage: true }, + }, + { type: "ask", ask: "completion_result", ts: 3, text: "Task complete", partial: false }, +] + describe("ChatView approval button behavior", () => { beforeEach(() => vi.clearAllMocks()) @@ -173,4 +225,62 @@ describe("ChatView approval button behavior", () => { ) expect(askResponseCalls).toHaveLength(0) }) + + it("shows Kilo-style completion checkpoint actions inline when a latest-prompt checkpoint is available", async () => { + const { queryByText, queryByTestId } = renderChatView() + + await act(async () => { + hydrateState(completionAskWithCheckpoint()) + }) + + await waitFor(() => { + expect(queryByTestId("completion-checkpoint-actions")).toBeInTheDocument() + expect(queryByText(SEE_NEW_CHANGES_BUTTON_LABEL)).toBeInTheDocument() + expect(queryByText(RESTORE_CHANGES_BUTTON_LABEL)).toBeInTheDocument() + expect(queryByText(START_NEW_TASK_BUTTON_LABEL)).toBeInTheDocument() + }) + }) + + it("shows inline checkpoint actions for ask-only completion rows with text", async () => { + const { queryByText, queryByTestId } = renderChatView() + + await act(async () => { + hydrateState(askOnlyCompletionWithCheckpoint()) + }) + + await waitFor(() => { + expect(queryByTestId("completion-checkpoint-actions")).toBeInTheDocument() + expect(queryByText(SEE_NEW_CHANGES_BUTTON_LABEL)).toBeInTheDocument() + expect(queryByText(RESTORE_CHANGES_BUTTON_LABEL)).toBeInTheDocument() + }) + }) + + it("keeps Start New Task for completion results without a latest-prompt checkpoint", async () => { + const { queryByText } = renderChatView() + + await act(async () => { + hydrateState(completionAskWithoutCheckpoint()) + }) + + await waitFor(() => { + expect(queryByText(START_NEW_TASK_BUTTON_LABEL)).toBeInTheDocument() + expect(queryByText(SEE_NEW_CHANGES_BUTTON_LABEL)).not.toBeInTheDocument() + expect(queryByText(RESTORE_CHANGES_BUTTON_LABEL)).not.toBeInTheDocument() + }) + }) + + it("keeps Start New Task as the bottom completion action even when inline checkpoint actions are available", async () => { + const { getByText } = renderChatView() + + await act(async () => { + hydrateState(completionAskWithCheckpoint()) + }) + + await waitFor(() => { + expect(getByText(START_NEW_TASK_BUTTON_LABEL)).toBeInTheDocument() + }) + + fireEvent.click(getByText(START_NEW_TASK_BUTTON_LABEL)) + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "clearTask" }) + }) }) diff --git a/webview-ui/src/components/chat/__tests__/SeeNewChangesButtons.spec.tsx b/webview-ui/src/components/chat/__tests__/SeeNewChangesButtons.spec.tsx new file mode 100644 index 0000000000..df276d5d8f --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/SeeNewChangesButtons.spec.tsx @@ -0,0 +1,71 @@ +import React from "react" +import { fireEvent, render } from "@testing-library/react" + +import { vscode } from "@src/utils/vscode" + +import { SeeNewChangesButtons } from "../SeeNewChangesButtons" + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeButton: ({ children, onClick, disabled, title }: React.ButtonHTMLAttributes) => ( + + ), +})) + +describe("SeeNewChangesButtons", () => { + beforeEach(() => vi.clearAllMocks()) + + it("posts the Kilo-style see-new-changes action", () => { + const { getByText } = render() + + fireEvent.click(getByText("chat:seeNewChanges.title")) + + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "completionCheckpointDiff" }) + }) + + it("requires confirmation before restoring changes", () => { + const { getByText, queryByText } = render() + + fireEvent.click(getByText("chat:restoreChanges.title")) + + expect(queryByText("chat:restoreChanges.title")).not.toBeInTheDocument() + expect(getByText("chat:checkpoint.menu.confirm chat:restoreChanges.title")).toBeInTheDocument() + expect(getByText("chat:checkpoint.menu.cancel")).toBeInTheDocument() + expect(vscode.postMessage).not.toHaveBeenCalled() + + fireEvent.click(getByText("chat:checkpoint.menu.confirm chat:restoreChanges.title")) + + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "completionCheckpointRestore" }) + }) + + it("returns to the initial actions when restore confirmation is cancelled", () => { + const { getByText, queryByText } = render() + + fireEvent.click(getByText("chat:restoreChanges.title")) + fireEvent.click(getByText("chat:checkpoint.menu.cancel")) + + expect(getByText("chat:seeNewChanges.title")).toBeInTheDocument() + expect(getByText("chat:restoreChanges.title")).toBeInTheDocument() + expect(queryByText("chat:checkpoint.menu.confirm chat:restoreChanges.title")).not.toBeInTheDocument() + }) + + it("wires tooltip copy into the primary actions", () => { + const { getByText } = render() + + expect(getByText("chat:seeNewChanges.title")).toHaveAttribute("title", "chat:seeNewChanges.tooltip") + expect(getByText("chat:restoreChanges.title")).toHaveAttribute("title", "chat:restoreChanges.tooltip") + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 41aa452ab1..252cbbb722 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -267,5 +267,16 @@ describe("TaskHeader", () => { // Should show 0% when available input space is 0 expect(screen.getByText("0%")).toBeInTheDocument() }) + + it("should treat a negative maxTokens (vscode-lm reports -1) as zero reserve", () => { + // vscode-lm reports maxTokens: -1 (unlimited). The guard must treat that negative reserve + // as zero, so available space == contextWindow rather than being inflated by a kept -1. + mockModelInfo = { contextWindow: 1000, maxTokens: -1 } + mockMaxOutputTokens = -1 + + renderTaskHeader({ contextTokens: 250 }) + + expect(screen.getByText("25%")).toBeInTheDocument() + }) }) }) diff --git a/webview-ui/src/components/settings/About.tsx b/webview-ui/src/components/settings/About.tsx index c33063e047..7459d5b8df 100644 --- a/webview-ui/src/components/settings/About.tsx +++ b/webview-ui/src/components/settings/About.tsx @@ -1,10 +1,10 @@ -import { HTMLAttributes } from "react" +import { HTMLAttributes, useEffect, useState } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { Trans } from "react-i18next" -import { Download, Upload, TriangleAlert, Bug, Lightbulb, Shield, MessagesSquare } from "lucide-react" -import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { ArrowRightLeft, Download, Upload, TriangleAlert, Bug, Lightbulb, Shield, MessagesSquare } from "lucide-react" +import { VSCodeButton, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import type { TelemetrySetting } from "@roo-code/types" +import type { ExtensionMessage, TelemetrySetting } from "@roo-code/types" import { Package } from "@roo/package" @@ -17,6 +17,8 @@ import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" +type RooHistoryImportProgress = NonNullable + type AboutProps = HTMLAttributes & { telemetrySetting: TelemetrySetting setTelemetrySetting: (setting: TelemetrySetting) => void @@ -26,6 +28,70 @@ type AboutProps = HTMLAttributes & { export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug, className, ...props }: AboutProps) => { const { t } = useAppTranslation() + const [rooHistoryImportProgress, setRooHistoryImportProgress] = useState(null) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type !== "rooHistoryImportProgress" || !message.rooHistoryImportProgress) { + return + } + + const progress = message.rooHistoryImportProgress + if (progress.status === "finished" && progress.totalFileCount === 0) { + setRooHistoryImportProgress(null) + return + } + + setRooHistoryImportProgress(progress) + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, []) + + const isImporting = + rooHistoryImportProgress?.status === "starting" || rooHistoryImportProgress?.status === "copying" + const isImportFailed = rooHistoryImportProgress?.status === "failed" + const isImportSuccessful = + rooHistoryImportProgress?.status === "finished" && rooHistoryImportProgress.totalFileCount > 0 + const shouldShowImportProgress = !!rooHistoryImportProgress && (isImporting || isImportFailed || isImportSuccessful) + const importProgressPercent = + rooHistoryImportProgress && rooHistoryImportProgress.totalFileCount > 0 + ? Math.round((rooHistoryImportProgress.copiedFileCount / rooHistoryImportProgress.totalFileCount) * 100) + : 0 + const importProgressSummary = !rooHistoryImportProgress + ? "" + : isImportFailed + ? rooHistoryImportProgress.totalFileCount > 0 + ? t("settings:about.rooHistoryImport.summaryFailedWithFiles", { + copied: rooHistoryImportProgress.copiedFileCount, + total: rooHistoryImportProgress.totalFileCount, + }) + : t("settings:about.rooHistoryImport.summaryFailedNoFiles") + : t("settings:about.rooHistoryImport.summaryCopied", { + copied: rooHistoryImportProgress.copiedFileCount, + total: rooHistoryImportProgress.totalFileCount, + }) + const importProgressDetail = isImportFailed + ? t("settings:about.rooHistoryImport.detailFailed") + : rooHistoryImportProgress && rooHistoryImportProgress.importedTaskCount > 0 + ? t("settings:about.rooHistoryImport.detailTasksImported", { + count: rooHistoryImportProgress.importedTaskCount, + total: rooHistoryImportProgress.totalTaskCount, + }) + : t("settings:about.rooHistoryImport.detailPreparing") + + const handleImportRooHistory = () => { + setRooHistoryImportProgress({ + status: "starting", + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + }) + vscode.postMessage({ type: "importRooHistory" }) + } return (
@@ -149,6 +215,86 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
+ +
+ +
+
+
+ +
+
+
+ {t("settings:about.rooHistoryImport.cardTitle")} +
+
+ {t("settings:about.rooHistoryImport.cardDescription")} +
+
+
+ {shouldShowImportProgress && ( +
+
+
+ {isImporting ? ( + + ) : isImportFailed ? ( + + ) : ( + + )} + + {isImporting + ? t("settings:about.rooHistoryImport.statusImporting") + : isImportFailed + ? t("settings:about.rooHistoryImport.statusFailed") + : t("settings:about.rooHistoryImport.statusComplete")} + +
+
+ {importProgressPercent}% +
+
+
+
+
+
+
{importProgressSummary}
+
{importProgressDetail}
+
+
+ )} + + {isImporting + ? t("settings:about.rooHistoryImport.buttonImporting") + : t("settings:about.rooHistoryImport.buttonIdle")} + +
+ +
) } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 70617a1ee6..6bfe20c718 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -71,6 +71,7 @@ import { XAI, ZAi, Fireworks, + Friendli, VercelAiGateway, OpenCodeGo, ZooGateway, @@ -222,7 +223,15 @@ const ApiOptions = ({ requestLmStudioModels(apiConfiguration?.lmStudioBaseUrl) } else if (selectedProvider === "vscode-lm") { vscode.postMessage({ type: "requestVsCodeLmModels" }) - } else if (selectedProvider === "litellm" || selectedProvider === "poe") { + } else if (selectedProvider === "litellm") { + vscode.postMessage({ + type: "requestRouterModels", + values: { + litellmApiKey: apiConfiguration?.litellmApiKey, + litellmBaseUrl: apiConfiguration?.litellmBaseUrl, + }, + }) + } else if (selectedProvider === "poe") { vscode.postMessage({ type: "requestRouterModels" }) } }, @@ -655,6 +664,13 @@ const ApiOptions = ({ /> )} + {selectedProvider === "friendli" && ( + + )} + {selectedProvider === "poe" && ( & { includeDiagnosticMessages?: boolean maxDiagnosticMessages?: number writeDelayMs: number + diffFuzzyThreshold?: number includeCurrentTime?: boolean includeCurrentCost?: boolean maxGitStatusFiles?: number @@ -57,6 +59,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "includeDiagnosticMessages" | "maxDiagnosticMessages" | "writeDelayMs" + | "diffFuzzyThreshold" | "includeCurrentTime" | "includeCurrentCost" | "maxGitStatusFiles" @@ -78,6 +81,7 @@ export const ContextManagementSettings = ({ includeDiagnosticMessages, maxDiagnosticMessages, writeDelayMs, + diffFuzzyThreshold, includeCurrentTime, includeCurrentCost, maxGitStatusFiles, @@ -406,6 +410,31 @@ export const ContextManagementSettings = ({ + + + {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.label")} + +
+ setCachedStateField("diffFuzzyThreshold", value)} + data-testid="diff-fuzzy-threshold-slider" + /> + + {((diffFuzzyThreshold ?? DEFAULT_DIFF_FUZZY_THRESHOLD) * 100).toFixed(0)}% + +
+
+ {t("settings:contextManagement.fileEdits.diffFuzzyThreshold.description")} +
+
+ void + onRuleCreated: () => void + hasWorkspace: boolean +} + +const validateRuleName = (name: string): string | null => { + if (!name.trim()) return "settings:rules.validation.nameRequired" + if (name.length > 64) return "settings:rules.validation.nameTooLong" + if (!/^[a-z0-9_-]+$/.test(name)) return "settings:rules.validation.nameInvalid" + return null +} + +export const CreateRuleDialog: React.FC = ({ + open, + onOpenChange, + onRuleCreated, + hasWorkspace, +}) => { + const { t } = useAppTranslation() + const { customModes } = useExtensionState() + + const [name, setName] = useState("") + const [scope, setScope] = useState<"global" | "project">(hasWorkspace ? "project" : "global") + const [kind, setKind] = useState<"generic" | "mode">("generic") + const [modeSlug, setModeSlug] = useState("") + const [nameError, setNameError] = useState(null) + const [modeError, setModeError] = useState(null) + + const availableModes = useMemo( + () => getAllModes(customModes).map((m) => ({ slug: m.slug, name: m.name })), + [customModes], + ) + + const resetForm = useCallback(() => { + setName("") + setScope(hasWorkspace ? "project" : "global") + setKind("generic") + setModeSlug("") + setNameError(null) + setModeError(null) + }, [hasWorkspace]) + + const handleClose = useCallback(() => { + resetForm() + onOpenChange(false) + }, [resetForm, onOpenChange]) + + const handleNameChange = useCallback((event: React.ChangeEvent) => { + const value = event.target.value.toLowerCase().replace(/[^a-z0-9_-]/g, "") + setName(value) + setNameError(null) + }, []) + + const handleKindChange = useCallback((value: string) => { + const nextKind = value as "generic" | "mode" + setKind(nextKind) + setModeError(null) + if (nextKind === "generic") { + setModeSlug("") + } + }, []) + + const handleCreate = useCallback(() => { + const nameValidationError = validateRuleName(name) + if (nameValidationError) { + setNameError(nameValidationError) + return + } + + if (kind === "mode" && !modeSlug) { + setModeError("settings:rules.validation.modeRequired") + return + } + + const fileName = name.trim().endsWith(".md") ? name.trim() : `${name.trim()}.md` + vscode.postMessage({ + type: "createRule", + values: { + scope, + kind, + modeSlug: kind === "mode" ? modeSlug : undefined, + fileName, + }, + }) + + handleClose() + onRuleCreated() + }, [name, kind, modeSlug, scope, handleClose, onRuleCreated]) + + return ( + { + if (!nextOpen) resetForm() + onOpenChange(nextOpen) + }}> + + + {t("settings:rules.createDialog.title")} + + + +
+
+ + + + {t("settings:rules.createDialog.nameHint")} + + {nameError && {t(nameError)}} +
+ +
+ + +
+ +
+ + +
+ + {kind === "mode" && ( +
+ + + {modeError && {t(modeError)}} +
+ )} +
+ + + + + +
+
+ ) +} diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index 23786ce0b9..d5c55297ea 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -51,6 +51,7 @@ export const ExperimentalSettings = ({
{Object.entries(experimentConfigsMap) .filter(([key]) => key in EXPERIMENT_IDS) + .filter(([, config]) => config.showInSettings !== false) .map((config) => { // Use the same translation key pattern as ExperimentalFeature const experimentKey = config[0] diff --git a/webview-ui/src/components/settings/RulesSettings.tsx b/webview-ui/src/components/settings/RulesSettings.tsx new file mode 100644 index 0000000000..028ca5c56c --- /dev/null +++ b/webview-ui/src/components/settings/RulesSettings.tsx @@ -0,0 +1,207 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react" +import { Edit, Folder, Globe, Plus, ScrollText, Trash2 } from "lucide-react" + +import type { RuleMetadata } from "@roo-code/types" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Button, + StandardTooltip, +} from "@/components/ui" +import { vscode } from "@/utils/vscode" + +import { SectionHeader } from "./SectionHeader" +import { CreateRuleDialog } from "./CreateRuleDialog" + +export const RulesSettings: React.FC = () => { + const { t } = useAppTranslation() + const { cwd, rules: rawRules } = useExtensionState() + const rules = useMemo(() => rawRules ?? [], [rawRules]) + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [ruleToDelete, setRuleToDelete] = useState(null) + const [createDialogOpen, setCreateDialogOpen] = useState(false) + + const hasWorkspace = Boolean(cwd) + + const handleRefresh = useCallback(() => { + vscode.postMessage({ type: "requestRules" }) + }, []) + + useEffect(() => { + handleRefresh() + }, [handleRefresh]) + + const handleDeleteClick = useCallback((rule: RuleMetadata) => { + setRuleToDelete(rule) + setDeleteDialogOpen(true) + }, []) + + const handleDeleteConfirm = useCallback(() => { + if (ruleToDelete) { + vscode.postMessage({ + type: "deleteRule", + values: { + id: ruleToDelete.id, + scope: ruleToDelete.scope, + kind: ruleToDelete.kind, + modeSlug: ruleToDelete.modeSlug, + relativePath: ruleToDelete.relativePath, + }, + }) + setDeleteDialogOpen(false) + setRuleToDelete(null) + } + }, [ruleToDelete]) + + const handleDeleteCancel = useCallback(() => { + setDeleteDialogOpen(false) + setRuleToDelete(null) + }, []) + + const handleEditClick = useCallback((rule: RuleMetadata) => { + vscode.postMessage({ + type: "openRuleFile", + values: { + id: rule.id, + scope: rule.scope, + kind: rule.kind, + modeSlug: rule.modeSlug, + relativePath: rule.relativePath, + }, + }) + }, []) + + const handleRuleCreated = useCallback(() => {}, []) + + const projectRules = useMemo(() => rules.filter((rule) => rule.scope === "project"), [rules]) + const globalRules = useMemo(() => rules.filter((rule) => rule.scope === "global"), [rules]) + + const renderRuleItem = useCallback( + (rule: RuleMetadata) => ( +
+
+
+
+ {rule.name} + + {rule.kind === "mode" + ? t("settings:rules.kind.modeBadge") + : t("settings:rules.kind.genericBadge")} + +
+
+ {rule.relativePath} +
+ {rule.kind === "mode" && ( +
+ {t("settings:rules.modeLabel", { mode: rule.modeName ?? rule.modeSlug })} +
+ )} +
+ +
+ + + + + + +
+
+
+ ), + [t, handleDeleteClick, handleEditClick], + ) + + return ( +
+
+ {t("settings:sections.rules")} +
+

{t("settings:rules.description")}

+ +
+
+ +
+
+ {hasWorkspace && ( + <> +
+ + {t("settings:rules.workspaceRules")} +
+ {projectRules.length > 0 ? ( + projectRules.map(renderRuleItem) + ) : ( +
+ {t("settings:rules.noWorkspaceRules")} +
+ )} + + )} + +
+ + {t("settings:rules.globalRules")} +
+ {globalRules.length > 0 ? ( + globalRules.map(renderRuleItem) + ) : ( +
+ {t("settings:rules.noGlobalRules")} +
+ )} +
+
+ +
+ + {t("settings:rules.footer")} +
+ + + + + {t("settings:rules.deleteDialog.title")} + + {t("settings:rules.deleteDialog.description", { name: ruleToDelete?.name })} + + + + + {t("settings:rules.deleteDialog.cancel")} + + + {t("settings:rules.deleteDialog.confirm")} + + + + + + +
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index a34a8634ff..14d6fa4413 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -29,12 +29,16 @@ import { ArrowLeft, GitCommitVertical, GraduationCap, + ScrollText, } from "lucide-react" import { type ProviderSettings, type ExperimentId, type TelemetrySetting, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, ImageGenerationProvider, } from "@roo-code/types" @@ -77,6 +81,7 @@ import { Section } from "./Section" import PromptsSettings from "./PromptsSettings" import { SlashCommandsSettings } from "./SlashCommandsSettings" import { SkillsSettings } from "./SkillsSettings" +import { RulesSettings } from "./RulesSettings" import { UISettings } from "./UISettings" import ModesView from "../modes/ModesView" import McpView from "../mcp/McpView" @@ -100,6 +105,7 @@ export const sectionNames = [ "autoApprove", "slashCommands", "skills", + "rules", "checkpoints", "notifications", "contextManagement", @@ -142,6 +148,7 @@ const SettingsView = forwardRef(({ onDone, t const contentRef = useRef(null) const prevApiConfigName = useRef(currentApiConfigName) + const handledSettingsImportedAt = useRef(undefined) const confirmDialogHandler = useRef<() => void>() const [cachedState, setCachedState] = useState(() => extensionState) @@ -185,6 +192,7 @@ const SettingsView = forwardRef(({ onDone, t terminalZdotdir, terminalProfile, writeDelayMs, + diffFuzzyThreshold, showRooIgnoredFiles, enableSubfolderRules, maxImageFileSize, @@ -226,10 +234,13 @@ const SettingsView = forwardRef(({ onDone, t // Bust the cache when settings are imported. useEffect(() => { - if (settingsImportedAt) { - setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) - setChangeDetected(false) + if (!settingsImportedAt || handledSettingsImportedAt.current === settingsImportedAt) { + return } + + handledSettingsImportedAt.current = settingsImportedAt + setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) + setChangeDetected(false) }, [settingsImportedAt, extensionState]) const setCachedStateField: SetCachedStateField = useCallback((field, value) => { @@ -393,6 +404,7 @@ const SettingsView = forwardRef(({ onDone, t enableCheckpoints: enableCheckpoints ?? false, checkpointTimeout: checkpointTimeout ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, writeDelayMs, + diffFuzzyThreshold, terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? 30_000, terminalShellIntegrationDisabled, terminalCommandDelay, @@ -423,9 +435,10 @@ const SettingsView = forwardRef(({ onDone, t includeCurrentTime: includeCurrentTime ?? true, includeCurrentCost: includeCurrentCost ?? true, maxGitStatusFiles: maxGitStatusFiles ?? 0, - autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? true, - autoCloseZooOpenedFilesAfterUserEdited: autoCloseZooOpenedFilesAfterUserEdited ?? false, - autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? false, + autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + autoCloseZooOpenedFilesAfterUserEdited: + autoCloseZooOpenedFilesAfterUserEdited ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, profileThresholds, imageGenerationProvider, openRouterImageApiKey, @@ -522,6 +535,7 @@ const SettingsView = forwardRef(({ onDone, t { id: "modes", icon: Users2 }, { id: "skills", icon: GraduationCap }, { id: "slashCommands", icon: SquareSlash }, + { id: "rules", icon: ScrollText }, { id: "autoApprove", icon: CheckCheck }, { id: "mcp", icon: Server }, { id: "checkpoints", icon: GitCommitVertical }, @@ -815,6 +829,9 @@ const SettingsView = forwardRef(({ onDone, t {/* Skills Section */} {renderTab === "skills" && } + {/* Rules Section */} + {renderTab === "rules" && } + {/* Checkpoints Section */} {renderTab === "checkpoints" && ( (({ onDone, t includeDiagnosticMessages={includeDiagnosticMessages} maxDiagnosticMessages={maxDiagnosticMessages} writeDelayMs={writeDelayMs} + diffFuzzyThreshold={diffFuzzyThreshold} includeCurrentTime={includeCurrentTime} includeCurrentCost={includeCurrentCost} maxGitStatusFiles={maxGitStatusFiles} diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index 0f7af4d549..9525ba7a95 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -2,9 +2,9 @@ Semantics for Reasoning Effort (ThinkingBudget) Capability surface: -- modelInfo.supportsReasoningEffort: boolean | Array<"disable" | "none" | "minimal" | "low" | "medium" | "high"> +- modelInfo.supportsReasoningEffort: boolean | Array<"disable" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"> - true → UI shows ["low","medium","high"] - - array → UI shows exactly the provided values + - array → UI shows exactly the provided values (e.g. GPT-5.5 includes "xhigh") Selection behavior: - "disable": @@ -17,7 +17,7 @@ Selection behavior: - set enableReasoningEffort = true - persist reasoningEffort = "none" - request builders include reasoning with value "none" -- "minimal" | "low" | "medium" | "high": +- "minimal" | "low" | "medium" | "high" | "xhigh" | "max": - set enableReasoningEffort = true - persist the selected value - request builders include reasoning with the selected effort @@ -35,12 +35,7 @@ Notes: import { useEffect } from "react" import { Checkbox } from "vscrui" -import { - type ProviderSettings, - type ModelInfo, - type ReasoningEffortWithMinimal, - reasoningEfforts, -} from "@roo-code/types" +import { type ProviderSettings, type ModelInfo, type ReasoningEffortExtended, reasoningEfforts } from "@roo-code/types" import { DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS, @@ -81,44 +76,49 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod // max-tokens control, so only surface this standalone slider when that branch is inactive. const isMaxTokensConfigurable = !!modelInfo && modelInfo.supportsMaxTokens && !isReasoningBudgetSupported - // Build available reasoning efforts list from capability + // "disable" turns off reasoning entirely; "none" is a valid reasoning level. + // Both display as "None" in the UI but behave differently. + // Arrays from supportsReasoningEffort may include "disable" (e.g. Z.ai GLM), so type the + // full option set as ReasoningEffortExtended | "disable" from the start to avoid casts. + type ReasoningEffortOption = ReasoningEffortExtended | "disable" const supports = modelInfo?.supportsReasoningEffort - const baseAvailableOptions: ReadonlyArray = + const baseAvailableOptions: ReadonlyArray = supports === true - ? (reasoningEfforts as readonly ReasoningEffortWithMinimal[]) + ? (reasoningEfforts as readonly ReasoningEffortOption[]) : Array.isArray(supports) - ? (supports as ReadonlyArray) - : (reasoningEfforts as readonly ReasoningEffortWithMinimal[]) + ? (supports as ReadonlyArray) + : (reasoningEfforts as readonly ReasoningEffortOption[]) - // "disable" turns off reasoning entirely; "none" is a valid reasoning level. - // Both display as "None" in the UI but behave differently. // Add "disable" option only when: // 1. requiredReasoningEffort is not true, AND // 2. supportsReasoningEffort is boolean true (not an explicit array) // When the model provides an explicit array, respect those exact values. - type ReasoningEffortOption = ReasoningEffortWithMinimal | "none" | "disable" const shouldAutoAddDisable = - !modelInfo?.requiredReasoningEffort && supports === true && !baseAvailableOptions.includes("disable" as any) + !modelInfo?.requiredReasoningEffort && supports === true && !baseAvailableOptions.includes("disable") const availableOptions: ReadonlyArray = shouldAutoAddDisable - ? (["disable", ...baseAvailableOptions] as ReasoningEffortOption[]) - : (baseAvailableOptions as ReadonlyArray) + ? ["disable", ...baseAvailableOptions] + : baseAvailableOptions // Default reasoning effort - use model's default if available // GPT-5 models have "medium" as their default in the model configuration - const modelDefaultReasoningEffort = modelInfo?.reasoningEffort as ReasoningEffortWithMinimal | undefined + const modelDefaultReasoningEffort = modelInfo?.reasoningEffort as ReasoningEffortExtended | undefined const defaultReasoningEffort: ReasoningEffortOption = modelInfo?.requiredReasoningEffort ? modelDefaultReasoningEffort || "medium" : "disable" - // Current reasoning effort from settings, or fall back to default + // Current reasoning effort from settings, or fall back to default. + // Clamp to availableOptions so the Select trigger always renders a valid option. const storedReasoningEffort = apiConfiguration.reasoningEffort as ReasoningEffortOption | undefined - const currentReasoningEffort: ReasoningEffortOption = storedReasoningEffort || defaultReasoningEffort + const rawReasoningEffort: ReasoningEffortOption = storedReasoningEffort || defaultReasoningEffort + const currentReasoningEffort: ReasoningEffortOption = availableOptions.includes(rawReasoningEffort) + ? rawReasoningEffort + : (availableOptions[0] ?? rawReasoningEffort) // Set default reasoning effort when model supports it and no value is set useEffect(() => { if (isReasoningEffortSupported && !apiConfiguration.reasoningEffort) { // Only set a default if reasoning is required, otherwise leave as undefined (which maps to "disable") if (modelInfo?.requiredReasoningEffort && defaultReasoningEffort !== "disable") { - setApiConfigurationField("reasoningEffort", defaultReasoningEffort as ReasoningEffortWithMinimal, false) + setApiConfigurationField("reasoningEffort", defaultReasoningEffort as ReasoningEffortExtended, false) } } }, [ @@ -282,7 +282,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod } else { // "none", "minimal", "low", "medium", "high" all enable reasoning setApiConfigurationField("enableReasoningEffort", true) - setApiConfigurationField("reasoningEffort", value as ReasoningEffortWithMinimal) + setApiConfigurationField("reasoningEffort", value as ReasoningEffortExtended) } }}> diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index 2b3ab08a84..b40ca8b681 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -2,6 +2,11 @@ import { HTMLAttributes, useMemo } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { telemetryClient } from "@/utils/TelemetryClient" +import { + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, +} from "@roo-code/types" import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" @@ -160,7 +165,7 @@ export const UISettings = ({ label={t("settings:ui.autoCloseZooOpenedFiles.label")}>
setCachedStateField("autoCloseZooOpenedFiles", e.target.checked)} data-testid="auto-close-zoo-opened-files-checkbox"> {t("settings:ui.autoCloseZooOpenedFiles.label")} @@ -178,7 +183,10 @@ export const UISettings = ({ label={t("settings:ui.autoCloseZooOpenedFilesAfterUserEdited.label")}>
setCachedStateField("autoCloseZooOpenedFilesAfterUserEdited", e.target.checked) } @@ -200,7 +208,7 @@ export const UISettings = ({ label={t("settings:ui.autoCloseZooOpenedNewFiles.label")}>
setCachedStateField("autoCloseZooOpenedNewFiles", e.target.checked) } diff --git a/webview-ui/src/components/settings/__tests__/About.spec.tsx b/webview-ui/src/components/settings/__tests__/About.spec.tsx index ce6b7feb95..5266be8bae 100644 --- a/webview-ui/src/components/settings/__tests__/About.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/About.spec.tsx @@ -1,8 +1,9 @@ import React from "react" -import { render, screen } from "@/utils/test-utils" +import { act, fireEvent, render, screen } from "@/utils/test-utils" import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" import { EXTERNAL_LINKS } from "@/constants/externalLinks" +import { vscode } from "@/utils/vscode" import { About } from "../About" @@ -11,6 +12,16 @@ vi.mock("@/utils/vscode", () => ({ })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeButton: ({ + children, + onClick, + disabled, + ...props + }: React.ButtonHTMLAttributes & { appearance?: string }) => ( + + ), VSCodeCheckbox: ({ children, ...props }: React.InputHTMLAttributes) => (