mirror of
https://github.com/cline/cline.git
synced 2026-09-13 09:50:12 +08:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48bac25548 | ||
|
|
37f5f104f3 | ||
|
|
3577b52404 | ||
|
|
1843bc8ed0 | ||
|
|
fead00ec57 | ||
|
|
238107d21c | ||
|
|
2063a661bd | ||
|
|
ec02d5862e | ||
|
|
8452084842 | ||
|
|
a41129a5db | ||
|
|
1ea34be611 | ||
|
|
e72bc3cd14 | ||
|
|
9c907af826 | ||
|
|
e8d3d82522 | ||
|
|
7f9d2e96d9 | ||
|
|
d618f8073a | ||
|
|
eb21ba583c | ||
|
|
f29c25395c | ||
|
|
84c9b587a6 | ||
|
|
6dca234d8e | ||
|
|
9217eacbbd |
@@ -8,8 +8,9 @@ HTTP-controlled debugger for the VSCode extension at `src/dev/debug-harness/serv
|
||||
# Build extension first if needed (protos + esbuild):
|
||||
bun run protos && IS_DEV=true bun esbuild.mjs
|
||||
|
||||
# Launch (skip-build if already built):
|
||||
bun src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
# Launch (skip-build if already built). Run with node, NOT bun — Playwright's
|
||||
# Electron launch times out under bun:
|
||||
node src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
|
||||
# In another terminal:
|
||||
curl localhost:19229/api -d '{"method":"status"}'
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
name: ext-vscode-ab-package
|
||||
|
||||
# Build (and optionally publish) the combined A/B VSIX: a tiny loader plus two
|
||||
# complete extension bundles — `next/` from the SDK-based apps/vscode on main,
|
||||
# `legacy/` from the legacy-extension branch. Cohort selection happens at
|
||||
# runtime via PostHog flags; see apps/vscode-rollout/README.md for the design
|
||||
# and the rollout runbook.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Combined VSIX version — must exceed every previously published version (e.g. 4.1.0)"
|
||||
required: true
|
||||
type: string
|
||||
next-ref:
|
||||
description: "Ref to build the next (SDK) bundle from"
|
||||
required: true
|
||||
default: "main"
|
||||
type: string
|
||||
legacy-ref:
|
||||
description: "Ref to build the legacy bundle from"
|
||||
required: true
|
||||
default: "legacy-extension"
|
||||
type: string
|
||||
publish:
|
||||
description: "Publish to the VS Code Marketplace (unchecked: just build the .vsix artifact)"
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ext-vscode-ab-package-${{ github.event.inputs.version }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
package:
|
||||
name: Build combined (legacy + next) VSIX
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish
|
||||
steps:
|
||||
- name: Checkout next (SDK) source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.next-ref }}
|
||||
path: next-src
|
||||
lfs: true
|
||||
|
||||
- name: Checkout legacy source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.legacy-ref }}
|
||||
path: legacy-src
|
||||
lfs: true
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install next workspace dependencies
|
||||
working-directory: next-src
|
||||
run: bun install
|
||||
|
||||
# @cline/* are local workspace symlinks to source packages; apps/vscode's
|
||||
# `package` script does NOT build them, so without this the esbuild step
|
||||
# fails on a fresh checkout. (The nightly workflow already does this.)
|
||||
- name: Build SDK packages
|
||||
working-directory: next-src
|
||||
run: bun run build:sdk
|
||||
|
||||
# Stamp the combined version into each bundle's package.json AFTER
|
||||
# install and BEFORE its build: the About tab and telemetry
|
||||
# extension_version read the bundle's own manifest, so without this
|
||||
# the VSIX reports three different versions depending on where you
|
||||
# look. (The nightly workflow gets the same alignment via nightlify.mjs.)
|
||||
- name: Align next bundle version
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
run: node scripts/set-version.mjs --dir "$GITHUB_WORKSPACE/next-src/apps/vscode" --version "${{ github.event.inputs.version }}"
|
||||
|
||||
- name: Build next bundle
|
||||
working-directory: next-src/apps/vscode
|
||||
env:
|
||||
CLINE_ENVIRONMENT: production
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
# Inlined by esbuild: attributes every telemetry event with
|
||||
# extension_variant and unlocks the bundle's authoritative
|
||||
# extension.rollout.bundle_activated capture. Rollout builds only.
|
||||
CLINE_ROLLOUT_VARIANT: next
|
||||
# Match the stable publish workflow's OpenTelemetry production defaults.
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: bun run package
|
||||
|
||||
- name: Install legacy dependencies
|
||||
working-directory: legacy-src
|
||||
run: |
|
||||
npm --prefix apps/vscode install --include=optional
|
||||
npm --prefix apps/vscode/webview-ui install --include=optional
|
||||
|
||||
- name: Align legacy bundle version
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
run: node scripts/set-version.mjs --dir "$GITHUB_WORKSPACE/legacy-src/apps/vscode" --version "${{ github.event.inputs.version }}"
|
||||
|
||||
- name: Build legacy bundle
|
||||
working-directory: legacy-src/apps/vscode
|
||||
env:
|
||||
CLINE_ENVIRONMENT: production
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ROLLOUT_VARIANT: legacy
|
||||
# Match the stable publish workflow's OpenTelemetry production defaults.
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run package
|
||||
|
||||
- name: Build loader and run rollout tests
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
run: |
|
||||
bun run typecheck
|
||||
bun run test
|
||||
bun run build:production
|
||||
|
||||
- name: Stitch combined VSIX staging
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
run: |
|
||||
node scripts/stitch.mjs \
|
||||
--next "$GITHUB_WORKSPACE/next-src/apps/vscode" \
|
||||
--legacy "$GITHUB_WORKSPACE/legacy-src/apps/vscode" \
|
||||
--loader dist/extension.js \
|
||||
--version "${{ github.event.inputs.version }}" \
|
||||
--out "$GITHUB_WORKSPACE/staging"
|
||||
|
||||
- name: Smoke-test loader against staging
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
run: node scripts/smoke-loader.mjs "$GITHUB_WORKSPACE/staging"
|
||||
|
||||
# This workflow publishes the STABLE identity. If nightlify ever leaks
|
||||
# into this path the union manifest would ship under the wrong name.
|
||||
# The bundle sub-manifest checks guard the set-version.mjs stamping:
|
||||
# the About tab and telemetry extension_version read those files.
|
||||
- name: Assert stable manifest identity
|
||||
working-directory: staging
|
||||
env:
|
||||
EXPECTED_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
node -e '
|
||||
const assert = require("node:assert");
|
||||
const expected = process.env.EXPECTED_VERSION;
|
||||
const pkg = require("./package.json");
|
||||
assert.equal(pkg.name, "claude-dev", `unexpected name ${pkg.name}`);
|
||||
assert.equal(pkg.publisher, "saoudrizwan", `unexpected publisher ${pkg.publisher}`);
|
||||
assert.equal(pkg.version, expected, `unexpected union version ${pkg.version}`);
|
||||
for (const bundle of ["next", "legacy"]) {
|
||||
const sub = require(`./${bundle}/package.json`);
|
||||
assert.equal(sub.version, expected, `unexpected ${bundle} bundle version ${sub.version}`);
|
||||
}
|
||||
console.log(`stable identity ok: ${pkg.publisher}.${pkg.name}@${pkg.version} (bundle versions aligned)`);
|
||||
'
|
||||
|
||||
- name: Package VSIX
|
||||
working-directory: staging
|
||||
run: |
|
||||
npm install -g @vscode/vsce
|
||||
# Preserve the narrowly scoped VSCE `sendgrid` scanner exemption used by
|
||||
# both standalone bundle workflows. No SendGrid credential is intentionally
|
||||
# supplied here; inspect the reported artifact before widening the exemption.
|
||||
vsce package --no-dependencies --allow-package-secrets sendgrid --out "claude-dev-${{ github.event.inputs.version }}.vsix"
|
||||
|
||||
- name: Upload VSIX artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-dev-${{ github.event.inputs.version }}
|
||||
path: staging/claude-dev-${{ github.event.inputs.version }}.vsix
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish to Marketplace
|
||||
if: ${{ github.event.inputs.publish == 'true' }}
|
||||
working-directory: staging
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
run: |
|
||||
if [[ -z "$VSCE_PAT" ]]; then
|
||||
echo "Error: VSCE_PAT is required to publish."
|
||||
exit 1
|
||||
fi
|
||||
vsce publish --no-dependencies --packagePath "claude-dev-${{ github.event.inputs.version }}.vsix"
|
||||
@@ -1,17 +1,40 @@
|
||||
name: ext-vscode-publish-nightly
|
||||
|
||||
# Publishes saoudrizwan.cline-nightly as the COMBINED A/B VSIX: the rollout
|
||||
# loader plus two complete extension bundles — `next/` from this ref's
|
||||
# apps/vscode (SDK-based) and `legacy/` from the legacy-extension branch.
|
||||
# Cohort selection happens at runtime via PostHog flags; see
|
||||
# apps/vscode-rollout/README.md for the design and rollout runbook.
|
||||
#
|
||||
# The stable-identity equivalent of this pipeline is ext-vscode-ab-package.yml
|
||||
# (manual dispatch, publishes claude-dev). Shared logic lives in
|
||||
# apps/vscode-rollout/scripts (nightlify/gen-manifest/stitch/smoke) so both
|
||||
# workflows stay thin. The single-bundle nightly path this replaced
|
||||
# (apps/vscode/scripts/publish-nightly.mjs) remains for manual feature-branch
|
||||
# pre-release publishes.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every day at 4:00 AM PST (12:00 UTC)
|
||||
- cron: "0 12 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
legacy-ref:
|
||||
description: "Ref to build the legacy bundle from"
|
||||
required: false
|
||||
default: "legacy-extension"
|
||||
type: string
|
||||
dry-run:
|
||||
description: "Build and upload the .vsix artifact without publishing or tagging"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
run-name: "Publish Nightly from ${{ github.ref_name }} @ ${{ github.sha }}"
|
||||
run-name: "Publish Combined Nightly from ${{ github.ref_name }} @ ${{ github.sha }}"
|
||||
|
||||
# Prevent concurrent publish runs on the same branch. The nightly publish script
|
||||
# generates the extension version from a seconds-resolution timestamp, so parallel
|
||||
# runs on the same ref can collide on the same version and cause publish failures
|
||||
# or inconsistent tagging. Runs on different branches proceed independently.
|
||||
# Prevent concurrent publish runs on the same branch: the version is generated
|
||||
# from a seconds-resolution timestamp, so parallel runs on the same ref can
|
||||
# collide on the same version and cause publish failures or inconsistent tagging.
|
||||
concurrency:
|
||||
group: ext-vscode-publish-nightly-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
@@ -20,7 +43,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
|
||||
if: github.repository == 'cline/cline'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
@@ -30,60 +53,79 @@ jobs:
|
||||
needs: test
|
||||
permissions:
|
||||
contents: write
|
||||
name: Publish Cline (Nightly) Extension
|
||||
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
|
||||
name: Publish Cline (Nightly) Combined Extension
|
||||
# Defense in depth: only protected main may enter the publishing environment.
|
||||
# This `if` is advisory because a dispatched branch runs its own copy of this
|
||||
# file; the enforced gate is the PublishNightly environment's deployment-branch
|
||||
# policy, which must also allow only main.
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
environment: PublishNightly
|
||||
# The VS Code extension's package.json and lockfiles live under apps/vscode/
|
||||
# (the repo root has no package.json). Mirror ext-vscode-test.yml so install
|
||||
# and publish steps run in the correct workspace.
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
|
||||
steps:
|
||||
- name: Checkout selected branch
|
||||
- name: Checkout next (SDK) source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
path: next-src
|
||||
lfs: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Show build source
|
||||
working-directory: ${{ github.workspace }}
|
||||
- name: Checkout legacy source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# NOTE: inputs are empty strings on `schedule` events, so the ||
|
||||
# fallback (not the input's declared default) is what the cron uses.
|
||||
ref: ${{ inputs.legacy-ref || 'legacy-extension' }}
|
||||
path: legacy-src
|
||||
lfs: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Show build sources
|
||||
env:
|
||||
# Routed through env rather than interpolated into the script body so
|
||||
# a crafted dispatch input can't inject shell (hygiene: dispatchers
|
||||
# need write access anyway, but keep the pattern clean).
|
||||
LEGACY_REF: ${{ inputs.legacy-ref || 'legacy-extension' }}
|
||||
run: |
|
||||
echo "Building ref: $GITHUB_REF"
|
||||
echo "Building sha: $GITHUB_SHA"
|
||||
git --no-pager log -1 --oneline
|
||||
echo "next: $(git -C next-src rev-parse HEAD)"
|
||||
echo "legacy: $(git -C legacy-src rev-parse HEAD) ($LEGACY_REF)"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
|
||||
# Node is still REQUIRED in the publish job (not just for install): the publish
|
||||
# scripts run as `node ./scripts/publish-*.mjs` and shell out to `npx ovsx`.
|
||||
# setup-bun does not provide a Node runtime, so keep setup-node here.
|
||||
# Pinned to Node 22 because newer LTS (Node 24 / npm 11) can make vsce's
|
||||
# `npm list` dependency detection fail with ELSPROBLEMS during packaging.
|
||||
# Node is required beyond install: the rollout scripts run under node and
|
||||
# publishing shells out to vsce/ovsx. Pinned to Node 22 because newer LTS
|
||||
# (Node 24 / npm 11) can make vsce's dependency detection fail.
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# Single root install resolves the whole bun workspace at once (replaces the
|
||||
# per-package `npm ci` steps for apps/vscode + webview-ui).
|
||||
- name: Install workspace dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
# ONE version for the next bundle, the legacy bundle, and the union
|
||||
# manifest: gen-manifest hard-fails if the bundle identities diverge.
|
||||
# Same scheme as the standalone nightly: <major>.<minor>.<unix-seconds>
|
||||
# from next's base version, so it keeps outranking earlier nightlies.
|
||||
- name: Compute nightly version
|
||||
id: version
|
||||
run: |
|
||||
BASE=$(node -p "require('./next-src/apps/vscode/package.json').version")
|
||||
VERSION="$(echo "$BASE" | cut -d. -f1,2).$(date +%s)"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Combined nightly version: $VERSION (base $BASE)"
|
||||
|
||||
- name: Install next workspace dependencies
|
||||
working-directory: next-src
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
# @cline/* are local workspace symlinks to source packages; build dist/ before
|
||||
# packaging/publishing the extension.
|
||||
- name: Build SDK packages
|
||||
working-directory: ${{ github.workspace }}
|
||||
working-directory: next-src
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Assert better-sqlite3 native binary present
|
||||
working-directory: next-src/apps/vscode
|
||||
run: |
|
||||
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
||||
if [ ! -f "$NODE_FILE" ]; then
|
||||
@@ -93,20 +135,24 @@ jobs:
|
||||
fi
|
||||
echo "Found better-sqlite3 native binary: $NODE_FILE"
|
||||
|
||||
# vsce is now a workspace devDependency (on node_modules/.bin), but ovsx is not
|
||||
# vendored and the publish script invokes it via `npx ovsx`, so install ovsx
|
||||
# globally here (npm is available via setup-node). vsce is installed globally
|
||||
# too to preserve the script's existing PATH expectations.
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
# Rewrite each bundle's package.json to the cline-nightly identity BEFORE
|
||||
# its build (runtime command/config IDs derive from the manifest) and
|
||||
# AFTER dependency install (workspace self-links key off the original
|
||||
# package name).
|
||||
- name: Nightlify next bundle manifest
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
run: node scripts/nightlify.mjs --dir "$GITHUB_WORKSPACE/next-src/apps/vscode" --version "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Publish Nightly Extension
|
||||
- name: Build next bundle
|
||||
working-directory: next-src/apps/vscode
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
# Inlined by esbuild: attributes every telemetry event with
|
||||
# extension_variant and unlocks the bundle's authoritative
|
||||
# extension.rollout.bundle_activated capture. Rollout builds only.
|
||||
CLINE_ROLLOUT_VARIANT: next
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
@@ -114,12 +160,129 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
# The script itself runs under `node ./scripts/publish-nightly.mjs`; bun run
|
||||
# just launches it. Node + npm (for `npx ovsx`) are provided by setup-node above.
|
||||
run: bun run publish:marketplace:nightly
|
||||
run: bun run package
|
||||
|
||||
- name: Install legacy dependencies
|
||||
working-directory: legacy-src
|
||||
run: |
|
||||
npm --prefix apps/vscode install --include=optional
|
||||
npm --prefix apps/vscode/webview-ui install --include=optional
|
||||
|
||||
- name: Nightlify legacy bundle manifest
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
run: node scripts/nightlify.mjs --dir "$GITHUB_WORKSPACE/legacy-src/apps/vscode" --version "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Build legacy bundle
|
||||
working-directory: legacy-src/apps/vscode
|
||||
env:
|
||||
CLINE_ENVIRONMENT: production
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ROLLOUT_VARIANT: legacy
|
||||
# Legacy's esbuild inlines these too (its own publish workflow passes
|
||||
# them) — omitting them here would ship the legacy bundle with the
|
||||
# OTel pipeline dead, unlike what legacy users get today.
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run package
|
||||
|
||||
- name: Build loader and run rollout tests
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
run: |
|
||||
bun run typecheck
|
||||
bun run test
|
||||
bun run build:production
|
||||
|
||||
- name: Stitch combined VSIX staging
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
run: |
|
||||
node scripts/stitch.mjs \
|
||||
--next "$GITHUB_WORKSPACE/next-src/apps/vscode" \
|
||||
--legacy "$GITHUB_WORKSPACE/legacy-src/apps/vscode" \
|
||||
--loader dist/extension.js \
|
||||
--version "${{ steps.version.outputs.version }}" \
|
||||
--out "$GITHUB_WORKSPACE/staging"
|
||||
|
||||
- name: Smoke-test loader against staging
|
||||
working-directory: next-src/apps/vscode-rollout
|
||||
run: node scripts/smoke-loader.mjs "$GITHUB_WORKSPACE/staging"
|
||||
|
||||
# The nightly identity must have fully propagated (nightlify -> both
|
||||
# bundle manifests -> union manifest) or we'd publish over the stable
|
||||
# extension ID. The bundle sub-manifest checks guard the version
|
||||
# stamping: the About tab and telemetry extension_version read those.
|
||||
- name: Assert nightly manifest identity
|
||||
working-directory: staging
|
||||
env:
|
||||
EXPECTED_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
node -e '
|
||||
const assert = require("node:assert");
|
||||
const expected = process.env.EXPECTED_VERSION;
|
||||
const pkg = require("./package.json");
|
||||
assert.equal(pkg.name, "cline-nightly", `unexpected name ${pkg.name}`);
|
||||
assert.equal(pkg.publisher, "saoudrizwan", `unexpected publisher ${pkg.publisher}`);
|
||||
assert.equal(pkg.version, expected, `unexpected union version ${pkg.version}`);
|
||||
for (const bundle of ["next", "legacy"]) {
|
||||
const sub = require(`./${bundle}/package.json`);
|
||||
assert.equal(sub.name, "cline-nightly", `unexpected ${bundle} bundle name ${sub.name}`);
|
||||
assert.equal(sub.version, expected, `unexpected ${bundle} bundle version ${sub.version}`);
|
||||
}
|
||||
console.log(`nightly identity ok: ${pkg.publisher}.${pkg.name}@${pkg.version} (bundle identities aligned)`);
|
||||
'
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
- name: Package VSIX
|
||||
working-directory: staging
|
||||
# Preserve the narrowly scoped VSCE `sendgrid` scanner exemption used by
|
||||
# both standalone bundle workflows. No SendGrid credential is intentionally
|
||||
# supplied here; inspect the reported artifact before widening the exemption.
|
||||
run: vsce package --no-dependencies --allow-package-secrets sendgrid --out "cline-nightly-${{ steps.version.outputs.version }}.vsix"
|
||||
|
||||
- name: Upload VSIX artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cline-nightly-${{ steps.version.outputs.version }}
|
||||
path: staging/cline-nightly-${{ steps.version.outputs.version }}.vsix
|
||||
if-no-files-found: error
|
||||
|
||||
# The job is main-only; step-level dry-run gating still permits a build-only
|
||||
# rehearsal without publishing or tagging.
|
||||
- name: Publish to VS Code Marketplace and Open VSX
|
||||
if: github.ref == 'refs/heads/main' && inputs.dry-run != true
|
||||
working-directory: staging
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
run: |
|
||||
if [[ -z "$VSCE_PAT" ]]; then
|
||||
echo "Error: VSCE_PAT is required to publish."
|
||||
exit 1
|
||||
fi
|
||||
vsce publish --no-dependencies --packagePath "cline-nightly-${{ steps.version.outputs.version }}.vsix"
|
||||
if [[ -n "$OVSX_PAT" ]]; then
|
||||
npx ovsx publish --packagePath "cline-nightly-${{ steps.version.outputs.version }}.vsix" --pat "$OVSX_PAT"
|
||||
else
|
||||
echo "WARNING: OVSX_PAT not set; skipping Open VSX publish."
|
||||
fi
|
||||
|
||||
- name: Tag published commit
|
||||
working-directory: ${{ github.workspace }}
|
||||
if: github.ref == 'refs/heads/main' && inputs.dry-run != true
|
||||
# Best-effort bookkeeping: the default GITHUB_TOKEN cannot create a ref
|
||||
# whose commit modifies workflow files (no workflows permission exists
|
||||
# for it), so this step fails whenever HEAD touched .github/workflows.
|
||||
# The publish already succeeded by this point — don't mark the run red;
|
||||
# push the tag manually with user credentials when it matters.
|
||||
continue-on-error: true
|
||||
working-directory: next-src
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
@@ -127,10 +290,11 @@ jobs:
|
||||
SHORT_SHA=$(git rev-parse --short=12 HEAD)
|
||||
TIMESTAMP=$(date -u +"%Y%m%d%H%M%S")
|
||||
TAG="nightly-${SAFE_REF}-${TIMESTAMP}-${SHORT_SHA}"
|
||||
LEGACY_SHA=$(git -C "$GITHUB_WORKSPACE/legacy-src" rev-parse HEAD)
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "$TAG" -m "Cline Nightly published from ${GITHUB_REF_NAME} at ${GITHUB_SHA}"
|
||||
git tag -a "$TAG" -m "Cline Nightly (combined A/B) published from ${GITHUB_REF_NAME} at ${GITHUB_SHA} (legacy bundle: ${LEGACY_SHA})"
|
||||
# Use an explicit HTTPS remote with GH_TOKEN because checkout was run with
|
||||
# persist-credentials: false, so actions/checkout did not persist a git credential helper.
|
||||
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${TAG}"
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Cline CLI Changelog
|
||||
|
||||
## 3.0.42
|
||||
|
||||
- Fixed Ollama native API routing so context window and timeout settings work again
|
||||
|
||||
## 3.0.41
|
||||
|
||||
- Compaction now shows progress status in the TUI
|
||||
- Model IDs are now suggested from OpenAI-compatible endpoints when configuring a provider
|
||||
- Workspace git info (branch/remote) is now persisted and refreshed across sessions
|
||||
- Compaction no longer runs during an active turn
|
||||
- Fixed a crash when the terminal title was updated during TUI teardown
|
||||
- The API key fallback hint is now highlighted for better visibility
|
||||
- Benign git states are no longer reported as workspace initialization errors
|
||||
|
||||
## 3.0.40
|
||||
|
||||
- Added a manual API key escape hatch for Cline OAuth providers, so you can enter a key by hand from settings
|
||||
|
||||
@@ -346,9 +346,24 @@ Desktop-integrated approval mode is also supported via env wiring (`CLINE_TOOL_A
|
||||
- `CLINE_LOG_LEVEL` - Runtime log level (`trace|debug|info|warn|error|fatal|silent`, default `info`)
|
||||
- `CLINE_LOG_PATH` - Runtime log file path (default `<CLINE_DATA_DIR>/logs/cline.log`)
|
||||
- `CLINE_LOG_NAME` - Logger name embedded in runtime log records
|
||||
- `CLINE_DEBUG` - Set to `1`/`true` to print wrapper diagnostics (e.g. the CA bundle summary)
|
||||
|
||||
`--key` takes precedence over environment variables.
|
||||
|
||||
## Certificate trust
|
||||
|
||||
The CLI automatically trusts your operating system's certificate store, so it
|
||||
works behind corporate TLS-inspecting proxies and with self-signed/internal
|
||||
endpoints without any setup. On launch the `cline` wrapper harvests the OS trust
|
||||
anchors and writes them to `~/.cline/cli-node-extra-ca-certs.pem`, then points
|
||||
the runtime's `NODE_EXTRA_CA_CERTS` at that bundle. The file is regenerated when
|
||||
it changes and is safe to delete (it is rebuilt on the next run).
|
||||
|
||||
If you set `NODE_EXTRA_CA_CERTS` yourself, your certificates are **merged** into
|
||||
that bundle alongside the system store rather than replacing it. Run with
|
||||
`CLINE_DEBUG=1` to see how many OS and user CAs were loaded and where the bundle
|
||||
was written.
|
||||
|
||||
## Contributing
|
||||
|
||||
See [DEVELOPMENT.md](./DEVELOPMENT.md) for local development setup, monorepo structure, and TUI architecture. See [DISTRIBUTION.md](./DISTRIBUTION.md) for how the CLI is packaged and distributed.
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
// Auto-discovery of OS trust anchors for the Cline CLI.
|
||||
//
|
||||
// Bun does not read the OS trust store, so the 3.x CLI cannot see corporate
|
||||
// MITM / self-signed CAs out of the box. This runs in the Node `bin/cline`
|
||||
// wrapper (not Bun), reads the full OS store via tls.getCACertificates("system")
|
||||
// (Node >= 22, no --use-system-ca flag), and hands the certs to the Bun child
|
||||
// via NODE_EXTRA_CA_CERTS, which both runtimes honor. Mirrors the JetBrains
|
||||
// plugin's configureCertificates(), sourcing from the OS instead of the IDE.
|
||||
//
|
||||
// Dependency-free CommonJS with injectable modules so it is unit-testable and
|
||||
// ships verbatim in the published wrapper package.
|
||||
|
||||
const PEM_MARKER = "-----BEGIN CERTIFICATE-----";
|
||||
const CERT_BLOCK =
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
||||
|
||||
/**
|
||||
* Returns only the complete certificate blocks from PEM text, or null when
|
||||
* there are none. User files may also hold private keys (combined cert+key
|
||||
* PEMs) or other sections, which must never be copied into the managed
|
||||
* bundle. Files that contain nothing but certificates pass through verbatim
|
||||
* so unchanged bundles keep hash-skipping the rewrite.
|
||||
*/
|
||||
function sanitizePem(text) {
|
||||
const blocks = text.match(CERT_BLOCK) ?? [];
|
||||
if (blocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const rest = text.replace(CERT_BLOCK, "");
|
||||
if (/^\s*$/.test(rest)) {
|
||||
return text;
|
||||
}
|
||||
return `${blocks.join("\n")}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns OS-trusted certificates as PEM strings, or [] when unavailable.
|
||||
* tls.getCACertificates("system") requires Node >= 22.
|
||||
*/
|
||||
function harvestSystemCerts(tlsModule) {
|
||||
try {
|
||||
const tls = tlsModule || require("node:tls");
|
||||
if (typeof tls.getCACertificates !== "function") {
|
||||
return [];
|
||||
}
|
||||
const certs = tls.getCACertificates("system");
|
||||
if (!Array.isArray(certs)) {
|
||||
return [];
|
||||
}
|
||||
return certs.filter(
|
||||
(cert) => typeof cert === "string" && cert.includes(PEM_MARKER),
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file's certificate blocks as PEM text, or null when missing,
|
||||
* unreadable, or holding no complete certificate block.
|
||||
*/
|
||||
function readUserBundle(fsModule, userPath) {
|
||||
if (!userPath) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const fs = fsModule || require("node:fs");
|
||||
const stat = fs.statSync(userPath, { throwIfNoEntry: false });
|
||||
if (!stat || !stat.isFile()) {
|
||||
return null;
|
||||
}
|
||||
// Binary DER would not have loaded in the runtime either; require PEM.
|
||||
return sanitizePem(fs.readFileSync(userPath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user's NODE_EXTRA_CA_CERTS value into PEM strings. Node treats the
|
||||
* value as a single file, but some users set an OS-path-delimited list; the
|
||||
* whole value is tried as one file first, then split.
|
||||
* The managed bundle is excluded so reading it back never re-appends its certs.
|
||||
*/
|
||||
function readUserCerts(fsModule, pathModule, value, managedPath) {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
const fs = fsModule || require("node:fs");
|
||||
const path = pathModule || require("node:path");
|
||||
const candidates = [];
|
||||
const whole = readUserBundle(fs, value);
|
||||
if (whole) {
|
||||
candidates.push({ filePath: value, pem: whole });
|
||||
} else if (value.includes(path.delimiter)) {
|
||||
for (const segment of value.split(path.delimiter)) {
|
||||
const trimmed = segment.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const pem = readUserBundle(fs, trimmed);
|
||||
if (pem) {
|
||||
candidates.push({ filePath: trimmed, pem });
|
||||
}
|
||||
}
|
||||
}
|
||||
const pems = [];
|
||||
for (const candidate of candidates) {
|
||||
const isManaged =
|
||||
managedPath &&
|
||||
path.resolve(candidate.filePath) === path.resolve(managedPath);
|
||||
if (!isManaged) {
|
||||
pems.push(candidate.pem);
|
||||
}
|
||||
}
|
||||
return pems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates the user PEMs (if any) and the system certificates into one
|
||||
* bundle. A separating newline is inserted between parts so adjacent END/BEGIN
|
||||
* markers cannot fuse into one invalid line.
|
||||
*/
|
||||
function buildBundle({ systemCerts, userPems }) {
|
||||
const parts = [...(userPems ?? []), ...systemCerts];
|
||||
return parts
|
||||
.map((part) => (part.endsWith("\n") ? part : `${part}\n`))
|
||||
.join("");
|
||||
}
|
||||
|
||||
/** Counts individual PEM certificates across the given bundle strings. */
|
||||
function countCerts(pems) {
|
||||
let count = 0;
|
||||
for (const pem of pems) {
|
||||
count += pem.split(PEM_MARKER).length - 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function readFileIfExists(fs, filePath) {
|
||||
try {
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveClineDir(env, os, path) {
|
||||
return env.CLINE_DIR?.trim() || path.join(os.homedir(), ".cline");
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the api-unavailable warning should print. Stamped per Node version
|
||||
* in the cline dir so the nudge shows once rather than on every command; a
|
||||
* version change (upgrade that still falls short, or downgrade) re-arms it.
|
||||
* When the stamp cannot be read or written, warn — bookkeeping failures must
|
||||
* never suppress a real diagnostic.
|
||||
*/
|
||||
function shouldWarnApiUnavailable(env, deps = {}) {
|
||||
const fs = deps.fs || require("node:fs");
|
||||
const os = deps.os || require("node:os");
|
||||
const path = deps.path || require("node:path");
|
||||
const version = deps.nodeVersion || process.versions.node;
|
||||
const dir = resolveClineDir(env, os, path);
|
||||
const stamp = path.join(dir, `.ca-api-warned-${version}`);
|
||||
try {
|
||||
if (fs.existsSync(stamp)) {
|
||||
return false;
|
||||
}
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(stamp, "", { mode: 0o600 });
|
||||
return true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Atomically writes [content] to [target]; returns true on success. */
|
||||
function writeBundle(fs, dir, target, content) {
|
||||
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
// Owner read/write: the bundle holds public CA material, not secrets,
|
||||
// but there is no reason to make it world-writable.
|
||||
fs.writeFileSync(tmp, content, { mode: 0o600 });
|
||||
try {
|
||||
fs.renameSync(tmp, target);
|
||||
} catch {
|
||||
// Windows can reject rename over a file a concurrent child holds open.
|
||||
fs.rmSync(target, { force: true });
|
||||
fs.renameSync(tmp, target);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
// Never leave a partial temp file behind (e.g. ENOSPC mid-write).
|
||||
try {
|
||||
fs.rmSync(tmp, { force: true });
|
||||
} catch {
|
||||
// Ignore: best-effort cleanup.
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Harvests OS trust anchors, merges them with any user NODE_EXTRA_CA_CERTS, and
|
||||
* points env.NODE_EXTRA_CA_CERTS at a single managed PEM bundle. Mutates `env`
|
||||
* in place. Returns an outcome the caller can log; `action` is one of
|
||||
* "unchanged" | "written" | "write-failed-reused" | "write-failed" |
|
||||
* "no-system-certs" | "api-unavailable".
|
||||
*/
|
||||
function configureNodeExtraCaCerts(env, deps = {}) {
|
||||
const fs = deps.fs || require("node:fs");
|
||||
const os = deps.os || require("node:os");
|
||||
const path = deps.path || require("node:path");
|
||||
const tls = deps.tls || require("node:tls");
|
||||
|
||||
// tls.getCACertificates("system") needs Node >= 22.15; on older Nodes the
|
||||
// harvest cannot run at all, which the caller should surface to the user.
|
||||
if (typeof tls.getCACertificates !== "function") {
|
||||
return {
|
||||
action: "api-unavailable",
|
||||
path: null,
|
||||
systemCertCount: 0,
|
||||
userCertCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const systemCerts = harvestSystemCerts(tls);
|
||||
if (systemCerts.length === 0) {
|
||||
// Nothing to add: leave any user-provided NODE_EXTRA_CA_CERTS untouched
|
||||
// and let the runtime fall back to its bundled CAs.
|
||||
return {
|
||||
action: "no-system-certs",
|
||||
path: null,
|
||||
systemCertCount: 0,
|
||||
userCertCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const managedDir = resolveClineDir(env, os, path);
|
||||
const managedPath = path.join(managedDir, "cli-node-extra-ca-certs.pem");
|
||||
const userValue = (env.NODE_EXTRA_CA_CERTS || "").trim() || null;
|
||||
const userPems = readUserCerts(fs, path, userValue, managedPath);
|
||||
const bundle = buildBundle({ systemCerts, userPems });
|
||||
const base = {
|
||||
path: managedPath,
|
||||
systemCertCount: systemCerts.length,
|
||||
userCertCount: countCerts(userPems),
|
||||
};
|
||||
|
||||
// Skip the rewrite when the bundle is already current. Avoids per-launch I/O
|
||||
// and the concurrent-rename race in the steady state.
|
||||
if (readFileIfExists(fs, managedPath) === bundle) {
|
||||
env.NODE_EXTRA_CA_CERTS = managedPath;
|
||||
return { ...base, action: "unchanged" };
|
||||
}
|
||||
|
||||
if (writeBundle(fs, managedDir, managedPath, bundle)) {
|
||||
env.NODE_EXTRA_CA_CERTS = managedPath;
|
||||
return { ...base, action: "written" };
|
||||
}
|
||||
|
||||
// Write failed: fall back to a previously-written bundle if one exists.
|
||||
if (readFileIfExists(fs, managedPath)) {
|
||||
env.NODE_EXTRA_CA_CERTS = managedPath;
|
||||
return { ...base, action: "write-failed-reused" };
|
||||
}
|
||||
return { ...base, path: null, action: "write-failed" };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
harvestSystemCerts,
|
||||
sanitizePem,
|
||||
readUserBundle,
|
||||
readUserCerts,
|
||||
buildBundle,
|
||||
countCerts,
|
||||
configureNodeExtraCaCerts,
|
||||
shouldWarnApiUnavailable,
|
||||
};
|
||||
@@ -23,6 +23,48 @@ const childEnv = {
|
||||
CLINE_WRAPPER_PATH: scriptPath,
|
||||
};
|
||||
|
||||
// Auto-discover OS trust anchors and pass them to the Bun child via
|
||||
// NODE_EXTRA_CA_CERTS. The Bun runtime does not read the OS store on its own,
|
||||
// so corporate/self-signed CAs would otherwise fail. This wrapper runs on
|
||||
// Node, which can read the full store here.
|
||||
try {
|
||||
const caCerts = require("./ca-certs.cjs");
|
||||
const outcome = caCerts.configureNodeExtraCaCerts(childEnv);
|
||||
const debug =
|
||||
process.env.CLINE_DEBUG === "1" || process.env.CLINE_DEBUG === "true";
|
||||
// Not debug-gated: on old Nodes the harvest silently doing nothing is
|
||||
// indistinguishable from a broken corporate proxy. Stamped per Node
|
||||
// version so the nudge shows once, not on every command.
|
||||
if (
|
||||
outcome &&
|
||||
outcome.action === "api-unavailable" &&
|
||||
!childEnv.NODE_EXTRA_CA_CERTS &&
|
||||
caCerts.shouldWarnApiUnavailable(childEnv)
|
||||
) {
|
||||
console.warn(
|
||||
`[cline] Node ${process.versions.node} cannot read the OS trust store (needs >= 22.15); ` +
|
||||
"corporate or self-signed CAs may fail TLS. Upgrade Node or set NODE_EXTRA_CA_CERTS.",
|
||||
);
|
||||
}
|
||||
if (debug && outcome) {
|
||||
if (outcome.action === "no-system-certs") {
|
||||
console.warn(
|
||||
"[cline] No OS trust anchors found; relying on the runtime's bundled CAs.",
|
||||
);
|
||||
} else if (outcome.action === "write-failed") {
|
||||
console.warn(
|
||||
"[cline] Could not write the managed CA bundle; relying on the runtime's bundled CAs.",
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`[cline] Trust: ${outcome.systemCertCount} OS + ${outcome.userCertCount} user CAs (${outcome.action}) -> ${outcome.path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best effort: fall back to the runtime's default trust on any failure.
|
||||
}
|
||||
|
||||
function run(target) {
|
||||
const result = childProcess.spawnSync(target, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cline/cli",
|
||||
"displayName": "cline",
|
||||
"version": "3.0.40",
|
||||
"version": "3.0.42",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
// The helper ships as CommonJS in the published wrapper package, so it is
|
||||
// loaded via require rather than an ESM import.
|
||||
const caCerts = require("../../bin/ca-certs.cjs") as {
|
||||
harvestSystemCerts: (tls?: unknown) => string[];
|
||||
readUserBundle: (fs: unknown, p: string | null) => string | null;
|
||||
readUserCerts: (
|
||||
fs: unknown,
|
||||
path: unknown,
|
||||
value: string | null,
|
||||
managedPath: string | null,
|
||||
) => string[];
|
||||
buildBundle: (input: {
|
||||
systemCerts: string[];
|
||||
userPems?: string[];
|
||||
}) => string;
|
||||
countCerts: (pems: string[]) => number;
|
||||
configureNodeExtraCaCerts: (
|
||||
env: Record<string, string>,
|
||||
deps?: { tls?: unknown; fs?: unknown },
|
||||
) => {
|
||||
action: string;
|
||||
path: string | null;
|
||||
systemCertCount: number;
|
||||
userCertCount: number;
|
||||
};
|
||||
shouldWarnApiUnavailable: (
|
||||
env: Record<string, string>,
|
||||
deps?: { fs?: unknown; nodeVersion?: string },
|
||||
) => boolean;
|
||||
};
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const certSystem =
|
||||
"-----BEGIN CERTIFICATE-----\nSYSTEM\n-----END CERTIFICATE-----\n";
|
||||
const certUser = "-----BEGIN CERTIFICATE-----\nUSER\n-----END CERTIFICATE-----";
|
||||
|
||||
function fakeTls(certs: unknown) {
|
||||
return { getCACertificates: () => certs };
|
||||
}
|
||||
|
||||
describe("ca-certs", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), "cline-ca-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("harvestSystemCerts", () => {
|
||||
it("returns only PEM strings from the system store", () => {
|
||||
expect(
|
||||
caCerts.harvestSystemCerts(fakeTls([certSystem, "not-a-cert", 42])),
|
||||
).toEqual([certSystem]);
|
||||
});
|
||||
|
||||
it("returns [] when getCACertificates is unavailable", () => {
|
||||
expect(caCerts.harvestSystemCerts({})).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns [] when getCACertificates throws", () => {
|
||||
expect(
|
||||
caCerts.harvestSystemCerts({
|
||||
getCACertificates: () => {
|
||||
throw new Error("nope");
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readUserBundle", () => {
|
||||
it("returns PEM contents for a PEM file", () => {
|
||||
const p = join(dir, "user.pem");
|
||||
writeFileSync(p, certUser);
|
||||
expect(caCerts.readUserBundle(fs, p)).toBe(certUser);
|
||||
});
|
||||
|
||||
it("returns null for a non-PEM (DER) file", () => {
|
||||
const p = join(dir, "user.der");
|
||||
writeFileSync(p, Buffer.from([0x30, 0x82, 0x01, 0x02]));
|
||||
expect(caCerts.readUserBundle(fs, p)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for a missing file and for null path", () => {
|
||||
expect(caCerts.readUserBundle(fs, join(dir, "nope.pem"))).toBeNull();
|
||||
expect(caCerts.readUserBundle(fs, null)).toBeNull();
|
||||
});
|
||||
|
||||
it("strips non-certificate sections such as private keys", () => {
|
||||
// Combined cert+key files (nginx/haproxy style) are common; the key
|
||||
// must never reach the managed bundle.
|
||||
const p = join(dir, "combined.pem");
|
||||
writeFileSync(
|
||||
p,
|
||||
`${certUser}\n-----BEGIN PRIVATE KEY-----\nSECRET\n-----END PRIVATE KEY-----\n`,
|
||||
);
|
||||
const out = caCerts.readUserBundle(fs, p);
|
||||
expect(out).toContain("USER");
|
||||
expect(out).not.toContain("PRIVATE KEY");
|
||||
expect(out).not.toContain("SECRET");
|
||||
});
|
||||
|
||||
it("keeps certificates-only files verbatim", () => {
|
||||
// Byte-identical passthrough keeps the unchanged-skip hash stable.
|
||||
const p = join(dir, "clean.pem");
|
||||
writeFileSync(p, `${certUser}\n${certSystem}`);
|
||||
expect(caCerts.readUserBundle(fs, p)).toBe(`${certUser}\n${certSystem}`);
|
||||
});
|
||||
|
||||
it("returns null for a BEGIN marker without a complete block", () => {
|
||||
const p = join(dir, "truncated.pem");
|
||||
writeFileSync(p, "-----BEGIN CERTIFICATE-----\ntruncated");
|
||||
expect(caCerts.readUserBundle(fs, p)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readUserCerts", () => {
|
||||
it("reads a single PEM file path", () => {
|
||||
const p = join(dir, "corp.pem");
|
||||
writeFileSync(p, certUser);
|
||||
expect(caCerts.readUserCerts(fs, path, p, null)).toEqual([certUser]);
|
||||
});
|
||||
|
||||
it("splits a legacy OS-path-delimited value and reads each PEM", () => {
|
||||
// Legacy footgun: NODE_EXTRA_CA_CERTS="a.pem;b.pem".
|
||||
const a = join(dir, "a.pem");
|
||||
const b = join(dir, "b.pem");
|
||||
writeFileSync(a, certUser);
|
||||
writeFileSync(b, certSystem);
|
||||
expect(
|
||||
caCerts.readUserCerts(fs, path, [a, b].join(delimiter), null),
|
||||
).toEqual([certUser, certSystem]);
|
||||
});
|
||||
|
||||
it("skips missing segments in a delimited value", () => {
|
||||
const a = join(dir, "a.pem");
|
||||
writeFileSync(a, certUser);
|
||||
const value = [a, join(dir, "missing.pem")].join(delimiter);
|
||||
expect(caCerts.readUserCerts(fs, path, value, null)).toEqual([certUser]);
|
||||
});
|
||||
|
||||
it("excludes the managed bundle from user certs", () => {
|
||||
const managed = join(dir, "cli-node-extra-ca-certs.pem");
|
||||
writeFileSync(managed, certUser);
|
||||
expect(caCerts.readUserCerts(fs, path, managed, managed)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns [] for empty value", () => {
|
||||
expect(caCerts.readUserCerts(fs, path, null, null)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildBundle", () => {
|
||||
it("merges user PEMs before system certs", () => {
|
||||
expect(
|
||||
caCerts.buildBundle({
|
||||
systemCerts: [certSystem],
|
||||
userPems: [certUser],
|
||||
}),
|
||||
).toBe(`${certUser}\n${certSystem}`);
|
||||
});
|
||||
|
||||
it("inserts a separating newline so END/BEGIN markers do not fuse", () => {
|
||||
// certUser has no trailing newline, so this proves the boundary fix.
|
||||
const merged = caCerts.buildBundle({
|
||||
systemCerts: [certSystem],
|
||||
userPems: [certUser],
|
||||
});
|
||||
expect(merged).not.toContain(
|
||||
"-----END CERTIFICATE----------BEGIN CERTIFICATE-----",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles no user PEMs", () => {
|
||||
expect(caCerts.buildBundle({ systemCerts: [certSystem] })).toBe(
|
||||
certSystem,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("configureNodeExtraCaCerts", () => {
|
||||
it("writes a managed bundle and points the env var at it", () => {
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
});
|
||||
expect(out.action).toBe("written");
|
||||
expect(out.path).toBe(join(dir, "cli-node-extra-ca-certs.pem"));
|
||||
expect(env.NODE_EXTRA_CA_CERTS).toBe(out.path);
|
||||
expect(readFileSync(out.path as string, "utf8")).toContain("SYSTEM");
|
||||
});
|
||||
|
||||
it("merges a user-supplied NODE_EXTRA_CA_CERTS with system certs", () => {
|
||||
const userPath = join(dir, "corp.pem");
|
||||
writeFileSync(userPath, certUser);
|
||||
const env: Record<string, string> = {
|
||||
CLINE_DIR: dir,
|
||||
NODE_EXTRA_CA_CERTS: userPath,
|
||||
};
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
});
|
||||
expect(out.userCertCount).toBe(1);
|
||||
const written = readFileSync(env.NODE_EXTRA_CA_CERTS, "utf8");
|
||||
expect(written).toContain("USER");
|
||||
expect(written).toContain("SYSTEM");
|
||||
});
|
||||
|
||||
it("reports unchanged and skips rewrite on the second run", () => {
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
expect(
|
||||
caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([certSystem]) })
|
||||
.action,
|
||||
).toBe("written");
|
||||
expect(
|
||||
caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([certSystem]) })
|
||||
.action,
|
||||
).toBe("unchanged");
|
||||
});
|
||||
|
||||
it("does not re-append when the user already points at the managed bundle", () => {
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
const first = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
}).path as string;
|
||||
const env2: Record<string, string> = {
|
||||
CLINE_DIR: dir,
|
||||
NODE_EXTRA_CA_CERTS: first,
|
||||
};
|
||||
caCerts.configureNodeExtraCaCerts(env2, { tls: fakeTls([certSystem]) });
|
||||
const written = readFileSync(env2.NODE_EXTRA_CA_CERTS, "utf8");
|
||||
expect(written.match(/SYSTEM/g)?.length).toBe(1);
|
||||
});
|
||||
|
||||
it("no-ops when no system certs are available", () => {
|
||||
const env: Record<string, string> = {
|
||||
CLINE_DIR: dir,
|
||||
NODE_EXTRA_CA_CERTS: "/user/corp.pem",
|
||||
};
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([]) });
|
||||
expect(out.action).toBe("no-system-certs");
|
||||
expect(out.path).toBeNull();
|
||||
expect(env.NODE_EXTRA_CA_CERTS).toBe("/user/corp.pem");
|
||||
});
|
||||
|
||||
it("reports api-unavailable on Nodes without getCACertificates", () => {
|
||||
const env: Record<string, string> = {
|
||||
CLINE_DIR: dir,
|
||||
NODE_EXTRA_CA_CERTS: "/user/corp.pem",
|
||||
};
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, { tls: {} });
|
||||
expect(out.action).toBe("api-unavailable");
|
||||
expect(out.path).toBeNull();
|
||||
expect(env.NODE_EXTRA_CA_CERTS).toBe("/user/corp.pem");
|
||||
});
|
||||
|
||||
it("reports write-failed when the bundle cannot be written", () => {
|
||||
const realFs = require("node:fs");
|
||||
const failingFs = {
|
||||
...realFs,
|
||||
mkdirSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
writeFileSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
};
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
const out = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
fs: failingFs,
|
||||
});
|
||||
expect(out.action).toBe("write-failed");
|
||||
expect(out.path).toBeNull();
|
||||
expect(env.NODE_EXTRA_CA_CERTS).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reuses a stale bundle when the rewrite fails", () => {
|
||||
// First run writes the bundle normally.
|
||||
const env: Record<string, string> = { CLINE_DIR: dir };
|
||||
const managedPath = caCerts.configureNodeExtraCaCerts(env, {
|
||||
tls: fakeTls([certSystem]),
|
||||
}).path as string;
|
||||
|
||||
// Second run: writes fail, but the stale bundle is still readable.
|
||||
const realFs = require("node:fs");
|
||||
const failingFs = {
|
||||
...realFs,
|
||||
mkdirSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
writeFileSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
};
|
||||
const env2: Record<string, string> = { CLINE_DIR: dir };
|
||||
const out = caCerts.configureNodeExtraCaCerts(env2, {
|
||||
// A different system cert forces a rewrite attempt (not "unchanged").
|
||||
tls: fakeTls([certUser]),
|
||||
fs: failingFs,
|
||||
});
|
||||
|
||||
expect(out.action).toBe("write-failed-reused");
|
||||
expect(env2.NODE_EXTRA_CA_CERTS).toBe(managedPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("countCerts", () => {
|
||||
it("counts individual certificates, not files", () => {
|
||||
// One file holding two certs must report 2, not 1.
|
||||
const twoInOne = `${certUser}\n${certSystem}`;
|
||||
expect(caCerts.countCerts([twoInOne])).toBe(2);
|
||||
expect(caCerts.countCerts([certUser, certSystem])).toBe(2);
|
||||
expect(caCerts.countCerts([])).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldWarnApiUnavailable", () => {
|
||||
it("warns once per Node version, then stays quiet", () => {
|
||||
const env = { CLINE_DIR: dir };
|
||||
const deps = { nodeVersion: "22.1.0" };
|
||||
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
|
||||
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(false);
|
||||
});
|
||||
|
||||
it("re-arms when the Node version changes", () => {
|
||||
const env = { CLINE_DIR: dir };
|
||||
expect(
|
||||
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.1.0" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.14.0" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.1.0" }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("still warns when the stamp cannot be written", () => {
|
||||
const realFs = require("node:fs");
|
||||
const failingFs = {
|
||||
...realFs,
|
||||
mkdirSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
writeFileSync: () => {
|
||||
throw new Error("EACCES");
|
||||
},
|
||||
};
|
||||
const env = { CLINE_DIR: dir };
|
||||
const deps = { fs: failingFs, nodeVersion: "22.1.0" };
|
||||
// Bookkeeping failure must never suppress the diagnostic.
|
||||
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
|
||||
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
# vscode-rollout — A/B loader for the SDK extension rollout
|
||||
|
||||
The VS Code Marketplace has no staged rollouts: publishing a version updates
|
||||
every user. This package lets us ship the **SDK-based extension** (main's
|
||||
`apps/vscode`, "next") to a percentage of users while everyone else keeps
|
||||
running the **legacy extension** (the `legacy-extension` branch), inside a
|
||||
single published VSIX.
|
||||
|
||||
## How it works
|
||||
|
||||
The published VSIX contains a ~40 KB loader as its entrypoint and two complete,
|
||||
independently built extension bundles:
|
||||
|
||||
```
|
||||
extension.js ← loader (this package)
|
||||
package.json ← UNION of both bundles' manifests (generated, see below)
|
||||
assets/, walkthrough/ ← manifest-referenced resources (VSIX-root-relative)
|
||||
next/ ← SDK extension (dist/, webview-ui/build/, assets/)
|
||||
legacy/ ← legacy extension (dist/, webview-ui/build/, assets/, codicons)
|
||||
```
|
||||
|
||||
Per window, the loader:
|
||||
|
||||
1. Reads the cached cohort assignment from its own `globalState` keys —
|
||||
synchronously, never from the network.
|
||||
2. Sets the `cline.sdkBundle` context key (gates cohort-specific menu items /
|
||||
palette entries in the union manifest).
|
||||
3. `require()`s exactly one bundle and calls its `activate()` with a
|
||||
Proxy-wrapped `ExtensionContext` whose `extensionUri` / `extensionPath` /
|
||||
`asAbsolutePath` point into that bundle's subdirectory — so each bundle
|
||||
resolves its own webview build and assets without knowing it was relocated.
|
||||
Storage properties pass through untouched: both bundles share the same
|
||||
`~/.cline/data` + VS Code storage they used as standalone extensions.
|
||||
4. After the selected bundle activates, evaluates the PostHog flags in the
|
||||
background and caches the assignment **for the next window**. Flag changes
|
||||
never flip a live window. A crash fallback skips this refresh so it cannot
|
||||
overwrite the legacy pin.
|
||||
|
||||
If the next bundle throws during activation, the loader disposes whatever it
|
||||
half-registered, pins this VSIX version back to legacy
|
||||
(`cline.rollout.nextActivationFailedVersion`), reports a `fallback` telemetry
|
||||
event, and activates legacy — a crashed rollout self-heals without a
|
||||
marketplace re-publish. A new version gets to try next again.
|
||||
|
||||
## Cohort rules
|
||||
|
||||
- **Two-way, one knob.** `ext-sdk-bundle-rollout` (percentage flag) is the
|
||||
entire remote control surface: each background refresh caches exactly what
|
||||
the flag says for the machine's next window. Dialing the percentage up
|
||||
promotes; dialing it down demotes on the next reload — the emergency lever
|
||||
is simply "set the rollout to 0%". Known demotion costs (accepted): tasks
|
||||
created on the SDK bundle are stored as SDK sessions the legacy bundle
|
||||
doesn't list (they reappear on re-promotion — nothing is deleted), and
|
||||
credentials rotated on next may require a re-login on legacy.
|
||||
- **The flag must stay a boolean flag.** The loader only promotes on a
|
||||
literal `true` from `/decide` — a multivariate variant, number, or anything
|
||||
else fails safe to legacy (see `parseRolloutAssignment` + tests). Don't
|
||||
convert it to multivariate.
|
||||
- The flag is evaluated against the same PostHog distinct id the extension's
|
||||
telemetry uses (machine id, mirroring `src/services/logging/distinctId.ts`),
|
||||
so cohort membership is correlatable with telemetry. Flag evaluation is
|
||||
always on (matching `FeatureFlagsService`); the loader's own
|
||||
`extension.rollout.loader_decision` event respects the user's telemetry
|
||||
opt-out and VS Code's global telemetry switch.
|
||||
- **Manual overrides, in either direction.** The `cline.rollout.bundleOverride`
|
||||
user setting (`"auto" | "next" | "legacy"`, editable straight from
|
||||
settings.json) forces a bundle for anyone — users in a pinch, or us
|
||||
debugging — beating the remote assignment both ways. Applies on window
|
||||
reload. `CLINE_BUNDLE_OVERRIDE=next|legacy` (env var) does the same for
|
||||
local dev and e2e and beats even the setting. Both are reported as
|
||||
`override` on the loader event so overridden machines don't pollute
|
||||
cohort comparisons.
|
||||
- **Crash pinning is local, not remote.** If the next bundle throws during
|
||||
activation, the loader falls back to legacy in the same window and pins
|
||||
that VSIX version on this machine (`cline.rollout.nextActivationFailedVersion`);
|
||||
a new release gets to try next again. This safety net is independent of the
|
||||
flag.
|
||||
|
||||
## The union manifest
|
||||
|
||||
`package.json` contributions are static — VS Code reads them before any code
|
||||
runs — so the shipped manifest must serve both cohorts. `scripts/gen-manifest.mjs`
|
||||
regenerates it at stitch time from both branches' real manifests:
|
||||
|
||||
- Contributions declared by both bundles pass through untouched.
|
||||
- Menu entries / keybindings declared by only one get `when` AND-ed with
|
||||
`cline.sdkBundle` / `!cline.sdkBundle`, so a cohort never sees a button its
|
||||
bundle didn't register (and shared buttons that moved position don't render
|
||||
twice).
|
||||
- Commands exclusive to one bundle are hidden from the other cohort's command
|
||||
palette.
|
||||
- `views` / `viewsContainers` / `configuration` / `walkthroughs`
|
||||
**must be identical** in both manifests — they can't be safely gated at
|
||||
runtime, so divergence fails the build. Keep these static contributions in
|
||||
sync between the branches. `engines` may diverge: the union takes the newer
|
||||
requirement (which necessarily satisfies the older one).
|
||||
|
||||
Because the manifest is regenerated from both branches on every build,
|
||||
contribution drift between the branches can't ship silently — it either merges
|
||||
cleanly or the stitch fails.
|
||||
|
||||
## Building locally
|
||||
|
||||
```bash
|
||||
# 1. build both bundles (their own toolchains)
|
||||
cd apps/vscode && bun run package # next
|
||||
cd <legacy worktree>/apps/vscode && npm run package # legacy (npm ci first)
|
||||
|
||||
# 2. build the loader + stitch + package
|
||||
cd apps/vscode-rollout
|
||||
bun run build # dev build; CI uses build:production with the PostHog key
|
||||
node scripts/stitch.mjs \
|
||||
--next ../vscode --legacy <legacy worktree>/apps/vscode \
|
||||
--loader dist/extension.js --version 4.1.0 --out /tmp/cline-ab-staging
|
||||
node scripts/smoke-loader.mjs /tmp/cline-ab-staging # loader behavior smoke
|
||||
cd /tmp/cline-ab-staging && vsce package --no-dependencies --allow-package-secrets sendgrid
|
||||
```
|
||||
|
||||
The narrowly scoped `sendgrid` scanner exemption mirrors the existing next and
|
||||
legacy packaging workflows. This workflow supplies only the existing PostHog
|
||||
project-key inputs; it does not declare a SendGrid credential. Identify the
|
||||
exact matching string in production staging output before changing or
|
||||
broadening the exemption.
|
||||
|
||||
Local builds have no `TELEMETRY_SERVICE_API_KEY`, so the loader skips PostHog
|
||||
entirely and everyone stays on legacy unless `CLINE_BUNDLE_OVERRIDE` is set.
|
||||
|
||||
CI: the `ext-vscode-ab-package` workflow (manual dispatch) builds both refs,
|
||||
stitches, smoke-tests, uploads the `.vsix` artifact, and optionally publishes.
|
||||
|
||||
## Nightly channel
|
||||
|
||||
The daily `ext-vscode-publish-nightly` workflow (cron + manual dispatch)
|
||||
publishes this same combined package as **`saoudrizwan.cline-nightly`**. Before
|
||||
each bundle builds, `scripts/nightlify.mjs` rewrites its manifest to the
|
||||
nightly identity — the same mutation the standalone nightly always applied
|
||||
(`apps/vscode/scripts/publish-nightly.mjs` on both branches is the source of
|
||||
truth), so nightly can be installed alongside stable:
|
||||
|
||||
| | stable | nightly |
|
||||
|---|---|---|
|
||||
| manifest `name` | `claude-dev` | `cline-nightly` |
|
||||
| contribution IDs / context key / settings | `cline.*` | `cline-nightly.*` |
|
||||
| version | operator-supplied (4.1.0+) | `<major>.<minor>.<unix-seconds>` |
|
||||
|
||||
The loader derives the namespace from its own `packageJSON.name` at runtime
|
||||
(`idPrefix` in `src/cohort.ts`), and gen-manifest derives it from the next
|
||||
manifest's name — no build flags involved. Nightly builds also show a
|
||||
status-bar indicator (`Cline: Next` / `Cline: Legacy`); stable builds never do.
|
||||
|
||||
Dispatching the nightly workflow from `main` with `dry-run` builds and uploads
|
||||
the installable `.vsix` without publishing or tagging. The publish job is
|
||||
intentionally restricted to `main` by both the workflow and the
|
||||
`PublishNightly` environment's deployment-branch policy.
|
||||
|
||||
### Telemetry events
|
||||
|
||||
- **`extension.rollout.bundle_activated`** (authoritative, captured by the
|
||||
activated bundle's own telemetry via its `reportRolloutActivation` export;
|
||||
requires the bundle to be built with `CLINE_ROLLOUT_VARIANT`): attempted vs
|
||||
actual bundle, fallback flag, error details on fallback. Every other event
|
||||
from a rollout build carries `extension_variant` as a common property.
|
||||
- **`extension.rollout.loader_decision`** (loader-owned, direct capture): the
|
||||
loader-side metadata the bundle event can't know — override source, launch
|
||||
cadence, loader version, `extension_name` (nightly vs stable) — and the only
|
||||
signal when BOTH bundles fail (`double_failure: true`).
|
||||
|
||||
## Rollout runbook
|
||||
|
||||
Until the stable combined VSIX ships, the flag governs **nightly installs
|
||||
only** — dialing it is safe for production users and is the lever for moving
|
||||
nightly dogfooders onto next.
|
||||
|
||||
1. Create `ext-sdk-bundle-rollout` in PostHog **before** the first publish: a
|
||||
plain boolean release flag with a percentage rollout, starting at **0%**.
|
||||
(There is deliberately no kill-switch flag — the assignment is two-way, so
|
||||
0% *is* the kill switch.)
|
||||
2. Publish the combined VSIX (version above every previously published one).
|
||||
With the rollout at 0% this release is behaviorally identical to legacy for
|
||||
everyone — it only validates the loader plumbing in the wild. Watch
|
||||
`extension.rollout.bundle_activated` and `extension.rollout.loader_decision`.
|
||||
3. Dial `ext-sdk-bundle-rollout` up: 1% → 5% → 25% → 100%. Assignments apply on
|
||||
each machine's next window reload after its flag refresh, so propagation
|
||||
speed is bounded by how often people reload windows — watch the
|
||||
`ms_since_last_activation` distribution on loader events to see real
|
||||
uptake lag before deciding the next step, and compare cohorts by the
|
||||
`bundle` property.
|
||||
4. Emergencies: dial the percentage **down** (0% pulls everyone back to legacy
|
||||
on their next reload). Demoted machines keep settings and creds; tasks
|
||||
created on the SDK bundle reappear when re-promoted. Ship the fix as a
|
||||
higher version, then dial back up. Machines whose next bundle *crashed*
|
||||
are additionally version-pinned to legacy locally, independent of the flag.
|
||||
5. When next reaches 100% and soaks, retire the loader: publish a plain SDK
|
||||
extension build and delete this package.
|
||||
|
||||
## Version numbering
|
||||
|
||||
The combined VSIX owns the marketplace version line and must always exceed the
|
||||
last version published from either branch (legacy stable was 4.0.x → start at
|
||||
4.1.0). The bundles' own `package.json` versions ride along inside their
|
||||
subdirectories for provenance; the loader reports the combined version as
|
||||
`loader_version`.
|
||||
@@ -0,0 +1,27 @@
|
||||
import esbuild from "esbuild";
|
||||
|
||||
const production = process.argv.includes("--production");
|
||||
|
||||
// Same build-time secret injection scheme as apps/vscode/esbuild.mjs: CI
|
||||
// provides TELEMETRY_SERVICE_API_KEY; local builds leave it undefined and the
|
||||
// loader skips all PostHog calls (everyone stays on legacy).
|
||||
const define = {};
|
||||
if (process.env.TELEMETRY_SERVICE_API_KEY) {
|
||||
define["process.env.TELEMETRY_SERVICE_API_KEY"] = JSON.stringify(
|
||||
process.env.TELEMETRY_SERVICE_API_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: ["src/extension.ts"],
|
||||
bundle: true,
|
||||
outfile: "dist/extension.js",
|
||||
platform: "node",
|
||||
format: "cjs",
|
||||
target: "node18",
|
||||
external: ["vscode"],
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
define,
|
||||
logLevel: "info",
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@cline/vscode-rollout",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Loader + packaging tooling for the staged (A/B) rollout of the SDK-based VS Code extension alongside the legacy extension",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node esbuild.mjs",
|
||||
"build:production": "node esbuild.mjs --production",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "bun test src scripts",
|
||||
"stitch": "node scripts/stitch.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.x",
|
||||
"@types/vscode": "1.84.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* Generate the combined VSIX's package.json as the UNION of the two bundles'
|
||||
* manifests, regenerated from both branches' actual package.json files at
|
||||
* stitch time so contribution drift between branches can't ship silently.
|
||||
*
|
||||
* Rules:
|
||||
* - Identity/top-level fields come from the next (main) manifest.
|
||||
* - `main` points at the loader; `version` comes from the release input.
|
||||
* - commands / menus / keybindings / activationEvents / icons are unioned.
|
||||
* Menu entries and keybindings present in only ONE manifest get their
|
||||
* `when` clause AND-ed with the `<prefix>.sdkBundle` context key (set by the
|
||||
* loader before activation; prefix follows the manifest identity — see
|
||||
* src/cohort.ts idPrefix), so a cohort never sees a button whose handler
|
||||
* its bundle doesn't register — and shared buttons that moved position
|
||||
* don't show up twice. Commands exclusive to one bundle are likewise hidden
|
||||
* from the other cohort's command palette.
|
||||
* - views / viewsContainers / configuration / engines MUST be
|
||||
* identical in both manifests — they can't be safely gated at runtime, so
|
||||
* divergence is a hard error.
|
||||
*
|
||||
* Usage: node gen-manifest.mjs --next <pkg.json> --legacy <pkg.json> --version <x.y.z> [--out <file>]
|
||||
*/
|
||||
|
||||
import { deepStrictEqual } from "node:assert";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
export function generateManifest(nextPkg, legacyPkg, version) {
|
||||
for (const field of ["name", "publisher", "main"]) {
|
||||
if (nextPkg[field] !== legacyPkg[field]) {
|
||||
throw new Error(
|
||||
`manifest field "${field}" differs: ${nextPkg[field]} vs ${legacyPkg[field]}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const field of ["views", "viewsContainers", "configuration"]) {
|
||||
try {
|
||||
deepStrictEqual(
|
||||
nextPkg.contributes?.[field],
|
||||
legacyPkg.contributes?.[field],
|
||||
);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`contributes.${field} diverged between bundles — it cannot be gated at runtime; reconcile the branches`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const engines = unionEngines(nextPkg.engines, legacyPkg.engines);
|
||||
|
||||
assertWalkthroughsCompatible(
|
||||
nextPkg.contributes?.walkthroughs,
|
||||
legacyPkg.contributes?.walkthroughs,
|
||||
);
|
||||
|
||||
// The nightly packaging rewrites the whole `cline.*` ID namespace to
|
||||
// `cline-nightly.*` (scripts/nightlify.mjs), so the context key and the
|
||||
// injected setting must follow the manifest's identity. Keep in sync with
|
||||
// idPrefix/bundleContextKey/settingSection in src/cohort.ts.
|
||||
const prefix = nextPkg.name === "cline-nightly" ? "cline-nightly" : "cline";
|
||||
const nextGate = `${prefix}.sdkBundle`;
|
||||
const legacyGate = `!${nextGate}`;
|
||||
|
||||
const nc = nextPkg.contributes ?? {};
|
||||
const lc = legacyPkg.contributes ?? {};
|
||||
const menus = unionMenus(nc.menus, lc.menus, nextGate, legacyGate);
|
||||
hideExclusiveCommandsFromPalette(
|
||||
menus,
|
||||
nc.commands,
|
||||
lc.commands,
|
||||
nextGate,
|
||||
legacyGate,
|
||||
);
|
||||
|
||||
const manifest = {
|
||||
name: nextPkg.name,
|
||||
displayName: nextPkg.displayName,
|
||||
description: nextPkg.description,
|
||||
version,
|
||||
icon: nextPkg.icon,
|
||||
engines,
|
||||
author: nextPkg.author,
|
||||
license: nextPkg.license,
|
||||
publisher: nextPkg.publisher,
|
||||
repository: nextPkg.repository,
|
||||
homepage: nextPkg.homepage,
|
||||
categories: nextPkg.categories,
|
||||
keywords: nextPkg.keywords,
|
||||
activationEvents: unionPrimitive(
|
||||
nextPkg.activationEvents,
|
||||
legacyPkg.activationEvents,
|
||||
),
|
||||
main: "./extension.js",
|
||||
contributes: {
|
||||
viewsContainers: nc.viewsContainers,
|
||||
views: nc.views,
|
||||
commands: unionBy(
|
||||
[...(nc.commands ?? []), ...(lc.commands ?? [])],
|
||||
(c) => c.command,
|
||||
),
|
||||
keybindings: unionGated(
|
||||
nc.keybindings,
|
||||
lc.keybindings,
|
||||
nextGate,
|
||||
legacyGate,
|
||||
),
|
||||
menus,
|
||||
icons: unionIcons(nc.icons, lc.icons),
|
||||
configuration: injectLoaderConfiguration(nc.configuration, prefix),
|
||||
walkthroughs: nc.walkthroughs,
|
||||
},
|
||||
scripts: {},
|
||||
};
|
||||
|
||||
assertSuperset(manifest, nextPkg, "next", nextGate);
|
||||
assertSuperset(manifest, legacyPkg, "legacy", legacyGate);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* The loader's own user-visible escape hatch, keyed by the manifest identity.
|
||||
* Neither bundle knows about it; only the loader reads it (src/cohort.ts
|
||||
* settingSection/SETTING_BUNDLE_OVERRIDE — keep the key and values in sync).
|
||||
* Injected after the configuration-equality invariant so it can't mask real
|
||||
* drift between the bundles.
|
||||
*/
|
||||
function loaderSettings(prefix) {
|
||||
return {
|
||||
[`${prefix}.rollout.bundleOverride`]: {
|
||||
type: "string",
|
||||
enum: ["auto", "next", "legacy"],
|
||||
enumDescriptions: [
|
||||
"Follow the remote rollout assignment.",
|
||||
"Force the new (SDK-based) extension.",
|
||||
"Force the previous (legacy) extension.",
|
||||
],
|
||||
default: "auto",
|
||||
scope: "application",
|
||||
markdownDescription:
|
||||
"Manual override for Cline's staged extension rollout. `next` forces the new (SDK-based) extension, `legacy` forces the previous one, `auto` follows the remote rollout assignment. Takes effect on window reload.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function injectLoaderConfiguration(configuration, prefix) {
|
||||
const properties = { ...(configuration?.properties ?? {}) };
|
||||
for (const [key, schema] of Object.entries(loaderSettings(prefix))) {
|
||||
if (properties[key]) {
|
||||
throw new Error(
|
||||
`bundle manifests must not declare loader-owned setting ${key}`,
|
||||
);
|
||||
}
|
||||
properties[key] = schema;
|
||||
}
|
||||
return { title: "Cline", ...(configuration ?? {}), properties };
|
||||
}
|
||||
|
||||
/**
|
||||
* Walkthroughs can't be gated per cohort, and their markdown at the VSIX root
|
||||
* always comes from the next checkout — so requiring byte-identical manifests
|
||||
* here would brick releases over copy tweaks while protecting nothing. Only
|
||||
* STRUCTURE must match (walkthrough/step ids, media paths, completion events —
|
||||
* the parts code and the manifest reference); when titles/descriptions
|
||||
* diverge, next's copy ships for everyone and the build says so.
|
||||
*/
|
||||
function assertWalkthroughsCompatible(next = [], legacy = []) {
|
||||
const structure = (walkthroughs) =>
|
||||
walkthroughs.map((walkthrough) => ({
|
||||
id: walkthrough.id,
|
||||
steps: (walkthrough.steps ?? []).map((step) => ({
|
||||
id: step.id,
|
||||
media: step.media,
|
||||
completionEvents: step.completionEvents,
|
||||
when: step.when,
|
||||
})),
|
||||
}));
|
||||
try {
|
||||
deepStrictEqual(structure(next), structure(legacy));
|
||||
} catch {
|
||||
throw new Error(
|
||||
"contributes.walkthroughs diverged structurally (ids/media/completionEvents) — reconcile the branches",
|
||||
);
|
||||
}
|
||||
try {
|
||||
deepStrictEqual(next, legacy);
|
||||
} catch {
|
||||
console.warn(
|
||||
"warning: walkthrough titles/descriptions differ between bundles; shipping next's copy for both cohorts",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Engines can safely diverge in ONE direction: the union requires whichever
|
||||
* bundle needs the NEWER host, which necessarily satisfies the other bundle's
|
||||
* older requirement too. (main routinely bumps the VS Code engine ahead of the
|
||||
* legacy branch — an equality assertion here would brick every combined build
|
||||
* over that.) Non-caret/complex ranges we can't compare fail hard rather than
|
||||
* guessing.
|
||||
*/
|
||||
function unionEngines(nextEngines = {}, legacyEngines = {}) {
|
||||
const union = {};
|
||||
for (const key of new Set([
|
||||
...Object.keys(nextEngines),
|
||||
...Object.keys(legacyEngines),
|
||||
])) {
|
||||
const a = nextEngines[key];
|
||||
const b = legacyEngines[key];
|
||||
if (a === undefined || b === undefined || a === b) {
|
||||
union[key] = a ?? b;
|
||||
continue;
|
||||
}
|
||||
const minimum = (range) => {
|
||||
const match = /^\^(\d+(?:\.\d+)*)$/.exec(range);
|
||||
return match?.[1];
|
||||
};
|
||||
const [minA, minB] = [minimum(a), minimum(b)];
|
||||
if (!minA || !minB) {
|
||||
throw new Error(
|
||||
`engines.${key} diverged with uncomparable ranges: ${a} vs ${b}`,
|
||||
);
|
||||
}
|
||||
union[key] = compareDotted(minA, minB) >= 0 ? a : b;
|
||||
console.warn(
|
||||
`warning: engines.${key} differs between bundles (next ${a}, legacy ${b}); union requires ${union[key]}`,
|
||||
);
|
||||
}
|
||||
return union;
|
||||
}
|
||||
|
||||
/** Compare dotted numeric versions. */
|
||||
function compareDotted(a, b) {
|
||||
const pa = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
||||
const pb = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function unionPrimitive(a = [], b = []) {
|
||||
return [...new Set([...a, ...b])];
|
||||
}
|
||||
|
||||
/** Union keeping first occurrence per key (next wins on shared ids). */
|
||||
function unionBy(items, keyFn) {
|
||||
const seen = new Map();
|
||||
for (const item of items) {
|
||||
const key = keyFn(item);
|
||||
if (!seen.has(key)) {
|
||||
seen.set(key, item);
|
||||
}
|
||||
}
|
||||
return [...seen.values()];
|
||||
}
|
||||
|
||||
function sortKeysDeep(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(sortKeysDeep);
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.keys(value)
|
||||
.sort()
|
||||
.map((key) => [key, sortKeysDeep(value[key])]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function stableJson(value) {
|
||||
return JSON.stringify(sortKeysDeep(value));
|
||||
}
|
||||
|
||||
function gateWhen(entry, gate) {
|
||||
return { ...entry, when: entry.when ? `(${entry.when}) && ${gate}` : gate };
|
||||
}
|
||||
|
||||
/**
|
||||
* Union two entry lists (menu entries or keybindings): entries declared by
|
||||
* both bundles pass through untouched; entries declared by only one get their
|
||||
* `when` AND-ed with that bundle's cohort gate.
|
||||
*/
|
||||
function unionGated(
|
||||
nextEntries = [],
|
||||
legacyEntries = [],
|
||||
nextGate,
|
||||
legacyGate,
|
||||
) {
|
||||
const nextSet = new Set(nextEntries.map(stableJson));
|
||||
const legacySet = new Set(legacyEntries.map(stableJson));
|
||||
const entries = [];
|
||||
for (const entry of nextEntries) {
|
||||
entries.push(
|
||||
legacySet.has(stableJson(entry)) ? entry : gateWhen(entry, nextGate),
|
||||
);
|
||||
}
|
||||
for (const entry of legacyEntries) {
|
||||
if (!nextSet.has(stableJson(entry))) {
|
||||
entries.push(gateWhen(entry, legacyGate));
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function unionMenus(a = {}, b = {}, nextGate, legacyGate) {
|
||||
const menus = {};
|
||||
for (const location of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
||||
menus[location] = unionGated(
|
||||
a[location],
|
||||
b[location],
|
||||
nextGate,
|
||||
legacyGate,
|
||||
);
|
||||
}
|
||||
return menus;
|
||||
}
|
||||
|
||||
/**
|
||||
* A command declared by only one bundle would surface in the other cohort's
|
||||
* command palette with no registered handler ("command not found" on run).
|
||||
* Hide it there unless that bundle's own manifest already constrains it.
|
||||
*/
|
||||
function hideExclusiveCommandsFromPalette(
|
||||
menus,
|
||||
nextCommands = [],
|
||||
legacyCommands = [],
|
||||
nextGate,
|
||||
legacyGate,
|
||||
) {
|
||||
const nextIds = new Set(nextCommands.map((c) => c.command));
|
||||
const legacyIds = new Set(legacyCommands.map((c) => c.command));
|
||||
const palette = menus.commandPalette ?? (menus.commandPalette = []);
|
||||
const alreadyListed = new Set(palette.map((e) => e.command));
|
||||
for (const id of nextIds) {
|
||||
if (!legacyIds.has(id) && !alreadyListed.has(id)) {
|
||||
palette.push({ command: id, when: nextGate });
|
||||
}
|
||||
}
|
||||
for (const id of legacyIds) {
|
||||
if (!nextIds.has(id) && !alreadyListed.has(id)) {
|
||||
palette.push({ command: id, when: legacyGate });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unionIcons(a = {}, b = {}) {
|
||||
const icons = { ...b, ...a };
|
||||
for (const id of Object.keys(icons)) {
|
||||
if (a[id] && b[id] && JSON.stringify(a[id]) !== JSON.stringify(b[id])) {
|
||||
throw new Error(
|
||||
`contributes.icons["${id}"] diverged between bundles — icon fonts resolve from the VSIX root`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every command/keybinding/menu entry/activation event a bundle declares must
|
||||
* survive the union, either verbatim or with its `when` AND-ed with that
|
||||
* bundle's cohort gate.
|
||||
*/
|
||||
function assertSuperset(manifest, sourcePkg, label, gate) {
|
||||
const missing = [];
|
||||
const commandIds = new Set(
|
||||
manifest.contributes.commands.map((c) => c.command),
|
||||
);
|
||||
for (const cmd of sourcePkg.contributes?.commands ?? []) {
|
||||
if (!commandIds.has(cmd.command)) {
|
||||
missing.push(`command ${cmd.command}`);
|
||||
}
|
||||
}
|
||||
for (const event of sourcePkg.activationEvents ?? []) {
|
||||
if (!manifest.activationEvents.includes(event)) {
|
||||
missing.push(`activationEvent ${event}`);
|
||||
}
|
||||
}
|
||||
const presentOrGated = (unionEntries, entry) => {
|
||||
const set = new Set((unionEntries ?? []).map(stableJson));
|
||||
return (
|
||||
set.has(stableJson(entry)) || set.has(stableJson(gateWhen(entry, gate)))
|
||||
);
|
||||
};
|
||||
for (const kb of sourcePkg.contributes?.keybindings ?? []) {
|
||||
if (!presentOrGated(manifest.contributes.keybindings, kb)) {
|
||||
missing.push(`keybinding ${kb.command}`);
|
||||
}
|
||||
}
|
||||
for (const [location, entries] of Object.entries(
|
||||
sourcePkg.contributes?.menus ?? {},
|
||||
)) {
|
||||
for (const entry of entries) {
|
||||
if (!presentOrGated(manifest.contributes.menus[location], entry)) {
|
||||
missing.push(
|
||||
`menu ${location}: ${entry.command ?? JSON.stringify(entry)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`union manifest is missing ${label} contributions:\n ${missing.join("\n ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let i = 2; i < argv.length; i += 2) {
|
||||
args[argv[i].replace(/^--/, "")] = argv[i + 1];
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const { next, legacy, version, out } = parseArgs(process.argv);
|
||||
if (!next || !legacy || !version) {
|
||||
console.error(
|
||||
"usage: gen-manifest.mjs --next <pkg.json> --legacy <pkg.json> --version <x.y.z> [--out <file>]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const manifest = generateManifest(
|
||||
JSON.parse(readFileSync(next, "utf8")),
|
||||
JSON.parse(readFileSync(legacy, "utf8")),
|
||||
version,
|
||||
);
|
||||
const json = `${JSON.stringify(manifest, null, "\t")}\n`;
|
||||
if (out) {
|
||||
writeFileSync(out, json);
|
||||
console.log(`wrote ${out}`);
|
||||
} else {
|
||||
process.stdout.write(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { generateManifest } from "./gen-manifest.mjs";
|
||||
|
||||
const shared = {
|
||||
name: "claude-dev",
|
||||
publisher: "saoudrizwan",
|
||||
main: "./dist/extension.js",
|
||||
engines: { vscode: "^1.84.0" },
|
||||
displayName: "Cline",
|
||||
};
|
||||
|
||||
function pkg(overrides) {
|
||||
return {
|
||||
...shared,
|
||||
activationEvents: ["onStartupFinished"],
|
||||
contributes: {
|
||||
viewsContainers: {
|
||||
activitybar: [{ id: "c", title: "Cline", icon: "assets/icon.svg" }],
|
||||
},
|
||||
views: { c: [{ type: "webview", id: "claude-dev.SidebarProvider" }] },
|
||||
commands: [],
|
||||
keybindings: [],
|
||||
menus: {},
|
||||
icons: {},
|
||||
...overrides.contributes,
|
||||
},
|
||||
...Object.fromEntries(
|
||||
Object.entries(overrides).filter(([k]) => k !== "contributes"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
describe("generateManifest", () => {
|
||||
it("unions commands, menus, keybindings and activation events", () => {
|
||||
const next = pkg({
|
||||
contributes: {
|
||||
commands: [
|
||||
{ command: "cline.a", title: "A" },
|
||||
{ command: "cline.shared", title: "S" },
|
||||
],
|
||||
menus: { "view/title": [{ command: "cline.a", when: "x" }] },
|
||||
},
|
||||
});
|
||||
const legacy = pkg({
|
||||
activationEvents: ["onStartupFinished", "workspaceContains:evals.env"],
|
||||
contributes: {
|
||||
commands: [
|
||||
{ command: "cline.b", title: "B" },
|
||||
{ command: "cline.shared", title: "S" },
|
||||
],
|
||||
menus: {
|
||||
"view/title": [{ command: "cline.b", when: "y" }],
|
||||
"comments/commentThread/title": [{ command: "cline.b" }],
|
||||
},
|
||||
keybindings: [{ command: "cline.b", key: "ctrl+k" }],
|
||||
},
|
||||
});
|
||||
const manifest = generateManifest(next, legacy, "4.1.0");
|
||||
expect(manifest.version).toBe("4.1.0");
|
||||
expect(manifest.main).toBe("./extension.js");
|
||||
expect(manifest.contributes.commands.map((c) => c.command).sort()).toEqual([
|
||||
"cline.a",
|
||||
"cline.b",
|
||||
"cline.shared",
|
||||
]);
|
||||
expect(manifest.contributes.menus["view/title"]).toHaveLength(2);
|
||||
expect(
|
||||
manifest.contributes.menus["comments/commentThread/title"],
|
||||
).toHaveLength(1);
|
||||
expect(manifest.contributes.keybindings).toHaveLength(1);
|
||||
expect(manifest.activationEvents).toContain("workspaceContains:evals.env");
|
||||
});
|
||||
|
||||
it("gates cohort-exclusive menu entries and keybindings on the context key", () => {
|
||||
const next = pkg({
|
||||
contributes: {
|
||||
commands: [
|
||||
{ command: "cline.a", title: "A" },
|
||||
{ command: "cline.shared", title: "S" },
|
||||
],
|
||||
menus: {
|
||||
"view/title": [
|
||||
{ command: "cline.a", when: "x" },
|
||||
{ command: "cline.shared", when: "v" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const legacy = pkg({
|
||||
contributes: {
|
||||
commands: [
|
||||
{ command: "cline.b", title: "B" },
|
||||
{ command: "cline.shared", title: "S" },
|
||||
],
|
||||
menus: {
|
||||
"view/title": [
|
||||
{ command: "cline.b", when: "y" },
|
||||
{ command: "cline.shared", when: "v" },
|
||||
],
|
||||
},
|
||||
keybindings: [{ command: "cline.b", key: "ctrl+k", when: "focus" }],
|
||||
},
|
||||
});
|
||||
const manifest = generateManifest(next, legacy, "4.1.0");
|
||||
const viewTitle = manifest.contributes.menus["view/title"];
|
||||
expect(viewTitle.find((e) => e.command === "cline.a").when).toBe(
|
||||
"(x) && cline.sdkBundle",
|
||||
);
|
||||
expect(viewTitle.find((e) => e.command === "cline.b").when).toBe(
|
||||
"(y) && !cline.sdkBundle",
|
||||
);
|
||||
expect(viewTitle.find((e) => e.command === "cline.shared").when).toBe("v");
|
||||
expect(manifest.contributes.keybindings[0].when).toBe(
|
||||
"(focus) && !cline.sdkBundle",
|
||||
);
|
||||
});
|
||||
|
||||
it("hides cohort-exclusive commands from the other cohort's palette", () => {
|
||||
const next = pkg({
|
||||
contributes: {
|
||||
commands: [
|
||||
{ command: "cline.nextOnly", title: "N" },
|
||||
{ command: "cline.shared", title: "S" },
|
||||
],
|
||||
},
|
||||
});
|
||||
const legacy = pkg({
|
||||
contributes: {
|
||||
commands: [
|
||||
{ command: "cline.legacyOnly", title: "L" },
|
||||
{ command: "cline.shared", title: "S" },
|
||||
],
|
||||
},
|
||||
});
|
||||
const palette = generateManifest(next, legacy, "4.1.0").contributes.menus
|
||||
.commandPalette;
|
||||
expect(palette).toContainEqual({
|
||||
command: "cline.nextOnly",
|
||||
when: "cline.sdkBundle",
|
||||
});
|
||||
expect(palette).toContainEqual({
|
||||
command: "cline.legacyOnly",
|
||||
when: "!cline.sdkBundle",
|
||||
});
|
||||
expect(palette.find((e) => e.command === "cline.shared")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("leaves commands alone when a bundle already declares a palette entry for them", () => {
|
||||
const next = pkg({
|
||||
contributes: { commands: [{ command: "cline.shared", title: "S" }] },
|
||||
});
|
||||
const legacy = pkg({
|
||||
contributes: {
|
||||
commands: [
|
||||
{ command: "cline.hidden", title: "H" },
|
||||
{ command: "cline.shared", title: "S" },
|
||||
],
|
||||
menus: { commandPalette: [{ command: "cline.hidden", when: "false" }] },
|
||||
},
|
||||
});
|
||||
const palette = generateManifest(next, legacy, "4.1.0").contributes.menus
|
||||
.commandPalette;
|
||||
expect(palette.filter((e) => e.command === "cline.hidden")).toEqual([
|
||||
{ command: "cline.hidden", when: "(false) && !cline.sdkBundle" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("dedupes structurally identical menu entries", () => {
|
||||
const entry = { command: "cline.a", when: "view == cline" };
|
||||
const next = pkg({
|
||||
contributes: {
|
||||
commands: [{ command: "cline.a", title: "A" }],
|
||||
menus: { "view/title": [entry] },
|
||||
},
|
||||
});
|
||||
const legacy = pkg({
|
||||
contributes: {
|
||||
commands: [{ command: "cline.a", title: "A" }],
|
||||
menus: { "view/title": [{ ...entry }] },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
generateManifest(next, legacy, "1.0.0").contributes.menus["view/title"],
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rejects diverged views/viewsContainers", () => {
|
||||
const next = pkg({});
|
||||
const legacy = pkg({
|
||||
contributes: { views: { c: [{ type: "webview", id: "other" }] } },
|
||||
});
|
||||
expect(() => generateManifest(next, legacy, "1.0.0")).toThrow(/views/);
|
||||
});
|
||||
|
||||
it("rejects structurally diverged walkthroughs", () => {
|
||||
const walkthrough = (stepId, media) => ({
|
||||
contributes: {
|
||||
walkthroughs: [
|
||||
{
|
||||
id: "ClineWalkthrough",
|
||||
title: "Meet Cline",
|
||||
steps: [{ id: stepId, title: "Start here", media }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(() =>
|
||||
generateManifest(
|
||||
pkg(walkthrough("welcome", { markdown: "walkthrough/step1.md" })),
|
||||
pkg(walkthrough("hello", { markdown: "walkthrough/step1.md" })),
|
||||
"1.0.0",
|
||||
),
|
||||
).toThrow(/walkthroughs diverged structurally/);
|
||||
expect(() =>
|
||||
generateManifest(
|
||||
pkg(walkthrough("welcome", { markdown: "walkthrough/step1.md" })),
|
||||
pkg(walkthrough("welcome", { markdown: "walkthrough/other.md" })),
|
||||
"1.0.0",
|
||||
),
|
||||
).toThrow(/walkthroughs diverged structurally/);
|
||||
});
|
||||
|
||||
it("tolerates copy-only walkthrough divergence, shipping next's text", () => {
|
||||
const walkthrough = (description) => ({
|
||||
contributes: {
|
||||
walkthroughs: [
|
||||
{
|
||||
id: "ClineWalkthrough",
|
||||
title: "Meet Cline",
|
||||
steps: [
|
||||
{
|
||||
id: "welcome",
|
||||
title: "Start here",
|
||||
description,
|
||||
media: { markdown: "walkthrough/step1.md" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const manifest = generateManifest(
|
||||
pkg(walkthrough("Connect via MCP.")),
|
||||
pkg(walkthrough("Discover the MCP Marketplace.")),
|
||||
"1.0.0",
|
||||
);
|
||||
expect(manifest.contributes.walkthroughs[0].steps[0].description).toBe(
|
||||
"Connect via MCP.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects diverged configuration", () => {
|
||||
const next = pkg({
|
||||
contributes: {
|
||||
configuration: {
|
||||
title: "Cline",
|
||||
properties: {
|
||||
"cline.enabled": { type: "boolean", default: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const legacy = pkg({
|
||||
contributes: {
|
||||
configuration: {
|
||||
title: "Cline",
|
||||
properties: {
|
||||
"cline.enabled": { type: "boolean", default: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(() => generateManifest(next, legacy, "1.0.0")).toThrow(
|
||||
/contributes\.configuration diverged/,
|
||||
);
|
||||
});
|
||||
|
||||
it("injects the loader-owned bundleOverride setting into the union", () => {
|
||||
const manifest = generateManifest(pkg({}), pkg({}), "4.1.0");
|
||||
const prop =
|
||||
manifest.contributes.configuration.properties[
|
||||
"cline.rollout.bundleOverride"
|
||||
];
|
||||
expect(prop).toBeDefined();
|
||||
expect(prop.enum).toEqual(["auto", "next", "legacy"]);
|
||||
expect(prop.default).toBe("auto");
|
||||
expect(prop.scope).toBe("application");
|
||||
});
|
||||
|
||||
it("rejects bundles that declare the loader-owned setting themselves", () => {
|
||||
const withClash = {
|
||||
contributes: {
|
||||
configuration: {
|
||||
title: "Cline",
|
||||
properties: { "cline.rollout.bundleOverride": { type: "string" } },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(() =>
|
||||
generateManifest(pkg(withClash), pkg(withClash), "4.1.0"),
|
||||
).toThrow(/loader-owned setting/);
|
||||
});
|
||||
|
||||
it("derives gates and the injected setting from the nightly identity", () => {
|
||||
const nightly = (overrides) => ({
|
||||
...pkg(overrides),
|
||||
name: "cline-nightly",
|
||||
displayName: "Cline (Nightly)",
|
||||
});
|
||||
const next = nightly({
|
||||
contributes: {
|
||||
commands: [{ command: "cline-nightly.nextOnly", title: "N" }],
|
||||
menus: {
|
||||
"view/title": [{ command: "cline-nightly.nextOnly", when: "x" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
const legacy = nightly({
|
||||
contributes: {
|
||||
commands: [{ command: "cline-nightly.legacyOnly", title: "L" }],
|
||||
keybindings: [{ command: "cline-nightly.legacyOnly", key: "ctrl+k" }],
|
||||
},
|
||||
});
|
||||
const manifest = generateManifest(next, legacy, "4.0.1752600000");
|
||||
expect(manifest.name).toBe("cline-nightly");
|
||||
expect(manifest.contributes.menus["view/title"][0].when).toBe(
|
||||
"(x) && cline-nightly.sdkBundle",
|
||||
);
|
||||
expect(manifest.contributes.keybindings[0].when).toBe(
|
||||
"!cline-nightly.sdkBundle",
|
||||
);
|
||||
expect(manifest.contributes.menus.commandPalette).toContainEqual({
|
||||
command: "cline-nightly.legacyOnly",
|
||||
when: "!cline-nightly.sdkBundle",
|
||||
});
|
||||
const properties = manifest.contributes.configuration.properties;
|
||||
expect(properties["cline-nightly.rollout.bundleOverride"]).toBeDefined();
|
||||
expect(properties["cline.rollout.bundleOverride"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("unions diverged engines to the newer requirement (either direction)", () => {
|
||||
const olderLegacy = { ...pkg({}), engines: { vscode: "^1.74.0" } };
|
||||
expect(generateManifest(pkg({}), olderLegacy, "1.0.0").engines).toEqual({
|
||||
vscode: "^1.84.0",
|
||||
});
|
||||
const newerLegacy = { ...pkg({}), engines: { vscode: "^1.101.0" } };
|
||||
expect(generateManifest(pkg({}), newerLegacy, "1.0.0").engines).toEqual({
|
||||
vscode: "^1.101.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects diverged engines it cannot compare", () => {
|
||||
const legacy = { ...pkg({}), engines: { vscode: ">=1.84.0 <2.0.0" } };
|
||||
expect(() => generateManifest(pkg({}), legacy, "1.0.0")).toThrow(
|
||||
/uncomparable/,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects conflicting icon definitions", () => {
|
||||
const next = pkg({
|
||||
contributes: {
|
||||
icons: {
|
||||
"cline-logo": {
|
||||
description: "d",
|
||||
default: { fontPath: "a.woff", fontCharacter: "\\E900" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const legacy = pkg({
|
||||
contributes: {
|
||||
icons: {
|
||||
"cline-logo": {
|
||||
description: "d",
|
||||
default: { fontPath: "b.woff", fontCharacter: "\\E900" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(() => generateManifest(next, legacy, "1.0.0")).toThrow(/icons/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Rewrite an apps/vscode package.json to the nightly identity, in place.
|
||||
*
|
||||
* Reproduces updatePackageJson() from apps/vscode/scripts/publish-nightly.mjs
|
||||
* (the same script exists on BOTH main and legacy-extension — those copies are
|
||||
* the source of truth for the mutation; if they change, change this too):
|
||||
* - textual rewrites: "claude-dev" -> "cline-nightly" everywhere, and every
|
||||
* `"cline.` ID prefix -> `"cline-nightly.` (commands, settings, view IDs,
|
||||
* when-clauses that START with the key — mid-string references like
|
||||
* `config.cline.x` are NOT rewritten, same as the standalone nightly)
|
||||
* - name / displayName / activity bar title / version
|
||||
*
|
||||
* Differences from publish-nightly.mjs, on purpose:
|
||||
* - the version is an explicit ARGUMENT, not computed here: the combined
|
||||
* VSIX applies ONE version to the next bundle, the legacy bundle, and the
|
||||
* union manifest, so gen-manifest's identity-equality assertions hold.
|
||||
* - no backup/restore, README swapping, or workspace-self-link reconciling:
|
||||
* this runs against a disposable CI checkout, BEFORE the bundle build and
|
||||
* never followed by vsce in that checkout (vsce only runs in the stitched
|
||||
* staging dir with --no-dependencies).
|
||||
*
|
||||
* Run it AFTER dependency install (the workspace self-link resolution keys off
|
||||
* the original package name) and BEFORE the bundle's package build.
|
||||
*
|
||||
* Usage: node nightlify.mjs --dir <apps/vscode checkout> --version <x.y.ts>
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export const NIGHTLY_NAME = "cline-nightly";
|
||||
export const NIGHTLY_DISPLAY_NAME = "Cline (Nightly)";
|
||||
|
||||
export function nightlifyPackageJson(rawContent, version) {
|
||||
if (!version) {
|
||||
throw new Error("version is required");
|
||||
}
|
||||
const content = rawContent
|
||||
.replaceAll("claude-dev", NIGHTLY_NAME)
|
||||
.replaceAll('"cline.', `"${NIGHTLY_NAME}.`);
|
||||
const pkg = JSON.parse(content);
|
||||
|
||||
pkg.name = NIGHTLY_NAME;
|
||||
pkg.displayName = NIGHTLY_DISPLAY_NAME;
|
||||
pkg.version = version;
|
||||
// publish-nightly.mjs assigns `.title` on the activitybar value directly,
|
||||
// which is a silent no-op on the real manifest (activitybar is an ARRAY —
|
||||
// JSON.stringify drops non-index properties). Retitle the actual entries.
|
||||
const activitybar = pkg.contributes?.viewsContainers?.activitybar;
|
||||
for (const container of Array.isArray(activitybar) ? activitybar : []) {
|
||||
container.title = NIGHTLY_DISPLAY_NAME;
|
||||
}
|
||||
|
||||
return `${JSON.stringify(pkg, null, "\t")}\n`;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let i = 2; i < argv.length; i += 2) {
|
||||
args[argv[i].replace(/^--/, "")] = argv[i + 1];
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === `file://${path.resolve(process.argv[1])}`
|
||||
) {
|
||||
const { dir, version } = parseArgs(process.argv);
|
||||
if (!dir || !version) {
|
||||
console.error(
|
||||
"usage: node nightlify.mjs --dir <apps/vscode checkout> --version <x.y.ts>",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const packageJsonPath = path.join(dir, "package.json");
|
||||
const before = readFileSync(packageJsonPath, "utf8");
|
||||
const beforeName = JSON.parse(before).name;
|
||||
writeFileSync(packageJsonPath, nightlifyPackageJson(before, version));
|
||||
console.log(
|
||||
`nightlified ${packageJsonPath}: ${beforeName} -> ${NIGHTLY_NAME}@${version}`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { nightlifyPackageJson } from "./nightlify.mjs";
|
||||
|
||||
const fixture = {
|
||||
name: "claude-dev",
|
||||
displayName: "Cline",
|
||||
publisher: "saoudrizwan",
|
||||
version: "4.0.0",
|
||||
main: "./dist/extension.js",
|
||||
contributes: {
|
||||
viewsContainers: {
|
||||
activitybar: [
|
||||
{
|
||||
id: "claude-dev-ActivityBar",
|
||||
title: "Cline",
|
||||
icon: "assets/icon.svg",
|
||||
},
|
||||
],
|
||||
},
|
||||
views: {
|
||||
"claude-dev-ActivityBar": [
|
||||
{ type: "webview", id: "claude-dev.SidebarProvider" },
|
||||
],
|
||||
},
|
||||
commands: [{ command: "cline.plusButtonClicked", title: "New Task" }],
|
||||
keybindings: [{ command: "cline.addToChat", key: "ctrl+'" }],
|
||||
menus: {
|
||||
"view/title": [
|
||||
{
|
||||
command: "cline.plusButtonClicked",
|
||||
when: "view == claude-dev.SidebarProvider",
|
||||
},
|
||||
// Mid-string references are NOT rewritten — a known limitation
|
||||
// shared with the standalone nightly's publish-nightly.mjs.
|
||||
{ command: "cline.addToChat", when: "config.cline.enableExtras" },
|
||||
],
|
||||
},
|
||||
configuration: {
|
||||
title: "Cline",
|
||||
properties: { "cline.enableExtras": { type: "boolean" } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("nightlifyPackageJson", () => {
|
||||
const pkg = JSON.parse(
|
||||
nightlifyPackageJson(JSON.stringify(fixture, null, "\t"), "4.0.1752600000"),
|
||||
);
|
||||
|
||||
it("sets the nightly identity and the supplied version", () => {
|
||||
expect(pkg.name).toBe("cline-nightly");
|
||||
expect(pkg.displayName).toBe("Cline (Nightly)");
|
||||
expect(pkg.version).toBe("4.0.1752600000");
|
||||
expect(pkg.publisher).toBe("saoudrizwan");
|
||||
});
|
||||
|
||||
it("rewrites claude-dev IDs and the cline.* namespace", () => {
|
||||
expect(pkg.contributes.viewsContainers.activitybar[0].id).toBe(
|
||||
"cline-nightly-ActivityBar",
|
||||
);
|
||||
expect(pkg.contributes.viewsContainers.activitybar[0].title).toBe(
|
||||
"Cline (Nightly)",
|
||||
);
|
||||
expect(Object.keys(pkg.contributes.views)).toEqual([
|
||||
"cline-nightly-ActivityBar",
|
||||
]);
|
||||
expect(pkg.contributes.views["cline-nightly-ActivityBar"][0].id).toBe(
|
||||
"cline-nightly.SidebarProvider",
|
||||
);
|
||||
expect(pkg.contributes.commands[0].command).toBe(
|
||||
"cline-nightly.plusButtonClicked",
|
||||
);
|
||||
expect(pkg.contributes.keybindings[0].command).toBe(
|
||||
"cline-nightly.addToChat",
|
||||
);
|
||||
expect(Object.keys(pkg.contributes.configuration.properties)).toEqual([
|
||||
"cline-nightly.enableExtras",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rewrites when-clauses that start with a rewritten ID, but not mid-string references", () => {
|
||||
const [gated, midString] = pkg.contributes.menus["view/title"];
|
||||
expect(gated.when).toBe("view == cline-nightly.SidebarProvider");
|
||||
// Documented limitation: `config.cline.` does not match the `"cline.`
|
||||
// pattern, so it survives unrewritten (matches publish-nightly.mjs).
|
||||
expect(midString.when).toBe("config.cline.enableExtras");
|
||||
});
|
||||
|
||||
it("requires a version", () => {
|
||||
expect(() => nightlifyPackageJson("{}", undefined)).toThrow(/version/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Stamp the combined VSIX's version into a bundle checkout's package.json,
|
||||
* in place, BEFORE that bundle builds.
|
||||
*
|
||||
* Why: the union manifest's version (what the Marketplace and auto-update
|
||||
* see) is supplied at stitch time, but each bundle's runtime reads its OWN
|
||||
* package.json — the About tab and every telemetry event's extension_version
|
||||
* come from there. Without this stamp the stable combined VSIX would report
|
||||
* three different versions (union input, main's base version, legacy's base
|
||||
* version) depending on where you look, which turns user bug reports into
|
||||
* archaeology. The nightly path gets the same alignment via nightlify.mjs
|
||||
* (which also rewrites identity); this script is the identity-preserving
|
||||
* version-only equivalent for the stable channel.
|
||||
*
|
||||
* Usage: node set-version.mjs --dir <apps/vscode checkout> --version <x.y.z>
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export function setPackageVersion(rawContent, version) {
|
||||
if (!version) {
|
||||
throw new Error("version is required");
|
||||
}
|
||||
const pkg = JSON.parse(rawContent);
|
||||
pkg.version = version;
|
||||
return `${JSON.stringify(pkg, null, "\t")}\n`;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let i = 2; i < argv.length; i += 2) {
|
||||
args[argv[i].replace(/^--/, "")] = argv[i + 1];
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === `file://${path.resolve(process.argv[1])}`
|
||||
) {
|
||||
const { dir, version } = parseArgs(process.argv);
|
||||
if (!dir || !version) {
|
||||
console.error(
|
||||
"usage: node set-version.mjs --dir <apps/vscode checkout> --version <x.y.z>",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const packageJsonPath = path.join(dir, "package.json");
|
||||
const before = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
|
||||
writeFileSync(
|
||||
packageJsonPath,
|
||||
setPackageVersion(readFileSync(packageJsonPath, "utf8"), version),
|
||||
);
|
||||
console.log(`set ${packageJsonPath} version: ${before} -> ${version}`);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { setPackageVersion } from "./set-version.mjs";
|
||||
|
||||
const fixture = JSON.stringify(
|
||||
{
|
||||
name: "claude-dev",
|
||||
displayName: "Cline",
|
||||
publisher: "saoudrizwan",
|
||||
version: "4.0.0",
|
||||
contributes: {
|
||||
commands: [{ command: "cline.plusButtonClicked", title: "New Task" }],
|
||||
},
|
||||
},
|
||||
null,
|
||||
"\t",
|
||||
);
|
||||
|
||||
describe("setPackageVersion", () => {
|
||||
it("stamps the version and touches nothing else", () => {
|
||||
const pkg = JSON.parse(setPackageVersion(fixture, "4.1.0"));
|
||||
expect(pkg.version).toBe("4.1.0");
|
||||
expect(pkg.name).toBe("claude-dev");
|
||||
expect(pkg.displayName).toBe("Cline");
|
||||
expect(pkg.publisher).toBe("saoudrizwan");
|
||||
expect(pkg.contributes.commands[0].command).toBe(
|
||||
"cline.plusButtonClicked",
|
||||
);
|
||||
});
|
||||
|
||||
it("requires a version", () => {
|
||||
expect(() => setPackageVersion(fixture, undefined)).toThrow(/version/);
|
||||
expect(() => setPackageVersion(fixture, "")).toThrow(/version/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,671 @@
|
||||
/**
|
||||
* Node-level smoke test for the built loader against a staged VSIX directory.
|
||||
* No real VS Code: `vscode` is stubbed just enough for the loader itself, and
|
||||
* the staging dir's next/legacy bundles are swapped for tiny recorders. Verifies
|
||||
* the loader's end-to-end behavior in a real require() environment:
|
||||
* 1. default (no cached cohort) -> activates legacy
|
||||
* 2. cached cohort "next" -> activates next, scoped context paths
|
||||
* 3. the flag refresh caches a TWO-WAY assignment for the next window
|
||||
* (rollout on promotes, rollout off demotes a cached "next")
|
||||
* 4. CLINE_BUNDLE_OVERRIDE / the cline.rollout.bundleOverride setting
|
||||
* force a bundle in either direction
|
||||
* 5. next activation throws -> disposes partial registrations, falls
|
||||
* back to legacy, pins version, and
|
||||
* skips the cohort refresh
|
||||
* 6. the activated bundle's reportRolloutActivation export receives the
|
||||
* authoritative attempted/actual/fallback record (and its absence is
|
||||
* tolerated); the loader's own loader_decision capture fires exactly
|
||||
* once per window
|
||||
* 7. the nightly identity (manifest name cline-nightly) switches the
|
||||
* setting section + context key namespace and shows the status bar
|
||||
* bundle indicator
|
||||
* 8. both bundles throwing surfaces the failure and captures a
|
||||
* double_failure loader event
|
||||
*
|
||||
* Usage: node smoke-loader.mjs <staging-dir>
|
||||
* Copies the staging dir to a temp sandbox; the input is never modified.
|
||||
*/
|
||||
import assert from "node:assert";
|
||||
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import Module from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const staging = process.argv[2];
|
||||
if (!staging) {
|
||||
console.error("usage: node smoke-loader.mjs <staging-dir>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---- vscode API stub (only what the loader touches) -------------------------
|
||||
const executedCommands = [];
|
||||
const statusBarItems = [];
|
||||
function makeVscodeStub(
|
||||
sandbox,
|
||||
settings = {},
|
||||
{ telemetryEnabled = false } = {},
|
||||
) {
|
||||
return {
|
||||
Uri: {
|
||||
file: (fsPath) => ({ fsPath, path: fsPath, scheme: "file" }),
|
||||
joinPath: (base, ...segments) => {
|
||||
const fsPath = path.join(base.fsPath, ...segments);
|
||||
return { fsPath, path: fsPath, scheme: "file" };
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
executeCommand: async (command, ...args) => {
|
||||
executedCommands.push([command, ...args]);
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: (section) => ({
|
||||
get: (key) => settings[`${section}.${key}`],
|
||||
}),
|
||||
},
|
||||
window: {
|
||||
createStatusBarItem: () => {
|
||||
const item = {
|
||||
text: "",
|
||||
tooltip: "",
|
||||
shown: false,
|
||||
show() {
|
||||
this.shown = true;
|
||||
},
|
||||
dispose() {},
|
||||
};
|
||||
statusBarItems.push(item);
|
||||
return item;
|
||||
},
|
||||
},
|
||||
StatusBarAlignment: { Left: 1, Right: 2 },
|
||||
env: { machineId: "smoke-machine", isTelemetryEnabled: telemetryEnabled },
|
||||
version: "0.0.0-smoke",
|
||||
_sandbox: sandbox,
|
||||
};
|
||||
}
|
||||
|
||||
function makeContext(sandbox, globalStateSeed = {}, packageJSON = {}) {
|
||||
const state = new Map(Object.entries(globalStateSeed));
|
||||
return {
|
||||
extensionUri: { fsPath: sandbox, path: sandbox, scheme: "file" },
|
||||
extensionPath: sandbox,
|
||||
extension: { packageJSON: { version: "4.1.0-smoke", ...packageJSON } },
|
||||
subscriptions: [],
|
||||
globalState: {
|
||||
get: (key) => state.get(key),
|
||||
update: async (key, value) => void state.set(key, value),
|
||||
_dump: () => Object.fromEntries(state),
|
||||
},
|
||||
asAbsolutePath: (rel) => path.join(sandbox, rel),
|
||||
};
|
||||
}
|
||||
|
||||
/** PostHog /capture/ POSTs recorded by a scenario's fetch stub, parsed. */
|
||||
function captureCalls(fetchCalls) {
|
||||
return fetchCalls
|
||||
.filter(([url]) => String(url).includes("/capture/"))
|
||||
.map(([, init]) => JSON.parse(init.body));
|
||||
}
|
||||
|
||||
function captureEvents(fetchCalls, event) {
|
||||
return captureCalls(fetchCalls).filter((capture) => capture.event === event);
|
||||
}
|
||||
|
||||
function loaderDecisionCaptures(fetchCalls) {
|
||||
return captureEvents(fetchCalls, "extension.rollout.loader_decision");
|
||||
}
|
||||
|
||||
function featureFlagCalledCaptures(fetchCalls) {
|
||||
return captureEvents(fetchCalls, "$feature_flag_called");
|
||||
}
|
||||
|
||||
function decideCalls(fetchCalls) {
|
||||
return fetchCalls.filter(([url]) => String(url).includes("/decide"));
|
||||
}
|
||||
|
||||
function flagResponse(flags = { rollout: false }) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
featureFlags: {
|
||||
"ext-sdk-bundle-rollout": flags.rollout,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeFlagFetch(flags) {
|
||||
const calls = [];
|
||||
const fetch = async (...args) => {
|
||||
calls.push(args);
|
||||
return flagResponse(flags);
|
||||
};
|
||||
return { calls, fetch };
|
||||
}
|
||||
|
||||
function makeDeferredFlagFetch(flags) {
|
||||
const calls = [];
|
||||
let resolveResponse;
|
||||
let markStarted;
|
||||
const started = new Promise((resolve) => {
|
||||
markStarted = resolve;
|
||||
});
|
||||
const fetch = (...args) => {
|
||||
calls.push(args);
|
||||
markStarted();
|
||||
return new Promise((resolve) => {
|
||||
resolveResponse = () => resolve(flagResponse(flags));
|
||||
});
|
||||
};
|
||||
return {
|
||||
calls,
|
||||
fetch,
|
||||
started,
|
||||
resolve: () => resolveResponse?.(),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitFor(predicate, message, timeoutMs = 500) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (!predicate()) {
|
||||
if (Date.now() >= deadline) {
|
||||
assert.fail(message);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
}
|
||||
|
||||
async function flushAsyncWork() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
// ---- sandbox setup -----------------------------------------------------------
|
||||
function makeSandbox({
|
||||
nextThrows = false,
|
||||
legacyThrows = false,
|
||||
omitReportExport = false,
|
||||
} = {}) {
|
||||
const sandbox = mkdtempSync(path.join(tmpdir(), "cline-ab-smoke-"));
|
||||
cpSync(
|
||||
path.join(staging, "extension.js"),
|
||||
path.join(sandbox, "extension.js"),
|
||||
);
|
||||
for (const bundle of ["next", "legacy"]) {
|
||||
mkdirSync(path.join(sandbox, bundle, "dist"), { recursive: true });
|
||||
const throws =
|
||||
(bundle === "next" && nextThrows) ||
|
||||
(bundle === "legacy" && legacyThrows);
|
||||
const throwLine = throws
|
||||
? `await global.__smoke.beforeNextFailure?.();\n\t\tctx.subscriptions.push({ dispose() { global.__smoke.disposed.push("${bundle}") } });\n\t\tthrow new Error("smoke: ${bundle} activation exploded");`
|
||||
: "";
|
||||
// Mirrors the reportRolloutActivation export both real bundles gained in
|
||||
// their rollout-telemetry PRs; recorded so scenarios can assert the
|
||||
// authoritative attempted/actual/fallback record.
|
||||
const reportExport = omitReportExport
|
||||
? ""
|
||||
: `exports.reportRolloutActivation = async (input) => { global.__smoke.reports.push({ reporter: "${bundle}", attemptedBundle: input.attemptedBundle, actualBundle: input.actualBundle, fallback: input.fallback, hasError: input.error !== undefined }); };`;
|
||||
writeFileSync(
|
||||
path.join(sandbox, bundle, "dist", "extension.js"),
|
||||
`exports.activate = async (ctx) => {
|
||||
${throwLine}
|
||||
global.__smoke.activated.push({ bundle: "${bundle}", extensionPath: ctx.extensionPath, asAbs: ctx.asAbsolutePath("webview-ui/build") });
|
||||
return { bundle: "${bundle}" };
|
||||
};
|
||||
exports.deactivate = () => { global.__smoke.deactivated.push("${bundle}"); };
|
||||
${reportExport}`,
|
||||
);
|
||||
}
|
||||
mkdirSync(path.join(sandbox, "data"), { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(sandbox, "data", "globalState.json"),
|
||||
JSON.stringify({ "cline.generatedMachineId": "smoke-machine" }),
|
||||
);
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
async function runScenario(
|
||||
name,
|
||||
{
|
||||
seed = {},
|
||||
env = {},
|
||||
settings = {},
|
||||
nextThrows = false,
|
||||
legacyThrows = false,
|
||||
omitReportExport = false,
|
||||
telemetryEnabled = false,
|
||||
contextPackageJSON = {},
|
||||
expectFailure = false,
|
||||
fetchController = makeFlagFetch(),
|
||||
beforeNextFailure,
|
||||
expectRefresh = true,
|
||||
},
|
||||
checks,
|
||||
afterDeactivateChecks = async () => {},
|
||||
) {
|
||||
const sandbox = makeSandbox({ nextThrows, legacyThrows, omitReportExport });
|
||||
global.__smoke = {
|
||||
activated: [],
|
||||
deactivated: [],
|
||||
disposed: [],
|
||||
reports: [],
|
||||
beforeNextFailure,
|
||||
};
|
||||
executedCommands.length = 0;
|
||||
statusBarItems.length = 0;
|
||||
|
||||
const previousEnv = {};
|
||||
const scenarioEnv = {
|
||||
CLINE_DIR: sandbox,
|
||||
// A dev build leaves this lookup dynamic; production builds inline the
|
||||
// real PostHog key. Either way, the smoke must exercise refreshCohort.
|
||||
TELEMETRY_SERVICE_API_KEY: "smoke-posthog-project-key",
|
||||
...env,
|
||||
};
|
||||
for (const [key, value] of Object.entries(scenarioEnv)) {
|
||||
previousEnv[key] = process.env[key];
|
||||
process.env[key] = value;
|
||||
}
|
||||
const originalFetch = global.fetch;
|
||||
global.fetch = fetchController.fetch;
|
||||
const originalResolve = Module._resolveFilename;
|
||||
Module._resolveFilename = function (request, ...rest) {
|
||||
if (request === "vscode") {
|
||||
return "vscode";
|
||||
}
|
||||
return originalResolve.call(this, request, ...rest);
|
||||
};
|
||||
require.cache.vscode = {
|
||||
id: "vscode",
|
||||
filename: "vscode",
|
||||
loaded: true,
|
||||
exports: makeVscodeStub(sandbox, settings, { telemetryEnabled }),
|
||||
};
|
||||
|
||||
try {
|
||||
const loaderPath = path.join(sandbox, "extension.js");
|
||||
delete require.cache[loaderPath];
|
||||
const loader = require(loaderPath);
|
||||
const context = makeContext(sandbox, seed, contextPackageJSON);
|
||||
let api;
|
||||
let activationError;
|
||||
try {
|
||||
api = await loader.activate(context);
|
||||
} catch (error) {
|
||||
activationError = error;
|
||||
}
|
||||
if (expectFailure) {
|
||||
assert.ok(activationError, `${name} should have failed to activate`);
|
||||
} else if (activationError) {
|
||||
throw activationError;
|
||||
}
|
||||
if (expectRefresh) {
|
||||
await waitFor(
|
||||
() => decideCalls(fetchController.calls).length > 0,
|
||||
`${name} did not refresh its cohort after activation`,
|
||||
);
|
||||
await flushAsyncWork();
|
||||
assert.equal(
|
||||
decideCalls(fetchController.calls).length,
|
||||
1,
|
||||
`${name} should refresh its cohort exactly once`,
|
||||
);
|
||||
}
|
||||
await checks({
|
||||
context,
|
||||
api,
|
||||
activationError,
|
||||
sandbox,
|
||||
fetchCalls: fetchController.calls,
|
||||
});
|
||||
await loader.deactivate();
|
||||
await afterDeactivateChecks({ context, api, sandbox });
|
||||
console.log(`PASS ${name}`);
|
||||
} finally {
|
||||
Module._resolveFilename = originalResolve;
|
||||
delete require.cache.vscode;
|
||||
if (originalFetch === undefined) {
|
||||
delete global.fetch;
|
||||
} else {
|
||||
global.fetch = originalFetch;
|
||||
}
|
||||
for (const [key, value] of Object.entries(previousEnv)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
rmSync(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const require = Module.createRequire(import.meta.url);
|
||||
|
||||
await runScenario("default cohort -> legacy", {}, async ({ api, sandbox }) => {
|
||||
assert.deepEqual(api, { bundle: "legacy" });
|
||||
assert.equal(
|
||||
global.__smoke.activated[0].extensionPath,
|
||||
path.join(sandbox, "legacy"),
|
||||
);
|
||||
assert.deepEqual(executedCommands[0], [
|
||||
"setContext",
|
||||
"cline.sdkBundle",
|
||||
false,
|
||||
]);
|
||||
assert.deepEqual(global.__smoke.deactivated, []);
|
||||
// The activated bundle received the authoritative activation record.
|
||||
assert.deepEqual(global.__smoke.reports, [
|
||||
{
|
||||
reporter: "legacy",
|
||||
attemptedBundle: "legacy",
|
||||
actualBundle: "legacy",
|
||||
fallback: false,
|
||||
hasError: false,
|
||||
},
|
||||
]);
|
||||
// Stable identity: no nightly status bar indicator.
|
||||
assert.equal(statusBarItems.length, 0);
|
||||
});
|
||||
|
||||
await runScenario(
|
||||
"cached next -> next with scoped paths",
|
||||
{ seed: { "cline.rollout.bundle": "next" } },
|
||||
async ({ api, sandbox }) => {
|
||||
assert.deepEqual(api, { bundle: "next" });
|
||||
const activation = global.__smoke.activated[0];
|
||||
assert.equal(activation.extensionPath, path.join(sandbox, "next"));
|
||||
assert.equal(
|
||||
activation.asAbs,
|
||||
path.join(sandbox, "next", "webview-ui", "build"),
|
||||
);
|
||||
assert.deepEqual(executedCommands[0], [
|
||||
"setContext",
|
||||
"cline.sdkBundle",
|
||||
true,
|
||||
]);
|
||||
assert.deepEqual(global.__smoke.reports, [
|
||||
{
|
||||
reporter: "next",
|
||||
attemptedBundle: "next",
|
||||
actualBundle: "next",
|
||||
fallback: false,
|
||||
hasError: false,
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"rollout flag on promotes for the NEXT window only",
|
||||
{
|
||||
fetchController: makeFlagFetch({ rollout: true }),
|
||||
},
|
||||
async ({ context, api, fetchCalls }) => {
|
||||
// This window already decided legacy from the (empty) cache; the refresh
|
||||
// promotes the NEXT window.
|
||||
assert.deepEqual(api, { bundle: "legacy" });
|
||||
assert.equal(context.globalState._dump()["cline.rollout.bundle"], "next");
|
||||
const [featureFlagCalled] = featureFlagCalledCaptures(fetchCalls);
|
||||
assert.ok(featureFlagCalled, "rollout refresh must emit the PostHog feature-flag exposure event");
|
||||
assert.equal(featureFlagCalled.properties.$feature_flag, "ext-sdk-bundle-rollout");
|
||||
assert.equal(featureFlagCalled.properties.$feature_flag_response, true);
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"rollout flag off demotes a cached next for the NEXT window (two-way)",
|
||||
{
|
||||
seed: { "cline.rollout.bundle": "next" },
|
||||
fetchController: makeFlagFetch({ rollout: false }),
|
||||
},
|
||||
async ({ context, api, fetchCalls }) => {
|
||||
// This window already ran next; dialing the flag down moves the machine
|
||||
// back to legacy on its next reload.
|
||||
assert.deepEqual(api, { bundle: "next" });
|
||||
assert.equal(context.globalState._dump()["cline.rollout.bundle"], "legacy");
|
||||
const [featureFlagCalled] = featureFlagCalledCaptures(fetchCalls);
|
||||
assert.ok(featureFlagCalled, "rollout refresh must emit the PostHog feature-flag exposure event");
|
||||
assert.equal(featureFlagCalled.event, "$feature_flag_called");
|
||||
assert.equal(featureFlagCalled.properties.$feature_flag, "ext-sdk-bundle-rollout");
|
||||
assert.equal(featureFlagCalled.properties.$feature_flag_response, false);
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"env override forces next",
|
||||
{ env: { CLINE_BUNDLE_OVERRIDE: "next" } },
|
||||
async ({ api }) => {
|
||||
assert.deepEqual(api, { bundle: "next" });
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"user setting overrides to legacy despite cached next",
|
||||
{
|
||||
seed: { "cline.rollout.bundle": "next" },
|
||||
settings: { "cline.rollout.bundleOverride": "legacy" },
|
||||
},
|
||||
async ({ api }) => {
|
||||
assert.deepEqual(api, { bundle: "legacy" });
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"user setting overrides to next despite a cached legacy assignment",
|
||||
{
|
||||
seed: { "cline.rollout.bundle": "legacy" },
|
||||
settings: { "cline.rollout.bundleOverride": "next" },
|
||||
},
|
||||
async ({ api }) => {
|
||||
assert.deepEqual(api, { bundle: "next" });
|
||||
},
|
||||
);
|
||||
|
||||
const failedNextRefresh = makeDeferredFlagFetch({ rollout: true });
|
||||
await runScenario(
|
||||
"next activation failure falls back to legacy",
|
||||
{
|
||||
seed: { "cline.rollout.bundle": "next" },
|
||||
nextThrows: true,
|
||||
fetchController: failedNextRefresh,
|
||||
expectRefresh: false,
|
||||
beforeNextFailure: () =>
|
||||
Promise.race([
|
||||
failedNextRefresh.started,
|
||||
new Promise((resolve) => setTimeout(resolve, 100)),
|
||||
]),
|
||||
},
|
||||
async ({ context, api, fetchCalls }) => {
|
||||
assert.deepEqual(api, { bundle: "legacy" });
|
||||
assert.deepEqual(
|
||||
global.__smoke.disposed,
|
||||
["next"],
|
||||
"partial registrations disposed",
|
||||
);
|
||||
const state = context.globalState._dump();
|
||||
assert.equal(state["cline.rollout.bundle"], "legacy");
|
||||
assert.equal(
|
||||
state["cline.rollout.nextActivationFailedVersion"],
|
||||
"4.1.0-smoke",
|
||||
);
|
||||
assert.equal(
|
||||
context.subscriptions.length,
|
||||
0,
|
||||
"failed bundle's subscriptions removed",
|
||||
);
|
||||
// setContext flipped back for the legacy UI
|
||||
assert.deepEqual(executedCommands.at(-1), [
|
||||
"setContext",
|
||||
"cline.sdkBundle",
|
||||
false,
|
||||
]);
|
||||
// The LEGACY bundle (the one whose telemetry pipeline is alive) received
|
||||
// the authoritative fallback record; the dead next bundle reported nothing.
|
||||
assert.deepEqual(global.__smoke.reports, [
|
||||
{
|
||||
reporter: "legacy",
|
||||
attemptedBundle: "next",
|
||||
actualBundle: "legacy",
|
||||
fallback: true,
|
||||
hasError: true,
|
||||
},
|
||||
]);
|
||||
// Keep the fetch stub installed long enough for an incorrectly delayed
|
||||
// refresh to reach the network boundary before asserting its absence.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
// Settle a refresh if the loader incorrectly launched one. With the old
|
||||
// ordering it would now promote COHORT_STATE_KEY back to next.
|
||||
if (decideCalls(fetchCalls).length > 0) {
|
||||
failedNextRefresh.resolve();
|
||||
await flushAsyncWork();
|
||||
}
|
||||
assert.equal(
|
||||
decideCalls(fetchCalls).length,
|
||||
0,
|
||||
"crash fallback must not refresh the failed cohort",
|
||||
);
|
||||
assert.equal(context.globalState._dump()["cline.rollout.bundle"], "legacy");
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"loader_decision capture carries the loader-side metadata",
|
||||
{
|
||||
env: { CLINE_BUNDLE_OVERRIDE: "next" },
|
||||
telemetryEnabled: true,
|
||||
contextPackageJSON: { name: "claude-dev" },
|
||||
},
|
||||
async ({ fetchCalls }) => {
|
||||
await waitFor(
|
||||
() => loaderDecisionCaptures(fetchCalls).length > 0,
|
||||
"loader_decision capture never reached the network",
|
||||
);
|
||||
const captures = loaderDecisionCaptures(fetchCalls);
|
||||
assert.equal(captures.length, 1);
|
||||
const [capture] = captures;
|
||||
assert.equal(capture.event, "extension.rollout.loader_decision");
|
||||
assert.equal(capture.properties.bundle, "next");
|
||||
assert.equal(capture.properties.attempted_bundle, "next");
|
||||
assert.equal(capture.properties.fallback, false);
|
||||
assert.equal(capture.properties.override, "env");
|
||||
assert.equal(capture.properties.loader_version, "4.1.0-smoke");
|
||||
assert.equal(capture.properties.extension_name, "claude-dev");
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"crash fallback captures exactly one loader_decision event",
|
||||
{
|
||||
seed: { "cline.rollout.bundle": "next" },
|
||||
nextThrows: true,
|
||||
telemetryEnabled: true,
|
||||
expectRefresh: false,
|
||||
},
|
||||
async ({ fetchCalls }) => {
|
||||
await waitFor(
|
||||
() => loaderDecisionCaptures(fetchCalls).length > 0,
|
||||
"fallback loader_decision capture never reached the network",
|
||||
);
|
||||
// Give an incorrect second capture (the pre-fix fallback:false event from
|
||||
// the recursive legacy success) time to reach the network before counting.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
const captures = loaderDecisionCaptures(fetchCalls);
|
||||
assert.equal(
|
||||
captures.length,
|
||||
1,
|
||||
"fallback must emit exactly ONE loader event (regression: duplicate fallback:false event)",
|
||||
);
|
||||
const [capture] = captures;
|
||||
assert.equal(capture.event, "extension.rollout.loader_decision");
|
||||
assert.equal(capture.properties.bundle, "legacy");
|
||||
assert.equal(capture.properties.attempted_bundle, "next");
|
||||
assert.equal(capture.properties.fallback, true);
|
||||
assert.match(capture.properties.error_message, /next activation exploded/);
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"a bundle without the reportRolloutActivation export still activates",
|
||||
{ omitReportExport: true },
|
||||
async ({ api }) => {
|
||||
assert.deepEqual(api, { bundle: "legacy" });
|
||||
assert.deepEqual(global.__smoke.reports, []);
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"nightly identity: namespaced setting + context key, status bar indicator",
|
||||
{
|
||||
contextPackageJSON: { name: "cline-nightly" },
|
||||
settings: { "cline-nightly.rollout.bundleOverride": "next" },
|
||||
},
|
||||
async ({ api, context }) => {
|
||||
assert.deepEqual(api, { bundle: "next" });
|
||||
assert.deepEqual(executedCommands[0], [
|
||||
"setContext",
|
||||
"cline-nightly.sdkBundle",
|
||||
true,
|
||||
]);
|
||||
assert.equal(statusBarItems.length, 1);
|
||||
const [item] = statusBarItems;
|
||||
assert.equal(item.shown, true);
|
||||
assert.equal(item.text, "Cline: Next");
|
||||
assert.match(item.tooltip, /bundleOverride setting/);
|
||||
assert.ok(
|
||||
context.subscriptions.includes(item),
|
||||
"indicator must be disposed with the extension",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"double failure: both bundles throw, loader reports and rethrows",
|
||||
{
|
||||
seed: { "cline.rollout.bundle": "next" },
|
||||
nextThrows: true,
|
||||
legacyThrows: true,
|
||||
telemetryEnabled: true,
|
||||
expectFailure: true,
|
||||
expectRefresh: false,
|
||||
},
|
||||
async ({ activationError, fetchCalls }) => {
|
||||
assert.match(String(activationError), /legacy activation exploded/);
|
||||
assert.deepEqual(
|
||||
global.__smoke.reports,
|
||||
[],
|
||||
"no bundle survived to report the authoritative event",
|
||||
);
|
||||
await waitFor(
|
||||
() => loaderDecisionCaptures(fetchCalls).length >= 2,
|
||||
"double failure should capture the fallback AND the double_failure events",
|
||||
);
|
||||
const captures = loaderDecisionCaptures(fetchCalls);
|
||||
assert.equal(captures.length, 2);
|
||||
for (const capture of captures) {
|
||||
assert.equal(capture.event, "extension.rollout.loader_decision");
|
||||
assert.equal(capture.properties.fallback, true);
|
||||
}
|
||||
const doubleFailure = captures.find(
|
||||
(c) => c.properties.double_failure === true,
|
||||
);
|
||||
assert.ok(doubleFailure, "one capture must be flagged double_failure");
|
||||
assert.equal(doubleFailure.properties.attempted_bundle, "next");
|
||||
assert.match(
|
||||
doubleFailure.properties.error_message,
|
||||
/legacy activation exploded/,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await runScenario(
|
||||
"deactivate delegates to active bundle",
|
||||
{},
|
||||
async () => {},
|
||||
async () => {
|
||||
assert.deepEqual(global.__smoke.deactivated, ["legacy"]);
|
||||
},
|
||||
);
|
||||
|
||||
console.log("\nall loader smoke scenarios passed");
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Assemble the combined (loader + next + legacy) VSIX staging directory.
|
||||
*
|
||||
* Layout produced:
|
||||
* <out>/
|
||||
* extension.js loader bundle (this package's dist/extension.js)
|
||||
* package.json union manifest (gen-manifest.mjs)
|
||||
* README.md next's marketplace README
|
||||
* LICENSE, CHANGELOG.md, assets/, walkthrough/ from next (manifest-referenced, VSIX-root-relative)
|
||||
* next/ SDK extension payload (dist/, webview-ui/build/, assets/, package.json)
|
||||
* legacy/ legacy extension payload (dist/, webview-ui/build/, assets/,
|
||||
* node_modules/@vscode/codicons/dist/, package.json)
|
||||
*
|
||||
* Each bundle resolves its own resources under its subdirectory because the
|
||||
* loader hands it an ExtensionContext whose extensionUri/extensionPath point
|
||||
* there (see src/scoped-context.ts). Manifest-referenced resources (icons,
|
||||
* walkthrough media, codicon font declared in contributes.icons) resolve from
|
||||
* the VSIX root, where the stitcher places next's copies.
|
||||
*
|
||||
* Usage:
|
||||
* node stitch.mjs --next <apps/vscode dir, built> --legacy <apps/vscode dir, built> \
|
||||
* --loader <dist/extension.js> --version <x.y.z> --out <staging dir>
|
||||
*/
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import path from "node:path";
|
||||
import { generateManifest } from "./gen-manifest.mjs";
|
||||
|
||||
// Legacy's webview loads codicon.css straight from node_modules (see its
|
||||
// WebviewProvider); next bundles the font into its webview build but its own
|
||||
// .vscodeignore still re-includes the codicons dist, so mirror that here.
|
||||
const BUNDLE_PAYLOAD = {
|
||||
next: [
|
||||
"dist",
|
||||
"webview-ui/build",
|
||||
"assets",
|
||||
"package.json",
|
||||
"node_modules/@vscode/codicons/dist",
|
||||
],
|
||||
legacy: [
|
||||
"dist",
|
||||
"webview-ui/build",
|
||||
"assets",
|
||||
"package.json",
|
||||
"node_modules/@vscode/codicons/dist",
|
||||
],
|
||||
};
|
||||
|
||||
/** VSIX-root files, all taken from the next checkout (manifest fields come from next too). */
|
||||
const ROOT_PAYLOAD = ["LICENSE", "CHANGELOG.md", "assets", "walkthrough"];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let i = 2; i < argv.length; i += 2) {
|
||||
args[argv[i].replace(/^--/, "")] = argv[i + 1];
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function copyInto(sourceRoot, relPaths, destRoot, { optional = [] } = {}) {
|
||||
for (const rel of relPaths) {
|
||||
const source = path.join(sourceRoot, rel);
|
||||
if (!existsSync(source)) {
|
||||
if (optional.includes(rel)) {
|
||||
console.warn(` skip (missing, optional): ${rel}`);
|
||||
continue;
|
||||
}
|
||||
throw new Error(
|
||||
`required payload missing: ${source} — did the bundle build run?`,
|
||||
);
|
||||
}
|
||||
cpSync(source, path.join(destRoot, rel), {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
});
|
||||
console.log(` + ${rel}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function stitch({ next, legacy, loader, version, out }) {
|
||||
for (const [name, value] of Object.entries({
|
||||
next,
|
||||
legacy,
|
||||
loader,
|
||||
version,
|
||||
out,
|
||||
})) {
|
||||
if (!value) {
|
||||
throw new Error(`--${name} is required`);
|
||||
}
|
||||
}
|
||||
// Refuse to stage from an unbuilt tree early, with a clear message.
|
||||
for (const [name, root] of [
|
||||
["next", next],
|
||||
["legacy", legacy],
|
||||
]) {
|
||||
if (!existsSync(path.join(root, "dist", "extension.js"))) {
|
||||
throw new Error(
|
||||
`${name} bundle not built: ${root}/dist/extension.js missing`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!existsSync(path.join(root, "webview-ui", "build", "assets", "index.js"))
|
||||
) {
|
||||
throw new Error(
|
||||
`${name} webview not built: ${root}/webview-ui/build/assets/index.js missing`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
rmSync(out, { recursive: true, force: true });
|
||||
mkdirSync(out, { recursive: true });
|
||||
|
||||
console.log("root payload (from next):");
|
||||
copyInto(next, ROOT_PAYLOAD, out, {
|
||||
optional: ["CHANGELOG.md", "walkthrough"],
|
||||
});
|
||||
cpSync(loader, path.join(out, "extension.js"));
|
||||
console.log(" + extension.js (loader)");
|
||||
|
||||
const readme = path.join(next, "README.marketplace.md");
|
||||
cpSync(
|
||||
existsSync(readme) ? readme : path.join(next, "README.md"),
|
||||
path.join(out, "README.md"),
|
||||
);
|
||||
console.log(" + README.md");
|
||||
|
||||
for (const [bundle, payload] of Object.entries(BUNDLE_PAYLOAD)) {
|
||||
const sourceRoot = bundle === "next" ? next : legacy;
|
||||
console.log(`${bundle} payload:`);
|
||||
copyInto(sourceRoot, payload, path.join(out, bundle), {
|
||||
optional: ["walkthrough"],
|
||||
});
|
||||
}
|
||||
|
||||
const manifest = generateManifest(
|
||||
JSON.parse(readFileSync(path.join(next, "package.json"), "utf8")),
|
||||
JSON.parse(readFileSync(path.join(legacy, "package.json"), "utf8")),
|
||||
version,
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(out, "package.json"),
|
||||
`${JSON.stringify(manifest, null, "\t")}\n`,
|
||||
);
|
||||
console.log(" + package.json (union manifest)");
|
||||
|
||||
// vsce packages everything in the staging dir; only strip sourcemaps and
|
||||
// junk. The codicons files under legacy/node_modules must survive, so no
|
||||
// blanket node_modules ignore here — staging only ever contains what this
|
||||
// script copied.
|
||||
writeFileSync(
|
||||
path.join(out, ".vscodeignore"),
|
||||
["**/*.map", "**/.DS_Store", ""].join("\n"),
|
||||
);
|
||||
|
||||
console.log(`\nstaged ${out} (version ${version})`);
|
||||
// Keep the scanner exemption category-scoped to match the standalone bundle
|
||||
// workflows; see the README for its scope and verification notes.
|
||||
console.log(
|
||||
`package it with:\n cd ${out} && vsce package --no-dependencies --allow-package-secrets sendgrid`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === `file://${path.resolve(process.argv[1])}`
|
||||
) {
|
||||
try {
|
||||
stitch(parseArgs(process.argv));
|
||||
} catch (error) {
|
||||
console.error(`stitch failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
bundleContextKey,
|
||||
decideBundle,
|
||||
decisionOverrideSource,
|
||||
idPrefix,
|
||||
parseRolloutAssignment,
|
||||
ROLLOUT_FLAG,
|
||||
settingSection,
|
||||
} from "./cohort";
|
||||
|
||||
const base = {
|
||||
envOverride: undefined,
|
||||
settingOverride: undefined,
|
||||
cached: undefined,
|
||||
previousFailure: false,
|
||||
};
|
||||
|
||||
describe("decideBundle", () => {
|
||||
it("defaults to legacy with no cached assignment", () => {
|
||||
expect(decideBundle(base)).toBe("legacy");
|
||||
});
|
||||
|
||||
it("uses the cached assignment", () => {
|
||||
expect(decideBundle({ ...base, cached: "next" })).toBe("next");
|
||||
expect(decideBundle({ ...base, cached: "legacy" })).toBe("legacy");
|
||||
});
|
||||
|
||||
it("treats unknown cached values as legacy", () => {
|
||||
expect(decideBundle({ ...base, cached: "garbage" })).toBe("legacy");
|
||||
});
|
||||
|
||||
it("a previous activation failure on this version forces legacy", () => {
|
||||
expect(
|
||||
decideBundle({ ...base, cached: "next", previousFailure: true }),
|
||||
).toBe("legacy");
|
||||
});
|
||||
|
||||
it("env override beats everything, including a previous failure", () => {
|
||||
expect(
|
||||
decideBundle({ ...base, envOverride: "next", previousFailure: true }),
|
||||
).toBe("next");
|
||||
expect(
|
||||
decideBundle({ ...base, envOverride: "legacy", cached: "next" }),
|
||||
).toBe("legacy");
|
||||
});
|
||||
|
||||
it("user setting overrides in both directions", () => {
|
||||
expect(
|
||||
decideBundle({ ...base, settingOverride: "next", previousFailure: true }),
|
||||
).toBe("next");
|
||||
expect(
|
||||
decideBundle({ ...base, settingOverride: "legacy", cached: "next" }),
|
||||
).toBe("legacy");
|
||||
});
|
||||
|
||||
it("env override beats the user setting", () => {
|
||||
expect(
|
||||
decideBundle({ ...base, envOverride: "legacy", settingOverride: "next" }),
|
||||
).toBe("legacy");
|
||||
});
|
||||
|
||||
it("ignores invalid and 'auto' overrides", () => {
|
||||
expect(decideBundle({ ...base, envOverride: "beta", cached: "next" })).toBe(
|
||||
"next",
|
||||
);
|
||||
expect(
|
||||
decideBundle({ ...base, settingOverride: "auto", cached: "next" }),
|
||||
).toBe("next");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decisionOverrideSource", () => {
|
||||
it("reports which override was active", () => {
|
||||
expect(decisionOverrideSource(base)).toBeUndefined();
|
||||
expect(decisionOverrideSource({ ...base, settingOverride: "next" })).toBe(
|
||||
"setting",
|
||||
);
|
||||
expect(
|
||||
decisionOverrideSource({
|
||||
...base,
|
||||
envOverride: "legacy",
|
||||
settingOverride: "next",
|
||||
}),
|
||||
).toBe("env");
|
||||
expect(
|
||||
decisionOverrideSource({ ...base, settingOverride: "auto" }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseRolloutAssignment", () => {
|
||||
it("promotes only on a literal boolean true", () => {
|
||||
expect(
|
||||
parseRolloutAssignment({ featureFlags: { [ROLLOUT_FLAG]: true } }),
|
||||
).toBe("next");
|
||||
});
|
||||
|
||||
it("is two-way: anything else resolves to legacy (fail-safe)", () => {
|
||||
// false = dialed out of the cohort; the rest = mis-configured flag.
|
||||
for (const value of ["test", "control", 1, 0.5, {}, false, undefined]) {
|
||||
expect(
|
||||
parseRolloutAssignment({ featureFlags: { [ROLLOUT_FLAG]: value } }),
|
||||
).toBe("legacy");
|
||||
}
|
||||
// Flag deleted / not created yet: nobody promoted.
|
||||
expect(parseRolloutAssignment({ featureFlags: {} })).toBe("legacy");
|
||||
});
|
||||
|
||||
it("returns undefined for malformed responses (cache left untouched)", () => {
|
||||
expect(parseRolloutAssignment(undefined)).toBeUndefined();
|
||||
expect(parseRolloutAssignment({})).toBeUndefined();
|
||||
expect(parseRolloutAssignment({ featureFlags: null })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("identity prefix", () => {
|
||||
it("maps the nightly manifest name to the cline-nightly namespace", () => {
|
||||
expect(idPrefix("cline-nightly")).toBe("cline-nightly");
|
||||
});
|
||||
|
||||
it("maps everything else (stable claude-dev, unknown, missing) to cline", () => {
|
||||
expect(idPrefix("claude-dev")).toBe("cline");
|
||||
expect(idPrefix("some-fork")).toBe("cline");
|
||||
expect(idPrefix(undefined)).toBe("cline");
|
||||
});
|
||||
|
||||
it("derives the setting section and context key from the prefix", () => {
|
||||
expect(settingSection("cline")).toBe("cline.rollout");
|
||||
expect(settingSection("cline-nightly")).toBe("cline-nightly.rollout");
|
||||
expect(bundleContextKey("cline")).toBe("cline.sdkBundle");
|
||||
expect(bundleContextKey("cline-nightly")).toBe("cline-nightly.sdkBundle");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
export type Bundle = "next" | "legacy";
|
||||
|
||||
/**
|
||||
* The combined VSIX ships under two identities: the stable extension
|
||||
* (manifest name "claude-dev", contribution IDs under "cline.*") and the
|
||||
* nightly (name "cline-nightly", IDs under "cline-nightly.*" — the nightly
|
||||
* packaging rewrites every `"cline.` prefix in the manifest, see
|
||||
* scripts/nightlify.mjs and apps/vscode/scripts/publish-nightly.mjs). Anything
|
||||
* the loader reads from or feeds back into the manifest namespace — the
|
||||
* bundleOverride setting and the sdkBundle context key — must use the prefix
|
||||
* matching the installed identity. scripts/gen-manifest.mjs derives the same
|
||||
* prefix when generating the union manifest; keep them in sync.
|
||||
*/
|
||||
export const NIGHTLY_EXTENSION_NAME = "cline-nightly";
|
||||
export type IdPrefix = "cline" | "cline-nightly";
|
||||
|
||||
export function idPrefix(extensionName: string | undefined): IdPrefix {
|
||||
return extensionName === NIGHTLY_EXTENSION_NAME ? "cline-nightly" : "cline";
|
||||
}
|
||||
|
||||
/** Settings section holding the bundleOverride escape hatch. */
|
||||
export function settingSection(prefix: IdPrefix): string {
|
||||
return `${prefix}.rollout`;
|
||||
}
|
||||
|
||||
/** Context key gating per-cohort menus/keybindings in the union manifest. */
|
||||
export function bundleContextKey(prefix: IdPrefix): string {
|
||||
return `${prefix}.sdkBundle`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader-owned VS Code memento keys. Never touched by either bundle. These
|
||||
* deliberately stay un-prefixed by identity: globalState is already scoped to
|
||||
* the extension ID, so a stable and a nightly install can never collide.
|
||||
*/
|
||||
export const COHORT_STATE_KEY = "cline.rollout.bundle";
|
||||
/** Version of the combined VSIX whose `next` bundle failed to activate, if any. */
|
||||
export const FAILED_VERSION_STATE_KEY =
|
||||
"cline.rollout.nextActivationFailedVersion";
|
||||
/** Epoch ms of the previous loader activation, for launch-cadence telemetry. */
|
||||
export const LAST_ACTIVATION_STATE_KEY = "cline.rollout.lastActivationAt";
|
||||
|
||||
/**
|
||||
* PostHog rollout flag (created in the Cline PostHog project). Must be a
|
||||
* plain BOOLEAN release flag with a percentage rollout.
|
||||
*
|
||||
* The assignment is TWO-WAY: each background refresh caches exactly what the
|
||||
* flag says (true => next, anything else => legacy) for the next window, so
|
||||
* dialing the percentage down moves machines back to legacy on their next
|
||||
* reload — the single emergency lever is "set the rollout to 0%". Demoted
|
||||
* machines keep their settings/creds (the state files round-trip), but tasks
|
||||
* created on the SDK bundle aren't visible in legacy's history until
|
||||
* re-promoted, and tokens rotated on next may require re-auth on legacy.
|
||||
*/
|
||||
export const ROLLOUT_FLAG = "ext-sdk-bundle-rollout";
|
||||
|
||||
/** Env var for local dev / e2e to force a bundle. Beats everything. */
|
||||
export const BUNDLE_OVERRIDE_ENV = "CLINE_BUNDLE_OVERRIDE";
|
||||
|
||||
/**
|
||||
* User-visible escape hatch: `<prefix>.rollout.bundleOverride` in VS Code
|
||||
* settings ("auto" | "next" | "legacy") — see settingSection() for the
|
||||
* identity-dependent section name. Editable from settings.json without
|
||||
* touching mementos, beats the remote flag in either direction, applies on
|
||||
* window reload. Injected into the union manifest by gen-manifest.mjs — keep
|
||||
* the schema there in sync with these constants.
|
||||
*/
|
||||
export const SETTING_BUNDLE_OVERRIDE = "bundleOverride";
|
||||
|
||||
function asBundle(value: unknown): Bundle | undefined {
|
||||
return value === "next" || value === "legacy" ? value : undefined;
|
||||
}
|
||||
|
||||
export interface CohortInputs {
|
||||
/** CLINE_BUNDLE_OVERRIDE, if set. */
|
||||
envOverride: string | undefined;
|
||||
/** The <prefix>.rollout.bundleOverride user setting ("auto" = no override). */
|
||||
settingOverride: string | undefined;
|
||||
/** Cached assignment from the previous window's background flag refresh. */
|
||||
cached: string | undefined;
|
||||
/** The next bundle failed to activate on this VSIX version before. */
|
||||
previousFailure: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide which bundle to activate for this window. Must be synchronous and
|
||||
* never block on the network: it only consumes state cached by the previous
|
||||
* window's background refresh, so a percentage change applies on the next
|
||||
* window reload, mirroring how VS Code's own experiments behave.
|
||||
*/
|
||||
export function decideBundle(inputs: CohortInputs): Bundle {
|
||||
const forced =
|
||||
asBundle(inputs.envOverride) ?? asBundle(inputs.settingOverride);
|
||||
if (forced) {
|
||||
return forced;
|
||||
}
|
||||
if (inputs.previousFailure) {
|
||||
return "legacy";
|
||||
}
|
||||
return inputs.cached === "next" ? "next" : "legacy";
|
||||
}
|
||||
|
||||
/** Which override produced the decision, if any — reported on the activation event. */
|
||||
export function decisionOverrideSource(
|
||||
inputs: Pick<CohortInputs, "envOverride" | "settingOverride">,
|
||||
): "env" | "setting" | undefined {
|
||||
if (asBundle(inputs.envOverride)) {
|
||||
return "env";
|
||||
}
|
||||
if (asBundle(inputs.settingOverride)) {
|
||||
return "setting";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a PostHog /decide (v3) response into the assignment to cache for the
|
||||
* next window, or undefined when the response is malformed (leave the cached
|
||||
* assignment untouched — sticky on transient failures).
|
||||
*
|
||||
* Deliberately strict so a mis-configured flag fails SAFE toward legacy: only
|
||||
* boolean `true` promotes. A multivariate variant string, a number, a payload,
|
||||
* or a missing/deleted flag all resolve to legacy — the flag must stay a plain
|
||||
* boolean release flag with a percentage rollout.
|
||||
*/
|
||||
export function parseRolloutAssignment(response: unknown): Bundle | undefined {
|
||||
const flags = (
|
||||
response as { featureFlags?: Record<string, unknown> } | undefined
|
||||
)?.featureFlags;
|
||||
if (!flags || typeof flags !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return flags[ROLLOUT_FLAG] === true ? "next" : "legacy";
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
BUNDLE_OVERRIDE_ENV,
|
||||
type Bundle,
|
||||
bundleContextKey,
|
||||
COHORT_STATE_KEY,
|
||||
decideBundle,
|
||||
decisionOverrideSource,
|
||||
FAILED_VERSION_STATE_KEY,
|
||||
type IdPrefix,
|
||||
idPrefix,
|
||||
LAST_ACTIVATION_STATE_KEY,
|
||||
SETTING_BUNDLE_OVERRIDE,
|
||||
settingSection,
|
||||
} from "./cohort";
|
||||
import { refreshCohort, reportLoaderDecision } from "./rollout";
|
||||
import { scopedContext } from "./scoped-context";
|
||||
|
||||
/**
|
||||
* Cline rollout loader.
|
||||
*
|
||||
* The VSIX ships two complete, independently built extension bundles:
|
||||
* next/ — the SDK-based extension (built from main's apps/vscode)
|
||||
* legacy/ — the pre-SDK extension (built from the legacy-extension branch)
|
||||
*
|
||||
* This entrypoint picks exactly one per window — from state cached by the
|
||||
* previous window's background flag refresh, never from a blocking network
|
||||
* call — activates it with a context whose install-root paths point into its
|
||||
* subdirectory, and delegates everything else to it. If the next bundle throws
|
||||
* during activation, the loader disposes whatever it half-registered, pins
|
||||
* this VSIX version back to legacy, and activates legacy instead.
|
||||
*/
|
||||
|
||||
// Resolved at runtime relative to the installed VSIX root; must stay opaque to
|
||||
// esbuild so the bundles aren't inlined into the loader.
|
||||
const requireFromVsixRoot = createRequire(__filename);
|
||||
|
||||
interface BundleModule {
|
||||
activate(context: vscode.ExtensionContext): Promise<unknown> | unknown;
|
||||
deactivate?(): Promise<void> | void;
|
||||
/**
|
||||
* Exported by both bundles' entrypoints (see rollout-metadata.ts on each
|
||||
* branch): captures the AUTHORITATIVE `extension.rollout.bundle_activated`
|
||||
* event through the bundle's own variant-attributed telemetry pipeline.
|
||||
* Optional so the loader keeps working against a bundle built before the
|
||||
* export existed.
|
||||
*/
|
||||
reportRolloutActivation?(input: {
|
||||
attemptedBundle: Bundle;
|
||||
actualBundle: Bundle;
|
||||
fallback: boolean;
|
||||
error?: unknown;
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
let activeBundle: { module: BundleModule; name: Bundle } | undefined;
|
||||
|
||||
interface ActivationMeta {
|
||||
msSinceLastActivation?: number;
|
||||
override?: "env" | "setting";
|
||||
}
|
||||
|
||||
/** Set when the original decision crashed and this activation is the fallback. */
|
||||
interface FallbackFrom {
|
||||
attempted: Bundle;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const loaderVersion: string =
|
||||
context.extension.packageJSON?.version ?? "unknown";
|
||||
const prefix = idPrefix(context.extension.packageJSON?.name);
|
||||
|
||||
// Launch-cadence telemetry: how stale the previous activation is bounds how
|
||||
// fast a percentage change can actually reach users' windows.
|
||||
const lastActivationAt = context.globalState.get<number>(
|
||||
LAST_ACTIVATION_STATE_KEY,
|
||||
);
|
||||
const now = Date.now();
|
||||
void context.globalState.update(LAST_ACTIVATION_STATE_KEY, now);
|
||||
|
||||
const overrides = {
|
||||
envOverride: process.env[BUNDLE_OVERRIDE_ENV],
|
||||
settingOverride: vscode.workspace
|
||||
.getConfiguration(settingSection(prefix))
|
||||
.get<string>(SETTING_BUNDLE_OVERRIDE),
|
||||
};
|
||||
const bundle = decideBundle({
|
||||
...overrides,
|
||||
cached: context.globalState.get<string>(COHORT_STATE_KEY),
|
||||
previousFailure:
|
||||
context.globalState.get<string>(FAILED_VERSION_STATE_KEY) ===
|
||||
loaderVersion,
|
||||
});
|
||||
const meta: ActivationMeta = {
|
||||
msSinceLastActivation:
|
||||
typeof lastActivationAt === "number" && lastActivationAt <= now
|
||||
? now - lastActivationAt
|
||||
: undefined,
|
||||
override: decisionOverrideSource(overrides),
|
||||
};
|
||||
|
||||
return activateBundle(context, prefix, bundle, loaderVersion, meta, true);
|
||||
}
|
||||
|
||||
async function activateBundle(
|
||||
context: vscode.ExtensionContext,
|
||||
prefix: IdPrefix,
|
||||
bundle: Bundle,
|
||||
loaderVersion: string,
|
||||
meta: ActivationMeta,
|
||||
refreshAssignmentOnSuccess: boolean,
|
||||
fallbackFrom?: FallbackFrom,
|
||||
): Promise<unknown> {
|
||||
// Menus/keybindings gated per cohort in package.json key off this.
|
||||
await vscode.commands.executeCommand(
|
||||
"setContext",
|
||||
bundleContextKey(prefix),
|
||||
bundle === "next",
|
||||
);
|
||||
|
||||
const subscriptionsBefore = context.subscriptions.length;
|
||||
try {
|
||||
const module = requireFromVsixRoot(
|
||||
path.join(__dirname, bundle, "dist", "extension.js"),
|
||||
) as BundleModule;
|
||||
const api = await module.activate(scopedContext(context, bundle));
|
||||
activeBundle = { module, name: bundle };
|
||||
// Cache the next window's assignment only after the originally selected
|
||||
// bundle activates. A crash fallback must not start a refresh that could
|
||||
// promote the cohort back to next after the handler pins it to legacy.
|
||||
if (refreshAssignmentOnSuccess) {
|
||||
void refreshCohort(context).catch(() => {});
|
||||
}
|
||||
// Authoritative activation event, captured by the bundle's own telemetry
|
||||
// (built with CLINE_ROLLOUT_VARIANT). On fallback this runs in the legacy
|
||||
// bundle — next's pipeline is the thing that just crashed.
|
||||
if (typeof module.reportRolloutActivation === "function") {
|
||||
void module
|
||||
.reportRolloutActivation({
|
||||
attemptedBundle: fallbackFrom?.attempted ?? bundle,
|
||||
actualBundle: bundle,
|
||||
fallback: fallbackFrom !== undefined,
|
||||
error: fallbackFrom?.error,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
// The loader's own decision event fires once per window: the fallback
|
||||
// path already reported (fallback: true) from the catch block below.
|
||||
if (!fallbackFrom) {
|
||||
void reportLoaderDecision(context, bundle, {
|
||||
...meta,
|
||||
fallback: false,
|
||||
}).catch(() => {});
|
||||
}
|
||||
showNightlyBundleIndicator(context, prefix, bundle, meta, fallbackFrom);
|
||||
return api;
|
||||
} catch (error) {
|
||||
if (bundle === "legacy") {
|
||||
// Nothing left to fall back to; let VS Code surface the failure. When
|
||||
// this was already the crash fallback, no bundle telemetry pipeline is
|
||||
// alive — the loader's direct event is the only record.
|
||||
void reportLoaderDecision(context, "legacy", {
|
||||
...meta,
|
||||
attemptedBundle: fallbackFrom?.attempted ?? "legacy",
|
||||
fallback: fallbackFrom !== undefined,
|
||||
doubleFailure: fallbackFrom !== undefined,
|
||||
errorMessage: formatActivationError(error),
|
||||
}).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
console.error(
|
||||
"[cline-rollout] next bundle failed to activate, falling back to legacy:",
|
||||
error,
|
||||
);
|
||||
disposeSubscriptionsAddedAfter(context, subscriptionsBefore);
|
||||
// Pin this VSIX version to legacy so we don't crash-loop every window.
|
||||
// A new release (new version string) gets to try next again.
|
||||
await context.globalState.update(FAILED_VERSION_STATE_KEY, loaderVersion);
|
||||
await context.globalState.update(COHORT_STATE_KEY, "legacy");
|
||||
void reportLoaderDecision(context, "legacy", {
|
||||
...meta,
|
||||
attemptedBundle: "next",
|
||||
fallback: true,
|
||||
errorMessage: formatActivationError(error),
|
||||
}).catch(() => {});
|
||||
return activateBundle(
|
||||
context,
|
||||
prefix,
|
||||
"legacy",
|
||||
loaderVersion,
|
||||
meta,
|
||||
false,
|
||||
{ attempted: "next", error },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function formatActivationError(error: unknown): string {
|
||||
return error instanceof Error
|
||||
? `${error.message}\n${error.stack ?? ""}`.slice(0, 2000)
|
||||
: String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nightly-only visible indicator of which bundle this window is running.
|
||||
* The stable combined VSIX (and any ordinary build) never shows it: the
|
||||
* prefix is derived from the packaged manifest name. Best-effort — the
|
||||
* indicator must never take down an otherwise successful activation.
|
||||
*/
|
||||
function showNightlyBundleIndicator(
|
||||
context: vscode.ExtensionContext,
|
||||
prefix: IdPrefix,
|
||||
bundle: Bundle,
|
||||
meta: ActivationMeta,
|
||||
fallbackFrom: FallbackFrom | undefined,
|
||||
) {
|
||||
if (prefix !== "cline-nightly") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const item = vscode.window.createStatusBarItem(
|
||||
vscode.StatusBarAlignment.Right,
|
||||
-1000,
|
||||
);
|
||||
item.text = bundle === "next" ? "Cline: Next" : "Cline: Legacy";
|
||||
const detail = fallbackFrom
|
||||
? "crash fallback from the next bundle"
|
||||
: meta.override
|
||||
? `forced by ${meta.override === "env" ? `the ${BUNDLE_OVERRIDE_ENV} env var` : "the bundleOverride setting"}`
|
||||
: "rollout assignment";
|
||||
item.tooltip = `Cline nightly A/B rollout: running the ${bundle === "next" ? "next (SDK)" : "legacy"} bundle (${detail}).`;
|
||||
item.show();
|
||||
context.subscriptions.push(item);
|
||||
} catch (error) {
|
||||
console.warn("[cline-rollout] could not show bundle indicator:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Dispose anything a failed activation managed to register before it threw. */
|
||||
function disposeSubscriptionsAddedAfter(
|
||||
context: vscode.ExtensionContext,
|
||||
startIndex: number,
|
||||
) {
|
||||
const added = context.subscriptions.splice(startIndex);
|
||||
for (const disposable of added) {
|
||||
try {
|
||||
disposable.dispose();
|
||||
} catch {
|
||||
// best effort — a broken disposable must not block the fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
return activeBundle?.module.deactivate?.();
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { machineId } from "node-machine-id";
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
type Bundle,
|
||||
COHORT_STATE_KEY,
|
||||
parseRolloutAssignment,
|
||||
ROLLOUT_FLAG,
|
||||
} from "./cohort";
|
||||
|
||||
/**
|
||||
* Same PostHog project + reverse proxy the extension's telemetry uses.
|
||||
* The API key is injected at build time by CI (see esbuild.mjs), matching how
|
||||
* apps/vscode injects TELEMETRY_SERVICE_API_KEY. Local builds without the key
|
||||
* skip all network calls, so the loader defaults everyone to legacy.
|
||||
*/
|
||||
const POSTHOG_HOST = "https://data.cline.bot";
|
||||
const POSTHOG_API_KEY = process.env.TELEMETRY_SERVICE_API_KEY;
|
||||
const FETCH_TIMEOUT_MS = 10_000;
|
||||
const FEATURE_FLAG_CALLED_EVENT = "$feature_flag_called";
|
||||
|
||||
/**
|
||||
* Mirror the distinct-id derivation in apps/vscode
|
||||
* (src/services/logging/distinctId.ts) so PostHog evaluates the rollout flag
|
||||
* against the same id the bundles report telemetry with — otherwise cohort
|
||||
* membership can't be correlated with cohort behavior in dashboards.
|
||||
* Falls back to vscode.env.machineId rather than generating + persisting a new
|
||||
* id: the loader must never write to the shared ~/.cline state files.
|
||||
*/
|
||||
async function getDistinctId(): Promise<string> {
|
||||
const generated = await readSharedGlobalStateKey("cline.generatedMachineId");
|
||||
if (typeof generated === "string" && generated.length > 0) {
|
||||
return generated;
|
||||
}
|
||||
try {
|
||||
const id = await machineId();
|
||||
if (id) {
|
||||
return id;
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return vscode.env.machineId;
|
||||
}
|
||||
|
||||
/** Read one key from the file-backed global state both bundles share. */
|
||||
async function readSharedGlobalStateKey(key: string): Promise<unknown> {
|
||||
try {
|
||||
const clineDir = process.env.CLINE_DIR || path.join(os.homedir(), ".cline");
|
||||
const raw = await readFile(
|
||||
path.join(clineDir, "data", "globalState.json"),
|
||||
"utf8",
|
||||
);
|
||||
const state = JSON.parse(raw);
|
||||
return state?.[key];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function postJson(
|
||||
url: string,
|
||||
body: object,
|
||||
): Promise<Response | undefined> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
return await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch {
|
||||
return undefined;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAssignment(
|
||||
distinctId: string,
|
||||
): Promise<Bundle | undefined> {
|
||||
if (!POSTHOG_API_KEY) {
|
||||
return undefined;
|
||||
}
|
||||
const response = await postJson(`${POSTHOG_HOST}/decide?v=3`, {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
distinct_id: distinctId,
|
||||
});
|
||||
if (!response?.ok) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const decideResponse = await response.json();
|
||||
const assignment = parseRolloutAssignment(decideResponse);
|
||||
if (!assignment) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Mirror FeatureFlagsService/PostHog SDK exposure tracking for this
|
||||
// loader-owned flag evaluation. This event is intentionally not gated by
|
||||
// telemetry opt-out: feature-flag evaluation remains enabled so PostHog can
|
||||
// correctly attribute rollout cohorts, while loader_decision below still
|
||||
// respects user/host telemetry settings.
|
||||
void reportFeatureFlagCalled(
|
||||
distinctId,
|
||||
getRolloutFlagResponse(decideResponse),
|
||||
).catch(() => {});
|
||||
|
||||
return assignment;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getRolloutFlagResponse(response: unknown): unknown {
|
||||
const flags = (
|
||||
response as { featureFlags?: Record<string, unknown> } | undefined
|
||||
)?.featureFlags;
|
||||
return flags && typeof flags === "object" ? flags[ROLLOUT_FLAG] : undefined;
|
||||
}
|
||||
|
||||
async function reportFeatureFlagCalled(
|
||||
distinctId: string,
|
||||
flagResponse: unknown,
|
||||
): Promise<void> {
|
||||
if (!POSTHOG_API_KEY) {
|
||||
return;
|
||||
}
|
||||
await postJson(`${POSTHOG_HOST}/capture/`, {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
event: FEATURE_FLAG_CALLED_EVENT,
|
||||
distinct_id: distinctId,
|
||||
properties: {
|
||||
$feature_flag: ROLLOUT_FLAG,
|
||||
$feature_flag_response: flagResponse,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Background refresh: evaluate the rollout flag and cache exactly what it
|
||||
* says for the NEXT window (two-way: dialing the percentage down demotes on
|
||||
* the next reload). Never affects the bundle already activated in this
|
||||
* window, and failures leave the cached assignment untouched (sticky on
|
||||
* transient errors only).
|
||||
*/
|
||||
export async function refreshCohort(
|
||||
context: vscode.ExtensionContext,
|
||||
): Promise<void> {
|
||||
const distinctId = await getDistinctId();
|
||||
const assignment = await fetchAssignment(distinctId);
|
||||
if (!assignment) {
|
||||
return;
|
||||
}
|
||||
await context.globalState.update(COHORT_STATE_KEY, assignment);
|
||||
}
|
||||
|
||||
/**
|
||||
* The loader's own decision event. Distinct from the AUTHORITATIVE
|
||||
* `extension.rollout.bundle_activated` event, which the activated bundle
|
||||
* itself captures through its variant-attributed telemetry pipeline (the
|
||||
* loader triggers it via the bundle's reportRolloutActivation export — see
|
||||
* src/extension.ts). This event carries the loader-side metadata that event
|
||||
* can't (override source, launch cadence, loader version) and is the only
|
||||
* signal left when BOTH bundles fail to activate.
|
||||
*/
|
||||
export const LOADER_DECISION_EVENT = "extension.rollout.loader_decision";
|
||||
|
||||
/**
|
||||
* Report the loader's bundle decision (and whether it was a crash fallback).
|
||||
* Feature-flag evaluation is always allowed (matching the extension's
|
||||
* FeatureFlagsService), but event capture respects the user's telemetry
|
||||
* opt-out and VS Code's global telemetry setting.
|
||||
*/
|
||||
export async function reportLoaderDecision(
|
||||
context: vscode.ExtensionContext,
|
||||
bundle: Bundle,
|
||||
options: {
|
||||
fallback: boolean;
|
||||
/** Bundle the loader originally decided on; differs from `bundle` on fallback. */
|
||||
attemptedBundle?: Bundle;
|
||||
/** Both bundles threw — nothing activated, and no bundle telemetry exists. */
|
||||
doubleFailure?: boolean;
|
||||
errorMessage?: string;
|
||||
/** Time since the previous loader activation on this machine, if known. */
|
||||
msSinceLastActivation?: number;
|
||||
/** Whether an env var or user setting forced this bundle. */
|
||||
override?: "env" | "setting";
|
||||
},
|
||||
): Promise<void> {
|
||||
if (!POSTHOG_API_KEY) {
|
||||
return;
|
||||
}
|
||||
const telemetrySetting = await readSharedGlobalStateKey("telemetrySetting");
|
||||
if (telemetrySetting === "disabled" || !vscode.env.isTelemetryEnabled) {
|
||||
return;
|
||||
}
|
||||
const distinctId = await getDistinctId();
|
||||
await postJson(`${POSTHOG_HOST}/capture/`, {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
event: LOADER_DECISION_EVENT,
|
||||
distinct_id: distinctId,
|
||||
properties: {
|
||||
bundle,
|
||||
attempted_bundle: options.attemptedBundle ?? bundle,
|
||||
fallback: options.fallback,
|
||||
double_failure: options.doubleFailure,
|
||||
error_message: options.errorMessage,
|
||||
// Launch-cadence distribution: how long promotions take to reach real
|
||||
// windows tells us how fast the rollout percentage can safely be dialed.
|
||||
ms_since_last_activation: options.msSinceLastActivation,
|
||||
override: options.override,
|
||||
loader_version: context.extension.packageJSON?.version,
|
||||
// Separates nightly traffic from the (future) stable combined VSIX.
|
||||
extension_name: context.extension.packageJSON?.name,
|
||||
vscode_version: vscode.version,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import path from "node:path";
|
||||
import * as vscode from "vscode";
|
||||
import type { Bundle } from "./cohort";
|
||||
|
||||
/**
|
||||
* Wrap the real ExtensionContext so a bundle living under `<vsix root>/<sub>/`
|
||||
* resolves extension-root-relative resources (webview-ui build, walkthrough
|
||||
* assets, bundled codicons, ...) from its own subtree, without either codebase
|
||||
* knowing it was relocated.
|
||||
*
|
||||
* Only install-root properties are redirected. Storage-related properties
|
||||
* (globalState, workspaceState, secrets, globalStorageUri, storageUri, logUri)
|
||||
* intentionally pass through untouched: both bundles must keep sharing the
|
||||
* exact storage the standalone extension used, so user state survives cohort
|
||||
* changes and VSIX upgrades.
|
||||
*/
|
||||
export function scopedContext(
|
||||
context: vscode.ExtensionContext,
|
||||
sub: Bundle,
|
||||
): vscode.ExtensionContext {
|
||||
const extensionUri = vscode.Uri.joinPath(context.extensionUri, sub);
|
||||
const extensionPath = extensionUri.fsPath;
|
||||
|
||||
const scopedExtension = new Proxy(context.extension, {
|
||||
get(target, prop, _receiver) {
|
||||
if (prop === "extensionUri") {
|
||||
return extensionUri;
|
||||
}
|
||||
if (prop === "extensionPath") {
|
||||
return extensionPath;
|
||||
}
|
||||
const value = Reflect.get(target, prop, target);
|
||||
return typeof value === "function" ? value.bind(target) : value;
|
||||
},
|
||||
});
|
||||
|
||||
const overrides = new Map<PropertyKey, unknown>([
|
||||
["extensionUri", extensionUri],
|
||||
["extensionPath", extensionPath],
|
||||
[
|
||||
"asAbsolutePath",
|
||||
(relativePath: string) => path.join(extensionPath, relativePath),
|
||||
],
|
||||
["extension", scopedExtension],
|
||||
]);
|
||||
|
||||
return new Proxy(context, {
|
||||
get(target, prop, _receiver) {
|
||||
if (overrides.has(prop)) {
|
||||
return overrides.get(prop);
|
||||
}
|
||||
const value = Reflect.get(target, prop, target);
|
||||
return typeof value === "function" ? value.bind(target) : value;
|
||||
},
|
||||
}) as vscode.ExtensionContext;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node", "vscode"],
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
@@ -63,7 +63,7 @@ service ModelsService {
|
||||
rpc readProviderConfig(StringRequest) returns (ProviderConfigResponse);
|
||||
// Writes provider configuration fields and returns redacted effective configuration
|
||||
rpc writeProviderConfig(WriteProviderConfigRequest) returns (ProviderConfigResponse);
|
||||
// Commits a mode-specific model selection atomically with its model metadata
|
||||
// Commits a mode-specific model ID with optional user-authored metadata overrides
|
||||
rpc commitModelSelection(CommitModelSelectionRequest) returns (Empty);
|
||||
}
|
||||
|
||||
@@ -122,6 +122,36 @@ message OpenRouterModelInfo {
|
||||
optional ApiFormat api_format = 16;
|
||||
}
|
||||
|
||||
// User-authored per-model metadata stored in models.json.
|
||||
//
|
||||
// Semantics:
|
||||
// - `capabilities` accepts only the SDK ModelCapability values (e.g.
|
||||
// "images", "tools", "prompt-cache", "reasoning", "files"); unknown
|
||||
// strings are silently dropped by the host. The array is additive over
|
||||
// the base metadata; the explicit supports_* booleans win when both are
|
||||
// present.
|
||||
// - `is_r1_format_required` is a legacy alias that forces the R1 chat
|
||||
// format only when true; `api_format` is canonical.
|
||||
// - Invalid numbers (non-positive token limits, negative prices or
|
||||
// temperature, non-finite values) are silently discarded, not rejected.
|
||||
message ModelOverrides {
|
||||
optional string name = 1;
|
||||
optional int64 max_tokens = 2;
|
||||
optional int64 context_window = 3;
|
||||
optional int64 max_input_tokens = 4;
|
||||
repeated string capabilities = 5;
|
||||
optional bool supports_vision = 6;
|
||||
optional bool supports_attachments = 7;
|
||||
optional bool supports_reasoning = 8;
|
||||
optional double input_price = 9;
|
||||
optional double output_price = 10;
|
||||
optional double cache_reads_price = 11;
|
||||
optional double cache_writes_price = 12;
|
||||
optional double temperature = 13;
|
||||
optional ApiFormat api_format = 14;
|
||||
optional bool is_r1_format_required = 15;
|
||||
}
|
||||
|
||||
// Shared response message for model information
|
||||
message OpenRouterCompatibleModelInfo {
|
||||
map<string, OpenRouterModelInfo> models = 1;
|
||||
@@ -222,12 +252,16 @@ message ProviderConfigResponse {
|
||||
optional CommittedModelSelection act_selection = 11;
|
||||
optional AwsProviderConfig aws = 12;
|
||||
optional GcpProviderConfig gcp = 13;
|
||||
// Provider-level context window (providers.json `contextWindow`). Used by
|
||||
// bring-your-own-model providers (e.g. Ollama, where it maps to num_ctx).
|
||||
optional int32 context_window = 14;
|
||||
}
|
||||
|
||||
message CommittedModelSelection {
|
||||
string provider_id = 1;
|
||||
string model_id = 2;
|
||||
OpenRouterModelInfo model_info = 3;
|
||||
optional ModelOverrides overrides = 4;
|
||||
}
|
||||
|
||||
message ProviderReasoningPatch {
|
||||
@@ -249,6 +283,8 @@ message WriteProviderConfigPatch {
|
||||
optional bool clear_headers = 10;
|
||||
optional AwsProviderConfig aws = 11;
|
||||
optional GcpProviderConfig gcp = 12;
|
||||
// Provider-level context window; 0 clears the setting.
|
||||
optional int32 context_window = 13;
|
||||
}
|
||||
|
||||
message WriteProviderConfigRequest {
|
||||
@@ -257,10 +293,18 @@ message WriteProviderConfigRequest {
|
||||
}
|
||||
|
||||
message CommitModelSelectionRequest {
|
||||
// Field 4 carried `OpenRouterModelInfo model_info` in earlier releases.
|
||||
// Reusing the number with a different message type mis-decodes on version
|
||||
// skew, so the retired field stays reserved.
|
||||
reserved 4;
|
||||
reserved "model_info";
|
||||
string provider_id = 1;
|
||||
string mode = 2;
|
||||
string model_id = 3;
|
||||
OpenRouterModelInfo model_info = 4;
|
||||
// Tri-state: ABSENT leaves the model's stored overrides unchanged, an
|
||||
// explicitly EMPTY message clears them, and a populated message replaces
|
||||
// them wholesale (no per-field merge).
|
||||
optional ModelOverrides overrides = 5;
|
||||
}
|
||||
|
||||
message ClineRecommendedModel {
|
||||
|
||||
@@ -16,6 +16,9 @@ service TaskService {
|
||||
rpc cancelQueuedPrompt(StringRequest) returns (Empty);
|
||||
// Cancels the currently running background command
|
||||
rpc cancelBackgroundCommand(EmptyRequest) returns (Empty);
|
||||
// Detaches the running foreground terminal command ("Proceed While Running"):
|
||||
// the agent receives the partial output and a log file path for the rest.
|
||||
rpc proceedWhileRunningCommand(EmptyRequest) returns (Empty);
|
||||
// Clears the current task
|
||||
rpc clearTask(EmptyRequest) returns (Empty);
|
||||
// Gets the total size of all tasks
|
||||
|
||||
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import type { EffectiveProviderConfig, ProviderCatalog, ProviderConfigStore } from "@/sdk/model-catalog/contracts"
|
||||
import { computeConfigFingerprint } from "@/sdk/model-catalog/fingerprint"
|
||||
import { parseProviderId } from "@/sdk/model-catalog/provider-id"
|
||||
import { ApiFormat, OpenRouterModelInfo } from "@/shared/proto/cline/models"
|
||||
import { ApiFormat, ModelOverrides } from "@/shared/proto/cline/models"
|
||||
import type { ProviderCatalogController } from "../providerCatalogShared"
|
||||
|
||||
type TestStateManager = {
|
||||
@@ -153,6 +153,22 @@ describe("provider model catalog handlers", () => {
|
||||
baseUrl: "https://api.example.com/v1",
|
||||
auth: { accessToken: "SECRET_SENTINEL_ACCESS", refreshToken: "SECRET_SENTINEL_REFRESH", accountId: "acct-1" },
|
||||
})
|
||||
vi.mocked(store.readSelection).mockImplementation((_providerId, mode) =>
|
||||
mode === "act"
|
||||
? {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
capabilities: ["tools", "custom-capability"],
|
||||
inputPrice: 1.25,
|
||||
supportsVision: false,
|
||||
temperature: 0.3,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
},
|
||||
modelInfo: { name: "Custom model", contextWindow: 64_000, supportsPromptCache: false },
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
const controller = makeController(store, makeCatalog())
|
||||
|
||||
const response = await readProviderConfig(controller, { value: "cline" })
|
||||
@@ -165,10 +181,24 @@ describe("provider model catalog handlers", () => {
|
||||
hasRefreshToken: true,
|
||||
accountId: "acct-1",
|
||||
})
|
||||
expect(JSON.stringify(response)).not.toContain("SECRET_SENTINEL")
|
||||
expect(response.actSelection).toMatchObject({
|
||||
providerId: "cline",
|
||||
modelId: "custom-model",
|
||||
modelInfo: { name: "Custom model", contextWindow: 64_000 },
|
||||
overrides: {
|
||||
capabilities: ["tools", "custom-capability"],
|
||||
inputPrice: 1.25,
|
||||
supportsVision: false,
|
||||
temperature: 0.3,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
},
|
||||
})
|
||||
expect(JSON.stringify(response)).not.toContain("SECRET_SENTINEL_API_KEY")
|
||||
expect(JSON.stringify(response)).not.toContain("SECRET_SENTINEL_ACCESS")
|
||||
expect(JSON.stringify(response)).not.toContain("SECRET_SENTINEL_REFRESH")
|
||||
})
|
||||
|
||||
it("writeProviderConfig writes a patch and returns redacted updated config", async () => {
|
||||
it("writeProviderConfig writes a patch and returns a redacted response", async () => {
|
||||
const { writeProviderConfig } = await import("../writeProviderConfig")
|
||||
const providerId = parseProviderId("ollama")
|
||||
const updatedConfig: EffectiveProviderConfig = {
|
||||
@@ -188,8 +218,10 @@ describe("provider model catalog handlers", () => {
|
||||
apiKey: "SECRET_SENTINEL_OLLAMA",
|
||||
baseUrl: "http://localhost:11434/v1",
|
||||
})
|
||||
expect(response.apiKeyLength).toBe("SECRET_SENTINEL_OLLAMA".length)
|
||||
expect(JSON.stringify(response)).not.toContain("SECRET_SENTINEL")
|
||||
expect(response).toMatchObject({
|
||||
apiKeyLength: "SECRET_SENTINEL_OLLAMA".length,
|
||||
})
|
||||
expect(JSON.stringify(response)).not.toContain("SECRET_SENTINEL_OLLAMA")
|
||||
})
|
||||
|
||||
it("writeProviderConfig can explicitly clear headers", async () => {
|
||||
@@ -210,7 +242,7 @@ describe("provider model catalog handlers", () => {
|
||||
expect(store.write).toHaveBeenCalledWith(providerId, { headers: {} })
|
||||
})
|
||||
|
||||
it("commitModelSelection validates mode and commits the full selection envelope", async () => {
|
||||
it("commitModelSelection validates mode and commits model settings", async () => {
|
||||
const { commitModelSelection } = await import("../commitModelSelection")
|
||||
const providerId = parseProviderId("deepseek")
|
||||
const store = makeStore({ providerId })
|
||||
@@ -224,22 +256,20 @@ describe("provider model catalog handlers", () => {
|
||||
providerId: "deepseek",
|
||||
mode: "act",
|
||||
modelId: "deepseek-v4-flash",
|
||||
modelInfo: OpenRouterModelInfo.create({
|
||||
overrides: ModelOverrides.create({
|
||||
name: "DeepSeek V4 Flash",
|
||||
contextWindow: 456,
|
||||
supportsPromptCache: true,
|
||||
apiFormat: ApiFormat.OPENAI_CHAT,
|
||||
capabilities: ["prompt-cache"],
|
||||
}),
|
||||
})
|
||||
|
||||
expect(store.commitSelection).toHaveBeenCalledWith(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "deepseek-v4-flash",
|
||||
modelInfo: expect.objectContaining({
|
||||
overrides: expect.objectContaining({
|
||||
name: "DeepSeek V4 Flash",
|
||||
contextWindow: 456,
|
||||
supportsPromptCache: true,
|
||||
apiFormat: ApiFormat.OPENAI_CHAT,
|
||||
capabilities: ["prompt-cache"],
|
||||
}),
|
||||
})
|
||||
expect(stateManager.setGlobalStateBatch).toHaveBeenCalledWith({
|
||||
@@ -249,6 +279,49 @@ describe("provider model catalog handlers", () => {
|
||||
expect(stateManager.flushPendingState).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// The overrides field is tri-state: absent preserves the model's stored
|
||||
// overrides, an explicitly empty message clears them, and a populated
|
||||
// message replaces them. The two boundary cases are pinned here because
|
||||
// the webview relies on both (see useProviderConfig.test.ts).
|
||||
it("commitModelSelection maps an ABSENT overrides field to undefined (preserve stored overrides)", async () => {
|
||||
const { commitModelSelection } = await import("../commitModelSelection")
|
||||
const providerId = parseProviderId("deepseek")
|
||||
const store = makeStore({ providerId })
|
||||
const controller = makeController(store, makeCatalog())
|
||||
|
||||
await commitModelSelection(controller, {
|
||||
providerId: "deepseek",
|
||||
mode: "act",
|
||||
modelId: "deepseek-v4-flash",
|
||||
})
|
||||
|
||||
expect(store.commitSelection).toHaveBeenCalledWith(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "deepseek-v4-flash",
|
||||
overrides: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("commitModelSelection maps an EMPTY overrides message to an empty object (clear stored overrides)", async () => {
|
||||
const { commitModelSelection } = await import("../commitModelSelection")
|
||||
const providerId = parseProviderId("deepseek")
|
||||
const store = makeStore({ providerId })
|
||||
const controller = makeController(store, makeCatalog())
|
||||
|
||||
await commitModelSelection(controller, {
|
||||
providerId: "deepseek",
|
||||
mode: "act",
|
||||
modelId: "deepseek-v4-flash",
|
||||
overrides: ModelOverrides.create({}),
|
||||
})
|
||||
|
||||
expect(store.commitSelection).toHaveBeenCalledWith(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "deepseek-v4-flash",
|
||||
overrides: {},
|
||||
})
|
||||
})
|
||||
|
||||
it("commitModelSelection reports provider changes when config is initialized", async () => {
|
||||
const { commitModelSelection } = await import("../commitModelSelection")
|
||||
const providerId = parseProviderId("deepseek")
|
||||
@@ -268,10 +341,7 @@ describe("provider model catalog handlers", () => {
|
||||
providerId: "deepseek",
|
||||
mode: "act",
|
||||
modelId: "deepseek-v4-flash",
|
||||
modelInfo: OpenRouterModelInfo.create({
|
||||
name: "DeepSeek V4 Flash",
|
||||
apiFormat: ApiFormat.OPENAI_CHAT,
|
||||
}),
|
||||
overrides: ModelOverrides.create({ name: "DeepSeek V4 Flash" }),
|
||||
})
|
||||
|
||||
expect(handleApiConfigurationChanged).toHaveBeenCalledWith({}, { actModeApiProvider: "deepseek" })
|
||||
@@ -289,7 +359,7 @@ describe("provider model catalog handlers", () => {
|
||||
providerId: "deepseek",
|
||||
mode: "invalid",
|
||||
modelId: "deepseek-v4-flash",
|
||||
modelInfo: OpenRouterModelInfo.create({ supportsPromptCache: true }),
|
||||
overrides: ModelOverrides.create({ capabilities: ["prompt-cache"] }),
|
||||
}),
|
||||
).rejects.toThrow('mode must be "plan" or "act"')
|
||||
expect(store.commitSelection).not.toHaveBeenCalled()
|
||||
|
||||
@@ -75,7 +75,6 @@ describe("provider model catalog backend smoke", () => {
|
||||
providerId: "deepseek",
|
||||
mode: "act",
|
||||
modelId,
|
||||
modelInfo,
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ import type {
|
||||
EffectiveProviderConfig,
|
||||
Mode,
|
||||
ModelSelection,
|
||||
ModelSelectionOverrides,
|
||||
ProviderCatalog,
|
||||
ProviderConfigPatch,
|
||||
ProviderConfigStore,
|
||||
ProviderId,
|
||||
ProviderListing,
|
||||
ProviderModelsResult,
|
||||
ResolvedModelSelection,
|
||||
} from "@/sdk/model-catalog/contracts"
|
||||
import { parseProviderId } from "@/sdk/model-catalog/provider-id"
|
||||
import {
|
||||
@@ -17,13 +19,15 @@ import {
|
||||
CommitModelSelectionRequest,
|
||||
CommittedModelSelection,
|
||||
GcpProviderConfig,
|
||||
ModelOverrides as ModelOverridesProto,
|
||||
OpenRouterModelInfo,
|
||||
ProviderConfigResponse,
|
||||
ProviderListing as ProviderListingProto,
|
||||
ProviderModelsResponse,
|
||||
WriteProviderConfigPatch,
|
||||
} from "@/shared/proto/cline/models"
|
||||
import { fromProtobufModelInfo, toProtobufModelInfo } from "@/shared/proto-conversions/models/typeConversion"
|
||||
import { fromProtobufModelOverrides, toProtobufModelOverrides } from "@/shared/proto-conversions/models/modelOverrides"
|
||||
import { toProtobufModelInfo } from "@/shared/proto-conversions/models/typeConversion"
|
||||
import type { GlobalStateAndSettings } from "@/shared/storage/state-keys"
|
||||
|
||||
export interface ProviderCatalogController {
|
||||
@@ -94,7 +98,11 @@ function toProtobufModels(models: ReadonlyMap<string, ModelInfo>): Record<string
|
||||
return result
|
||||
}
|
||||
|
||||
function toCommittedModelSelectionProto(selection: ModelSelection | undefined): CommittedModelSelection | undefined {
|
||||
function toModelOverridesProto(overrides: ModelSelectionOverrides | undefined): ModelOverridesProto | undefined {
|
||||
return overrides ? toProtobufModelOverrides(overrides) : undefined
|
||||
}
|
||||
|
||||
function toCommittedModelSelectionProto(selection: ResolvedModelSelection | undefined): CommittedModelSelection | undefined {
|
||||
if (!selection) {
|
||||
return undefined
|
||||
}
|
||||
@@ -102,6 +110,7 @@ function toCommittedModelSelectionProto(selection: ModelSelection | undefined):
|
||||
providerId: selection.providerId,
|
||||
modelId: selection.modelId,
|
||||
modelInfo: toProtobufModelInfo(selection.modelInfo),
|
||||
overrides: toModelOverridesProto(selection.overrides),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -199,6 +208,7 @@ export function toRedactedProviderConfigResponse(
|
||||
actSelection: toCommittedModelSelectionProto(store?.readSelection(config.providerId, "act")),
|
||||
aws: toRedactedAwsProviderConfigProto(config.aws),
|
||||
gcp: toRedactedGcpProviderConfigProto(config.gcp),
|
||||
contextWindow: config.contextWindow,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -218,6 +228,10 @@ export function toProviderConfigPatch(protoPatch: WriteProviderConfigPatch | und
|
||||
...(protoPatch.apiLine !== undefined ? { apiLine: protoPatch.apiLine } : {}),
|
||||
...(protoPatch.aws !== undefined ? { aws: toAwsProviderConfigPatch(protoPatch) } : {}),
|
||||
...(protoPatch.gcp !== undefined ? { gcp: toGcpProviderConfigPatch(protoPatch) } : {}),
|
||||
// A zero context window over the wire means "clear the setting".
|
||||
...(protoPatch.contextWindow !== undefined
|
||||
? { contextWindow: protoPatch.contextWindow > 0 ? protoPatch.contextWindow : null }
|
||||
: {}),
|
||||
...(protoPatch.accessToken !== undefined || protoPatch.refreshToken !== undefined || protoPatch.accountId !== undefined
|
||||
? {
|
||||
auth: {
|
||||
@@ -241,17 +255,18 @@ export function toProviderConfigPatch(protoPatch: WriteProviderConfigPatch | und
|
||||
}
|
||||
}
|
||||
|
||||
function toSelectionOverrides(overrides: ModelOverridesProto | undefined): ModelSelectionOverrides | undefined {
|
||||
return fromProtobufModelOverrides(overrides)
|
||||
}
|
||||
|
||||
export function toModelSelection(request: CommitModelSelectionRequest, providerId: ProviderId): ModelSelection {
|
||||
const modelId = request.modelId.trim()
|
||||
if (!modelId) {
|
||||
throw new Error("model_id is required")
|
||||
}
|
||||
if (!request.modelInfo) {
|
||||
throw new Error("model_info is required")
|
||||
}
|
||||
return {
|
||||
providerId,
|
||||
modelId,
|
||||
modelInfo: fromProtobufModelInfo(request.modelInfo),
|
||||
overrides: toSelectionOverrides(request.overrides),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ import { type ProviderCatalogController, parseProviderIdRequest } from "./provid
|
||||
* Resolution order:
|
||||
*
|
||||
* 1. Committed selection — the user's most-recently-chosen plan/act
|
||||
* selection in the provider config store. This is the source of
|
||||
* truth for dynamic-list providers (openrouter, openai-compatible,
|
||||
* ollama, lmstudio, requesty, litellm, …) where the picker writes
|
||||
* the live `ModelInfo` into the selection when the user commits.
|
||||
* model ID resolved against SDK catalog metadata, the picker's state
|
||||
* snapshot, and stored overrides by the provider config store. A
|
||||
* selection whose metadata is pure fallback fabrication (no catalog or
|
||||
* state base, no overrides) is deferred behind the catalog steps below
|
||||
* and only returned as a last resort.
|
||||
*
|
||||
* 2. Catalog peek — a non-fetching look-up of the catalog cache for
|
||||
* the provider's current effective config fingerprint. Hits when
|
||||
@@ -41,23 +42,24 @@ export async function resolveModelInfo(
|
||||
const requestedModelId = request.modelId?.trim() || ""
|
||||
|
||||
const store = controller.getProviderConfigStore()
|
||||
// A committed selection whose metadata is pure fallback fabrication (no
|
||||
// catalog/state base and no user overrides) must not shadow the live
|
||||
// catalog below; it is kept only as a last resort before "unknown".
|
||||
let fallbackSelection: ReturnType<typeof store.readSelection>
|
||||
if (requestedModelId) {
|
||||
const actSelection = store.readSelection(providerId, "act")
|
||||
if (actSelection?.modelId === requestedModelId) {
|
||||
for (const mode of ["act", "plan"] as const) {
|
||||
const selection = store.readSelection(providerId, mode)
|
||||
if (selection?.modelId !== requestedModelId) {
|
||||
continue
|
||||
}
|
||||
if (selection.modelInfoSource === "fallback" && !selection.overrides) {
|
||||
fallbackSelection ??= selection
|
||||
continue
|
||||
}
|
||||
return ResolveModelInfoResponse.create({
|
||||
providerId,
|
||||
modelId: actSelection.modelId,
|
||||
modelInfo: toProtobufModelInfo(actSelection.modelInfo),
|
||||
source: "committed-selection",
|
||||
})
|
||||
}
|
||||
|
||||
const planSelection = store.readSelection(providerId, "plan")
|
||||
if (planSelection?.modelId === requestedModelId) {
|
||||
return ResolveModelInfoResponse.create({
|
||||
providerId,
|
||||
modelId: planSelection.modelId,
|
||||
modelInfo: toProtobufModelInfo(planSelection.modelInfo),
|
||||
modelId: selection.modelId,
|
||||
modelInfo: toProtobufModelInfo(selection.modelInfo),
|
||||
source: "committed-selection",
|
||||
})
|
||||
}
|
||||
@@ -74,7 +76,9 @@ export async function resolveModelInfo(
|
||||
const cached = catalog.peekModels(providerId)
|
||||
if (cached?.ok) {
|
||||
const hit = pickFromCatalog(cached, requestedModelId, allowCustomModelIds)
|
||||
if (hit) {
|
||||
// A default-model substitution answers a question about a different
|
||||
// model; the committed selection, even fallback-grade, is closer.
|
||||
if (hit && (hit.matchedRequested || !fallbackSelection)) {
|
||||
return ResolveModelInfoResponse.create({
|
||||
providerId,
|
||||
modelId: hit.modelId,
|
||||
@@ -90,7 +94,7 @@ export async function resolveModelInfo(
|
||||
const resolved = await catalog.resolveModels(providerId).catch(() => undefined)
|
||||
if (resolved?.ok) {
|
||||
const hit = pickFromCatalog(resolved, requestedModelId, allowCustomModelIds)
|
||||
if (hit) {
|
||||
if (hit && (hit.matchedRequested || !fallbackSelection)) {
|
||||
return ResolveModelInfoResponse.create({
|
||||
providerId,
|
||||
modelId: hit.modelId,
|
||||
@@ -100,6 +104,15 @@ export async function resolveModelInfo(
|
||||
}
|
||||
}
|
||||
|
||||
if (fallbackSelection) {
|
||||
return ResolveModelInfoResponse.create({
|
||||
providerId,
|
||||
modelId: fallbackSelection.modelId,
|
||||
modelInfo: toProtobufModelInfo(fallbackSelection.modelInfo),
|
||||
source: "committed-selection",
|
||||
})
|
||||
}
|
||||
|
||||
return ResolveModelInfoResponse.create({
|
||||
providerId,
|
||||
modelId: requestedModelId,
|
||||
|
||||
@@ -25,6 +25,7 @@ export async function getStateToPostToWebview(controller: {
|
||||
mcpHub?: any
|
||||
backgroundCommandRunning?: boolean
|
||||
backgroundCommandTaskId?: string
|
||||
foregroundCommandRunning?: boolean
|
||||
workspaceManager?: any
|
||||
checkpointRestoreInput?: ExtensionState["checkpointRestoreInput"]
|
||||
}): Promise<ExtensionState> {
|
||||
@@ -157,6 +158,7 @@ export async function getStateToPostToWebview(controller: {
|
||||
favoritedModelIds,
|
||||
backgroundCommandRunning: controller.backgroundCommandRunning ?? false,
|
||||
backgroundCommandTaskId: controller.backgroundCommandTaskId,
|
||||
foregroundCommandRunning: controller.foregroundCommandRunning ?? false,
|
||||
workspaceRoots: controller.workspaceManager?.getRoots?.() ?? [],
|
||||
primaryRootIndex: controller.workspaceManager?.getPrimaryIndex?.() ?? 0,
|
||||
isMultiRootWorkspace: (controller.workspaceManager?.getRoots?.()?.length ?? 0) > 1,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* "Proceed While Running": detach the in-flight foreground terminal command(s)
|
||||
* so the agent turn continues with the partial output while the commands keep
|
||||
* running in the user's terminal, streaming further output to a log file.
|
||||
*/
|
||||
export async function proceedWhileRunningCommand(controller: Controller, _request: EmptyRequest): Promise<Empty> {
|
||||
const controllerWithProceed = controller as Controller & {
|
||||
proceedWhileRunningCommand: () => Promise<void>
|
||||
}
|
||||
await controllerWithProceed.proceedWhileRunningCommand()
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -15,8 +15,9 @@ Designed to be driven from an agentic loop via `curl` commands.
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start the debug harness server
|
||||
bun src/dev/debug-harness/server.ts --auto-launch --skip-build
|
||||
# Terminal 1: Start the debug harness server.
|
||||
# Run with node, NOT bun — Playwright's Electron launch times out under bun.
|
||||
node src/dev/debug-harness/server.ts --auto-launch --skip-build
|
||||
|
||||
# Terminal 2: Interact via curl
|
||||
curl localhost:19229/api -d '{"method":"status"}'
|
||||
@@ -27,7 +28,7 @@ curl localhost:19229/api -d '{"method":"ui.screenshot"}'
|
||||
## Server Options
|
||||
|
||||
```
|
||||
bun src/dev/debug-harness/server.ts [options]
|
||||
node src/dev/debug-harness/server.ts [options]
|
||||
|
||||
Options:
|
||||
--skip-build Skip building extension/webview (use existing dist/)
|
||||
@@ -42,7 +43,7 @@ Options:
|
||||
```bash
|
||||
# This builds protos, extension (unminified+sourcemaps), webview (unminified+sourcemaps),
|
||||
# downloads VSCode, launches it, and connects CDP to the extension host.
|
||||
bun src/dev/debug-harness/server.ts --auto-launch
|
||||
node src/dev/debug-harness/server.ts --auto-launch
|
||||
```
|
||||
|
||||
## Data Isolation
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bun
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Debug Harness Server
|
||||
@@ -10,7 +10,12 @@
|
||||
* - UI automation (click, type, screenshot) via Playwright
|
||||
*
|
||||
* Usage:
|
||||
* bun src/dev/debug-harness/server.ts [options]
|
||||
* node src/dev/debug-harness/server.ts [options]
|
||||
*
|
||||
* Run with node, not bun: Playwright's _electron.launch() never finishes
|
||||
* attaching to the debugee under bun (the Electron process starts, but the
|
||||
* launch times out), while the same launch works under node. Node >= 22.6
|
||||
* runs this file directly via type stripping.
|
||||
*
|
||||
* Options:
|
||||
* --skip-build Skip building extension/webview
|
||||
@@ -39,7 +44,6 @@ import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { downloadAndUnzipVSCode, SilentReporter } from "@vscode/test-electron"
|
||||
import { _electron, type CDPSession, type ElectronApplication, type Frame, type Page } from "playwright"
|
||||
import WebSocket from "ws"
|
||||
|
||||
const __script_dir = typeof __dirname !== "undefined" ? __dirname : path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
@@ -201,19 +205,21 @@ class CdpClient {
|
||||
|
||||
async connect(wsUrl: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// The runtime's built-in WebSocket (browser-style events), so the
|
||||
// harness has no dependency on the `ws` package.
|
||||
const ws = new WebSocket(wsUrl)
|
||||
ws.on("open", () => {
|
||||
ws.addEventListener("open", () => {
|
||||
this.ws = ws
|
||||
resolve()
|
||||
})
|
||||
ws.on("error", (e: Error) => {
|
||||
if (!this.ws) reject(e)
|
||||
ws.addEventListener("error", () => {
|
||||
if (!this.ws) reject(new Error(`WebSocket connection failed: ${wsUrl}`))
|
||||
})
|
||||
ws.on("close", () => {
|
||||
ws.addEventListener("close", () => {
|
||||
this.ws = null
|
||||
})
|
||||
ws.on("message", (raw: WebSocket.Data) => {
|
||||
const msg = JSON.parse(raw.toString())
|
||||
ws.addEventListener("message", (event: MessageEvent) => {
|
||||
const msg = JSON.parse(typeof event.data === "string" ? event.data : Buffer.from(event.data).toString())
|
||||
if (msg.id !== undefined) {
|
||||
const p = this.pending.get(msg.id)
|
||||
if (p) {
|
||||
|
||||
@@ -694,4 +694,41 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
;(emitSpy as sinon.SinonSpy).calledWith("line", "line 3 continued").should.be.true()
|
||||
processAny.buffer.should.equal("")
|
||||
})
|
||||
|
||||
it("detach emits continue but keeps line listeners attached and listening", () => {
|
||||
const processAny = process as any
|
||||
const continueEvents: number[] = []
|
||||
const lines: string[] = []
|
||||
process.on("continue", () => continueEvents.push(1))
|
||||
process.on("line", (line) => lines.push(line))
|
||||
|
||||
process.detach()
|
||||
continueEvents.length.should.equal(1)
|
||||
|
||||
// Unlike continue(), detach must not stop listening or drop 'line'
|
||||
// listeners: output after detach still reaches subscribers (this is
|
||||
// what streams the rest of a detached command to the log file).
|
||||
processAny.isListening.should.be.true()
|
||||
processAny.emitIfEol("after detach\n")
|
||||
lines.should.containEql("after detach")
|
||||
})
|
||||
|
||||
it("detach flushes a buffered partial line before emitting continue", () => {
|
||||
const processAny = process as any
|
||||
const events: string[] = []
|
||||
process.on("continue", () => events.push("continue"))
|
||||
process.on("line", (line) => events.push(`line:${line}`))
|
||||
|
||||
// A chunk with no trailing newline stays in the internal buffer.
|
||||
processAny.emitIfEol("partial output")
|
||||
processAny.buffer.should.equal("partial output")
|
||||
|
||||
process.detach()
|
||||
|
||||
// The partial line must reach listeners before 'continue' resolves the
|
||||
// awaited promise; otherwise it is missing from the partial output and
|
||||
// from the log's initial flush.
|
||||
events.should.eql(["line:partial output", "continue"])
|
||||
processAny.buffer.should.equal("")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
TRUNCATE_KEEP_LINES,
|
||||
} from "@/integrations/terminal/constants"
|
||||
import type { ITerminalProcess, TerminalCompletionDetails, TerminalProcessEvents } from "@/integrations/terminal/types"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { MarkerlessCompletionCause } from "@/services/telemetry/TelemetryService"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Osc633EventType, Osc633Parser } from "./osc633Parser"
|
||||
import { classifyShellPrompt, getLastLine } from "./shellPromptHeuristics"
|
||||
|
||||
@@ -522,6 +522,23 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
|
||||
this.emit("continue")
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the awaited promise while the command keeps running ("Proceed
|
||||
* While Running"). Listeners stay attached and 'line' events keep
|
||||
* flowing — unlike continue() — so callers can stream the remaining
|
||||
* output until the command actually completes. Because 'completed' is
|
||||
* only emitted by the read loop when the command genuinely ends, the
|
||||
* terminal stays busy and is not eligible for reuse until then.
|
||||
*/
|
||||
detach() {
|
||||
// Flush any partial line (no trailing newline yet) so it reaches
|
||||
// listeners before the awaited promise resolves; otherwise it would be
|
||||
// dropped from both the partial output and the log capture if the
|
||||
// command exits without further newline-terminated output.
|
||||
this.emitRemainingBufferIfListening()
|
||||
this.emit("continue")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output that hasn't been retrieved yet.
|
||||
* Truncates if output is too large to prevent context window overflow.
|
||||
|
||||
@@ -63,10 +63,17 @@ export interface ITerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
/**
|
||||
* Continue execution without waiting for completion.
|
||||
* Stops event emission and resolves the promise.
|
||||
* This is called when user clicks "Proceed While Running".
|
||||
*/
|
||||
continue(): void
|
||||
|
||||
/**
|
||||
* Resolve the awaited promise while the command keeps running ("Proceed
|
||||
* While Running"). Unlike continue(), output listeners stay attached and
|
||||
* 'line'/'completed' events keep flowing, so callers can stream the rest
|
||||
* of the output (e.g. to a log file) until the command completes.
|
||||
*/
|
||||
detach(): void
|
||||
|
||||
/**
|
||||
* Get output that hasn't been retrieved yet.
|
||||
* @returns The unretrieved output
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
import { SdkCompactionCoordinator } from "./sdk-compaction-coordinator"
|
||||
import { SdkDiffEditCoordinator } from "./sdk-diff-edit-coordinator"
|
||||
import { SdkFollowupCoordinator } from "./sdk-followup-coordinator"
|
||||
import { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import { SdkInteractionCoordinator } from "./sdk-interaction-coordinator"
|
||||
import { SdkMcpCoordinator } from "./sdk-mcp-coordinator"
|
||||
import { SdkMessageCoordinator, type SessionEventListener } from "./sdk-message-coordinator"
|
||||
@@ -204,6 +205,15 @@ export class Controller {
|
||||
// standalone (JetBrains/CLI) host run commands through the SDK's built-in tool.
|
||||
private _terminalManager?: VscodeTerminalManager
|
||||
|
||||
// Registry of in-flight foreground (VS Code terminal) command executions.
|
||||
// Owned here — not by the session — so it survives session rebuilds, which
|
||||
// recreate the tool set. Drives the "Proceed While Running" button.
|
||||
private readonly foregroundCommands = new SdkForegroundCommandCoordinator({
|
||||
onRunningChanged: () => {
|
||||
void this.postStateToWebview()
|
||||
},
|
||||
})
|
||||
|
||||
// Private state kept for stub compatibility
|
||||
private backgroundCommandRunning = false
|
||||
private backgroundCommandTaskId?: string
|
||||
@@ -330,6 +340,7 @@ export class Controller {
|
||||
},
|
||||
onDidBecomeIdle: () => this.handleSessionBecameIdle(),
|
||||
getRemoteConfigIntegration: () => this.remoteConfigCoreIntegration,
|
||||
foregroundCommands: this.foregroundCommands,
|
||||
getTerminalManager: () => {
|
||||
// Guarded by getEffectiveTerminalExecutionMode() at the read sites
|
||||
// (vscode-session-host.ts, sdk-terminal-execution-mode-coordinator.ts):
|
||||
@@ -1174,6 +1185,19 @@ export class Controller {
|
||||
stubWarn("cancelBackgroundCommand")
|
||||
}
|
||||
|
||||
/**
|
||||
* "Proceed While Running": detach every in-flight foreground terminal
|
||||
* command. Each pending run_commands call returns its partial output plus
|
||||
* the log file path the remaining output is redirected to, and the agent
|
||||
* turn continues while the commands keep running in their terminals.
|
||||
*/
|
||||
async proceedWhileRunningCommand(): Promise<void> {
|
||||
const detached = this.foregroundCommands.proceedWhileRunning()
|
||||
if (detached === 0) {
|
||||
Logger.warn("[SdkController] proceedWhileRunningCommand: No foreground command is running")
|
||||
}
|
||||
}
|
||||
|
||||
async cancelQueuedPrompt(promptId: string): Promise<void> {
|
||||
const trimmedPromptId = promptId.trim()
|
||||
if (!trimmedPromptId) {
|
||||
@@ -1868,6 +1892,7 @@ export class Controller {
|
||||
mcpHub: this.mcpHub,
|
||||
backgroundCommandRunning: this.backgroundCommandRunning,
|
||||
backgroundCommandTaskId: this.backgroundCommandTaskId,
|
||||
foregroundCommandRunning: this.foregroundCommands.isRunning,
|
||||
})
|
||||
const sdkTaskHistory = (await this.taskHistory.listHistory({ limit: 100, hydrate: false }))
|
||||
.map(sessionHistoryRecordToHistoryItem)
|
||||
|
||||
@@ -2,6 +2,9 @@ import fs from "node:fs"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import type { CoreSessionConfig } from "@cline/core"
|
||||
import * as LlmsModels from "@cline/llms"
|
||||
import { ApiFormat } from "@shared/proto/cline/models"
|
||||
import { Logger } from "@shared/services/Logger"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import {
|
||||
buildResumeSessionInput,
|
||||
@@ -15,9 +18,12 @@ import {
|
||||
resolveApiKey,
|
||||
updateHistoryItem,
|
||||
} from "./cline-session-factory"
|
||||
import { parseProviderId } from "./model-catalog/provider-id"
|
||||
import { createProviderConfigStore } from "./model-catalog/store"
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const providerSettingsManager = {
|
||||
getFilePath: vi.fn(() => path.join(tempDir, "settings", "providers.json")),
|
||||
getLastUsedProviderSettings: vi.fn(() => undefined),
|
||||
getProviderSettings: vi.fn((_providerId?: string) => undefined),
|
||||
saveProviderSettings: vi.fn(),
|
||||
@@ -39,6 +45,9 @@ const mocks = vi.hoisted(() => {
|
||||
}
|
||||
return undefined
|
||||
}),
|
||||
setGlobalStateBatch: vi.fn(),
|
||||
setGlobalState: vi.fn(),
|
||||
setSecret: vi.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -72,11 +81,14 @@ vi.mock("@shared/services/Logger", () => ({
|
||||
|
||||
let tempDir: string
|
||||
const previousGlobalSettingsPath = process.env.CLINE_GLOBAL_SETTINGS_PATH
|
||||
const previousDataDir = process.env.CLINE_DATA_DIR
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cline-session-factory-"))
|
||||
process.env.CLINE_DATA_DIR = tempDir
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = path.join(tempDir, "global-settings.json")
|
||||
vi.clearAllMocks()
|
||||
LlmsModels.resetRegistry()
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "anthropic",
|
||||
actModeApiModelId: "claude-sonnet-4-6",
|
||||
@@ -88,12 +100,14 @@ beforeEach(() => {
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
mocks.providerSettingsManager.getFilePath.mockReturnValue(path.join(tempDir, "settings", "providers.json"))
|
||||
mocks.providerSettingsManager.getLastUsedProviderSettings.mockReturnValue(undefined)
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.CLINE_GLOBAL_SETTINGS_PATH = previousGlobalSettingsPath
|
||||
process.env.CLINE_DATA_DIR = previousDataDir
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
@@ -135,6 +149,11 @@ describe("getDefaultModelIdForProvider", () => {
|
||||
expect(getDefaultModelIdForProvider("unknown-provider")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns no default for local-model-source providers so a cloud-catalog model is never silently selected", () => {
|
||||
expect(getDefaultModelIdForProvider("ollama")).toBeUndefined()
|
||||
expect(getDefaultModelIdForProvider("lmstudio")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("resolves the OpenAI Compatible default through the extension's openai alias", () => {
|
||||
// The extension stores the OpenAI Compatible provider as "openai" while
|
||||
// the SDK catalog keys it as "openai-compatible". toSdkProviderId bridges
|
||||
@@ -230,9 +249,11 @@ describe("normalizeSdkBaseUrl", () => {
|
||||
expect(normalizeSdkBaseUrl("openai-compatible", " ")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("uses provider catalog defaults to add the SDK endpoint path when the user supplies only an origin", () => {
|
||||
expect(normalizeSdkBaseUrl("ollama", "http://localhost:11434")).toBe("http://localhost:11434/v1")
|
||||
expect(normalizeSdkBaseUrl("ollama", "http://localhost:11434/")).toBe("http://localhost:11434/v1")
|
||||
it("passes Ollama origins through unchanged (the native-API vendor appends /api itself)", () => {
|
||||
expect(normalizeSdkBaseUrl("ollama", "http://localhost:11434")).toBe("http://localhost:11434")
|
||||
expect(normalizeSdkBaseUrl("ollama", "http://localhost:11434/")).toBe("http://localhost:11434/")
|
||||
// Legacy 4.0.x configs may carry the OpenAI-compat /v1 suffix; it is
|
||||
// preserved here and rewritten to /api by the vendor.
|
||||
expect(normalizeSdkBaseUrl("ollama", "http://localhost:11434/v1")).toBe("http://localhost:11434/v1")
|
||||
})
|
||||
|
||||
@@ -429,6 +450,40 @@ describe("buildSessionConfig", () => {
|
||||
expect(config.providerConfig).not.toHaveProperty("apiKey")
|
||||
})
|
||||
|
||||
it("preserves rich SDK catalog entries without extension-side replacement", async () => {
|
||||
const expectedModel = structuredClone((await LlmsModels.getModelsForProvider("anthropic"))["claude-sonnet-4-6"])
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "anthropic",
|
||||
actModeApiModelId: "claude-sonnet-4-6",
|
||||
apiKey: "anthropic-key",
|
||||
} as any)
|
||||
|
||||
const config = await buildSessionConfig({ cwd: "/tmp/workspace" })
|
||||
const knownModel = (config.providerConfig as any).knownModels["claude-sonnet-4-6"]
|
||||
|
||||
expect(knownModel).toEqual(expectedModel)
|
||||
expect(knownModel.capabilities).toEqual(
|
||||
expect.arrayContaining(["images", "files", "tools", "reasoning", "structured_output", "temperature", "prompt-cache"]),
|
||||
)
|
||||
expect(knownModel.pricing).toEqual(expectedModel.pricing)
|
||||
expect(knownModel.releaseDate).toBe(expectedModel.releaseDate)
|
||||
expect(knownModel.family).toBe(expectedModel.family)
|
||||
})
|
||||
|
||||
it("keeps session creation non-fatal when known-model lookup fails", async () => {
|
||||
const lookupError = new Error("registry unavailable")
|
||||
const getModelsSpy = vi.spyOn(LlmsModels, "getModelsForProvider").mockRejectedValueOnce(lookupError)
|
||||
|
||||
const config = await buildSessionConfig({ cwd: "/tmp/workspace" })
|
||||
|
||||
expect(config.providerConfig).not.toHaveProperty("knownModels")
|
||||
expect(Logger.warn).toHaveBeenCalledWith(
|
||||
"[SessionFactory] Failed to resolve known models for provider=anthropic:",
|
||||
lookupError,
|
||||
)
|
||||
getModelsSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("passes OpenAI Compatible max output tokens as an explicit request limit", async () => {
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "openai",
|
||||
@@ -451,11 +506,87 @@ describe("buildSessionConfig", () => {
|
||||
expect(config.providerId).toBe("openai-compatible")
|
||||
expect(config.modelId).toBe("custom-reasoner")
|
||||
expect(config.knownModels).toBeUndefined()
|
||||
expect((config.providerConfig as any).knownModels).toBeUndefined()
|
||||
expect((config.providerConfig as any).knownModels).toBeDefined()
|
||||
expect((config.providerConfig as any).maxOutputTokens).toBeUndefined()
|
||||
expect((config as any).maxTokensPerTurn).toBe(4_096)
|
||||
})
|
||||
|
||||
it("uses OpenAI Compatible overrides from models.json for runtime request settings", async () => {
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "openai",
|
||||
actModeOpenAiModelId: "custom-reasoner",
|
||||
openAiApiKey: "openai-compatible-key",
|
||||
openAiBaseUrl: "https://openai-compatible.example/v1",
|
||||
actModeOpenAiModelInfo: { supportsPromptCache: false },
|
||||
} as any)
|
||||
createProviderConfigStore().commitSelection(parseProviderId("openai"), "act", {
|
||||
providerId: parseProviderId("openai"),
|
||||
modelId: "custom-reasoner",
|
||||
overrides: {
|
||||
name: "Custom Reasoner",
|
||||
contextWindow: 16_000,
|
||||
maxInputTokens: 15_000,
|
||||
maxTokens: 1_234,
|
||||
capabilities: ["images", "reasoning", "streaming", "tools"],
|
||||
supportsVision: false,
|
||||
supportsAttachments: true,
|
||||
supportsReasoning: false,
|
||||
temperature: 0,
|
||||
inputPrice: 1,
|
||||
outputPrice: 2,
|
||||
cacheReadsPrice: 0.1,
|
||||
cacheWritesPrice: 0.5,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
},
|
||||
})
|
||||
|
||||
const config = await buildSessionConfig({ cwd: "/tmp/workspace" })
|
||||
const knownModel = (config.providerConfig as any).knownModels["custom-reasoner"]
|
||||
|
||||
expect(config.providerId).toBe("openai-compatible")
|
||||
expect(config.modelId).toBe("custom-reasoner")
|
||||
expect((config as any).maxTokensPerTurn).toBe(1_234)
|
||||
expect((config as any).temperature).toBe(0)
|
||||
expect(knownModel).toMatchObject({
|
||||
id: "custom-reasoner",
|
||||
name: "Custom Reasoner",
|
||||
contextWindow: 16_000,
|
||||
maxInputTokens: 15_000,
|
||||
maxTokens: 1_234,
|
||||
capabilities: ["streaming", "tools", "files"],
|
||||
apiFormat: "openai-responses",
|
||||
temperature: 0,
|
||||
pricing: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 0.5 },
|
||||
})
|
||||
})
|
||||
|
||||
it("keeps -1 OpenAI Compatible values out of request settings and fallback knownModels", async () => {
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "openai",
|
||||
actModeOpenAiModelId: "custom-reasoner",
|
||||
openAiApiKey: "openai-compatible-key",
|
||||
openAiBaseUrl: "https://openai-compatible.example/v1",
|
||||
actModeOpenAiModelInfo: { supportsPromptCache: false },
|
||||
} as any)
|
||||
createProviderConfigStore().commitSelection(parseProviderId("openai"), "act", {
|
||||
providerId: parseProviderId("openai"),
|
||||
modelId: "custom-reasoner",
|
||||
overrides: {
|
||||
name: "Custom Reasoner",
|
||||
maxTokens: -1,
|
||||
temperature: -1,
|
||||
},
|
||||
})
|
||||
|
||||
const config = await buildSessionConfig({ cwd: "/tmp/workspace" })
|
||||
|
||||
expect((config as any).maxTokensPerTurn).toBeUndefined()
|
||||
expect((config as any).temperature).toBeUndefined()
|
||||
const knownModel = (config.providerConfig as any).knownModels["custom-reasoner"]
|
||||
expect(knownModel).not.toHaveProperty("maxTokens")
|
||||
expect(knownModel).not.toHaveProperty("temperature", -1)
|
||||
})
|
||||
|
||||
it("passes OCA reasoning effort from legacy mode settings to SDK sessions", async () => {
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "oca",
|
||||
|
||||
@@ -17,7 +17,13 @@ import {
|
||||
resolveProviderApiKeyFromSettings,
|
||||
type StartSessionResult,
|
||||
} from "@cline/core"
|
||||
import { getGeneratedModelsForProvider, MODEL_COLLECTIONS_BY_PROVIDER_ID } from "@cline/llms"
|
||||
import type { ModelInfo as SdkModelInfo } from "@cline/llms"
|
||||
import {
|
||||
getGeneratedModelsForProvider,
|
||||
getModelsForProvider,
|
||||
MODEL_COLLECTIONS_BY_PROVIDER_ID,
|
||||
OLLAMA_DEFAULT_CONTEXT_WINDOW,
|
||||
} from "@cline/llms"
|
||||
import { buildClineSystemPrompt } from "@cline/shared"
|
||||
import type { ApiConfiguration } from "@shared/api"
|
||||
import { ClineClient } from "@shared/cline"
|
||||
@@ -37,7 +43,11 @@ import { FeatureFlag } from "@/shared/services/feature-flags/feature-flags"
|
||||
import { type BedrockProviderConfig, buildBedrockProviderConfig } from "./bedrock-config"
|
||||
import { buildAgentHooks } from "./hooks-adapter"
|
||||
import { readTaskHistory, resolveDataDir } from "./legacy-state-reader"
|
||||
import type { ResolvedModelSelection } from "./model-catalog/contracts"
|
||||
import { nonNegativeFiniteNumber, positiveFiniteNumber, toSdkApiFormat } from "./model-catalog/model-values"
|
||||
import { parseProviderId } from "./model-catalog/provider-id"
|
||||
import { toSdkProviderId } from "./model-catalog/sdk-provider-id"
|
||||
import { createProviderConfigStore, resolveRuntimeModelSelection } from "./model-catalog/store"
|
||||
import { getProviderSettingsManager } from "./provider-migration"
|
||||
import { buildSapProviderConfig, type SapProviderConfig } from "./sap-config"
|
||||
import type { SdkSessionHost } from "./session-host"
|
||||
@@ -209,8 +219,73 @@ function resolveOcaReasoningConfig(mode: Mode, apiConfig: ApiConfiguration | und
|
||||
|
||||
function resolveOpenAiCompatibleMaxTokens(config: ApiConfiguration | undefined, mode: Mode): number | undefined {
|
||||
const modelInfo = mode === "plan" ? config?.planModeOpenAiModelInfo : config?.actModeOpenAiModelInfo
|
||||
const maxTokens = modelInfo?.maxTokens
|
||||
return typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 ? maxTokens : undefined
|
||||
return positiveFiniteNumber(modelInfo?.maxTokens)
|
||||
}
|
||||
|
||||
function toSdkModelInfo(selection: ResolvedModelSelection): SdkModelInfo {
|
||||
const modelInfo = selection.modelInfo
|
||||
const capabilities = new Set<NonNullable<SdkModelInfo["capabilities"]>[number]>(
|
||||
(selection.overrides?.capabilities ?? []) as NonNullable<SdkModelInfo["capabilities"]>,
|
||||
)
|
||||
const setCapability = (capability: NonNullable<SdkModelInfo["capabilities"]>[number], enabled: boolean): void => {
|
||||
if (enabled) capabilities.add(capability)
|
||||
else capabilities.delete(capability)
|
||||
}
|
||||
if (modelInfo.supportsImages !== undefined) setCapability("images", modelInfo.supportsImages)
|
||||
setCapability("prompt-cache", modelInfo.supportsPromptCache)
|
||||
if (modelInfo.supportsReasoning !== undefined) setCapability("reasoning", modelInfo.supportsReasoning)
|
||||
if (selection.overrides?.supportsAttachments !== undefined) setCapability("files", selection.overrides.supportsAttachments)
|
||||
|
||||
const maxTokens = positiveFiniteNumber(modelInfo.maxTokens)
|
||||
const contextWindow = positiveFiniteNumber(modelInfo.contextWindow)
|
||||
const maxInputTokens = positiveFiniteNumber(selection.overrides?.maxInputTokens)
|
||||
const temperature = nonNegativeFiniteNumber(modelInfo.temperature)
|
||||
const inputPrice = nonNegativeFiniteNumber(modelInfo.inputPrice)
|
||||
const outputPrice = nonNegativeFiniteNumber(modelInfo.outputPrice)
|
||||
const cacheRead = nonNegativeFiniteNumber(modelInfo.cacheReadsPrice)
|
||||
const cacheWrite = nonNegativeFiniteNumber(modelInfo.cacheWritesPrice)
|
||||
const apiFormat = toSdkApiFormat(modelInfo.apiFormat)
|
||||
const hasPricing =
|
||||
inputPrice !== undefined || outputPrice !== undefined || cacheRead !== undefined || cacheWrite !== undefined
|
||||
|
||||
return {
|
||||
id: selection.modelId,
|
||||
name: modelInfo.name ?? selection.modelId,
|
||||
...(maxTokens !== undefined ? { maxTokens } : {}),
|
||||
...(contextWindow !== undefined ? { contextWindow } : {}),
|
||||
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
||||
...(capabilities.size > 0 ? { capabilities: [...capabilities] } : {}),
|
||||
...(apiFormat !== undefined ? { apiFormat } : {}),
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
...(hasPricing
|
||||
? {
|
||||
pricing: {
|
||||
...(inputPrice !== undefined ? { input: inputPrice } : {}),
|
||||
...(outputPrice !== undefined ? { output: outputPrice } : {}),
|
||||
...(cacheRead !== undefined ? { cacheRead } : {}),
|
||||
...(cacheWrite !== undefined ? { cacheWrite } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCommittedRuntimeModel(
|
||||
providerId: string,
|
||||
mode: Mode,
|
||||
modelId: string | undefined,
|
||||
): ResolvedModelSelection | undefined {
|
||||
if (!modelId) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
const parsedProviderId = parseProviderId(providerId)
|
||||
const selection = createProviderConfigStore().readSelection(parsedProviderId, mode)
|
||||
return selection?.modelId === modelId ? selection : resolveRuntimeModelSelection(parsedProviderId, modelId)
|
||||
} catch (error) {
|
||||
Logger.warn(`[SessionFactory] Failed to resolve committed model settings for provider=${providerId}:`, error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -303,8 +378,21 @@ const PROVIDER_MODEL_ID_MAP: Record<string, { plan: keyof ApiConfiguration; act:
|
||||
|
||||
const DEFAULT_PROVIDER_ID = "cline"
|
||||
|
||||
/**
|
||||
* Providers whose model list comes from a live local endpoint (Ollama's
|
||||
* `/api/tags`, LM Studio's `/v1/models`). Their installed models are the only
|
||||
* meaningful catalog; a bundled-catalog default would silently select a model
|
||||
* the user never installed (e.g. an Ollama Cloud nemotron model).
|
||||
*/
|
||||
function providerHasLocalModelSource(providerId: string): boolean {
|
||||
return Boolean(MODEL_COLLECTIONS_BY_PROVIDER_ID[toSdkProviderId(providerId)]?.provider.modelsSourceUrl)
|
||||
}
|
||||
|
||||
export function getDefaultModelIdForProvider(providerId: string): string | undefined {
|
||||
const sdkProviderId = toSdkProviderId(providerId)
|
||||
if (providerHasLocalModelSource(providerId)) {
|
||||
return undefined
|
||||
}
|
||||
const collection = MODEL_COLLECTIONS_BY_PROVIDER_ID[sdkProviderId]
|
||||
if (!collection) {
|
||||
return undefined
|
||||
@@ -479,6 +567,42 @@ export function resolveVertexProviderConfig(config: ApiConfiguration): Pick<Prov
|
||||
}
|
||||
}
|
||||
|
||||
type OllamaProviderConfig = {
|
||||
modelInfo?: { id: string; name: string; contextWindow: number }
|
||||
timeoutMs?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the user's "Model Context Window" setting for Ollama and surface it
|
||||
* as the selected model's `contextWindow`. The gateway carries it on the
|
||||
* resolved model definition, and the Ollama vendor maps it onto the wire as
|
||||
* `options.num_ctx` — without it Ollama loads every model with its 4096-token
|
||||
* server default. Keeping it on the model also means context management
|
||||
* budgets against the window Ollama actually applies (Ollama truncates the
|
||||
* prompt to `num_ctx` server-side).
|
||||
*/
|
||||
export function resolveOllamaProviderConfig(config: ApiConfiguration, modelId: string | undefined): OllamaProviderConfig {
|
||||
// providers.json (`contextWindow`) is the source of truth; the legacy
|
||||
// StateManager string is a migration fallback (the config store mirrors
|
||||
// writes to both).
|
||||
let settingsContextWindow: number | undefined
|
||||
try {
|
||||
const value = getProviderSettingsManager().getProviderSettings("ollama")?.contextWindow
|
||||
settingsContextWindow = typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined
|
||||
} catch {
|
||||
Logger.warn("[SessionFactory] Failed to read Ollama settings from providers.json")
|
||||
}
|
||||
const raw = config.ollamaApiOptionsCtxNum?.trim()
|
||||
const parsed = raw ? Number(raw) : Number.NaN
|
||||
const legacyContextWindow = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : undefined
|
||||
const contextWindow = settingsContextWindow ?? legacyContextWindow ?? OLLAMA_DEFAULT_CONTEXT_WINDOW
|
||||
const timeoutMs = config.requestTimeoutMs
|
||||
return {
|
||||
...(typeof timeoutMs === "number" && timeoutMs > 0 ? { timeoutMs } : {}),
|
||||
...(modelId ? { modelInfo: { id: modelId, name: modelId, contextWindow } } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveBaseUrl(providerId: string, config: ApiConfiguration): string | undefined {
|
||||
const baseUrlMap: Record<string, keyof ApiConfiguration> = {
|
||||
anthropic: "anthropicBaseUrl",
|
||||
@@ -536,6 +660,7 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
let bedrockProviderConfig: BedrockProviderConfig | undefined
|
||||
let vertexProviderConfig: Pick<ProviderSettings, "gcp" | "region"> | undefined
|
||||
let sapProviderConfig: SapProviderConfig | undefined
|
||||
let ollamaProviderConfig: ReturnType<typeof resolveOllamaProviderConfig> | undefined
|
||||
|
||||
try {
|
||||
const stateManager = StateManager.get()
|
||||
@@ -571,6 +696,10 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
baseUrl = sapProviderConfig.baseUrl
|
||||
}
|
||||
|
||||
if (providerId === "ollama") {
|
||||
ollamaProviderConfig = resolveOllamaProviderConfig(apiConfig, modelId)
|
||||
}
|
||||
|
||||
Logger.log(
|
||||
`[SessionFactory] Resolved from StateManager: provider=${providerId}, model=${modelId}, hasApiKey=${!!apiKey}`,
|
||||
)
|
||||
@@ -606,12 +735,31 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
// Final defaults. Keep this aligned with the provider catalog so the UI and
|
||||
// session factory share one source of truth for default models.
|
||||
providerId = providerId ?? DEFAULT_PROVIDER_ID
|
||||
modelId = modelId ?? getDefaultModelIdForProvider(providerId) ?? getDefaultModelIdForProvider(DEFAULT_PROVIDER_ID) ?? ""
|
||||
if (!modelId && providerHasLocalModelSource(providerId)) {
|
||||
// Local-model-source providers: the committed selection lives in
|
||||
// providers.json when the legacy state slot is empty (e.g. configs
|
||||
// created through the SDK settings store). Never fall through to a
|
||||
// catalog default — an empty model id surfaces an explicit "select a
|
||||
// model" state instead of silently running a model the user never chose.
|
||||
try {
|
||||
modelId = getProviderSettingsManager().getProviderSettings(providerSettingsProviderId(providerId))?.model?.trim()
|
||||
} catch {
|
||||
Logger.warn(`[SessionFactory] Failed to read ${providerId} model from providers.json`)
|
||||
}
|
||||
modelId = modelId || ""
|
||||
} else {
|
||||
modelId = modelId ?? getDefaultModelIdForProvider(providerId) ?? getDefaultModelIdForProvider(DEFAULT_PROVIDER_ID) ?? ""
|
||||
}
|
||||
if (!apiKey && apiConfig) {
|
||||
apiKey = resolveApiKey(providerId, apiConfig)
|
||||
}
|
||||
apiKey = apiKey ?? ""
|
||||
const maxTokensPerTurn = providerId === "openai" ? resolveOpenAiCompatibleMaxTokens(apiConfig, mode) : undefined
|
||||
const committedRuntimeModel = resolveCommittedRuntimeModel(providerId, mode, modelId)
|
||||
const overriddenMaxTokens = committedRuntimeModel?.overrides?.maxTokens
|
||||
const maxTokensPerTurn =
|
||||
positiveFiniteNumber(overriddenMaxTokens) ??
|
||||
(providerId === "openai" ? resolveOpenAiCompatibleMaxTokens(apiConfig, mode) : undefined)
|
||||
const temperature = nonNegativeFiniteNumber(committedRuntimeModel?.overrides?.temperature)
|
||||
const reasoningConfig =
|
||||
providerId === "oca"
|
||||
? (resolveOcaReasoningConfig(mode, apiConfig) ?? resolveProviderReasoningConfig(providerId))
|
||||
@@ -662,13 +810,34 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
const sdkProviderId = toSdkProviderId(providerId)
|
||||
const hostIdentity = await resolveHostIdentity()
|
||||
const isMultiRoot = await resolveIsMultiRootWorkspace()
|
||||
let knownModels: Awaited<ReturnType<typeof getModelsForProvider>> | undefined
|
||||
try {
|
||||
// Constructing the settings manager loads providers.json and models.json into
|
||||
// the @cline/llms registry. Reading models from that registry ensures custom
|
||||
// model overrides are included in the inference provider config, not just in
|
||||
// the webview/display path.
|
||||
getProviderSettingsManager(resolveDataDir())
|
||||
knownModels = await getModelsForProvider(sdkProviderId)
|
||||
// Only inject host-resolved metadata that carries real information
|
||||
// (catalog/state base or user overrides). Pure fallback fabrications
|
||||
// must not reach the runtime; the SDK's own resolution handles those.
|
||||
const isPureFallbackModel = committedRuntimeModel?.modelInfoSource === "fallback" && !committedRuntimeModel.overrides
|
||||
if (committedRuntimeModel && !isPureFallbackModel && !knownModels?.[modelId]) {
|
||||
knownModels = {
|
||||
...(knownModels ?? {}),
|
||||
[modelId]: toSdkModelInfo(committedRuntimeModel),
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.warn(`[SessionFactory] Failed to resolve known models for provider=${sdkProviderId}:`, error)
|
||||
}
|
||||
|
||||
// Always pass a providerConfig so the proxy/CA-aware fetch reaches the SDK
|
||||
// gateway; without it the agent loop uses bare global fetch and corporate
|
||||
// proxy/self-signed CA setups fail on JetBrains and CLI. Cloud providers
|
||||
// additionally need structured options (region/project/auth/SAP OAuth), which core
|
||||
// reads from providerConfig in createAgentModelFromConfig.
|
||||
const cloudProviderConfig = bedrockProviderConfig ?? vertexProviderConfig ?? sapProviderConfig
|
||||
const cloudProviderConfig = bedrockProviderConfig ?? vertexProviderConfig ?? sapProviderConfig ?? ollamaProviderConfig
|
||||
// Spread the cloud config first so the explicit fields below — notably the
|
||||
// proxy/CA-aware fetch — can never be clobbered if those types gain matching keys.
|
||||
const providerConfig = {
|
||||
@@ -677,6 +846,7 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
modelId,
|
||||
...(apiKey ? { apiKey } : {}),
|
||||
...(baseUrl !== undefined ? { baseUrl } : {}),
|
||||
...(knownModels && Object.keys(knownModels).length > 0 ? { knownModels } : {}),
|
||||
fetch,
|
||||
}
|
||||
|
||||
@@ -707,6 +877,7 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
mode: mode === "plan" ? "plan" : "act",
|
||||
...reasoningConfig,
|
||||
...(maxTokensPerTurn !== undefined ? { maxTokensPerTurn } : {}),
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
maxIterations: undefined,
|
||||
logger: sdkLogger,
|
||||
extensionContext: {
|
||||
|
||||
@@ -3,10 +3,10 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import type {
|
||||
EffectiveProviderConfig,
|
||||
Fingerprint,
|
||||
ModelSelection,
|
||||
ProviderConfigChange,
|
||||
ProviderConfigReader,
|
||||
ProviderModelsResult,
|
||||
ResolvedModelSelection,
|
||||
} from "./contracts"
|
||||
import { computeConfigFingerprint } from "./fingerprint"
|
||||
import { parseProviderId } from "./provider-id"
|
||||
@@ -95,7 +95,7 @@ function record(
|
||||
}
|
||||
}
|
||||
|
||||
function makeReader(initialConfig: EffectiveProviderConfig, selection?: ModelSelection): TestReader {
|
||||
function makeReader(initialConfig: EffectiveProviderConfig, selection?: ResolvedModelSelection): TestReader {
|
||||
let config = initialConfig
|
||||
const listeners = new Set<(event: ProviderConfigChange) => void>()
|
||||
return {
|
||||
@@ -241,7 +241,7 @@ describe("ProviderCatalog Phase 3.2 resolveModels happy path", () => {
|
||||
})
|
||||
const providerId = parseProviderId("openrouter")
|
||||
const config: EffectiveProviderConfig = { providerId, apiKey: "secret", baseUrl: "https://provider.example.com" }
|
||||
const selection: ModelSelection = { providerId, modelId: "selected", modelInfo }
|
||||
const selection: ResolvedModelSelection = { providerId, modelId: "selected", modelInfo }
|
||||
const reader = makeReader(config, selection)
|
||||
const catalog = createProviderCatalog(reader)
|
||||
|
||||
@@ -507,7 +507,7 @@ describe("ProviderCatalog Phase 3.4 store-driven invalidation", () => {
|
||||
const providerId = parseProviderId("openrouter")
|
||||
const reader = makeReader({ providerId, apiKey: "same" })
|
||||
const catalog = createProviderCatalog(reader)
|
||||
const selection: ModelSelection = { providerId, modelId: "different", modelInfo }
|
||||
const selection: ResolvedModelSelection = { providerId, modelId: "different", modelInfo }
|
||||
|
||||
const first = await catalog.resolveModels(providerId)
|
||||
reader.emit({ kind: "selection", providerId, mode: "act", selection })
|
||||
@@ -692,7 +692,7 @@ describe("ProviderCatalog Phase 3.6 subscribe", () => {
|
||||
const reader = makeReader({ providerId, baseUrl: "http://localhost:11434/v1" })
|
||||
const catalog = createProviderCatalog(reader)
|
||||
const listener = vi.fn()
|
||||
const selection: ModelSelection = { providerId, modelId: "custom:latest", modelInfo }
|
||||
const selection: ResolvedModelSelection = { providerId, modelId: "custom:latest", modelInfo }
|
||||
catalog.subscribe(providerId, listener)
|
||||
|
||||
reader.emit({ kind: "selection", providerId, mode: "act", selection })
|
||||
|
||||
@@ -68,8 +68,8 @@ export type Fingerprint = string & { readonly [FingerprintBrand]: void }
|
||||
* - Two reads with no intervening write return structurally equal values.
|
||||
* - Consumers must not mutate. The shape is `Readonly`.
|
||||
*
|
||||
* Mode-dependent selection (modelId, modelInfo) is *not* part of this
|
||||
* type. Use `ProviderConfigStore.readSelection(providerId, mode)`.
|
||||
* Mode-dependent selection is *not* part of this type. Use
|
||||
* `ProviderConfigStore.readSelection(providerId, mode)`.
|
||||
*/
|
||||
export interface AwsProviderConfig {
|
||||
readonly accessKey?: string
|
||||
@@ -98,6 +98,12 @@ export interface EffectiveProviderConfig {
|
||||
readonly region?: string
|
||||
readonly aws?: AwsProviderConfig
|
||||
readonly gcp?: GcpProviderConfig
|
||||
/**
|
||||
* Provider-level context window (providers.json `contextWindow`).
|
||||
* Provider-neutral: for Ollama it maps to `options.num_ctx` at the
|
||||
* vendor boundary.
|
||||
*/
|
||||
readonly contextWindow?: number
|
||||
/**
|
||||
* OAuth-style auth bundle (e.g. cline provider's WorkOS token).
|
||||
* Compatible with `apiKey`; some providers populate both.
|
||||
@@ -119,9 +125,9 @@ export interface EffectiveProviderConfig {
|
||||
* A patch describing a field-level write to `ProviderConfigStore`.
|
||||
*
|
||||
* Invariant: `ProviderConfigPatch` cannot describe a model selection. The
|
||||
* type does not contain `modelId` or `modelInfo`. To write a selection,
|
||||
* use `commitSelection`, which is a structurally distinct method on the
|
||||
* store.
|
||||
* type does not contain `modelId` or per-model overrides. To write a
|
||||
* selection, use `commitSelection`, which is a structurally distinct method
|
||||
* on the store.
|
||||
*
|
||||
* Empty patches are allowed and are no-ops. A field present with value
|
||||
* `null` means "clear this field"; an absent field means "leave unchanged."
|
||||
@@ -140,6 +146,7 @@ export interface ProviderConfigPatch {
|
||||
readonly region?: string | null
|
||||
readonly aws?: AwsProviderConfig | null
|
||||
readonly gcp?: GcpProviderConfig | null
|
||||
readonly contextWindow?: number | null
|
||||
readonly auth?: {
|
||||
readonly accessToken?: string
|
||||
readonly refreshToken?: string
|
||||
@@ -153,22 +160,65 @@ export interface ProviderConfigPatch {
|
||||
// Model selection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Per-model metadata overrides authored by the user for custom models. */
|
||||
export interface ModelSelectionOverrides {
|
||||
readonly name?: string
|
||||
readonly maxTokens?: number
|
||||
readonly contextWindow?: number
|
||||
readonly maxInputTokens?: number
|
||||
readonly capabilities?: readonly string[]
|
||||
readonly supportsVision?: boolean
|
||||
readonly supportsAttachments?: boolean
|
||||
readonly supportsReasoning?: boolean
|
||||
readonly inputPrice?: number
|
||||
readonly outputPrice?: number
|
||||
readonly cacheReadsPrice?: number
|
||||
readonly cacheWritesPrice?: number
|
||||
readonly temperature?: number
|
||||
readonly apiFormat?: ModelInfo["apiFormat"]
|
||||
readonly isR1FormatRequired?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A user's committed model selection. The triple is atomic by type: every
|
||||
* write of `modelId` carries its `modelInfo` envelope, and vice versa.
|
||||
* A user's committed model selection. The committed write stores only the
|
||||
* selected model id plus optional user-authored overrides. `ModelInfo` is
|
||||
* derived by the host from the SDK catalog, then layered with `models.json`
|
||||
* overrides, then per-provider safe fallback metadata.
|
||||
*
|
||||
* Invariants:
|
||||
* - `modelInfo` was either taken from a `ProviderCatalog.resolveModels`
|
||||
* result, or constructed from per-provider safe defaults when the user
|
||||
* entered a custom id manually. Either way it represents the picker's
|
||||
* best knowledge at the moment of commit. The runtime uses it verbatim.
|
||||
* - The stored selection wins over later SDK catalog changes. Refresh
|
||||
* does not retroactively change committed selections.
|
||||
* - The webview does not commit a `ModelInfo` snapshot.
|
||||
* - Catalog metadata updates may affect the resolved `ModelInfo` for an
|
||||
* existing selection unless the user has explicitly overridden the field.
|
||||
*/
|
||||
export interface ModelSelection {
|
||||
readonly providerId: ProviderId
|
||||
readonly modelId: string
|
||||
readonly overrides?: ModelSelectionOverrides
|
||||
}
|
||||
|
||||
/** A committed selection as read back by consumers that need display/runtime metadata. */
|
||||
export interface ResolvedModelSelection extends ModelSelection {
|
||||
readonly modelInfo: ModelInfo
|
||||
/**
|
||||
* Where the base `modelInfo` came from, before overrides were applied:
|
||||
*
|
||||
* - "catalog" — SDK catalog metadata (generated snapshot or registry).
|
||||
* - "state" — the mode-specific `*ModeModelInfo` snapshot persisted by
|
||||
* the picker at selection time. Authoritative for dynamic-list providers
|
||||
* (openrouter, litellm, requesty, …) whose models are not in the static
|
||||
* catalog.
|
||||
* - "fallback" — provider-safe defaults fabricated because nothing better
|
||||
* was available. Consumers should treat a "fallback" resolution without
|
||||
* overrides as weak data and prefer live catalog lookups over it.
|
||||
*/
|
||||
readonly modelInfoSource?: "catalog" | "state" | "fallback"
|
||||
/**
|
||||
* The base metadata `modelInfo` was resolved from, before overrides were
|
||||
* applied. Persisted (rather than the resolved value) into the legacy
|
||||
* state snapshot so that deleting an override cannot resurrect it from a
|
||||
* snapshot it was previously baked into.
|
||||
*/
|
||||
readonly baseModelInfo?: ModelInfo
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -190,7 +240,7 @@ export type ProviderConfigChange =
|
||||
readonly kind: "selection"
|
||||
readonly providerId: ProviderId
|
||||
readonly mode: Mode
|
||||
readonly selection: ModelSelection
|
||||
readonly selection: ResolvedModelSelection
|
||||
}
|
||||
|
||||
export type ProviderConfigChangeListener = (event: ProviderConfigChange) => void
|
||||
@@ -317,7 +367,7 @@ export interface ProviderModelsEvent {
|
||||
*/
|
||||
export interface ProviderConfigReader {
|
||||
read(providerId: ProviderId): EffectiveProviderConfig
|
||||
readSelection(providerId: ProviderId, mode: Mode): ModelSelection | undefined
|
||||
readSelection(providerId: ProviderId, mode: Mode): ResolvedModelSelection | undefined
|
||||
subscribe(listener: ProviderConfigChangeListener): Disposable
|
||||
}
|
||||
|
||||
@@ -351,8 +401,9 @@ export interface ProviderConfigStore extends ProviderConfigReader {
|
||||
write(providerId: ProviderId, patch: ProviderConfigPatch): EffectiveProviderConfig
|
||||
|
||||
/**
|
||||
* Commit a model selection atomically with its info envelope. The only
|
||||
* entry point that writes `{providerId, modelId, modelInfo}` triples.
|
||||
* Commit a model ID atomically with optional user-authored overrides.
|
||||
* Supplying overrides replaces that model's stored override entry; omitting
|
||||
* them leaves the existing entry unchanged.
|
||||
*
|
||||
* I2: refresh handlers do not have access to this method by type, since
|
||||
* `ProviderCatalog` holds only a `ProviderConfigReader`.
|
||||
|
||||
@@ -54,7 +54,27 @@ describe("buildEffectiveProviderConfig", () => {
|
||||
providerId: parseProviderId("ollama"),
|
||||
apiKey: "provider-ollama-key",
|
||||
baseUrl: "http://state-ollama:11434",
|
||||
extras: { ollamaApiOptionsCtxNum: "8192" },
|
||||
// The legacy state string surfaces as the provider-neutral
|
||||
// contextWindow when providers.json has none.
|
||||
contextWindow: 8192,
|
||||
})
|
||||
})
|
||||
|
||||
it("prefers the providers.json contextWindow over the legacy Ollama state key", async () => {
|
||||
const { buildEffectiveProviderConfig } = await import("./effective-config")
|
||||
mocks.setProviderSettings({
|
||||
ollama: {
|
||||
provider: "ollama",
|
||||
contextWindow: 65536,
|
||||
},
|
||||
})
|
||||
mocks.setApiConfiguration({
|
||||
ollamaApiOptionsCtxNum: "8192",
|
||||
})
|
||||
|
||||
expect(buildEffectiveProviderConfig(parseProviderId("ollama"))).toEqual({
|
||||
providerId: parseProviderId("ollama"),
|
||||
contextWindow: 65536,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ type ProviderSettingsLike = {
|
||||
readonly region?: string
|
||||
readonly aws?: AwsProviderConfig
|
||||
readonly gcp?: GcpProviderConfig
|
||||
readonly contextWindow?: number
|
||||
readonly auth?: AuthConfig
|
||||
readonly extras?: ExtrasConfig
|
||||
}
|
||||
@@ -102,7 +103,6 @@ const headerFields: Partial<Record<string, keyof ApiConfiguration>> = {
|
||||
}
|
||||
|
||||
const extrasFields: Partial<Record<string, Partial<Record<string, keyof ApiConfiguration>>>> = {
|
||||
ollama: { ollamaApiOptionsCtxNum: "ollamaApiOptionsCtxNum" },
|
||||
lmstudio: { lmStudioMaxTokens: "lmStudioMaxTokens" },
|
||||
litellm: { liteLlmUsePromptCache: "liteLlmUsePromptCache" },
|
||||
openrouter: { openRouterProviderSorting: "openRouterProviderSorting" },
|
||||
@@ -157,6 +157,14 @@ function readBoolean(record: Record<string, unknown>, key: string): boolean | un
|
||||
return typeof value === "boolean" ? value : undefined
|
||||
}
|
||||
|
||||
function readPositiveInteger(value: unknown): number | undefined {
|
||||
const parsed = typeof value === "string" ? Number(value) : value
|
||||
if (typeof parsed === "number" && Number.isFinite(parsed) && parsed > 0) {
|
||||
return Math.floor(parsed)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function readGcp(record: Record<string, unknown>): GcpProviderConfig | undefined {
|
||||
const gcp = record.gcp
|
||||
if (!isPlainRecord(gcp)) {
|
||||
@@ -206,6 +214,7 @@ function readProviderSettings(providerId: ProviderId): ConfigParts {
|
||||
region: readString(settings, "region"),
|
||||
aws: readAws(settings),
|
||||
gcp: readGcp(settings),
|
||||
contextWindow: readPositiveInteger(settings.contextWindow),
|
||||
auth: readAuth(settings),
|
||||
extras: isPlainRecord(settings.extras) ? settings.extras : undefined,
|
||||
} satisfies ProviderSettingsLike
|
||||
@@ -298,6 +307,16 @@ function readStateAws(provider: string, config: ApiConfiguration): AwsProviderCo
|
||||
return Object.values(aws).some((value) => value !== undefined) ? aws : undefined
|
||||
}
|
||||
|
||||
function readStateContextWindow(provider: string, config: ApiConfiguration): number | undefined {
|
||||
// Only Ollama has a legacy context-window state key; other providers keep
|
||||
// theirs in providers.json exclusively.
|
||||
if (provider !== "ollama") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return readPositiveInteger(config.ollamaApiOptionsCtxNum)
|
||||
}
|
||||
|
||||
function readStateConfig(providerId: ProviderId, config: ApiConfiguration): ConfigParts {
|
||||
const provider = providerId.toString()
|
||||
return {
|
||||
@@ -308,6 +327,7 @@ function readStateConfig(providerId: ProviderId, config: ApiConfiguration): Conf
|
||||
region: readStringFromConfig(config, regionFields[provider]),
|
||||
aws: readStateAws(provider, config),
|
||||
gcp: readStateGcp(provider, config),
|
||||
contextWindow: readStateContextWindow(provider, config),
|
||||
auth: readStateAuth(provider, config),
|
||||
extras: readStateExtras(provider, config),
|
||||
}
|
||||
@@ -372,6 +392,10 @@ export function buildEffectiveProviderConfig(providerId: ProviderId): EffectiveP
|
||||
// fields as a fallback for old installs, but let providers.json win when both exist.
|
||||
assignIfDefined(merged, "aws", mergeAws(stateConfig.aws, providerSettings.aws))
|
||||
assignIfDefined(merged, "gcp", mergeGcp(stateConfig.gcp, providerSettings.gcp))
|
||||
// providers.json is the source of truth for the context window; the legacy
|
||||
// Ollama StateManager key is a migration fallback (the store mirrors writes
|
||||
// to both).
|
||||
assignIfDefined(merged, "contextWindow", providerSettings.contextWindow ?? stateConfig.contextWindow)
|
||||
assignIfDefined(merged, "auth", stateConfig.auth ?? providerSettings.auth)
|
||||
assignIfDefined(merged, "extras", mergeExtras(providerSettings.extras, stateConfig.extras))
|
||||
|
||||
|
||||
@@ -170,6 +170,9 @@ export function computeConfigFingerprint(providerId: ProviderId, config: Effecti
|
||||
region: config.region ?? null,
|
||||
aws: sanitizeAws(config.aws),
|
||||
gcp: sanitizeGcp(config.gcp),
|
||||
// The context window is not a secret; include it raw so changes
|
||||
// invalidate cached model lists.
|
||||
contextWindow: config.contextWindow ?? null,
|
||||
extras: sanitizeExtras(config.extras),
|
||||
auth: {
|
||||
accountId: config.auth?.accountId ?? null,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Applies the `ModelInfo` fields the extension owns locally, on top of
|
||||
* an adapted SDK `ModelInfo`. Today this is just Vertex's
|
||||
* `supportsGlobalEndpoint` allowlist (see `./vertex-global-endpoint.ts`).
|
||||
* an adapted SDK `ModelInfo`. Today this is Vertex's
|
||||
* `supportsGlobalEndpoint` allowlist (see `./vertex-global-endpoint.ts`)
|
||||
* and Ollama's effective context window.
|
||||
*
|
||||
* Both the model-list resolution path (`resolveSdkModels`) and the
|
||||
* single-model lookup path (`resolveModelInfo`) pass adapted
|
||||
@@ -10,13 +11,52 @@
|
||||
* flags upstream, the override and this file can be removed together.
|
||||
*/
|
||||
|
||||
import { OLLAMA_DEFAULT_CONTEXT_WINDOW } from "@cline/llms"
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { getProviderSettingsManager } from "../provider-migration"
|
||||
import type { ProviderId } from "./contracts"
|
||||
import { vertexModelSupportsGlobalEndpoint } from "./vertex-global-endpoint"
|
||||
|
||||
/**
|
||||
* The context window Ollama actually applies is the requested `num_ctx`,
|
||||
* not the model's native maximum — Ollama truncates the prompt to it
|
||||
* server-side. Surface the user's "Model Context Window" setting (or the
|
||||
* request default) instead of catalog/safe-default values so the chat
|
||||
* indicator and context management match reality.
|
||||
*/
|
||||
function resolveOllamaContextWindow(): number {
|
||||
// providers.json (`contextWindow`) is the source of truth; the legacy
|
||||
// StateManager string is a migration fallback (the config store mirrors
|
||||
// writes to both).
|
||||
try {
|
||||
const value = getProviderSettingsManager().getProviderSettings("ollama")?.contextWindow
|
||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||
return Math.floor(value)
|
||||
}
|
||||
} catch {
|
||||
// providers.json unavailable — fall through to the legacy state key.
|
||||
}
|
||||
try {
|
||||
const raw = StateManager.get().getApiConfiguration().ollamaApiOptionsCtxNum?.trim()
|
||||
if (raw) {
|
||||
const value = Number(raw)
|
||||
if (Number.isFinite(value) && value > 0) {
|
||||
return Math.floor(value)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// StateManager unavailable (e.g. tests) — fall through to the default.
|
||||
}
|
||||
return OLLAMA_DEFAULT_CONTEXT_WINDOW
|
||||
}
|
||||
|
||||
export function applyHostModelInfoOverrides(providerId: ProviderId, modelId: string, modelInfo: ModelInfo): ModelInfo {
|
||||
if (providerId === "vertex" && vertexModelSupportsGlobalEndpoint(providerId, modelId)) {
|
||||
return { ...modelInfo, supportsGlobalEndpoint: true }
|
||||
}
|
||||
if (providerId === "ollama") {
|
||||
return { ...modelInfo, contextWindow: resolveOllamaContextWindow() }
|
||||
}
|
||||
return modelInfo
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import { ApiFormat } from "@shared/proto/cline/models"
|
||||
|
||||
/** SDK string spelling of an API format (matches @cline/shared ApiFormatSchema). */
|
||||
export type SdkApiFormatString = "r1" | "openai-responses" | "default"
|
||||
|
||||
export function finiteNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
||||
}
|
||||
|
||||
export function positiveFiniteNumber(value: unknown): number | undefined {
|
||||
const number = finiteNumber(value)
|
||||
return number !== undefined && number > 0 ? number : undefined
|
||||
}
|
||||
|
||||
export function nonNegativeFiniteNumber(value: unknown): number | undefined {
|
||||
const number = finiteNumber(value)
|
||||
return number !== undefined && number >= 0 ? number : undefined
|
||||
}
|
||||
|
||||
export function toSdkApiFormat(apiFormat: ModelInfo["apiFormat"]): SdkApiFormatString | undefined {
|
||||
switch (apiFormat) {
|
||||
case ApiFormat.R1_CHAT:
|
||||
return "r1"
|
||||
case ApiFormat.OPENAI_RESPONSES:
|
||||
return "openai-responses"
|
||||
case ApiFormat.OPENAI_CHAT:
|
||||
return "default"
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function fromSdkApiFormat(apiFormat: string | undefined): ModelInfo["apiFormat"] | undefined {
|
||||
switch (apiFormat) {
|
||||
case "r1":
|
||||
return ApiFormat.R1_CHAT
|
||||
case "openai-responses":
|
||||
return ApiFormat.OPENAI_RESPONSES
|
||||
case "default":
|
||||
return ApiFormat.OPENAI_CHAT
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { ApiConfiguration, ModelInfo } from "@shared/api"
|
||||
import { syncStoredProviderRegistration } from "@cline/core"
|
||||
import { type ApiConfiguration, type ModelInfo, openAiModelInfoSafeDefaults } from "@shared/api"
|
||||
import { ApiFormat } from "@shared/proto/cline/models"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import type { ProviderConfigChange } from "./contracts"
|
||||
import { parseProviderId } from "./provider-id"
|
||||
@@ -7,6 +9,11 @@ const mocks = vi.hoisted(() => {
|
||||
type MockApiConfiguration = ApiConfiguration & { planActSeparateModelsSetting?: boolean }
|
||||
let apiConfiguration: MockApiConfiguration = {}
|
||||
let providerSettingsById: Record<string, Record<string, unknown>> = {}
|
||||
let generatedModelsByProvider: Record<string, Record<string, ModelInfo>> = {}
|
||||
let modelsFile: { version: 1; providers: Record<string, { models?: Record<string, Record<string, unknown>> }> } = {
|
||||
version: 1,
|
||||
providers: {},
|
||||
}
|
||||
const saveProviderSettings = vi.fn((settings: Record<string, unknown>, _options?: { setLastUsed?: boolean }) => {
|
||||
const provider = settings.provider
|
||||
if (typeof provider !== "string") {
|
||||
@@ -20,6 +27,8 @@ const mocks = vi.hoisted(() => {
|
||||
reset(): void {
|
||||
apiConfiguration = {}
|
||||
providerSettingsById = {}
|
||||
generatedModelsByProvider = {}
|
||||
modelsFile = { version: 1, providers: {} }
|
||||
saveProviderSettings.mockClear()
|
||||
},
|
||||
setApiConfiguration(value: MockApiConfiguration): void {
|
||||
@@ -28,6 +37,12 @@ const mocks = vi.hoisted(() => {
|
||||
setProviderSettings(value: Record<string, Record<string, unknown>>): void {
|
||||
providerSettingsById = { ...value }
|
||||
},
|
||||
setGeneratedModels(providerId: string, models: Record<string, ModelInfo>): void {
|
||||
generatedModelsByProvider = { ...generatedModelsByProvider, [providerId]: models }
|
||||
},
|
||||
getGeneratedModels(providerId: string): Record<string, ModelInfo> {
|
||||
return generatedModelsByProvider[providerId] ?? {}
|
||||
},
|
||||
getSavedProviderSettings(providerId: string): Record<string, unknown> | undefined {
|
||||
return providerSettingsById[providerId]
|
||||
},
|
||||
@@ -37,6 +52,12 @@ const mocks = vi.hoisted(() => {
|
||||
getSaveProviderSettingsMock(): typeof saveProviderSettings {
|
||||
return saveProviderSettings
|
||||
},
|
||||
getModelsFile() {
|
||||
return modelsFile
|
||||
},
|
||||
setModelsFile(value: typeof modelsFile): void {
|
||||
modelsFile = value
|
||||
},
|
||||
getStateManager() {
|
||||
return {
|
||||
getApiConfiguration: () => ({ ...apiConfiguration }),
|
||||
@@ -69,11 +90,24 @@ vi.mock("../provider-migration", () => ({
|
||||
getProviderSettingsManager: mocks.getProviderSettingsManager,
|
||||
}))
|
||||
|
||||
vi.mock("@cline/core", () => ({
|
||||
syncStoredProviderRegistration: vi.fn(),
|
||||
readModelsFileSync: vi.fn(() => mocks.getModelsFile()),
|
||||
resolveModelsRegistryPath: vi.fn(() => "/tmp/models.json"),
|
||||
writeModelsFileSync: vi.fn((_filePath: string, state: ReturnType<typeof mocks.getModelsFile>) => mocks.setModelsFile(state)),
|
||||
}))
|
||||
|
||||
vi.mock("@cline/llms", () => ({
|
||||
getGeneratedModelsForProvider: vi.fn((providerId: string) => mocks.getGeneratedModels(providerId)),
|
||||
MODEL_COLLECTIONS_BY_PROVIDER_ID: {},
|
||||
}))
|
||||
|
||||
const modelInfoA: ModelInfo = {
|
||||
name: "Model A",
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 8_192,
|
||||
supportsPromptCache: true,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
}
|
||||
|
||||
const modelInfoB: ModelInfo = {
|
||||
@@ -83,9 +117,41 @@ const modelInfoB: ModelInfo = {
|
||||
supportsPromptCache: false,
|
||||
}
|
||||
|
||||
function selectionFromModelInfo(providerId: ReturnType<typeof parseProviderId>, modelId: string, modelInfo: ModelInfo) {
|
||||
const capabilities: string[] = []
|
||||
if (modelInfo.supportsPromptCache) capabilities.push("prompt-cache")
|
||||
if (modelInfo.supportsImages) capabilities.push("images")
|
||||
if (modelInfo.supportsReasoning) capabilities.push("reasoning")
|
||||
return {
|
||||
providerId,
|
||||
modelId,
|
||||
overrides: {
|
||||
name: modelInfo.name,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
...(modelInfo.apiFormat !== undefined ? { apiFormat: modelInfo.apiFormat } : {}),
|
||||
...(capabilities.length > 0 ? { capabilities } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function expectResolvedSelection(
|
||||
actual: unknown,
|
||||
selection: ReturnType<typeof selectionFromModelInfo>,
|
||||
modelInfo: ModelInfo,
|
||||
): void {
|
||||
expect(actual).toMatchObject({
|
||||
providerId: selection.providerId,
|
||||
modelId: selection.modelId,
|
||||
overrides: selection.overrides,
|
||||
modelInfo,
|
||||
})
|
||||
}
|
||||
|
||||
describe("createProviderConfigStore", () => {
|
||||
beforeEach(() => {
|
||||
mocks.reset()
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
@@ -121,22 +187,29 @@ describe("createProviderConfigStore", () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openrouter")
|
||||
const selection = { providerId, modelId: "anthropic/claude-sonnet-4", modelInfo: modelInfoA }
|
||||
const selection = selectionFromModelInfo(providerId, "anthropic/claude-sonnet-4", modelInfoA)
|
||||
|
||||
store.commitSelection(providerId, "act", selection)
|
||||
|
||||
expect(store.readSelection(providerId, "act")).toEqual(selection)
|
||||
expectResolvedSelection(store.readSelection(providerId, "act"), selection, modelInfoA)
|
||||
expect(mocks.getModelsFile().providers.openrouter?.models?.["anthropic/claude-sonnet-4"]).toMatchObject({
|
||||
name: "Model A",
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 8_192,
|
||||
apiFormat: "openai-responses",
|
||||
capabilities: ["prompt-cache"],
|
||||
})
|
||||
})
|
||||
|
||||
it("round-trips generic provider selections using the in-process modelInfo envelope", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("deepseek")
|
||||
const selection = { providerId, modelId: "deepseek-v4-pro", modelInfo: modelInfoA }
|
||||
const selection = selectionFromModelInfo(providerId, "deepseek-v4-pro", modelInfoA)
|
||||
|
||||
store.commitSelection(providerId, "act", selection)
|
||||
|
||||
expect(store.readSelection(providerId, "act")).toEqual(selection)
|
||||
expectResolvedSelection(store.readSelection(providerId, "act"), selection, modelInfoA)
|
||||
})
|
||||
|
||||
it("hydrates a generic provider selection from providers.json after reload", async () => {
|
||||
@@ -148,6 +221,8 @@ describe("createProviderConfigStore", () => {
|
||||
expect(store.readSelection(providerId, "act")).toEqual({
|
||||
providerId,
|
||||
modelId: "manual-zai-model",
|
||||
modelInfoSource: "fallback",
|
||||
baseModelInfo: expect.objectContaining({ name: "manual-zai-model" }),
|
||||
modelInfo: expect.objectContaining({
|
||||
name: "manual-zai-model",
|
||||
supportsPromptCache: false,
|
||||
@@ -160,27 +235,27 @@ describe("createProviderConfigStore", () => {
|
||||
const store = createProviderConfigStore()
|
||||
const geminiProviderId = parseProviderId("gemini")
|
||||
const deepSeekProviderId = parseProviderId("deepseek")
|
||||
const geminiSelection = { providerId: geminiProviderId, modelId: "gemini-3.1-pro-preview", modelInfo: modelInfoA }
|
||||
const deepSeekSelection = { providerId: deepSeekProviderId, modelId: "deepseek-v4-pro", modelInfo: modelInfoB }
|
||||
const geminiSelection = selectionFromModelInfo(geminiProviderId, "gemini-3.1-pro-preview", modelInfoA)
|
||||
const deepSeekSelection = selectionFromModelInfo(deepSeekProviderId, "deepseek-v4-pro", modelInfoB)
|
||||
|
||||
store.commitSelection(geminiProviderId, "act", geminiSelection)
|
||||
store.commitSelection(deepSeekProviderId, "act", deepSeekSelection)
|
||||
|
||||
expect(store.readSelection(geminiProviderId, "act")).toEqual(geminiSelection)
|
||||
expect(store.readSelection(deepSeekProviderId, "act")).toEqual(deepSeekSelection)
|
||||
expectResolvedSelection(store.readSelection(geminiProviderId, "act"), geminiSelection, modelInfoA)
|
||||
expectResolvedSelection(store.readSelection(deepSeekProviderId, "act"), deepSeekSelection, modelInfoB)
|
||||
})
|
||||
|
||||
it("handles normalized nousResearch provider casing for writes and selections", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("nousResearch")
|
||||
const selection = { providerId, modelId: "nousresearch/hermes-4-70b", modelInfo: modelInfoA }
|
||||
const selection = selectionFromModelInfo(providerId, "nousresearch/hermes-4-70b", modelInfoA)
|
||||
|
||||
const written = store.write(providerId, { apiKey: "nous-key" })
|
||||
store.commitSelection(providerId, "act", selection)
|
||||
|
||||
expect(written).toEqual({ providerId, apiKey: "nous-key" })
|
||||
expect(store.readSelection(providerId, "act")).toEqual(selection)
|
||||
expectResolvedSelection(store.readSelection(providerId, "act"), selection, modelInfoA)
|
||||
expect(mocks.getSavedProviderSettings("nousResearch")).toMatchObject({
|
||||
provider: "nousResearch",
|
||||
apiKey: "nous-key",
|
||||
@@ -227,6 +302,179 @@ describe("createProviderConfigStore", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("lazily migrates meaningful legacy custom-model metadata once", async () => {
|
||||
const legacyModelInfo = {
|
||||
name: "Legacy Custom",
|
||||
maxTokens: 4_096,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 1,
|
||||
outputPrice: 2,
|
||||
cacheReadsPrice: 0.25,
|
||||
cacheWritesPrice: 0.5,
|
||||
temperature: 0.3,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
isR1FormatRequired: true,
|
||||
}
|
||||
mocks.setApiConfiguration({
|
||||
actModeOpenAiModelId: "legacy-custom",
|
||||
actModeOpenAiModelInfo: legacyModelInfo,
|
||||
})
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
const first = store.readSelection(providerId, "act")
|
||||
const second = store.readSelection(providerId, "act")
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["legacy-custom"]).toEqual({
|
||||
name: "Legacy Custom",
|
||||
maxTokens: 4_096,
|
||||
contextWindow: 64_000,
|
||||
capabilities: ["prompt-cache"],
|
||||
supportsVision: false,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 1,
|
||||
outputPrice: 2,
|
||||
cacheReadsPrice: 0.25,
|
||||
cacheWritesPrice: 0.5,
|
||||
temperature: 0.3,
|
||||
apiFormat: "openai-responses",
|
||||
isR1FormatRequired: true,
|
||||
})
|
||||
expect(first?.overrides).toEqual(second?.overrides)
|
||||
expect(first?.modelInfo).toMatchObject({
|
||||
name: "Legacy Custom",
|
||||
maxTokens: 4_096,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 1,
|
||||
outputPrice: 2,
|
||||
cacheReadsPrice: 0.25,
|
||||
cacheWritesPrice: 0.5,
|
||||
temperature: 0.3,
|
||||
apiFormat: ApiFormat.R1_CHAT,
|
||||
})
|
||||
expect(syncStoredProviderRegistration).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("does not create migration noise for legacy safe defaults", async () => {
|
||||
mocks.setApiConfiguration({
|
||||
actModeOpenAiModelId: "default-custom",
|
||||
actModeOpenAiModelInfo: { ...openAiModelInfoSafeDefaults, name: "default-custom" },
|
||||
})
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
const first = store.readSelection(providerId, "act")
|
||||
const second = store.readSelection(providerId, "act")
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["default-custom"]).toBeUndefined()
|
||||
expect(first?.overrides).toBeUndefined()
|
||||
expect(second?.overrides).toBeUndefined()
|
||||
expect(first?.modelInfo).toMatchObject({ contextWindow: 128_000, supportsImages: true, temperature: 0 })
|
||||
expect(first?.modelInfo.maxTokens).toBeUndefined()
|
||||
expect(syncStoredProviderRegistration).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("never overwrites an existing models.json entry during migration", async () => {
|
||||
mocks.setModelsFile({
|
||||
version: 1,
|
||||
providers: {
|
||||
"openai-compatible": { models: { "existing-custom": { temperature: 0.7 } } },
|
||||
},
|
||||
})
|
||||
mocks.setApiConfiguration({
|
||||
actModeOpenAiModelId: "existing-custom",
|
||||
actModeOpenAiModelInfo: { ...openAiModelInfoSafeDefaults, temperature: 0.2 },
|
||||
})
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
const selection = store.readSelection(providerId, "act")
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["existing-custom"]).toEqual({
|
||||
temperature: 0.7,
|
||||
})
|
||||
expect(selection?.overrides).toEqual({ temperature: 0.7 })
|
||||
expect(selection?.modelInfo.temperature).toBe(0.7)
|
||||
expect(syncStoredProviderRegistration).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("does not migrate stale legacy snapshots for catalog-known models", async () => {
|
||||
mocks.setGeneratedModels("openai-compatible", {
|
||||
"known-model": {
|
||||
name: "Current Catalog Model",
|
||||
contextWindow: 256_000,
|
||||
supportsPromptCache: false,
|
||||
temperature: 0.1,
|
||||
},
|
||||
})
|
||||
mocks.setApiConfiguration({
|
||||
actModeOpenAiModelId: "known-model",
|
||||
actModeOpenAiModelInfo: {
|
||||
name: "Stale Catalog Model",
|
||||
contextWindow: 32_000,
|
||||
supportsPromptCache: false,
|
||||
temperature: 0.9,
|
||||
},
|
||||
})
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
const selection = store.readSelection(providerId, "act")
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["known-model"]).toBeUndefined()
|
||||
expect(selection?.overrides).toBeUndefined()
|
||||
expect(selection?.modelInfo).toMatchObject({
|
||||
name: "Current Catalog Model",
|
||||
contextWindow: 256_000,
|
||||
temperature: 0.1,
|
||||
})
|
||||
expect(syncStoredProviderRegistration).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("migrates separate Plan and Act legacy custom models independently", async () => {
|
||||
mocks.setApiConfiguration({
|
||||
planActSeparateModelsSetting: true,
|
||||
planModeOpenAiModelId: "legacy-plan",
|
||||
planModeOpenAiModelInfo: {
|
||||
...openAiModelInfoSafeDefaults,
|
||||
contextWindow: 64_000,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
},
|
||||
actModeOpenAiModelId: "legacy-act",
|
||||
actModeOpenAiModelInfo: {
|
||||
...openAiModelInfoSafeDefaults,
|
||||
maxTokens: 2_048,
|
||||
isR1FormatRequired: true,
|
||||
},
|
||||
})
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
const plan = store.readSelection(providerId, "plan")
|
||||
const act = store.readSelection(providerId, "act")
|
||||
store.readSelection(providerId, "plan")
|
||||
store.readSelection(providerId, "act")
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models).toMatchObject({
|
||||
"legacy-plan": { contextWindow: 64_000, apiFormat: "openai-responses" },
|
||||
"legacy-act": { maxTokens: 2_048, isR1FormatRequired: true },
|
||||
})
|
||||
expect(plan?.modelInfo).toMatchObject({ contextWindow: 64_000, apiFormat: ApiFormat.OPENAI_RESPONSES })
|
||||
expect(act?.modelInfo).toMatchObject({ maxTokens: 2_048, apiFormat: ApiFormat.R1_CHAT })
|
||||
expect(syncStoredProviderRegistration).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("preserves migrated OpenAI Compatible settings when committing model selections", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
mocks.setProviderSettings({
|
||||
@@ -237,7 +485,7 @@ describe("createProviderConfigStore", () => {
|
||||
})
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
const selection = { providerId, modelId: "gpt-oss-120b", modelInfo: modelInfoA }
|
||||
const selection = selectionFromModelInfo(providerId, "gpt-oss-120b", modelInfoA)
|
||||
|
||||
store.commitSelection(providerId, "act", selection)
|
||||
|
||||
@@ -248,6 +496,242 @@ describe("createProviderConfigStore", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves per-model OpenAI Compatible overrides when switching models without new overrides", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
const modelASelection = selectionFromModelInfo(providerId, "model-a", modelInfoA)
|
||||
|
||||
store.commitSelection(providerId, "act", modelASelection)
|
||||
store.commitSelection(providerId, "act", { providerId, modelId: "model-b" })
|
||||
store.commitSelection(providerId, "act", { providerId, modelId: "model-a" })
|
||||
|
||||
expectResolvedSelection(store.readSelection(providerId, "act"), modelASelection, modelInfoA)
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["model-a"]).toMatchObject({
|
||||
name: "Model A",
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 128_000,
|
||||
})
|
||||
})
|
||||
|
||||
it("deletes a model entry when an explicit replacement override set is empty", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["tools", "streaming"],
|
||||
temperature: 0.2,
|
||||
},
|
||||
})
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["custom-model"]).toMatchObject({
|
||||
apiFormat: "openai-responses",
|
||||
capabilities: ["tools", "streaming"],
|
||||
temperature: 0.2,
|
||||
})
|
||||
|
||||
store.commitSelection(providerId, "act", { providerId, modelId: "custom-model", overrides: {} })
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["custom-model"]).toBeUndefined()
|
||||
expect(store.readSelection(providerId, "act")?.overrides).toBeUndefined()
|
||||
})
|
||||
|
||||
it("replaces an existing model override set instead of merging stale fields", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: { apiFormat: ApiFormat.OPENAI_RESPONSES, inputPrice: 1, temperature: 0.2 },
|
||||
})
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: { temperature: 0.4 },
|
||||
})
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["custom-model"]).toEqual({ temperature: 0.4 })
|
||||
expect(store.readSelection(providerId, "act")?.overrides).toEqual({ temperature: 0.4 })
|
||||
})
|
||||
|
||||
it.each([
|
||||
[ApiFormat.OPENAI_CHAT, "default"],
|
||||
[ApiFormat.R1_CHAT, "r1"],
|
||||
[ApiFormat.OPENAI_RESPONSES, "openai-responses"],
|
||||
] as const)("round-trips supported apiFormat %s through models.json", async (apiFormat, storedApiFormat) => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: { apiFormat },
|
||||
})
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["custom-model"]).toEqual({
|
||||
apiFormat: storedApiFormat,
|
||||
})
|
||||
expect(store.readSelection(providerId, "act")?.overrides).toEqual({ apiFormat })
|
||||
})
|
||||
|
||||
it("normalizes invalid override values before storage and resolved legacy state", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
name: "Custom model",
|
||||
maxTokens: -1,
|
||||
contextWindow: Number.POSITIVE_INFINITY,
|
||||
maxInputTokens: 0,
|
||||
capabilities: ["tools", "tools", "vision", "unknown"],
|
||||
supportsVision: false,
|
||||
supportsReasoning: false,
|
||||
inputPrice: Number.NaN,
|
||||
outputPrice: 2,
|
||||
cacheReadsPrice: 0,
|
||||
cacheWritesPrice: -1,
|
||||
temperature: -1,
|
||||
apiFormat: 999 as ApiFormat,
|
||||
},
|
||||
})
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["custom-model"]).toEqual({
|
||||
name: "Custom model",
|
||||
capabilities: ["tools"],
|
||||
supportsVision: false,
|
||||
supportsReasoning: false,
|
||||
outputPrice: 2,
|
||||
cacheReadsPrice: 0,
|
||||
})
|
||||
const selection = store.readSelection(providerId, "act")
|
||||
expect(selection?.overrides).toEqual({
|
||||
name: "Custom model",
|
||||
capabilities: ["tools"],
|
||||
supportsVision: false,
|
||||
supportsReasoning: false,
|
||||
outputPrice: 2,
|
||||
cacheReadsPrice: 0,
|
||||
})
|
||||
expect(selection?.modelInfo.maxTokens).toBeUndefined()
|
||||
expect(selection?.modelInfo.temperature).toBe(0)
|
||||
expect(mocks.getApiConfiguration().actModeOpenAiModelInfo).not.toHaveProperty("maxTokens")
|
||||
expect(mocks.getApiConfiguration().actModeOpenAiModelInfo).not.toHaveProperty("temperature", -1)
|
||||
expect(syncStoredProviderRegistration).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("deletes a stored entry when normalization removes every replacement field", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: { temperature: 0.2 },
|
||||
})
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
maxTokens: -1,
|
||||
contextWindow: 0,
|
||||
capabilities: ["vision", "unknown"],
|
||||
inputPrice: Number.NaN,
|
||||
temperature: -1,
|
||||
apiFormat: 999 as ApiFormat,
|
||||
},
|
||||
})
|
||||
|
||||
expect(mocks.getModelsFile().providers["openai-compatible"]?.models?.["custom-model"]).toBeUndefined()
|
||||
expect(store.readSelection(providerId, "act")?.overrides).toBeUndefined()
|
||||
expect(syncStoredProviderRegistration).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("normalizes invalid values already present in models.json on read", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
mocks.setProviderSettings({
|
||||
"openai-compatible": { provider: "openai-compatible", model: "custom-model" },
|
||||
})
|
||||
mocks.setModelsFile({
|
||||
version: 1,
|
||||
providers: {
|
||||
"openai-compatible": {
|
||||
models: {
|
||||
"custom-model": {
|
||||
maxTokens: -1,
|
||||
contextWindow: 64_000,
|
||||
inputPrice: -2,
|
||||
temperature: -1,
|
||||
capabilities: ["tools"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
const selection = store.readSelection(providerId, "act")
|
||||
|
||||
expect(selection?.overrides).toEqual({ contextWindow: 64_000, capabilities: ["tools"] })
|
||||
expect(selection?.modelInfo.maxTokens).toBeUndefined()
|
||||
expect(selection?.modelInfo.temperature).toBe(0)
|
||||
expect(syncStoredProviderRegistration).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("lets explicit capability booleans win and applies the R1 alias deterministically", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["images", "prompt-cache", "reasoning"],
|
||||
supportsVision: false,
|
||||
supportsReasoning: false,
|
||||
isR1FormatRequired: false,
|
||||
},
|
||||
})
|
||||
let selection = store.readSelection(providerId, "act")
|
||||
expect(selection?.modelInfo).toMatchObject({
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoning: false,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
})
|
||||
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["prompt-cache"],
|
||||
supportsVision: true,
|
||||
isR1FormatRequired: true,
|
||||
},
|
||||
})
|
||||
selection = store.readSelection(providerId, "act")
|
||||
expect(selection?.modelInfo).toMatchObject({
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
apiFormat: ApiFormat.R1_CHAT,
|
||||
})
|
||||
})
|
||||
|
||||
it("keeps OpenAI Compatible Plan and Act selections independent when separate models are enabled", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
mocks.setApiConfiguration({ planActSeparateModelsSetting: true })
|
||||
@@ -259,14 +743,14 @@ describe("createProviderConfigStore", () => {
|
||||
})
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
const planSelection = { providerId, modelId: "plan-openai-model", modelInfo: modelInfoA }
|
||||
const actSelection = { providerId, modelId: "act-openai-model", modelInfo: modelInfoB }
|
||||
const planSelection = selectionFromModelInfo(providerId, "plan-openai-model", modelInfoA)
|
||||
const actSelection = selectionFromModelInfo(providerId, "act-openai-model", modelInfoB)
|
||||
|
||||
store.commitSelection(providerId, "plan", planSelection)
|
||||
store.commitSelection(providerId, "act", actSelection)
|
||||
|
||||
expect(store.readSelection(providerId, "plan")).toEqual(planSelection)
|
||||
expect(store.readSelection(providerId, "act")).toEqual(actSelection)
|
||||
expectResolvedSelection(store.readSelection(providerId, "plan"), planSelection, modelInfoA)
|
||||
expectResolvedSelection(store.readSelection(providerId, "act"), actSelection, modelInfoB)
|
||||
expect(mocks.getApiConfiguration()).toMatchObject({
|
||||
planModeOpenAiModelId: "plan-openai-model",
|
||||
planModeOpenAiModelInfo: modelInfoA,
|
||||
@@ -286,12 +770,12 @@ describe("createProviderConfigStore", () => {
|
||||
mocks.setApiConfiguration({ planActSeparateModelsSetting: false })
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
const selection = { providerId, modelId: "shared-openai-model", modelInfo: modelInfoA }
|
||||
const selection = selectionFromModelInfo(providerId, "shared-openai-model", modelInfoA)
|
||||
|
||||
store.commitSelection(providerId, "act", selection)
|
||||
|
||||
expect(store.readSelection(providerId, "plan")).toEqual(selection)
|
||||
expect(store.readSelection(providerId, "act")).toEqual(selection)
|
||||
expectResolvedSelection(store.readSelection(providerId, "plan"), selection, modelInfoA)
|
||||
expectResolvedSelection(store.readSelection(providerId, "act"), selection, modelInfoA)
|
||||
expect(mocks.getApiConfiguration()).toMatchObject({
|
||||
planModeOpenAiModelId: "shared-openai-model",
|
||||
planModeOpenAiModelInfo: modelInfoA,
|
||||
@@ -320,14 +804,22 @@ describe("createProviderConfigStore", () => {
|
||||
expect(mocks.getApiConfiguration().zaiApiKey).toBe("shared-zai-key")
|
||||
})
|
||||
|
||||
it("returns undefined from readSelection when modelId or modelInfo is missing", async () => {
|
||||
it("resolves a bare state modelId with fallback metadata and ignores a bare modelInfo", async () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openrouter")
|
||||
|
||||
// The mode-specific model id alone identifies the selection; commits
|
||||
// whose resolution was pure fallback intentionally leave the state
|
||||
// modelInfo snapshot unset.
|
||||
mocks.setApiConfiguration({ actModeOpenRouterModelId: "anthropic/claude-sonnet-4" })
|
||||
expect(store.readSelection(providerId, "act")).toBeUndefined()
|
||||
expect(store.readSelection(providerId, "act")).toMatchObject({
|
||||
providerId,
|
||||
modelId: "anthropic/claude-sonnet-4",
|
||||
modelInfoSource: "fallback",
|
||||
})
|
||||
|
||||
// A modelInfo snapshot without a model id is not a selection.
|
||||
mocks.setApiConfiguration({ actModeOpenRouterModelInfo: modelInfoA })
|
||||
expect(store.readSelection(providerId, "act")).toBeUndefined()
|
||||
})
|
||||
@@ -337,14 +829,14 @@ describe("createProviderConfigStore", () => {
|
||||
mocks.setApiConfiguration({ planActSeparateModelsSetting: true })
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openrouter")
|
||||
const planSelection = { providerId, modelId: "provider/model-a", modelInfo: modelInfoA }
|
||||
const actSelection = { providerId, modelId: "provider/model-b", modelInfo: modelInfoB }
|
||||
const planSelection = selectionFromModelInfo(providerId, "provider/model-a", modelInfoA)
|
||||
const actSelection = selectionFromModelInfo(providerId, "provider/model-b", modelInfoB)
|
||||
|
||||
store.commitSelection(providerId, "plan", planSelection)
|
||||
store.commitSelection(providerId, "act", actSelection)
|
||||
|
||||
expect(store.readSelection(providerId, "plan")).toEqual(planSelection)
|
||||
expect(store.readSelection(providerId, "act")).toEqual(actSelection)
|
||||
expectResolvedSelection(store.readSelection(providerId, "plan"), planSelection, modelInfoA)
|
||||
expectResolvedSelection(store.readSelection(providerId, "act"), actSelection, modelInfoB)
|
||||
expect(mocks.getSavedProviderSettings("openrouter")).toMatchObject({
|
||||
provider: "openrouter",
|
||||
model: "provider/model-b",
|
||||
@@ -361,7 +853,7 @@ describe("createProviderConfigStore", () => {
|
||||
})
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openrouter")
|
||||
const selection = { providerId, modelId: "provider/model-a", modelInfo: modelInfoA }
|
||||
const selection = selectionFromModelInfo(providerId, "provider/model-a", modelInfoA)
|
||||
|
||||
store.commitSelection(providerId, "act", selection)
|
||||
|
||||
@@ -381,7 +873,7 @@ describe("createProviderConfigStore", () => {
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("claude-code")
|
||||
const selection = { providerId, modelId: "haiku", modelInfo: modelInfoA }
|
||||
const selection = selectionFromModelInfo(providerId, "haiku", modelInfoA)
|
||||
|
||||
store.commitSelection(providerId, "act", selection)
|
||||
|
||||
@@ -423,7 +915,7 @@ describe("createProviderConfigStore", () => {
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openrouter")
|
||||
const events: ProviderConfigChange[] = []
|
||||
const selection = { providerId, modelId: "provider/model-a", modelInfo: modelInfoA }
|
||||
const selection = selectionFromModelInfo(providerId, "provider/model-a", modelInfoA)
|
||||
|
||||
store.subscribe((event) => events.push(event))
|
||||
store.write(providerId, { apiKey: "openrouter-key" })
|
||||
@@ -431,7 +923,12 @@ describe("createProviderConfigStore", () => {
|
||||
|
||||
expect(events.map((event) => event.kind)).toEqual(["fields", "selection"])
|
||||
expect(events[0]).toMatchObject({ kind: "fields", providerId })
|
||||
expect(events[1]).toEqual({ kind: "selection", providerId, mode: "act", selection })
|
||||
expect(events[1]).toEqual({
|
||||
kind: "selection",
|
||||
providerId,
|
||||
mode: "act",
|
||||
selection: store.readSelection(providerId, "act"),
|
||||
})
|
||||
})
|
||||
|
||||
it("dispose unregisters listeners", async () => {
|
||||
@@ -446,4 +943,50 @@ describe("createProviderConfigStore", () => {
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Contract test against the REAL SDK schemas (imported by relative path,
|
||||
// bypassing the @cline/core mock above): the store's converters must pass
|
||||
// every SDK capability through, and a fully-populated stored entry must
|
||||
// parse under the schema `writeModelsFileSync` enforces in production.
|
||||
it("round-trips every SDK model capability and a full override set under the real stored-entry schema", async () => {
|
||||
const { ModelCapabilitySchema } = await import("@cline/shared")
|
||||
// vi.importActual bypasses the @cline/core mock above and resolves via
|
||||
// the vitest alias to the stub, which re-exports the real schema.
|
||||
const { StoredModelEntrySchema } = (await vi.importActual("@cline/core")) as {
|
||||
StoredModelEntrySchema: { parse(input: unknown): unknown }
|
||||
}
|
||||
const { createProviderConfigStore } = await import("./store")
|
||||
const store = createProviderConfigStore()
|
||||
const providerId = parseProviderId("openai")
|
||||
|
||||
store.commitSelection(providerId, "act", {
|
||||
providerId,
|
||||
modelId: "contract-model",
|
||||
overrides: {
|
||||
name: "Contract Model",
|
||||
maxTokens: 1024,
|
||||
contextWindow: 200_000,
|
||||
maxInputTokens: 100_000,
|
||||
capabilities: [...ModelCapabilitySchema.options],
|
||||
supportsVision: true,
|
||||
supportsAttachments: true,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 0.5,
|
||||
outputPrice: 1.5,
|
||||
cacheReadsPrice: 0.1,
|
||||
cacheWritesPrice: 0.2,
|
||||
temperature: 0.7,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
isR1FormatRequired: true,
|
||||
},
|
||||
})
|
||||
|
||||
const entry = mocks.getModelsFile().providers["openai-compatible"]?.models?.["contract-model"]
|
||||
expect(entry).toBeDefined()
|
||||
// No SDK capability may be silently stripped by the store's converter.
|
||||
expect([...(entry?.capabilities as string[])].sort()).toEqual([...ModelCapabilitySchema.options].sort())
|
||||
// The entry written by the extension must satisfy the real schema that
|
||||
// the SDK's writeModelsFileSync enforces.
|
||||
expect(() => StoredModelEntrySchema.parse(entry)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import {
|
||||
readModelsFileSync,
|
||||
resolveModelsRegistryPath,
|
||||
type StoredModelEntry,
|
||||
syncStoredProviderRegistration,
|
||||
writeModelsFileSync,
|
||||
} from "@cline/core"
|
||||
import { getGeneratedModelsForProvider, MODEL_COLLECTIONS_BY_PROVIDER_ID } from "@cline/llms"
|
||||
import { ModelCapabilitySchema } from "@cline/shared"
|
||||
import { type ApiConfiguration, type ApiProvider, type ModelInfo, openAiModelInfoSafeDefaults } from "@shared/api"
|
||||
import { ApiFormat } from "@shared/proto/cline/models"
|
||||
import { Logger } from "@shared/services/Logger"
|
||||
import { getProviderModelIdKey } from "@shared/storage/provider-keys"
|
||||
import { isSecretKey, isSettingsKey, type SecretKey, type SettingsKey } from "@shared/storage/state-keys"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
@@ -9,14 +19,17 @@ import type {
|
||||
EffectiveProviderConfig,
|
||||
Mode,
|
||||
ModelSelection,
|
||||
ModelSelectionOverrides,
|
||||
ProviderConfigChange,
|
||||
ProviderConfigChangeListener,
|
||||
ProviderConfigPatch,
|
||||
ProviderConfigStore,
|
||||
ProviderId,
|
||||
ResolvedModelSelection,
|
||||
} from "./contracts"
|
||||
import { buildEffectiveProviderConfig } from "./effective-config"
|
||||
import { applyHostModelInfoOverrides } from "./host-overrides"
|
||||
import { fromSdkApiFormat, nonNegativeFiniteNumber, positiveFiniteNumber, toSdkApiFormat } from "./model-values"
|
||||
import { toSdkProviderId } from "./sdk-provider-id"
|
||||
import { adaptSdkModelInfo } from "./shape-adapter"
|
||||
|
||||
@@ -113,7 +126,7 @@ const modelInfoKeysByProvider: Partial<Record<string, ModelInfoKeys>> = {
|
||||
// provider+mode so that switching between providers that share the same
|
||||
// `*ModeApiModelId` key does not combine one provider's model id with
|
||||
// another provider's model info.
|
||||
const selectionMemory = new Map<string, ModelSelection>()
|
||||
const selectionMemory = new Map<string, ResolvedModelSelection>()
|
||||
|
||||
function providerKey(providerId: ProviderId): string {
|
||||
return providerId.toString()
|
||||
@@ -168,11 +181,210 @@ function readProviderSettingsModelId(providerId: ProviderId): string | undefined
|
||||
return typeof model === "string" && model.trim().length > 0 ? model.trim() : undefined
|
||||
}
|
||||
|
||||
function fallbackModelInfo(modelId: string): ModelInfo {
|
||||
return { ...openAiModelInfoSafeDefaults, name: modelId }
|
||||
function sanitizeResolvedModelInfo(modelInfo: ModelInfo): ModelInfo {
|
||||
const next = { ...modelInfo }
|
||||
if (positiveFiniteNumber(next.maxTokens) === undefined) delete next.maxTokens
|
||||
if (nonNegativeFiniteNumber(next.temperature) === undefined) delete next.temperature
|
||||
return next
|
||||
}
|
||||
|
||||
function readKnownModelInfoForProvider(providerId: ProviderId, modelId: string): ModelInfo | undefined {
|
||||
function fallbackModelInfo(modelId: string): ModelInfo {
|
||||
return sanitizeResolvedModelInfo({ ...openAiModelInfoSafeDefaults, name: modelId })
|
||||
}
|
||||
|
||||
function toStoredCapabilities(capabilities: readonly string[] | undefined): StoredModelEntry["capabilities"] | undefined {
|
||||
if (!capabilities) {
|
||||
return undefined
|
||||
}
|
||||
// Validate against the SDK schema rather than a hardcoded list so new
|
||||
// capabilities added to ModelCapabilitySchema are never silently stripped.
|
||||
const next = new Set<NonNullable<StoredModelEntry["capabilities"]>[number]>()
|
||||
for (const capability of capabilities) {
|
||||
const parsed = ModelCapabilitySchema.safeParse(capability)
|
||||
if (parsed.success) {
|
||||
next.add(parsed.data)
|
||||
}
|
||||
}
|
||||
return next.size > 0 ? [...next] : undefined
|
||||
}
|
||||
|
||||
function toStoredApiFormat(apiFormat: ModelInfo["apiFormat"]): StoredModelEntry["apiFormat"] | undefined {
|
||||
return toSdkApiFormat(apiFormat)
|
||||
}
|
||||
|
||||
function fromStoredApiFormat(apiFormat: StoredModelEntry["apiFormat"]): ModelInfo["apiFormat"] | undefined {
|
||||
return fromSdkApiFormat(apiFormat)
|
||||
}
|
||||
|
||||
function readModelsState() {
|
||||
return readModelsFileSync(resolveModelsRegistryPath(getProviderSettingsManager()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes user-authored model metadata at the host/storage boundary.
|
||||
* Token limits must be positive, prices and temperatures non-negative, and
|
||||
* unsupported capabilities/formats are omitted. UI sentinels never cross
|
||||
* this boundary; an object with no meaningful fields becomes undefined.
|
||||
*/
|
||||
function normalizeModelSelectionOverrides(overrides: ModelSelectionOverrides | undefined): ModelSelectionOverrides | undefined {
|
||||
if (!overrides) {
|
||||
return undefined
|
||||
}
|
||||
const maxTokens = positiveFiniteNumber(overrides.maxTokens)
|
||||
const contextWindow = positiveFiniteNumber(overrides.contextWindow)
|
||||
const maxInputTokens = positiveFiniteNumber(overrides.maxInputTokens)
|
||||
const capabilities = toStoredCapabilities(overrides.capabilities)
|
||||
const inputPrice = nonNegativeFiniteNumber(overrides.inputPrice)
|
||||
const outputPrice = nonNegativeFiniteNumber(overrides.outputPrice)
|
||||
const cacheReadsPrice = nonNegativeFiniteNumber(overrides.cacheReadsPrice)
|
||||
const cacheWritesPrice = nonNegativeFiniteNumber(overrides.cacheWritesPrice)
|
||||
const temperature = nonNegativeFiniteNumber(overrides.temperature)
|
||||
const apiFormat = toStoredApiFormat(overrides.apiFormat) !== undefined ? overrides.apiFormat : undefined
|
||||
const next: ModelSelectionOverrides = {
|
||||
...(overrides.name !== undefined ? { name: overrides.name } : {}),
|
||||
...(maxTokens !== undefined ? { maxTokens } : {}),
|
||||
...(contextWindow !== undefined ? { contextWindow } : {}),
|
||||
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
||||
...(capabilities !== undefined ? { capabilities } : {}),
|
||||
...(overrides.supportsVision !== undefined ? { supportsVision: overrides.supportsVision } : {}),
|
||||
...(overrides.supportsAttachments !== undefined ? { supportsAttachments: overrides.supportsAttachments } : {}),
|
||||
...(overrides.supportsReasoning !== undefined ? { supportsReasoning: overrides.supportsReasoning } : {}),
|
||||
...(inputPrice !== undefined ? { inputPrice } : {}),
|
||||
...(outputPrice !== undefined ? { outputPrice } : {}),
|
||||
...(cacheReadsPrice !== undefined ? { cacheReadsPrice } : {}),
|
||||
...(cacheWritesPrice !== undefined ? { cacheWritesPrice } : {}),
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
...(apiFormat !== undefined ? { apiFormat } : {}),
|
||||
...(overrides.isR1FormatRequired !== undefined ? { isR1FormatRequired: overrides.isR1FormatRequired } : {}),
|
||||
}
|
||||
return Object.keys(next).length > 0 ? next : undefined
|
||||
}
|
||||
|
||||
function toStoredModelEntry(overrides: ModelSelectionOverrides): StoredModelEntry {
|
||||
const capabilities = toStoredCapabilities(overrides.capabilities)
|
||||
const apiFormat = toStoredApiFormat(overrides.apiFormat)
|
||||
return {
|
||||
...(overrides.name !== undefined ? { name: overrides.name } : {}),
|
||||
...(overrides.maxTokens !== undefined ? { maxTokens: overrides.maxTokens } : {}),
|
||||
...(overrides.contextWindow !== undefined ? { contextWindow: overrides.contextWindow } : {}),
|
||||
...(overrides.maxInputTokens !== undefined ? { maxInputTokens: overrides.maxInputTokens } : {}),
|
||||
...(capabilities !== undefined ? { capabilities } : {}),
|
||||
...(overrides.supportsVision !== undefined ? { supportsVision: overrides.supportsVision } : {}),
|
||||
...(overrides.supportsAttachments !== undefined ? { supportsAttachments: overrides.supportsAttachments } : {}),
|
||||
...(overrides.supportsReasoning !== undefined ? { supportsReasoning: overrides.supportsReasoning } : {}),
|
||||
...(overrides.inputPrice !== undefined ? { inputPrice: overrides.inputPrice } : {}),
|
||||
...(overrides.outputPrice !== undefined ? { outputPrice: overrides.outputPrice } : {}),
|
||||
...(overrides.cacheReadsPrice !== undefined ? { cacheReadsPrice: overrides.cacheReadsPrice } : {}),
|
||||
...(overrides.cacheWritesPrice !== undefined ? { cacheWritesPrice: overrides.cacheWritesPrice } : {}),
|
||||
...(overrides.temperature !== undefined ? { temperature: overrides.temperature } : {}),
|
||||
...(apiFormat !== undefined ? { apiFormat } : {}),
|
||||
...(overrides.isR1FormatRequired !== undefined ? { isR1FormatRequired: overrides.isR1FormatRequired } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function toSelectionOverrides(entry: StoredModelEntry | undefined): ModelSelectionOverrides | undefined {
|
||||
if (!entry) {
|
||||
return undefined
|
||||
}
|
||||
const apiFormat = fromStoredApiFormat(entry.apiFormat)
|
||||
return normalizeModelSelectionOverrides({
|
||||
...(entry.name !== undefined ? { name: entry.name } : {}),
|
||||
...(entry.maxTokens !== undefined ? { maxTokens: entry.maxTokens } : {}),
|
||||
...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
|
||||
...(entry.maxInputTokens !== undefined ? { maxInputTokens: entry.maxInputTokens } : {}),
|
||||
...(entry.capabilities !== undefined ? { capabilities: [...entry.capabilities] } : {}),
|
||||
...(entry.supportsVision !== undefined ? { supportsVision: entry.supportsVision } : {}),
|
||||
...(entry.supportsAttachments !== undefined ? { supportsAttachments: entry.supportsAttachments } : {}),
|
||||
...(entry.supportsReasoning !== undefined ? { supportsReasoning: entry.supportsReasoning } : {}),
|
||||
...(entry.inputPrice !== undefined ? { inputPrice: entry.inputPrice } : {}),
|
||||
...(entry.outputPrice !== undefined ? { outputPrice: entry.outputPrice } : {}),
|
||||
...(entry.cacheReadsPrice !== undefined ? { cacheReadsPrice: entry.cacheReadsPrice } : {}),
|
||||
...(entry.cacheWritesPrice !== undefined ? { cacheWritesPrice: entry.cacheWritesPrice } : {}),
|
||||
...(entry.temperature !== undefined ? { temperature: entry.temperature } : {}),
|
||||
...(apiFormat !== undefined ? { apiFormat } : {}),
|
||||
...(entry.isR1FormatRequired !== undefined ? { isR1FormatRequired: entry.isR1FormatRequired } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
function readStoredModelEntry(providerId: ProviderId, modelId: string): { exists: boolean; entry: StoredModelEntry | undefined } {
|
||||
const models = readModelsState().providers[providerSettingsProviderId(providerId)]?.models
|
||||
return {
|
||||
exists: models ? Object.hasOwn(models, modelId) : false,
|
||||
entry: models?.[modelId],
|
||||
}
|
||||
}
|
||||
|
||||
function readModelOverrides(providerId: ProviderId, modelId: string): ModelSelectionOverrides | undefined {
|
||||
return toSelectionOverrides(readStoredModelEntry(providerId, modelId).entry)
|
||||
}
|
||||
|
||||
function writeModelOverrides(providerId: ProviderId, modelId: string, overrides: ModelSelectionOverrides | undefined): void {
|
||||
const modelsPath = resolveModelsRegistryPath(getProviderSettingsManager())
|
||||
const state = readModelsFileSync(modelsPath)
|
||||
const provider = providerSettingsProviderId(providerId)
|
||||
const providerEntry = state.providers[provider] ?? {}
|
||||
const nextModels = { ...(providerEntry.models ?? {}) }
|
||||
const normalizedOverrides = normalizeModelSelectionOverrides(overrides)
|
||||
const storedEntry = normalizedOverrides ? toStoredModelEntry(normalizedOverrides) : undefined
|
||||
if (storedEntry && Object.keys(storedEntry).length > 0) {
|
||||
nextModels[modelId] = storedEntry
|
||||
} else {
|
||||
delete nextModels[modelId]
|
||||
}
|
||||
const nextProviderEntry = {
|
||||
...providerEntry,
|
||||
models: nextModels,
|
||||
}
|
||||
writeModelsFileSync(modelsPath, {
|
||||
...state,
|
||||
providers: {
|
||||
...state.providers,
|
||||
[provider]: nextProviderEntry,
|
||||
},
|
||||
})
|
||||
// ensureCustomProvidersLoadedSync is load-once per path and would no-op
|
||||
// here; sync the live registry explicitly so this write is visible to new
|
||||
// sessions without a restart.
|
||||
syncStoredProviderRegistration(provider, state.providers[provider], nextProviderEntry)
|
||||
}
|
||||
|
||||
function applyModelOverrides(modelInfo: ModelInfo, overrides: ModelSelectionOverrides | undefined): ModelInfo {
|
||||
if (!overrides) {
|
||||
return modelInfo
|
||||
}
|
||||
const next: ModelInfo = { ...modelInfo }
|
||||
if (overrides.name !== undefined) next.name = overrides.name
|
||||
if (overrides.maxTokens !== undefined) next.maxTokens = overrides.maxTokens
|
||||
if (overrides.contextWindow !== undefined) next.contextWindow = overrides.contextWindow
|
||||
if (overrides.maxInputTokens !== undefined)
|
||||
(next as ModelInfo & { maxInputTokens?: number }).maxInputTokens = overrides.maxInputTokens
|
||||
if (overrides.inputPrice !== undefined) next.inputPrice = overrides.inputPrice
|
||||
if (overrides.outputPrice !== undefined) next.outputPrice = overrides.outputPrice
|
||||
if (overrides.cacheReadsPrice !== undefined) next.cacheReadsPrice = overrides.cacheReadsPrice
|
||||
if (overrides.cacheWritesPrice !== undefined) next.cacheWritesPrice = overrides.cacheWritesPrice
|
||||
if (overrides.temperature !== undefined) next.temperature = overrides.temperature
|
||||
if (overrides.apiFormat !== undefined) next.apiFormat = overrides.apiFormat
|
||||
|
||||
// Capability arrays are additive fallback flags: they can only enable
|
||||
// capabilities the base metadata lacks, never disable base capabilities
|
||||
// (an array authored for one purpose, e.g. prompt-cache, must not strip
|
||||
// unrelated base flags like vision). Explicit booleans win when both
|
||||
// representations are present.
|
||||
if (overrides.capabilities !== undefined) {
|
||||
if (overrides.capabilities.includes("images")) next.supportsImages = true
|
||||
if (overrides.capabilities.includes("prompt-cache")) next.supportsPromptCache = true
|
||||
if (overrides.capabilities.includes("reasoning")) next.supportsReasoning = true
|
||||
}
|
||||
if (overrides.supportsVision !== undefined) next.supportsImages = overrides.supportsVision
|
||||
if (overrides.supportsReasoning !== undefined) next.supportsReasoning = overrides.supportsReasoning
|
||||
|
||||
// apiFormat is canonical. The legacy R1 flag remains a compatibility alias
|
||||
// that forces R1 only when explicitly true.
|
||||
if (overrides.isR1FormatRequired) next.apiFormat = ApiFormat.R1_CHAT
|
||||
return next
|
||||
}
|
||||
|
||||
function readBaseModelInfoForProvider(providerId: ProviderId, modelId: string): ModelInfo | undefined {
|
||||
const sdkProviderId = toSdkProviderId(providerId)
|
||||
const generatedModelInfo = getGeneratedModelsForProvider(sdkProviderId)[modelId]
|
||||
if (isModelInfo(generatedModelInfo)) {
|
||||
@@ -201,17 +413,36 @@ function readKnownModelInfoForProvider(providerId: ProviderId, modelId: string):
|
||||
return undefined
|
||||
}
|
||||
|
||||
function readSelectionFromProviderSettings(providerId: ProviderId): ModelSelection | undefined {
|
||||
function resolveSelection(selection: ModelSelection, stateModelInfoHint?: ModelInfo): ResolvedModelSelection {
|
||||
const overrides = normalizeModelSelectionOverrides(
|
||||
selection.overrides ?? readModelOverrides(selection.providerId, selection.modelId),
|
||||
)
|
||||
// Base resolution order: SDK catalog, then the picker's persisted state
|
||||
// snapshot (the only accurate data for dynamic-list models the static
|
||||
// catalog does not know), then provider-safe fallback defaults.
|
||||
const catalogModelInfo = readBaseModelInfoForProvider(selection.providerId, selection.modelId)
|
||||
const baseModelInfo = catalogModelInfo ?? stateModelInfoHint ?? fallbackModelInfo(selection.modelId)
|
||||
const modelInfoSource = catalogModelInfo ? "catalog" : stateModelInfoHint ? "state" : "fallback"
|
||||
return {
|
||||
...selection,
|
||||
overrides,
|
||||
modelInfoSource,
|
||||
baseModelInfo,
|
||||
modelInfo: sanitizeResolvedModelInfo(applyModelOverrides(baseModelInfo, overrides)),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRuntimeModelSelection(providerId: ProviderId, modelId: string): ResolvedModelSelection {
|
||||
return resolveSelection({ providerId, modelId })
|
||||
}
|
||||
|
||||
function readSelectionFromProviderSettings(providerId: ProviderId): ResolvedModelSelection | undefined {
|
||||
const modelId = readProviderSettingsModelId(providerId)
|
||||
if (!modelId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
providerId,
|
||||
modelId,
|
||||
modelInfo: readKnownModelInfoForProvider(providerId, modelId) ?? fallbackModelInfo(modelId),
|
||||
}
|
||||
return resolveSelection({ providerId, modelId })
|
||||
}
|
||||
|
||||
function writeStateKey(key: SecretKey | SettingsKey, value: unknown): void {
|
||||
@@ -281,6 +512,17 @@ function writeStateFields(providerId: ProviderId, patch: ProviderConfigPatch): v
|
||||
writeStateKey("clineApiKey", patch.auth?.accessToken)
|
||||
writeStateKey("clineAccountId", patch.auth?.accountId)
|
||||
}
|
||||
|
||||
// Mirror the Ollama context window to the legacy state key so older
|
||||
// readers (proto ApiConfiguration, webview display fallback) stay in sync
|
||||
// with providers.json.
|
||||
if (provider === "ollama" && "contextWindow" in patch) {
|
||||
const contextWindow = patch.contextWindow
|
||||
writeStateKey(
|
||||
"ollamaApiOptionsCtxNum",
|
||||
typeof contextWindow === "number" && contextWindow > 0 ? String(contextWindow) : undefined,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderSettings(providerId: ProviderId): ProviderSettingsRecord {
|
||||
@@ -330,6 +572,15 @@ function writeProviderSettingsFields(providerId: ProviderId, patch: ProviderConf
|
||||
}
|
||||
}
|
||||
|
||||
if ("contextWindow" in patch) {
|
||||
const contextWindow = patch.contextWindow
|
||||
if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
|
||||
next.contextWindow = Math.floor(contextWindow)
|
||||
} else {
|
||||
delete next.contextWindow
|
||||
}
|
||||
}
|
||||
|
||||
if ("aws" in patch) {
|
||||
const awsPatch = patch.aws
|
||||
if (awsPatch === null || awsPatch === undefined) {
|
||||
@@ -389,13 +640,27 @@ function syncedModes(mode: Mode): Mode[] {
|
||||
return StateManager.get().getGlobalSettingsKey("planActSeparateModelsSetting") ? [mode] : ["plan", "act"]
|
||||
}
|
||||
|
||||
function writeSelectionToState(providerId: ProviderId, mode: Mode, selection: ModelSelection): void {
|
||||
function writeSelectionToState(providerId: ProviderId, mode: Mode, selection: ResolvedModelSelection): void {
|
||||
const updates: Partial<Record<SettingsKey, unknown>> = {}
|
||||
for (const targetMode of syncedModes(mode)) {
|
||||
updates[getModelIdKey(providerId, targetMode)] = selection.modelId
|
||||
const modelInfoKey = getModelInfoKey(providerId, targetMode)
|
||||
if (modelInfoKey) {
|
||||
updates[modelInfoKey] = selection.modelInfo
|
||||
// For hint-eligible providers the snapshot must stay genuine base
|
||||
// metadata: never persist fabricated fallback data (later reads
|
||||
// would treat it as authoritative "state" data and shadow live
|
||||
// catalog lookups), and persist the pre-override base rather than
|
||||
// the resolved value (a deleted override must not be resurrected
|
||||
// from a snapshot it was baked into). openai-compatible keeps the
|
||||
// legacy resolved write — its snapshot is never used as a
|
||||
// resolution base, and old extension versions still read it after
|
||||
// a rollback.
|
||||
if (usesStateModelInfoHint(providerId)) {
|
||||
updates[modelInfoKey] =
|
||||
selection.modelInfoSource === "fallback" ? undefined : (selection.baseModelInfo ?? selection.modelInfo)
|
||||
} else {
|
||||
updates[modelInfoKey] = selection.modelInfo
|
||||
}
|
||||
}
|
||||
selectionMemory.set(memoryKey(providerId, targetMode), { ...selection, providerId })
|
||||
}
|
||||
@@ -404,28 +669,164 @@ function writeSelectionToState(providerId: ProviderId, mode: Mode, selection: Mo
|
||||
|
||||
function writeSelectionToProviderSettings(providerId: ProviderId, selection: ModelSelection): void {
|
||||
const next: ProviderSettingsRecord = { ...getProviderSettings(providerId), model: selection.modelId }
|
||||
// Prune model metadata that earlier builds may have written to providers.json.
|
||||
delete next.contextWindow
|
||||
// Prune model metadata that earlier builds may have written to
|
||||
// providers.json — except for Ollama, whose contextWindow is a real
|
||||
// user setting (maps to num_ctx) written by the settings UI.
|
||||
if (providerKey(providerId) !== "ollama") {
|
||||
delete next.contextWindow
|
||||
}
|
||||
delete next.maxTokens
|
||||
|
||||
saveProviderSettings(providerId, next)
|
||||
}
|
||||
|
||||
function readSelectionFromState(providerId: ProviderId, mode: Mode): ModelSelection | undefined {
|
||||
type LegacyModelInfo = ModelInfo & { maxInputTokens?: number; isR1FormatRequired?: boolean }
|
||||
type MutableModelSelectionOverrides = { -readonly [Key in keyof ModelSelectionOverrides]: ModelSelectionOverrides[Key] }
|
||||
|
||||
function legacyModelInfoToOverrides(modelInfo: LegacyModelInfo, fallback: ModelInfo): ModelSelectionOverrides | undefined {
|
||||
const fallbackInfo = fallback as LegacyModelInfo
|
||||
const overrides: MutableModelSelectionOverrides = {}
|
||||
if (modelInfo.name !== undefined && modelInfo.name !== fallback.name) overrides.name = modelInfo.name
|
||||
if (modelInfo.maxTokens !== undefined && modelInfo.maxTokens !== fallback.maxTokens) overrides.maxTokens = modelInfo.maxTokens
|
||||
if (modelInfo.contextWindow !== undefined && modelInfo.contextWindow !== fallback.contextWindow)
|
||||
overrides.contextWindow = modelInfo.contextWindow
|
||||
if (modelInfo.maxInputTokens !== undefined && modelInfo.maxInputTokens !== fallbackInfo.maxInputTokens)
|
||||
overrides.maxInputTokens = modelInfo.maxInputTokens
|
||||
|
||||
const supportsVision = modelInfo.supportsImages ?? fallback.supportsImages
|
||||
if (Boolean(supportsVision) !== Boolean(fallback.supportsImages)) overrides.supportsVision = Boolean(supportsVision)
|
||||
if (Boolean(modelInfo.supportsReasoning) !== Boolean(fallback.supportsReasoning))
|
||||
overrides.supportsReasoning = Boolean(modelInfo.supportsReasoning)
|
||||
if (modelInfo.supportsPromptCache !== fallback.supportsPromptCache) {
|
||||
const capabilities: string[] = []
|
||||
if (supportsVision) capabilities.push("images")
|
||||
if (modelInfo.supportsPromptCache) capabilities.push("prompt-cache")
|
||||
overrides.capabilities = capabilities
|
||||
}
|
||||
|
||||
if (modelInfo.inputPrice !== undefined && modelInfo.inputPrice !== fallback.inputPrice)
|
||||
overrides.inputPrice = modelInfo.inputPrice
|
||||
if (modelInfo.outputPrice !== undefined && modelInfo.outputPrice !== fallback.outputPrice)
|
||||
overrides.outputPrice = modelInfo.outputPrice
|
||||
if (modelInfo.cacheReadsPrice !== undefined && modelInfo.cacheReadsPrice !== fallback.cacheReadsPrice)
|
||||
overrides.cacheReadsPrice = modelInfo.cacheReadsPrice
|
||||
if (modelInfo.cacheWritesPrice !== undefined && modelInfo.cacheWritesPrice !== fallback.cacheWritesPrice)
|
||||
overrides.cacheWritesPrice = modelInfo.cacheWritesPrice
|
||||
if (modelInfo.temperature !== undefined && modelInfo.temperature !== fallback.temperature)
|
||||
overrides.temperature = modelInfo.temperature
|
||||
if (modelInfo.apiFormat !== undefined && modelInfo.apiFormat !== fallback.apiFormat) overrides.apiFormat = modelInfo.apiFormat
|
||||
if (modelInfo.isR1FormatRequired === true && fallbackInfo.isR1FormatRequired !== true) overrides.isR1FormatRequired = true
|
||||
return normalizeModelSelectionOverrides(overrides)
|
||||
}
|
||||
|
||||
// Providers/models whose legacy-state migration has already been attempted in
|
||||
// this process. The migration runs from the read path, so it must be cheap on
|
||||
// repeat reads and must never run more than once per selection — including
|
||||
// when the legacy snapshot diffs to an empty override set and nothing is
|
||||
// written.
|
||||
const attemptedLegacyMigrations = new Set<string>()
|
||||
|
||||
function migrateLegacyModelOverridesIfNeeded(providerId: ProviderId, modelId: string, modelInfo: ModelInfo): void {
|
||||
if (providerSettingsProviderId(providerId) !== "openai-compatible") {
|
||||
return
|
||||
}
|
||||
const migrationKey = `${providerId}:${modelId}`
|
||||
if (attemptedLegacyMigrations.has(migrationKey)) {
|
||||
return
|
||||
}
|
||||
attemptedLegacyMigrations.add(migrationKey)
|
||||
if (readStoredModelEntry(providerId, modelId).exists) {
|
||||
return
|
||||
}
|
||||
if (readBaseModelInfoForProvider(providerId, modelId) !== undefined) {
|
||||
return
|
||||
}
|
||||
const overrides = legacyModelInfoToOverrides(modelInfo as LegacyModelInfo, fallbackModelInfo(modelId))
|
||||
if (overrides) {
|
||||
try {
|
||||
writeModelOverrides(providerId, modelId, overrides)
|
||||
} catch (error) {
|
||||
// The migration is best-effort and runs inside read paths; a
|
||||
// failed write (read-only fs, disk full) must not fail read RPCs.
|
||||
Logger.warn(
|
||||
`[ModelCatalog] Failed to migrate legacy overrides for provider=${providerId} model=${modelId}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The picker writes the live model metadata to the mode-specific
|
||||
* `*ModeModelInfo` state key before committing. When the state still refers to
|
||||
* the model being resolved, that snapshot is the best available base for
|
||||
* dynamic-list models the static catalog does not know.
|
||||
*
|
||||
* openai-compatible is excluded: its legacy state snapshot is user-authored
|
||||
* metadata that {@link migrateLegacyModelOverridesIfNeeded} converts into
|
||||
* models.json overrides, which are the source of truth there. Feeding the
|
||||
* snapshot back as a base would resurrect overrides the user deleted.
|
||||
*/
|
||||
function usesStateModelInfoHint(providerId: ProviderId): boolean {
|
||||
return providerSettingsProviderId(providerId) !== "openai-compatible"
|
||||
}
|
||||
|
||||
/**
|
||||
* Pickers write `{ ...openAiModelInfoSafeDefaults, name: modelId }` to the
|
||||
* state key when the user selects an id the live model list does not (yet)
|
||||
* contain. Such a snapshot carries no real information and must not be
|
||||
* treated as authoritative "state" metadata.
|
||||
*/
|
||||
function isSafeDefaultsSnapshot(modelInfo: ModelInfo, modelId: string): boolean {
|
||||
const fabricated: Record<string, unknown> = { ...openAiModelInfoSafeDefaults, name: modelId }
|
||||
const snapshot = modelInfo as unknown as Record<string, unknown>
|
||||
for (const key of new Set([...Object.keys(fabricated), ...Object.keys(snapshot)])) {
|
||||
if (fabricated[key] !== snapshot[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function readStateModelInfoHint(providerId: ProviderId, mode: Mode, modelId: string): ModelInfo | undefined {
|
||||
if (!usesStateModelInfoHint(providerId)) {
|
||||
return undefined
|
||||
}
|
||||
const modelInfoKey = getModelInfoKey(providerId, mode)
|
||||
if (!modelInfoKey) {
|
||||
return undefined
|
||||
}
|
||||
const apiConfiguration = StateManager.get().getApiConfiguration()
|
||||
const stateModelId = apiConfiguration[getModelIdKey(providerId, mode)]
|
||||
const stateModelInfo = apiConfiguration[modelInfoKey]
|
||||
return stateModelId === modelId && isModelInfo(stateModelInfo) && !isSafeDefaultsSnapshot(stateModelInfo, modelId)
|
||||
? stateModelInfo
|
||||
: undefined
|
||||
}
|
||||
|
||||
function readSelectionFromState(providerId: ProviderId, mode: Mode): ResolvedModelSelection | undefined {
|
||||
const apiConfiguration = StateManager.get().getApiConfiguration()
|
||||
const modelId = apiConfiguration[getModelIdKey(providerId, mode)]
|
||||
const modelInfoKey = getModelInfoKey(providerId, mode)
|
||||
const rememberedSelection = selectionMemory.get(memoryKey(providerId, mode))
|
||||
const providerSettingsSelection = readSelectionFromProviderSettings(providerId)
|
||||
|
||||
if (modelInfoKey) {
|
||||
const modelInfo = apiConfiguration[modelInfoKey]
|
||||
if (typeof modelId !== "string" || modelId.length === 0 || !isModelInfo(modelInfo)) {
|
||||
return providerSettingsSelection
|
||||
if (typeof modelId !== "string" || modelId.length === 0) {
|
||||
return readSelectionFromProviderSettings(providerId)
|
||||
}
|
||||
return { providerId, modelId, modelInfo }
|
||||
// The mode-specific model id alone identifies the selection; the state
|
||||
// modelInfo snapshot is optional input for legacy migration and, for
|
||||
// dynamic-list providers, the base-metadata hint. Fallback-tier commits
|
||||
// intentionally leave it unset.
|
||||
if (isModelInfo(modelInfo)) {
|
||||
migrateLegacyModelOverridesIfNeeded(providerId, modelId, modelInfo)
|
||||
}
|
||||
return resolveSelection({ providerId, modelId }, readStateModelInfoHint(providerId, mode, modelId))
|
||||
}
|
||||
|
||||
const providerSettingsSelection = readSelectionFromProviderSettings(providerId)
|
||||
const activeProvider = mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
|
||||
const provider = providerForStorage(providerId)
|
||||
if (activeProvider !== provider) {
|
||||
@@ -464,7 +865,7 @@ export function createProviderConfigStore(): ProviderConfigStore {
|
||||
return { ...buildEffectiveProviderConfig(providerId) }
|
||||
},
|
||||
|
||||
readSelection(providerId: ProviderId, mode: Mode): ModelSelection | undefined {
|
||||
readSelection(providerId: ProviderId, mode: Mode): ResolvedModelSelection | undefined {
|
||||
return readSelectionFromState(providerId, mode)
|
||||
},
|
||||
|
||||
@@ -482,9 +883,17 @@ export function createProviderConfigStore(): ProviderConfigStore {
|
||||
},
|
||||
|
||||
commitSelection(providerId: ProviderId, mode: Mode, selection: ModelSelection): void {
|
||||
writeSelectionToState(providerId, mode, selection)
|
||||
writeSelectionToProviderSettings(providerId, selection)
|
||||
emit({ kind: "selection", providerId, mode, selection })
|
||||
if (selection.overrides !== undefined) {
|
||||
writeModelOverrides(providerId, selection.modelId, selection.overrides)
|
||||
}
|
||||
// Read the picker-written state snapshot before writeSelectionToState
|
||||
// replaces it, so dynamic-list models keep their live metadata instead
|
||||
// of being re-resolved to fallback defaults.
|
||||
const stateModelInfoHint = readStateModelInfoHint(providerId, mode, selection.modelId)
|
||||
const resolvedSelection = resolveSelection({ providerId, modelId: selection.modelId }, stateModelInfoHint)
|
||||
writeSelectionToState(providerId, mode, resolvedSelection)
|
||||
emit({ kind: "selection", providerId, mode, selection: resolvedSelection })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { readModelsFileSync, writeModelsFileSync } from "@cline/core"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
type StoredModelsFile = ReturnType<typeof readModelsFileSync>
|
||||
|
||||
const firstPath = "/tmp/first-models.json"
|
||||
const secondPath = "/tmp/second-models.json"
|
||||
|
||||
const storedModelsFile = (): StoredModelsFile => ({
|
||||
version: 1,
|
||||
providers: {
|
||||
"openai-compatible": {
|
||||
models: {
|
||||
custom: { name: "Custom", capabilities: ["tools"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
describe("cline core model-file test stub", () => {
|
||||
it("isolates paths and returns defensive copies", () => {
|
||||
const input = storedModelsFile()
|
||||
writeModelsFileSync(firstPath, input)
|
||||
|
||||
input.providers["openai-compatible"].models!.custom.name = "mutated input"
|
||||
const firstRead = readModelsFileSync(firstPath)
|
||||
firstRead.providers["openai-compatible"].models!.custom.name = "mutated read"
|
||||
|
||||
expect(readModelsFileSync(firstPath)).toEqual(storedModelsFile())
|
||||
expect(readModelsFileSync(secondPath)).toEqual({ version: 1, providers: {} })
|
||||
})
|
||||
|
||||
it("cannot observe model writes from the preceding test", () => {
|
||||
expect(readModelsFileSync(firstPath)).toEqual({ version: 1, providers: {} })
|
||||
})
|
||||
})
|
||||
@@ -454,13 +454,35 @@ describe("SdkDiffEditCoordinator", () => {
|
||||
expect(callOrder).toEqual(["close", "apply"])
|
||||
})
|
||||
|
||||
it("applies patches without preview sessions directly", async () => {
|
||||
it("shows a brief preview around auto-approved patches", async () => {
|
||||
await writeFile("patched.ts", "line one\nline two\n")
|
||||
const patch = ["*** Begin Patch", "*** Update File: patched.ts", "@@", "-line one", "+line ONE", "*** End Patch"].join(
|
||||
"\n",
|
||||
)
|
||||
|
||||
const result = await coordinator.executeApplyPatchTool({ input: patch }, tempDir, makeContext("tc9"))
|
||||
|
||||
expect(result).toBe("fallback apply_patch result")
|
||||
expect(fallbackApplyPatch).toHaveBeenCalledOnce()
|
||||
expect(previews).toHaveLength(1)
|
||||
expect(previews[0].opened).toMatchObject({
|
||||
absolutePath: path.join(tempDir, "patched.ts"),
|
||||
leftContent: "line one\nline two\n",
|
||||
rightContent: "line ONE\nline two\n",
|
||||
})
|
||||
expect(previews[0].closed).toBe(1)
|
||||
})
|
||||
|
||||
it("applies auto-approved patches without a preview when background edit is enabled", async () => {
|
||||
backgroundEdit = true
|
||||
const result = await coordinator.executeApplyPatchTool(
|
||||
{ input: "*** Begin Patch\n*** End Patch" },
|
||||
tempDir,
|
||||
makeContext("tc9"),
|
||||
)
|
||||
|
||||
expect(result).toBe("fallback apply_patch result")
|
||||
expect(fallbackApplyPatch).toHaveBeenCalledOnce()
|
||||
expect(previews).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -122,12 +122,32 @@ export class SdkDiffEditCoordinator {
|
||||
}
|
||||
|
||||
/**
|
||||
* The `apply_patch` tool executor override: close the preview, then delegate the
|
||||
* whole patch application to the SDK's default executor.
|
||||
* The `apply_patch` tool executor override: manually-approved patches close their
|
||||
* approval preview before applying; auto-approved patches show a brief preview
|
||||
* around execution, matching the `editor` tool behavior.
|
||||
*/
|
||||
async executeApplyPatchTool(input: ApplyPatchInput, cwd: string, context: AgentToolContext): Promise<string> {
|
||||
await this.discardPreview(context.toolCallId ?? "")
|
||||
return this.fallbackApplyPatchExecutor(input, cwd, context)
|
||||
const toolCallId = context.toolCallId ?? ""
|
||||
const hadPreApprovalPreview = this.sessions.has(toolCallId)
|
||||
try {
|
||||
if (hadPreApprovalPreview) {
|
||||
await this.discardPreview(toolCallId)
|
||||
} else if (!this.options.isBackgroundEditEnabled()) {
|
||||
try {
|
||||
await this.openPatchPreview(toolCallId, input)
|
||||
} catch (error) {
|
||||
Logger.warn(`[SdkDiffEditCoordinator] Failed to show auto-approve patch preview: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.fallbackApplyPatchExecutor(input, cwd, context)
|
||||
if (!hadPreApprovalPreview && this.sessions.get(toolCallId)?.preview) {
|
||||
await lingerDelay(this.autoApprovePreviewLingerMs, context.signal)
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
await this.discardPreview(toolCallId)
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes one preview (reject / abort / edit applied). Never throws; unknown ids are a no-op. */
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
|
||||
describe("SdkForegroundCommandCoordinator", () => {
|
||||
it("reports isRunning while a handle is registered and notifies on changes", () => {
|
||||
const onRunningChanged = vi.fn()
|
||||
const coordinator = new SdkForegroundCommandCoordinator({ onRunningChanged })
|
||||
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
|
||||
const unregister = coordinator.register({ detach: () => {} })
|
||||
expect(coordinator.isRunning).toBe(true)
|
||||
expect(onRunningChanged).toHaveBeenCalledWith(true)
|
||||
|
||||
unregister()
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
expect(onRunningChanged).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it("only notifies on actual transitions, not per handle", () => {
|
||||
const onRunningChanged = vi.fn()
|
||||
const coordinator = new SdkForegroundCommandCoordinator({ onRunningChanged })
|
||||
|
||||
const unregister1 = coordinator.register({ detach: () => {} })
|
||||
const unregister2 = coordinator.register({ detach: () => {} })
|
||||
expect(onRunningChanged).toHaveBeenCalledTimes(1)
|
||||
|
||||
unregister1()
|
||||
expect(onRunningChanged).toHaveBeenCalledTimes(1)
|
||||
unregister2()
|
||||
expect(onRunningChanged).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("unregister is idempotent", () => {
|
||||
const onRunningChanged = vi.fn()
|
||||
const coordinator = new SdkForegroundCommandCoordinator({ onRunningChanged })
|
||||
|
||||
const unregister = coordinator.register({ detach: () => {} })
|
||||
unregister()
|
||||
unregister()
|
||||
expect(onRunningChanged).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("proceedWhileRunning detaches every registered handle and reports the count", () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const detach1 = vi.fn()
|
||||
const detach2 = vi.fn()
|
||||
coordinator.register({ detach: detach1 })
|
||||
coordinator.register({ detach: detach2 })
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(2)
|
||||
expect(detach1).toHaveBeenCalledTimes(1)
|
||||
expect(detach2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("proceedWhileRunning is a no-op returning 0 when nothing is running", () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
expect(coordinator.proceedWhileRunning()).toBe(0)
|
||||
})
|
||||
|
||||
it("proceedWhileRunning survives a handle whose detach throws", () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const detach2 = vi.fn()
|
||||
coordinator.register({
|
||||
detach: () => {
|
||||
throw new Error("boom")
|
||||
},
|
||||
})
|
||||
coordinator.register({ detach: detach2 })
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(2)
|
||||
expect(detach2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Tracks in-flight foreground (VS Code terminal) command executions so the
|
||||
* "Proceed While Running" button can detach them: each pending tool call
|
||||
* returns with its partial output while the command keeps running in the
|
||||
* user's terminal, streaming further output to a log file.
|
||||
*
|
||||
* Owned by SdkController so it outlives session rebuilds (which recreate the
|
||||
* tool set and its reused executor closure). Handles are registered per tool
|
||||
* invocation — never on the reused executor — so parallel commands in one
|
||||
* tool call each get their own handle and log file.
|
||||
*/
|
||||
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
export interface ForegroundCommandHandle {
|
||||
/**
|
||||
* Stop waiting for the command: flush the output captured so far to a
|
||||
* log file, keep appending until the command completes, and resolve the
|
||||
* pending tool execution with the partial output. Idempotent.
|
||||
*/
|
||||
detach(): void
|
||||
}
|
||||
|
||||
export interface SdkForegroundCommandCoordinatorOptions {
|
||||
/** Called whenever isRunning flips; used to push the flag to the webview. */
|
||||
onRunningChanged?: (running: boolean) => void
|
||||
}
|
||||
|
||||
export class SdkForegroundCommandCoordinator {
|
||||
private readonly handles = new Set<ForegroundCommandHandle>()
|
||||
|
||||
constructor(private readonly options: SdkForegroundCommandCoordinatorOptions = {}) {}
|
||||
|
||||
/** Whether any foreground command is currently awaited by a tool call. */
|
||||
get isRunning(): boolean {
|
||||
return this.handles.size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Track one in-flight foreground execution. Returns an unregister
|
||||
* function the caller must invoke when the execution settles (completes,
|
||||
* fails, aborts, or detaches) — typically from a `finally` block.
|
||||
*/
|
||||
register(handle: ForegroundCommandHandle): () => void {
|
||||
const wasRunning = this.isRunning
|
||||
this.handles.add(handle)
|
||||
this.notifyIfChanged(wasRunning)
|
||||
return () => {
|
||||
const wasRunningBefore = this.isRunning
|
||||
if (this.handles.delete(handle)) {
|
||||
this.notifyIfChanged(wasRunningBefore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach every in-flight foreground command ("Proceed While Running").
|
||||
* Each pending tool execution resolves with its partial output and log
|
||||
* file path; the commands keep running in their terminals.
|
||||
*
|
||||
* @returns the number of commands detached (0 when none were running).
|
||||
*/
|
||||
proceedWhileRunning(): number {
|
||||
const handles = [...this.handles]
|
||||
for (const handle of handles) {
|
||||
try {
|
||||
handle.detach()
|
||||
} catch (error) {
|
||||
Logger.error("[ForegroundCommands] Failed to detach foreground command:", error)
|
||||
}
|
||||
}
|
||||
return handles.length
|
||||
}
|
||||
|
||||
private notifyIfChanged(wasRunning: boolean): void {
|
||||
if (this.isRunning !== wasRunning) {
|
||||
this.options.onRunningChanged?.(this.isRunning)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTermin
|
||||
import { McpHub } from "@/services/mcp/McpHub"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ActiveSession } from "./cline-session-factory"
|
||||
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import { buildToolPolicies } from "./sdk-tool-policies"
|
||||
import type { SdkSessionHost } from "./session-host"
|
||||
import { VscodeSessionHost } from "./vscode-session-host"
|
||||
@@ -32,6 +33,8 @@ export interface SdkSessionLifecycleOptions {
|
||||
onSessionEvent: (event: CoreSessionEvent) => void
|
||||
/** Lazy factory for the VscodeTerminalManager (foreground terminal support). */
|
||||
getTerminalManager?: () => VscodeTerminalManager
|
||||
/** Registry of in-flight foreground executions for "Proceed While Running". */
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator
|
||||
/** Returns the latest prepared remote-config integration, if remote config is active. */
|
||||
getRemoteConfigIntegration?: () => PreparedRemoteConfigCoreIntegration | undefined
|
||||
/** Shared SDK telemetry service owned by SdkController. */
|
||||
@@ -322,6 +325,7 @@ export class SdkSessionLifecycle {
|
||||
editorExecutor: this.options.editorExecutor,
|
||||
applyPatchExecutor: this.options.applyPatchExecutor,
|
||||
getTerminalManager: this.options.getTerminalManager,
|
||||
foregroundCommands: this.options.foregroundCommands,
|
||||
getRemoteConfigIntegration: this.options.getRemoteConfigIntegration,
|
||||
telemetry: this.options.telemetry,
|
||||
})
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { CommandExitError } from "@cline/core"
|
||||
import { EventEmitter } from "events"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import type { TerminalCompletionDetails } from "@/integrations/terminal/types"
|
||||
import * as fs from "fs"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTerminalManager"
|
||||
import { executeForeground, formatCommandForTerminal } from "./vscode-run-commands-tool"
|
||||
import type { TerminalCompletionDetails } from "@/integrations/terminal/types"
|
||||
import { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import { executeForeground, formatCommandForTerminal, PROCEED_LOG_MAX_BYTES } from "./vscode-run-commands-tool"
|
||||
|
||||
// The real telemetry proxy lazily initializes TelemetryService, which requires
|
||||
// a HostProvider that unit tests don't set up.
|
||||
vi.mock("@services/telemetry", () => ({
|
||||
TerminalUserInterventionAction: { PROCESS_WHILE_RUNNING: "process_while_running" },
|
||||
telemetryService: {
|
||||
captureTerminalUserIntervention: () => {},
|
||||
captureTerminalExecution: () => {},
|
||||
},
|
||||
}))
|
||||
|
||||
/**
|
||||
* Minimal fake of the process object returned by VscodeTerminalManager.runCommand():
|
||||
@@ -12,11 +24,13 @@ import { executeForeground, formatCommandForTerminal } from "./vscode-run-comman
|
||||
*/
|
||||
function createFakeTerminalProcess(options: { lines?: string[]; completionDetails?: TerminalCompletionDetails } = {}) {
|
||||
const emitter = new EventEmitter()
|
||||
let resolvePromise!: () => void
|
||||
// Emit on a macrotask (not a microtask) so executeForeground's
|
||||
// `await terminalManager.getOrCreateTerminal(cwd)` and subsequent
|
||||
// `process.on("line", ...)` registration are guaranteed to run first,
|
||||
// matching the ordering a real terminal process provides.
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
resolvePromise = resolve
|
||||
setTimeout(() => {
|
||||
for (const line of options.lines ?? []) {
|
||||
emitter.emit("line", line)
|
||||
@@ -31,6 +45,10 @@ function createFakeTerminalProcess(options: { lines?: string[]; completionDetail
|
||||
catch: promise.catch.bind(promise),
|
||||
finally: promise.finally.bind(promise),
|
||||
getCompletionDetails: () => options.completionDetails ?? {},
|
||||
detach: () => {
|
||||
emitter.emit("continue")
|
||||
resolvePromise()
|
||||
},
|
||||
})
|
||||
return fakeProcess as unknown as ReturnType<VscodeTerminalManager["runCommand"]>
|
||||
}
|
||||
@@ -42,6 +60,50 @@ function createFakeTerminalManager(process: ReturnType<VscodeTerminalManager["ru
|
||||
} as unknown as VscodeTerminalManager
|
||||
}
|
||||
|
||||
/**
|
||||
* A controllable fake terminal process for detach tests: the caller decides
|
||||
* when lines are emitted and when the command completes. Mirrors the real
|
||||
* VscodeTerminalProcess contract: detach() resolves the awaited promise while
|
||||
* 'line'/'completed' events keep flowing.
|
||||
*/
|
||||
function createControllableTerminalProcess() {
|
||||
const emitter = new EventEmitter()
|
||||
let resolvePromise!: () => void
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
resolvePromise = resolve
|
||||
})
|
||||
const fakeProcess = Object.assign(emitter, {
|
||||
then: promise.then.bind(promise),
|
||||
catch: promise.catch.bind(promise),
|
||||
finally: promise.finally.bind(promise),
|
||||
getCompletionDetails: () => ({}),
|
||||
detach: () => {
|
||||
emitter.emit("continue")
|
||||
resolvePromise()
|
||||
},
|
||||
})
|
||||
return {
|
||||
process: fakeProcess as unknown as ReturnType<VscodeTerminalManager["runCommand"]>,
|
||||
emitLine: (line: string) => emitter.emit("line", line),
|
||||
complete: (details?: TerminalCompletionDetails) => {
|
||||
emitter.emit("completed", details)
|
||||
emitter.emit("continue")
|
||||
resolvePromise()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Poll until the predicate holds, for asserting on async log-file writes. */
|
||||
async function waitFor(predicate: () => boolean, timeoutMs = 2000): Promise<void> {
|
||||
const start = Date.now()
|
||||
while (!predicate()) {
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
throw new Error("waitFor timed out")
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
}
|
||||
|
||||
describe("formatCommandForTerminal", () => {
|
||||
it.each([
|
||||
{
|
||||
@@ -172,4 +234,207 @@ describe("executeForeground", () => {
|
||||
expect((error as InstanceType<typeof CommandExitError>).output).toContain("Terminal closed")
|
||||
}
|
||||
})
|
||||
|
||||
it("unregisters its foreground handle when the command completes normally", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const terminalManager = createFakeTerminalManager(createFakeTerminalProcess({ lines: ["hello"] }))
|
||||
|
||||
const result = await executeForeground("echo hello", "/workspace", terminalManager, 1000, undefined, coordinator)
|
||||
|
||||
expect(result).toBe("hello")
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("executeForeground — Proceed While Running", () => {
|
||||
it("detach returns the partial output with the log file path, and later output lands in the log", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const { process, emitLine, complete } = createControllableTerminalProcess()
|
||||
const terminalManager = createFakeTerminalManager(process)
|
||||
|
||||
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
|
||||
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
emitLine("listening on :3000")
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(1)
|
||||
const result = await resultPromise
|
||||
|
||||
expect(result).toContain("still running")
|
||||
expect(result).toContain("listening on :3000")
|
||||
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
|
||||
expect(logFilePath).toBeTruthy()
|
||||
|
||||
// The handle is unregistered once the tool call returns.
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
|
||||
// Output emitted after detach is appended to the log file, and
|
||||
// completion closes it out with a completion marker.
|
||||
emitLine("compiled successfully")
|
||||
complete({ exitCode: 0 })
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const log = fs.readFileSync(logFilePath!, "utf8")
|
||||
expect(log).toContain("listening on :3000") // buffered lines flushed at detach
|
||||
expect(log).toContain("compiled successfully") // streamed after detach
|
||||
fs.rmSync(logFilePath!, { force: true })
|
||||
})
|
||||
|
||||
it("detaches each parallel command into its own log file", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const first = createControllableTerminalProcess()
|
||||
const second = createControllableTerminalProcess()
|
||||
|
||||
const firstPromise = executeForeground(
|
||||
"first-cmd",
|
||||
"/workspace",
|
||||
createFakeTerminalManager(first.process),
|
||||
100_000,
|
||||
undefined,
|
||||
coordinator,
|
||||
)
|
||||
const secondPromise = executeForeground(
|
||||
"second-cmd",
|
||||
"/workspace",
|
||||
createFakeTerminalManager(second.process),
|
||||
100_000,
|
||||
undefined,
|
||||
coordinator,
|
||||
)
|
||||
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
first.emitLine("first output")
|
||||
second.emitLine("second output")
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(2)
|
||||
const [firstResult, secondResult] = await Promise.all([firstPromise, secondPromise])
|
||||
|
||||
const firstLog = /redirected to this file[^:]*: (.+)$/m.exec(firstResult)?.[1]?.trim()
|
||||
const secondLog = /redirected to this file[^:]*: (.+)$/m.exec(secondResult)?.[1]?.trim()
|
||||
expect(firstLog).toBeTruthy()
|
||||
expect(secondLog).toBeTruthy()
|
||||
expect(firstLog).not.toBe(secondLog)
|
||||
expect(coordinator.isRunning).toBe(false)
|
||||
|
||||
first.complete()
|
||||
second.complete()
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return (
|
||||
fs.readFileSync(firstLog!, "utf8").includes("[Command completed]") &&
|
||||
fs.readFileSync(secondLog!, "utf8").includes("[Command completed]")
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
expect(fs.readFileSync(firstLog!, "utf8")).toContain("first output")
|
||||
expect(fs.readFileSync(secondLog!, "utf8")).toContain("second output")
|
||||
fs.rmSync(firstLog!, { force: true })
|
||||
fs.rmSync(secondLog!, { force: true })
|
||||
})
|
||||
|
||||
it("stops logging before a line that would exceed the size cap", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const { process, emitLine, complete } = createControllableTerminalProcess()
|
||||
const terminalManager = createFakeTerminalManager(process)
|
||||
|
||||
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(1)
|
||||
const result = await resultPromise
|
||||
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
|
||||
expect(logFilePath).toBeTruthy()
|
||||
|
||||
// A single line larger than the whole cap must not be written at all —
|
||||
// the cap is checked before writing, so one huge line (e.g. a dumped
|
||||
// blob) cannot blow the log far past PROCEED_LOG_MAX_BYTES.
|
||||
emitLine("small line before the blob")
|
||||
emitLine("x".repeat(PROCEED_LOG_MAX_BYTES))
|
||||
emitLine("after the cap")
|
||||
complete({ exitCode: 0 })
|
||||
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const log = fs.readFileSync(logFilePath!, "utf8")
|
||||
expect(log).toContain("small line before the blob")
|
||||
expect(log).toContain(`[Log size cap of ${PROCEED_LOG_MAX_BYTES} bytes reached`)
|
||||
expect(log).not.toContain("xxxx")
|
||||
expect(log).not.toContain("after the cap")
|
||||
expect(log.length).toBeLessThan(PROCEED_LOG_MAX_BYTES)
|
||||
fs.rmSync(logFilePath!, { force: true })
|
||||
})
|
||||
|
||||
it("applies the size cap to lines buffered before detach", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const { process, emitLine, complete } = createControllableTerminalProcess()
|
||||
const terminalManager = createFakeTerminalManager(process)
|
||||
|
||||
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
emitLine("x".repeat(PROCEED_LOG_MAX_BYTES))
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(1)
|
||||
const result = await resultPromise
|
||||
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
|
||||
expect(logFilePath).toBeTruthy()
|
||||
complete({ exitCode: 0 })
|
||||
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const log = fs.readFileSync(logFilePath!, "utf8")
|
||||
expect(log).toContain(`[Log size cap of ${PROCEED_LOG_MAX_BYTES} bytes reached`)
|
||||
expect(log).not.toContain("xxxx")
|
||||
expect(Buffer.byteLength(log)).toBeLessThanOrEqual(PROCEED_LOG_MAX_BYTES)
|
||||
fs.rmSync(logFilePath!, { force: true })
|
||||
})
|
||||
|
||||
it("freezes the partial output at detach while later output still reaches the log", async () => {
|
||||
const coordinator = new SdkForegroundCommandCoordinator()
|
||||
const { process, emitLine, complete } = createControllableTerminalProcess()
|
||||
const terminalManager = createFakeTerminalManager(process)
|
||||
|
||||
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
|
||||
await waitFor(() => coordinator.isRunning)
|
||||
emitLine("before detach")
|
||||
|
||||
expect(coordinator.proceedWhileRunning()).toBe(1)
|
||||
// Emitted after detach but before the tool call's result is built:
|
||||
// must appear only in the log, never in the partial output.
|
||||
emitLine("after detach")
|
||||
const result = await resultPromise
|
||||
|
||||
expect(result).toContain("before detach")
|
||||
expect(result).not.toContain("after detach")
|
||||
|
||||
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
|
||||
complete({ exitCode: 0 })
|
||||
await waitFor(() => {
|
||||
try {
|
||||
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const log = fs.readFileSync(logFilePath!, "utf8")
|
||||
expect(log).toContain("before detach")
|
||||
expect(log).toContain("after detach")
|
||||
fs.rmSync(logFilePath!, { force: true })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,12 +21,16 @@ import {
|
||||
truncateCommandOutput,
|
||||
} from "@cline/core"
|
||||
import type { AgentTool } from "@cline/shared"
|
||||
import { telemetryService } from "@services/telemetry"
|
||||
import { TerminalUserInterventionAction, telemetryService } from "@services/telemetry"
|
||||
import { ClineTempManager } from "@services/temp"
|
||||
import * as fs from "fs"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTerminalManager"
|
||||
import { MAX_UNRETRIEVED_LINES } from "@/integrations/terminal/constants"
|
||||
import type { ITerminalProcess } from "@/integrations/terminal/types"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { getShellForProfile } from "@/utils/shell"
|
||||
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -38,6 +42,14 @@ type VscodeTerminalExecutionMode = "vscodeTerminal" | "backgroundExec"
|
||||
/** Foreground VS Code terminals cannot be forcibly terminated; give long-running commands room to finish. */
|
||||
export const VSCODE_FOREGROUND_RUN_COMMANDS_TIMEOUT_MS = 60 * 60 * 1000
|
||||
|
||||
/**
|
||||
* Cap on the "Proceed While Running" log file. A detached devserver can log
|
||||
* for days; once the cap is hit we stop appending and note the truncation.
|
||||
* ClineTempManager's periodic cleanup (age + total-size caps) is the backstop
|
||||
* for the files themselves.
|
||||
*/
|
||||
export const PROCEED_LOG_MAX_BYTES = 10 * 1024 * 1024
|
||||
|
||||
/** Options for creating the VSCode run_commands tool. */
|
||||
export interface VscodeRunCommandsToolOptions {
|
||||
/** Workspace root directory. */
|
||||
@@ -48,6 +60,14 @@ export interface VscodeRunCommandsToolOptions {
|
||||
bashTimeoutMs?: number
|
||||
/** Terminal execution mode captured when this session's tool set is built. */
|
||||
vscodeTerminalExecutionMode?: VscodeTerminalExecutionMode
|
||||
/**
|
||||
* Registry of in-flight foreground executions, owned by SdkController.
|
||||
* When provided, each foreground command can be detached via the
|
||||
* "Proceed While Running" button. Foreground-only: background (SDK
|
||||
* child_process) executions cannot be detached — their abort signal
|
||||
* kills the process tree.
|
||||
*/
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -74,6 +94,69 @@ export function formatCommandForTerminal(command: ShellCommand): string {
|
||||
return [command.command, ...(command.args ?? [])].map(quoteShellArg).join(" ")
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the rest of a detached command's output to a log file: write the
|
||||
* lines buffered so far, then append each further 'line' event until
|
||||
* 'completed'. The write volume is capped at PROCEED_LOG_MAX_BYTES; the
|
||||
* stream is always closed by the 'completed' event, which the terminal
|
||||
* process emits on every exit path (command end, Ctrl+C, terminal closed,
|
||||
* markerless fallback).
|
||||
*/
|
||||
function beginLogCapture(process: ITerminalProcess, terminalCommand: string, existingLines: string[]): string {
|
||||
const logFilePath = ClineTempManager.createTempFilePath("proceed-while-running")
|
||||
const stream = fs.createWriteStream(logFilePath, { flags: "a" })
|
||||
const sizeCapMessage = `[Log size cap of ${PROCEED_LOG_MAX_BYTES} bytes reached; further output is not logged.]`
|
||||
stream.on("error", (error) => {
|
||||
Logger.error(`[VscodeRunCommands] Failed writing proceed-while-running log ${logFilePath}:`, error)
|
||||
})
|
||||
|
||||
let bytesWritten = 0
|
||||
const tryWriteLine = (line: string): boolean => {
|
||||
const chunk = `${line}\n`
|
||||
const chunkBytes = Buffer.byteLength(chunk)
|
||||
if (bytesWritten + chunkBytes > PROCEED_LOG_MAX_BYTES) {
|
||||
return false
|
||||
}
|
||||
bytesWritten += chunkBytes
|
||||
stream.write(chunk)
|
||||
return true
|
||||
}
|
||||
|
||||
let sizeCapReached = !tryWriteLine(`[Running command: ${terminalCommand}]`)
|
||||
for (const line of existingLines) {
|
||||
if (!tryWriteLine(line)) {
|
||||
sizeCapReached = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const onLine = (line: string): void => {
|
||||
// Check the cap before writing: a single huge line (e.g. a dumped
|
||||
// binary blob or minified bundle) must not blow past the cap.
|
||||
if (!tryWriteLine(line)) {
|
||||
tryWriteLine(sizeCapMessage)
|
||||
process.removeListener("line", onLine)
|
||||
}
|
||||
}
|
||||
if (sizeCapReached) {
|
||||
tryWriteLine(sizeCapMessage)
|
||||
} else {
|
||||
process.on("line", onLine)
|
||||
}
|
||||
process.once("completed", (details) => {
|
||||
process.removeListener("line", onLine)
|
||||
const exitCode = details?.exitCode
|
||||
tryWriteLine(
|
||||
exitCode !== undefined && exitCode !== null
|
||||
? `[Command completed with exit code ${exitCode}]`
|
||||
: "[Command completed]",
|
||||
)
|
||||
stream.end()
|
||||
})
|
||||
|
||||
return logFilePath
|
||||
}
|
||||
|
||||
/** Exported for direct unit testing of the CommandExitError/terminalClosed mapping. */
|
||||
export async function executeForeground(
|
||||
command: ShellCommand,
|
||||
@@ -81,6 +164,7 @@ export async function executeForeground(
|
||||
terminalManager: VscodeTerminalManager,
|
||||
maxOutputChars: number,
|
||||
abortSignal?: AbortSignal,
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator,
|
||||
): Promise<string> {
|
||||
const terminalCommand = formatCommandForTerminal(command)
|
||||
const terminalInfo = await terminalManager.getOrCreateTerminal(cwd)
|
||||
@@ -100,7 +184,7 @@ export async function executeForeground(
|
||||
// truncateCommandOutput's own head/tail strategy below — since build/test
|
||||
// failures usually appear at the end of output.
|
||||
const maxBufferedLines = MAX_UNRETRIEVED_LINES
|
||||
process.on("line", (line: string) => {
|
||||
const bufferLine = (line: string): void => {
|
||||
if (outputLines.length < maxBufferedLines) {
|
||||
outputLines.push(line)
|
||||
} else {
|
||||
@@ -108,7 +192,8 @@ export async function executeForeground(
|
||||
outputLines.push(line)
|
||||
droppedLines++
|
||||
}
|
||||
})
|
||||
}
|
||||
process.on("line", bufferLine)
|
||||
|
||||
// Handle abort signal
|
||||
if (abortSignal) {
|
||||
@@ -121,8 +206,33 @@ export async function executeForeground(
|
||||
process.once("continue", cleanupAbortListener)
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
await process
|
||||
// "Proceed While Running": register a per-invocation handle so the user
|
||||
// can detach this command. Detaching redirects the remaining output to a
|
||||
// log file and resolves the awaited promise; the command keeps running in
|
||||
// the user's terminal (and the terminal stays busy until it completes).
|
||||
let detachedLogFilePath: string | undefined
|
||||
const unregister = foregroundCommands?.register({
|
||||
detach: () => {
|
||||
if (detachedLogFilePath !== undefined) {
|
||||
return
|
||||
}
|
||||
detachedLogFilePath = beginLogCapture(process, terminalCommand, outputLines)
|
||||
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.PROCESS_WHILE_RUNNING, "vscode")
|
||||
// detach() flushes any partial line (reaching both bufferLine and
|
||||
// the log) before resolving the awaited promise. After that the
|
||||
// partial output is final: stop buffering so the remaining
|
||||
// (log-only) output doesn't mutate outputLines while it's read.
|
||||
process.detach()
|
||||
process.removeListener("line", bufferLine)
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
// Wait for completion (or detach, which also resolves the promise)
|
||||
await process
|
||||
} finally {
|
||||
unregister?.()
|
||||
}
|
||||
if (abortSignal?.aborted) {
|
||||
throw new Error("Command execution aborted")
|
||||
}
|
||||
@@ -135,6 +245,14 @@ export async function executeForeground(
|
||||
maxChars: maxOutputChars,
|
||||
})
|
||||
|
||||
if (detachedLogFilePath !== undefined) {
|
||||
return [
|
||||
"The user chose to proceed while the command is still running in their terminal.",
|
||||
`This is partial output; further output is being redirected to this file, which you can read to check progress: ${detachedLogFilePath}`,
|
||||
output.length > 0 ? `Output so far:\n${output}` : "No output so far.",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
const completionDetails = process.getCompletionDetails?.()
|
||||
|
||||
// A terminal closed mid-command has no exit code and no reliable output —
|
||||
@@ -240,6 +358,13 @@ function createVscodeShellExecutor(options: VscodeRunCommandsToolOptions): Shell
|
||||
if (!terminalManager) {
|
||||
terminalManager = getTerminalManager()
|
||||
}
|
||||
return await executeForeground(command, commandCwd || cwd, terminalManager, MAX_COMMAND_OUTPUT_CHARS, context.signal)
|
||||
return await executeForeground(
|
||||
command,
|
||||
commandCwd || cwd,
|
||||
terminalManager,
|
||||
MAX_COMMAND_OUTPUT_CHARS,
|
||||
context.signal,
|
||||
options.foregroundCommands,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type AgentTool, type AgentToolContext, createTool } from "@cline/shared
|
||||
import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTerminalManager"
|
||||
import type { McpHub } from "@/services/mcp/McpHub"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import { createVscodeRunCommandsTool, VSCODE_FOREGROUND_RUN_COMMANDS_TIMEOUT_MS } from "./vscode-run-commands-tool"
|
||||
|
||||
interface McpToolDescriptor {
|
||||
@@ -124,6 +125,8 @@ export interface VscodeExtraToolsOptions {
|
||||
getTerminalManager?: () => VscodeTerminalManager
|
||||
/** Current VS Code terminal execution mode, captured when the session tools are built. */
|
||||
vscodeTerminalExecutionMode?: "vscodeTerminal" | "backgroundExec"
|
||||
/** Registry of in-flight foreground executions for "Proceed While Running". */
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator
|
||||
}
|
||||
|
||||
export async function createVscodeExtraTools(mcpHub: McpHub, options?: VscodeExtraToolsOptions): Promise<AgentTool[]> {
|
||||
@@ -159,6 +162,7 @@ export async function createVscodeExtraTools(mcpHub: McpHub, options?: VscodeExt
|
||||
getTerminalManager: options.getTerminalManager,
|
||||
bashTimeoutMs: executionMode === "vscodeTerminal" ? VSCODE_FOREGROUND_RUN_COMMANDS_TIMEOUT_MS : undefined,
|
||||
vscodeTerminalExecutionMode: executionMode,
|
||||
foregroundCommands: options.foregroundCommands,
|
||||
}),
|
||||
)
|
||||
Logger.log(
|
||||
|
||||
@@ -36,9 +36,10 @@ import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTermin
|
||||
import { getDistinctId } from "@/services/logging/distinctId"
|
||||
import type { McpHub } from "@/services/mcp/McpHub"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
|
||||
import type { SdkSessionHost } from "./session-host"
|
||||
import { getEffectiveTerminalExecutionMode } from "./vscode-terminal-execution-mode"
|
||||
import { createVscodeExtraTools } from "./vscode-runtime-builder"
|
||||
import { getEffectiveTerminalExecutionMode } from "./vscode-terminal-execution-mode"
|
||||
|
||||
export interface VscodeSessionHostOptions {
|
||||
mcpHub: McpHub
|
||||
@@ -75,6 +76,8 @@ export interface VscodeSessionHostOptions {
|
||||
* with a custom tool that supports foreground/background terminal execution.
|
||||
*/
|
||||
getTerminalManager?: () => VscodeTerminalManager
|
||||
/** Registry of in-flight foreground executions for "Proceed While Running". */
|
||||
foregroundCommands?: SdkForegroundCommandCoordinator
|
||||
}
|
||||
|
||||
export class VscodeSessionHost implements SdkSessionHost {
|
||||
@@ -133,6 +136,7 @@ export class VscodeSessionHost implements SdkSessionHost {
|
||||
cwd: inputWithRemoteConfig.config.cwd,
|
||||
getTerminalManager: options.getTerminalManager,
|
||||
vscodeTerminalExecutionMode: getEffectiveTerminalExecutionMode(requestedTerminalExecutionMode),
|
||||
foregroundCommands: options.foregroundCommands,
|
||||
})
|
||||
return {
|
||||
...inputWithRemoteConfig,
|
||||
|
||||
@@ -124,6 +124,7 @@ export class WebviewGrpcBridge {
|
||||
stateManager,
|
||||
mcpHub: undefined,
|
||||
backgroundCommandRunning: false,
|
||||
foregroundCommandRunning: false,
|
||||
backgroundCommandTaskId: undefined,
|
||||
})
|
||||
await sendStateUpdate(state)
|
||||
|
||||
@@ -93,6 +93,11 @@ export interface ExtensionState {
|
||||
vscodeTerminalExecutionMode: string
|
||||
backgroundCommandRunning?: boolean
|
||||
backgroundCommandTaskId?: string
|
||||
/**
|
||||
* True while a foreground (VS Code terminal) command is awaited by a
|
||||
* run_commands tool call. Drives the "Proceed While Running" button.
|
||||
*/
|
||||
foregroundCommandRunning?: boolean
|
||||
lastCompletedCommandTs?: number
|
||||
userInfo?: UserInfo
|
||||
version: string
|
||||
|
||||
@@ -61,8 +61,10 @@
|
||||
* JetBrains exports trusted certificates from the OS and writes them to a
|
||||
* temporary file, then configures node TLS by setting NODE_EXTRA_CA_CERTS.
|
||||
*
|
||||
* CLI users should set the NODE_EXTRA_CA_CERTS environment variable if
|
||||
* necessary, because node does not automatically use the OS' trusted certs.
|
||||
* The CLI's npm wrapper (bin/cline) does the same automatically: it harvests
|
||||
* the OS trust store and points the child's NODE_EXTRA_CA_CERTS at a managed
|
||||
* bundle, because the Bun runtime does not read the OS store on its own. A
|
||||
* user-set NODE_EXTRA_CA_CERTS is merged in rather than replaced.
|
||||
*
|
||||
* ## Limitations in JetBrains & CLI
|
||||
*
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { ModelInfo } from "@shared/api"
|
||||
import { ModelOverrides } from "@shared/proto/cline/models"
|
||||
|
||||
/**
|
||||
* Domain shape of user-authored per-model metadata overrides, shared by the
|
||||
* webview (commit path) and the host (read/commit handlers). Mirrors the
|
||||
* `ModelOverrides` proto message.
|
||||
*
|
||||
* Semantics (enforced host-side in the provider config store):
|
||||
* - `capabilities` accepts only SDK `ModelCapability` values; unknown
|
||||
* strings are silently dropped. The array is additive over the base
|
||||
* metadata; the explicit `supports*` booleans win when both are present.
|
||||
* - `isR1FormatRequired` is a legacy alias that forces the R1 chat format
|
||||
* only when true; `apiFormat` is canonical.
|
||||
* - Invalid numbers (non-positive token limits, negative prices or
|
||||
* temperature, non-finite values) are silently discarded, not rejected.
|
||||
*
|
||||
* When committing a selection, the overrides value is tri-state: `undefined`
|
||||
* preserves the model's stored overrides, an explicitly empty object clears
|
||||
* them, and a non-empty object replaces them wholesale (no per-field merge).
|
||||
*/
|
||||
export interface ProviderModelOverrides {
|
||||
name?: string
|
||||
maxTokens?: number
|
||||
contextWindow?: number
|
||||
maxInputTokens?: number
|
||||
capabilities?: readonly string[]
|
||||
supportsVision?: boolean
|
||||
supportsAttachments?: boolean
|
||||
supportsReasoning?: boolean
|
||||
inputPrice?: number
|
||||
outputPrice?: number
|
||||
cacheReadsPrice?: number
|
||||
cacheWritesPrice?: number
|
||||
temperature?: number
|
||||
apiFormat?: ModelInfo["apiFormat"]
|
||||
isR1FormatRequired?: boolean
|
||||
}
|
||||
|
||||
export function toProtobufModelOverrides(overrides: ProviderModelOverrides): ModelOverrides {
|
||||
return ModelOverrides.create({
|
||||
name: overrides.name,
|
||||
maxTokens: overrides.maxTokens,
|
||||
contextWindow: overrides.contextWindow,
|
||||
maxInputTokens: overrides.maxInputTokens,
|
||||
capabilities: overrides.capabilities ? [...overrides.capabilities] : [],
|
||||
supportsVision: overrides.supportsVision,
|
||||
supportsAttachments: overrides.supportsAttachments,
|
||||
supportsReasoning: overrides.supportsReasoning,
|
||||
inputPrice: overrides.inputPrice,
|
||||
outputPrice: overrides.outputPrice,
|
||||
cacheReadsPrice: overrides.cacheReadsPrice,
|
||||
cacheWritesPrice: overrides.cacheWritesPrice,
|
||||
temperature: overrides.temperature,
|
||||
apiFormat: overrides.apiFormat,
|
||||
isR1FormatRequired: overrides.isR1FormatRequired,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserves the proto tri-state: `undefined` stays `undefined` (no override
|
||||
* payload), and an empty message becomes an empty object (explicit clear).
|
||||
*/
|
||||
export function fromProtobufModelOverrides(overrides: ModelOverrides | undefined): ProviderModelOverrides | undefined {
|
||||
if (!overrides) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
...(overrides.name !== undefined ? { name: overrides.name } : {}),
|
||||
...(overrides.maxTokens !== undefined ? { maxTokens: overrides.maxTokens } : {}),
|
||||
...(overrides.contextWindow !== undefined ? { contextWindow: overrides.contextWindow } : {}),
|
||||
...(overrides.maxInputTokens !== undefined ? { maxInputTokens: overrides.maxInputTokens } : {}),
|
||||
...(overrides.capabilities.length > 0 ? { capabilities: [...overrides.capabilities] } : {}),
|
||||
...(overrides.supportsVision !== undefined ? { supportsVision: overrides.supportsVision } : {}),
|
||||
...(overrides.supportsAttachments !== undefined ? { supportsAttachments: overrides.supportsAttachments } : {}),
|
||||
...(overrides.supportsReasoning !== undefined ? { supportsReasoning: overrides.supportsReasoning } : {}),
|
||||
...(overrides.inputPrice !== undefined ? { inputPrice: overrides.inputPrice } : {}),
|
||||
...(overrides.outputPrice !== undefined ? { outputPrice: overrides.outputPrice } : {}),
|
||||
...(overrides.cacheReadsPrice !== undefined ? { cacheReadsPrice: overrides.cacheReadsPrice } : {}),
|
||||
...(overrides.cacheWritesPrice !== undefined ? { cacheWritesPrice: overrides.cacheWritesPrice } : {}),
|
||||
...(overrides.temperature !== undefined ? { temperature: overrides.temperature } : {}),
|
||||
...(overrides.apiFormat !== undefined ? { apiFormat: overrides.apiFormat } : {}),
|
||||
...(overrides.isR1FormatRequired !== undefined ? { isR1FormatRequired: overrides.isR1FormatRequired } : {}),
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,9 @@
|
||||
// Importing the real package here is safe: the preload runs before any test
|
||||
// file, so this is the only point the real module is linked, and we only read
|
||||
// its export *names*, never its behavior (the mock shadows it everywhere tests look).
|
||||
import { vi as bunVi, mock } from "bun:test"
|
||||
import { beforeEach as bunBeforeEach, vi as bunVi, mock } from "bun:test"
|
||||
import * as realClineCore from "@cline/core"
|
||||
import * as LlmsModels from "@cline/llms"
|
||||
import * as clineCoreStub from "./cline-core-vitest-stub"
|
||||
import * as vscodeStub from "./vscode-vitest-stub"
|
||||
|
||||
@@ -43,6 +44,13 @@ for (const name of Object.keys(realClineCore)) {
|
||||
}
|
||||
Object.assign(clineCoreNamespace, clineCoreStub)
|
||||
|
||||
bunBeforeEach(() => {
|
||||
clineCoreStub.resetModelsFileState()
|
||||
// The stub's syncStoredProviderRegistration mutates the real shared
|
||||
// @cline/llms registry; reset it so registrations never leak across tests.
|
||||
LlmsModels.resetRegistry()
|
||||
})
|
||||
|
||||
mock.module("@cline/core", () => clineCoreNamespace)
|
||||
|
||||
// `vscode`: the stub provides both named exports (Position, Uri, …) and a
|
||||
|
||||
@@ -13,6 +13,54 @@ export interface StartSessionResult {
|
||||
|
||||
export const MAX_COMMAND_OUTPUT_CHARS = 200_000
|
||||
|
||||
export interface StoredModelEntry {
|
||||
id?: string
|
||||
name?: string
|
||||
maxTokens?: number
|
||||
contextWindow?: number
|
||||
maxInputTokens?: number
|
||||
capabilities?: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface StoredModelsFile {
|
||||
version: 1
|
||||
providers: Record<string, { models?: Record<string, StoredModelEntry> }>
|
||||
}
|
||||
|
||||
const modelsFiles = new Map<string, StoredModelsFile>()
|
||||
|
||||
function cloneModelsFile(modelsFile: StoredModelsFile): StoredModelsFile {
|
||||
return structuredClone(modelsFile)
|
||||
}
|
||||
|
||||
export function resetModelsFileState(): void {
|
||||
modelsFiles.clear()
|
||||
}
|
||||
|
||||
export function readModelsFileSync(filePath: string): StoredModelsFile {
|
||||
return cloneModelsFile(modelsFiles.get(filePath) ?? { version: 1, providers: {} })
|
||||
}
|
||||
|
||||
export function writeModelsFileSync(filePath: string, next: StoredModelsFile): void {
|
||||
modelsFiles.set(filePath, cloneModelsFile(next))
|
||||
}
|
||||
|
||||
export function resolveModelsRegistryPath(): string {
|
||||
return "/tmp/models.json"
|
||||
}
|
||||
|
||||
export function ensureCustomProvidersLoadedSync(): void {}
|
||||
|
||||
// Real implementation re-exported from the sdk source (same pattern as the
|
||||
// apply-patch executors below) so store writes are reflected in the live
|
||||
// @cline/llms registry exactly as in production. Tests that touch it must
|
||||
// reset the registry (LlmsModels.resetRegistry()) between tests.
|
||||
export {
|
||||
StoredModelEntrySchema,
|
||||
syncStoredProviderRegistration,
|
||||
} from "../../../../sdk/packages/core/src/services/providers/local-provider-registry"
|
||||
|
||||
export type GlobalCompactionStrategy = "basic" | "agentic"
|
||||
|
||||
export function readCompactionStrategyGlobally(): GlobalCompactionStrategy {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Under vitest, `@cline/core` is aliased to src/test/cline-core-vitest-stub.ts
|
||||
// (see vitest.config.ts), which holds models.json state in memory and exposes
|
||||
// the stub-only `resetModelsFileState` — hence the cast below.
|
||||
import * as ClineCore from "@cline/core"
|
||||
import { resetRegistry } from "@cline/llms"
|
||||
import { beforeEach } from "vitest"
|
||||
|
||||
const { resetModelsFileState } = ClineCore as typeof ClineCore & { resetModelsFileState(): void }
|
||||
|
||||
beforeEach(() => {
|
||||
resetModelsFileState()
|
||||
// The stub's syncStoredProviderRegistration mutates the real shared
|
||||
// @cline/llms registry; reset it so registrations never leak across tests.
|
||||
resetRegistry()
|
||||
})
|
||||
@@ -23,6 +23,7 @@ export default defineConfig({
|
||||
"src/core/controller/models/__tests__/refreshGroqModels.test.ts",
|
||||
],
|
||||
environment: "node",
|
||||
setupFiles: ["./src/test/vitest-setup.ts"],
|
||||
// Several suites lazily `await import()` their subject inside the first test
|
||||
// (needed so vi.mock factories apply first). That import pulls in heavy
|
||||
// workspace packages (@cline/core/@cline/llms/@cline/shared), and on loaded
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@ interface ActionButtonsProps {
|
||||
*/
|
||||
export const ActionButtons: React.FC<ActionButtonsProps> = ({ task, messages, chatState, mode, messageHandlers }) => {
|
||||
const { inputValue, selectedImages, selectedFiles, setSendingDisabled } = chatState
|
||||
const { turnState } = useExtensionState()
|
||||
const { turnState, foregroundCommandRunning } = useExtensionState()
|
||||
|
||||
// Tracks the ask the user last acted on. Clicking a footer button latches this so the
|
||||
// buttons disable immediately (and survive the trailing bookkeeping re-renders before the
|
||||
@@ -41,8 +41,8 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({ task, messages, ch
|
||||
// buttons immune to trailing bookkeeping messages and never disagree with the thinking
|
||||
// indicator (RC1).
|
||||
const buttonConfig = useMemo(() => {
|
||||
return getButtonConfigFromState(messages, turnState, mode)
|
||||
}, [messages, turnState, mode])
|
||||
return getButtonConfigFromState(messages, turnState, mode, foregroundCommandRunning)
|
||||
}, [messages, turnState, mode, foregroundCommandRunning])
|
||||
|
||||
// Identity of the ask that currently owns the footer buttons. The button config objects are
|
||||
// shared singletons (e.g. BUTTON_CONFIGS.tool_approve), so two consecutive identical asks
|
||||
|
||||
@@ -367,6 +367,15 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
|
||||
clearInputState()
|
||||
break
|
||||
|
||||
case "proceed_while_running":
|
||||
// Detach the running foreground terminal command: the agent
|
||||
// receives the partial output plus a log file path for the
|
||||
// rest, and the command keeps running in the terminal.
|
||||
await TaskServiceClient.proceedWhileRunningCommand(EmptyRequest.create({})).catch((err) =>
|
||||
console.error("Failed to proceed while running:", err),
|
||||
)
|
||||
break
|
||||
|
||||
case "new_task":
|
||||
if (clineAsk === "new_task") {
|
||||
await TaskServiceClient.newTask(
|
||||
|
||||
@@ -252,4 +252,19 @@ describe("getButtonConfigFromState (dispatch + legacy fallback)", () => {
|
||||
const turnState: TurnState = { phase: "completed", seq: 3 }
|
||||
expect(getButtonConfigFromState(messages, turnState, "act")).toEqual(BUTTON_CONFIGS.completion_result)
|
||||
})
|
||||
|
||||
it("streaming phase shows Proceed While Running when a foreground command is running", () => {
|
||||
const messages: ClineMessage[] = [{ ts: 1, type: "say", say: "command", text: "npm run dev", partial: true }]
|
||||
const turnState: TurnState = { phase: "streaming", seq: 4 }
|
||||
expect(getButtonConfigFromState(messages, turnState, "act", true)).toEqual(BUTTON_CONFIGS.foreground_command_running)
|
||||
expect(getButtonConfigFromState(messages, turnState, "act", false)).toEqual(BUTTON_CONFIGS.partial)
|
||||
})
|
||||
|
||||
it("foreground command flag only affects the streaming phase", () => {
|
||||
const messages: ClineMessage[] = []
|
||||
expect(getButtonConfigFromState(messages, { phase: "completed", seq: 5 }, "act", true)).toEqual(
|
||||
BUTTON_CONFIGS.completion_result,
|
||||
)
|
||||
expect(getButtonConfigFromState(messages, { phase: "idle", seq: 6 }, "act", true)).toEqual(BUTTON_CONFIGS.default)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ButtonActionType =
|
||||
| "approve" // Send yesButtonClicked
|
||||
| "reject" // Send noButtonClicked
|
||||
| "proceed" // Send messageResponse or yesButtonClicked
|
||||
| "proceed_while_running" // Detach the running foreground terminal command
|
||||
| "new_task" // Start a new task
|
||||
| "cancel" // Cancel streaming
|
||||
| "utility" // Execute utility function (condense, report_bug)
|
||||
@@ -188,6 +189,17 @@ export const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
|
||||
secondaryAction: "cancel",
|
||||
},
|
||||
|
||||
// A foreground terminal command is running (SDK path): the user can detach
|
||||
// it and let the agent proceed with the partial output, or cancel the task.
|
||||
foreground_command_running: {
|
||||
sendingDisabled: true,
|
||||
enableButtons: true,
|
||||
primaryText: "Proceed While Running",
|
||||
secondaryText: "Cancel",
|
||||
primaryAction: "proceed_while_running",
|
||||
secondaryAction: "cancel",
|
||||
},
|
||||
|
||||
// Default states
|
||||
default: {
|
||||
sendingDisabled: false,
|
||||
@@ -360,12 +372,19 @@ export function getButtonConfigForMessages(messages: ClineMessage[], mode: Mode
|
||||
* The button SET is chosen by phase; the LABEL/variant for approvals (Save vs Approve, command
|
||||
* vs tool vs MCP vs subagents) comes from the anchored message (turnState.anchorTs).
|
||||
*/
|
||||
export function buttonsForPhase(turnState: TurnState, anchoredMessage: ClineMessage | undefined): ButtonConfig {
|
||||
export function buttonsForPhase(
|
||||
turnState: TurnState,
|
||||
anchoredMessage: ClineMessage | undefined,
|
||||
foregroundCommandRunning = false,
|
||||
): ButtonConfig {
|
||||
switch (turnState.phase) {
|
||||
case "idle":
|
||||
return BUTTON_CONFIGS.default
|
||||
case "streaming":
|
||||
return BUTTON_CONFIGS.partial
|
||||
// A running foreground terminal command offers "Proceed While Running":
|
||||
// detach the command (output continues to a log file) and let the
|
||||
// agent continue with the partial output.
|
||||
return foregroundCommandRunning ? BUTTON_CONFIGS.foreground_command_running : BUTTON_CONFIGS.partial
|
||||
case "completed":
|
||||
return BUTTON_CONFIGS.completion_result
|
||||
case "resumable":
|
||||
@@ -398,10 +417,11 @@ export function getButtonConfigFromState(
|
||||
messages: ClineMessage[],
|
||||
turnState: TurnState | undefined,
|
||||
mode: Mode = "act",
|
||||
foregroundCommandRunning = false,
|
||||
): ButtonConfig {
|
||||
if (turnState) {
|
||||
const anchored = turnState.anchorTs !== undefined ? messages.find((m) => m.ts === turnState.anchorTs) : undefined
|
||||
return buttonsForPhase(turnState, anchored)
|
||||
return buttonsForPhase(turnState, anchored, foregroundCommandRunning)
|
||||
}
|
||||
return getButtonConfigForMessages(messages, mode)
|
||||
}
|
||||
|
||||
@@ -471,12 +471,10 @@ const OnboardingViewContent = ({ onboardingModels }: { onboardingModels: Onboard
|
||||
commitSelection("plan", {
|
||||
providerId: "cline",
|
||||
modelId: selectedModelId,
|
||||
modelInfo: selectedModelInfo,
|
||||
}),
|
||||
commitSelection("act", {
|
||||
providerId: "cline",
|
||||
modelId: selectedModelId,
|
||||
modelInfo: selectedModelInfo,
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
@@ -510,7 +510,7 @@ const ApiOptions = ({
|
||||
<AIhubmixProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
|
||||
)}
|
||||
|
||||
{apiConfiguration && (selectedProvider.includes("openai") || isCustomProvider) && (
|
||||
{apiConfiguration && (selectedProvider === "openai" || isCustomProvider) && (
|
||||
<OpenAICompatibleProvider
|
||||
currentMode={currentMode}
|
||||
isPopup={isPopup}
|
||||
|
||||
@@ -105,11 +105,6 @@ describe("ClineModelPicker", () => {
|
||||
expect(mocks.commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "cline",
|
||||
modelId: "cline-next",
|
||||
modelInfo: {
|
||||
name: "Cline Next",
|
||||
supportsPromptCache: true,
|
||||
contextWindow: 128_000,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -218,7 +218,6 @@ const ClineModelPicker: React.FC<ClineModelPickerProps> = ({ isPopup, currentMod
|
||||
void commitSelection(currentMode, {
|
||||
providerId: "cline",
|
||||
modelId: newModelId,
|
||||
modelInfo,
|
||||
}).catch((err) => console.error("Failed to commit Cline model selection:", err))
|
||||
|
||||
void handleModeFieldsChange(
|
||||
|
||||
@@ -77,7 +77,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
|
||||
currentMode,
|
||||
)
|
||||
|
||||
void commitSelection(currentMode, { providerId: "openrouter", modelId: newModelId, modelInfo }).catch((err) =>
|
||||
void commitSelection(currentMode, { providerId: "openrouter", modelId: newModelId }).catch((err) =>
|
||||
console.error("Failed to commit OpenRouter model selection:", err),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,6 +97,25 @@ describe("ApiOptions Component", () => {
|
||||
expect(modelIdInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
["openai-native", "OpenAI API Key"],
|
||||
["openai-codex", "Sign in to OpenAI Codex"],
|
||||
])("renders only the dedicated form for %s", (provider, dedicatedFormText) => {
|
||||
mockExtensionState({
|
||||
planModeApiProvider: provider as any,
|
||||
actModeApiProvider: provider as any,
|
||||
})
|
||||
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions currentMode="plan" showModelOptions={false} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(dedicatedFormText)).toBeInTheDocument()
|
||||
expect(screen.queryByText("Custom Headers")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders the OpenAI-compatible form for custom/unknown catalog providers", () => {
|
||||
vi.mocked(useProviderListings).mockReturnValue({
|
||||
providers: [
|
||||
|
||||
-1
@@ -97,7 +97,6 @@ describe("GenericProviderSettings", () => {
|
||||
expect(commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "deepseek",
|
||||
modelId: "deepseek-reasoner",
|
||||
modelInfo: { name: "DeepSeek Reasoner", supportsPromptCache: true, contextWindow: 128_000, supportsReasoning: true },
|
||||
})
|
||||
expect(useProviderModels).toHaveBeenCalledWith("deepseek")
|
||||
expect(useProviderConfig).toHaveBeenCalledWith("deepseek")
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Mode } from "@shared/storage/types"
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useInterval } from "react-use"
|
||||
import UseCustomPromptCheckbox from "@/components/settings/UseCustomPromptCheckbox"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useProviderConfig } from "@/hooks/useProviderConfig"
|
||||
import { useProviderModelSelection } from "@/hooks/useProviderModelSelection"
|
||||
@@ -36,13 +35,15 @@ export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: Ollam
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
|
||||
const ollamaBaseUrl = config?.baseUrl ?? apiConfiguration?.ollamaBaseUrl
|
||||
// providers.json (config.contextWindow) is the source of truth; the legacy
|
||||
// apiConfiguration string is a migration fallback.
|
||||
const ollamaNumCtx = config?.contextWindow || Number.parseInt(apiConfiguration?.ollamaApiOptionsCtxNum || "", 10)
|
||||
const ollamaModelInfo = useMemo(() => {
|
||||
const contextWindow = Number.parseInt(apiConfiguration?.ollamaApiOptionsCtxNum || "", 10)
|
||||
return {
|
||||
...openAiModelInfoSafeDefaults,
|
||||
...(Number.isFinite(contextWindow) && contextWindow > 0 ? { contextWindow } : {}),
|
||||
...(Number.isFinite(ollamaNumCtx) && ollamaNumCtx > 0 ? { contextWindow: ollamaNumCtx } : {}),
|
||||
}
|
||||
}, [apiConfiguration?.ollamaApiOptionsCtxNum])
|
||||
}, [ollamaNumCtx])
|
||||
const ollamaModelInfoById = useMemo(
|
||||
() => Object.fromEntries(ollamaModels.map((modelId) => [modelId, { ...ollamaModelInfo, name: modelId }])),
|
||||
[ollamaModelInfo, ollamaModels],
|
||||
@@ -137,27 +138,45 @@ export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: Ollam
|
||||
</p>
|
||||
)}
|
||||
|
||||
<DebouncedTextField
|
||||
initialValue={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
|
||||
onChange={(v) => {
|
||||
handleFieldChange("ollamaApiOptionsCtxNum", v || undefined)
|
||||
{/* Render only after the provider config RPC has resolved: the
|
||||
debounced input fires onChange for its initial value shortly
|
||||
after mount, so mounting before `config` loads would persist
|
||||
the 32768 fallback over a value saved in providers.json. */}
|
||||
{config !== undefined && (
|
||||
<DebouncedTextField
|
||||
initialValue={Number.isFinite(ollamaNumCtx) && ollamaNumCtx > 0 ? String(ollamaNumCtx) : ""}
|
||||
onChange={(v) => {
|
||||
const contextWindow = Number.parseInt(v, 10)
|
||||
const numCtx = Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : undefined
|
||||
// The debounced input also fires for its initial value and
|
||||
// external prop syncs — only persist actual changes.
|
||||
const currentNumCtx = Number.isFinite(ollamaNumCtx) && ollamaNumCtx > 0 ? ollamaNumCtx : undefined
|
||||
if (numCtx === currentNumCtx) {
|
||||
return
|
||||
}
|
||||
// Persist to providers.json (`contextWindow`); the store
|
||||
// mirrors the value to the legacy state key for older
|
||||
// readers. Zero clears the setting.
|
||||
void write({ contextWindow: numCtx ?? 0 }).catch((error) =>
|
||||
console.error("Failed to update Ollama context window:", error),
|
||||
)
|
||||
|
||||
const contextWindow = Number.parseInt(v, 10)
|
||||
if (selectedModel.modelId) {
|
||||
void commitModelSelection({
|
||||
modelId: selectedModel.modelId,
|
||||
modelInfo: {
|
||||
...openAiModelInfoSafeDefaults,
|
||||
name: selectedModel.modelId,
|
||||
...(Number.isFinite(contextWindow) && contextWindow > 0 ? { contextWindow } : {}),
|
||||
},
|
||||
}).catch((error) => console.error("Failed to update Ollama context window:", error))
|
||||
}
|
||||
}}
|
||||
placeholder={"e.g. 32768"}
|
||||
style={{ width: "100%" }}>
|
||||
<span className="font-semibold">Model Context Window</span>
|
||||
</DebouncedTextField>
|
||||
if (selectedModel.modelId) {
|
||||
void commitModelSelection({
|
||||
modelId: selectedModel.modelId,
|
||||
modelInfo: {
|
||||
...openAiModelInfoSafeDefaults,
|
||||
name: selectedModel.modelId,
|
||||
...(numCtx ? { contextWindow: numCtx } : {}),
|
||||
},
|
||||
}).catch((error) => console.error("Failed to update Ollama context window:", error))
|
||||
}
|
||||
}}
|
||||
placeholder={"Default: 32768"}
|
||||
style={{ width: "100%" }}>
|
||||
<span className="font-semibold">Model Context Window</span>
|
||||
</DebouncedTextField>
|
||||
)}
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
@@ -180,8 +199,6 @@ export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: Ollam
|
||||
</>
|
||||
)}
|
||||
|
||||
<UseCustomPromptCheckbox providerId="ollama" />
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
import { ApiFormat } from "@shared/proto/cline/models"
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react"
|
||||
import type { ChangeEventHandler, ReactNode } from "react"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { OpenAICompatibleProvider } from "./OpenAICompatible"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
commitSelection: vi.fn(),
|
||||
handleFieldChange: vi.fn(),
|
||||
handleModeFieldChange: vi.fn(),
|
||||
refreshOpenAiModels: vi.fn(),
|
||||
useDynamicProviderSelection: vi.fn(),
|
||||
useExtensionState: vi.fn(),
|
||||
useProviderConfig: vi.fn(),
|
||||
write: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@/context/ExtensionStateContext", () => ({
|
||||
useExtensionState: mocks.useExtensionState,
|
||||
}))
|
||||
|
||||
vi.mock("@/hooks/useDynamicProviderSelection", () => ({
|
||||
useDynamicProviderSelection: mocks.useDynamicProviderSelection,
|
||||
}))
|
||||
|
||||
vi.mock("@/hooks/useProviderConfig", () => ({
|
||||
fromProtobufProviderModelOverrides: (overrides: Record<string, unknown> | undefined) =>
|
||||
overrides ? { ...overrides } : undefined,
|
||||
useProviderConfig: mocks.useProviderConfig,
|
||||
}))
|
||||
|
||||
vi.mock("@/services/grpc-client", () => ({
|
||||
ModelsServiceClient: {
|
||||
refreshOpenAiModels: mocks.refreshOpenAiModels,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../utils/useApiConfigurationHandlers", () => ({
|
||||
useApiConfigurationHandlers: () => ({
|
||||
handleFieldChange: mocks.handleFieldChange,
|
||||
handleModeFieldChange: mocks.handleModeFieldChange,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("@radix-ui/react-tooltip", () => ({
|
||||
TooltipContent: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
vi.mock("@/components/ui/tooltip", () => ({
|
||||
Tooltip: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodeButton: ({ children, disabled, onClick }: { children?: ReactNode; disabled?: boolean; onClick?: () => void }) => (
|
||||
<button disabled={disabled} onClick={onClick} type="button">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
VSCodeCheckbox: ({
|
||||
checked,
|
||||
children,
|
||||
onChange,
|
||||
}: {
|
||||
checked?: boolean
|
||||
children?: ReactNode
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>
|
||||
}) => (
|
||||
<label>
|
||||
<input checked={checked} onChange={onChange} type="checkbox" />
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("../common/ApiKeyField", () => ({
|
||||
ApiKeyField: ({
|
||||
initialValue,
|
||||
onChange,
|
||||
providerName,
|
||||
}: {
|
||||
initialValue?: string
|
||||
onChange: (value: string) => void
|
||||
providerName: string
|
||||
}) => (
|
||||
<input aria-label={`${providerName} API key`} onChange={(event) => onChange(event.target.value)} value={initialValue} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("../common/BaseUrlField", () => ({
|
||||
BaseUrlField: ({
|
||||
disabled,
|
||||
initialValue,
|
||||
label,
|
||||
onChange,
|
||||
}: {
|
||||
disabled?: boolean
|
||||
initialValue?: string
|
||||
label: string
|
||||
onChange: (value: string) => void
|
||||
}) => (
|
||||
<label>
|
||||
{label}
|
||||
<input
|
||||
aria-label={label}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
value={initialValue ?? ""}
|
||||
/>
|
||||
</label>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("../common/DebouncedTextField", () => ({
|
||||
DebouncedTextField: ({
|
||||
children,
|
||||
disabled,
|
||||
initialValue,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
children?: ReactNode
|
||||
disabled?: boolean
|
||||
initialValue?: string
|
||||
onChange: (value: string) => void
|
||||
placeholder?: string
|
||||
}) => (
|
||||
<label>
|
||||
{children}
|
||||
<input
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
value={initialValue ?? ""}
|
||||
/>
|
||||
</label>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("../common/ModelInfoView", () => ({ ModelInfoView: () => null }))
|
||||
vi.mock("../ReasoningEffortSelector", () => ({ default: () => null }))
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
resolve = resolvePromise
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
function renderProvider() {
|
||||
return render(<OpenAICompatibleProvider currentMode="act" providerId="custom-openai" showModelOptions={false} />)
|
||||
}
|
||||
|
||||
function setCommittedSelection(overrides: Record<string, unknown>, modelInfo: Record<string, unknown> = {}) {
|
||||
mocks.useProviderConfig.mockReturnValue({
|
||||
config: {
|
||||
actSelection: {
|
||||
providerId: "custom-openai",
|
||||
modelId: "custom-model",
|
||||
modelInfo: {
|
||||
contextWindow: 128_000,
|
||||
inputPrice: 0,
|
||||
maxTokens: -1,
|
||||
outputPrice: 0,
|
||||
temperature: 0,
|
||||
tiers: [],
|
||||
...modelInfo,
|
||||
},
|
||||
overrides,
|
||||
},
|
||||
apiKeyLength: 12,
|
||||
baseUrl: "http://localhost:1234/v1",
|
||||
headers: {},
|
||||
providerId: "custom-openai",
|
||||
},
|
||||
commitSelection: mocks.commitSelection,
|
||||
write: mocks.write,
|
||||
})
|
||||
}
|
||||
|
||||
describe("OpenAICompatibleProvider", () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
mocks.commitSelection.mockResolvedValue(undefined)
|
||||
mocks.write.mockResolvedValue(undefined)
|
||||
mocks.refreshOpenAiModels.mockResolvedValue({ values: [] })
|
||||
mocks.useExtensionState.mockReturnValue({
|
||||
apiConfiguration: { azureApiVersion: "2025-04-01-preview", azureIdentity: false },
|
||||
remoteConfigSettings: undefined,
|
||||
})
|
||||
mocks.useDynamicProviderSelection.mockReturnValue({
|
||||
selectedModelId: "custom-model",
|
||||
selectedModelInfo: {
|
||||
contextWindow: 128_000,
|
||||
inputPrice: 0,
|
||||
maxTokens: -1,
|
||||
outputPrice: 0,
|
||||
temperature: 0,
|
||||
},
|
||||
})
|
||||
mocks.useProviderConfig.mockReturnValue({
|
||||
config: {
|
||||
apiKeyLength: 12,
|
||||
baseUrl: "http://localhost:1234/v1",
|
||||
headers: {},
|
||||
providerId: "custom-openai",
|
||||
},
|
||||
commitSelection: mocks.commitSelection,
|
||||
write: mocks.write,
|
||||
})
|
||||
})
|
||||
|
||||
it("refreshes keyless endpoints and displays only the saved-key mask", async () => {
|
||||
renderProvider()
|
||||
|
||||
await act(async () => {})
|
||||
|
||||
expect(mocks.refreshOpenAiModels).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ baseUrl: "http://localhost:1234/v1", apiKey: "" }),
|
||||
)
|
||||
expect(screen.getByLabelText("OpenAI Compatible API key")).toHaveValue("••••••••••••")
|
||||
})
|
||||
|
||||
it("writes a newly entered API key without echoing a stored key into config", async () => {
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
|
||||
fireEvent.change(screen.getByLabelText("OpenAI Compatible API key"), { target: { value: "new-secret" } })
|
||||
|
||||
expect(mocks.write).toHaveBeenCalledWith({ apiKey: "new-secret" })
|
||||
})
|
||||
|
||||
it("commits ordinary model selections by ID only", async () => {
|
||||
mocks.refreshOpenAiModels.mockResolvedValue({ values: ["listed-model"] })
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Model ID"), { target: { value: "listed-model" } })
|
||||
|
||||
expect(mocks.commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-openai",
|
||||
modelId: "listed-model",
|
||||
})
|
||||
})
|
||||
|
||||
it("persists only the edited vision field while preserving existing overrides", async () => {
|
||||
setCommittedSelection({
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
cacheReadsPrice: 0.5,
|
||||
cacheWritesPrice: 0.75,
|
||||
capabilities: ["tools", "streaming"],
|
||||
outputPrice: 2,
|
||||
})
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: "Supports Images" }))
|
||||
|
||||
expect(mocks.commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-openai",
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
cacheReadsPrice: 0.5,
|
||||
cacheWritesPrice: 0.75,
|
||||
capabilities: ["tools", "streaming"],
|
||||
outputPrice: 2,
|
||||
supportsVision: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("restores the R1 checkbox from authored override readback", async () => {
|
||||
setCommittedSelection({ isR1FormatRequired: true })
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
expect(screen.getByRole("checkbox", { name: "Enable R1 messages format" })).toBeChecked()
|
||||
})
|
||||
|
||||
it("restores the R1 checkbox from canonical resolved apiFormat", async () => {
|
||||
setCommittedSelection({}, { apiFormat: ApiFormat.R1_CHAT })
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
expect(screen.getByRole("checkbox", { name: "Enable R1 messages format" })).toBeChecked()
|
||||
})
|
||||
|
||||
it("persists the R1 checkbox as one explicit override", async () => {
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: "Enable R1 messages format" }))
|
||||
|
||||
expect(mocks.commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-openai",
|
||||
modelId: "custom-model",
|
||||
overrides: { isR1FormatRequired: true },
|
||||
})
|
||||
})
|
||||
|
||||
it("persists a temperature edit without adding resolved defaults", async () => {
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Temperature"), { target: { value: "0.25" } })
|
||||
|
||||
expect(mocks.commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-openai",
|
||||
modelId: "custom-model",
|
||||
overrides: { temperature: 0.25 },
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
["Context Window Size", "contextWindow", "64000", 64_000],
|
||||
["Max Output Tokens", "maxTokens", "4096", 4_096],
|
||||
["Output Price / 1M tokens", "outputPrice", "2.5", 2.5],
|
||||
] as const)("maps %s only to the %s override", async (label, key, input, expected) => {
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
fireEvent.change(screen.getByLabelText(label), { target: { value: input } })
|
||||
|
||||
expect(mocks.commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-openai",
|
||||
modelId: "custom-model",
|
||||
overrides: { [key]: expected },
|
||||
})
|
||||
})
|
||||
|
||||
it("clears one override while preserving unrelated fields", async () => {
|
||||
setCommittedSelection({
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["tools", "streaming"],
|
||||
temperature: 0.4,
|
||||
})
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Temperature"), { target: { value: "" } })
|
||||
|
||||
expect(mocks.commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-openai",
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["tools", "streaming"],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves apiFormat while editing pricing", async () => {
|
||||
setCommittedSelection({
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["tools"],
|
||||
})
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Input Price / 1M tokens"), { target: { value: "1.25" } })
|
||||
|
||||
expect(mocks.commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-openai",
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["tools"],
|
||||
inputPrice: 1.25,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("sends an empty replacement when the final override is cleared", async () => {
|
||||
setCommittedSelection({ temperature: 0.4 })
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Temperature"), { target: { value: "" } })
|
||||
|
||||
expect(mocks.commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-openai",
|
||||
modelId: "custom-model",
|
||||
overrides: {},
|
||||
})
|
||||
})
|
||||
|
||||
it("shows invalid-number feedback without committing", async () => {
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Max Output Tokens"), { target: { value: "80000o" } })
|
||||
|
||||
expect(screen.getByRole("alert")).toHaveTextContent("Max Output Tokens must be a valid number.")
|
||||
expect(mocks.commitSelection).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("merges rapid edits using the pending override set", async () => {
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
fireEvent.click(screen.getByText("Model Configuration"))
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Temperature"), { target: { value: "0.25" } })
|
||||
fireEvent.change(screen.getByLabelText("Input Price / 1M tokens"), { target: { value: "1.5" } })
|
||||
|
||||
expect(mocks.commitSelection).toHaveBeenLastCalledWith("act", {
|
||||
providerId: "custom-openai",
|
||||
modelId: "custom-model",
|
||||
overrides: { inputPrice: 1.5, temperature: 0.25 },
|
||||
})
|
||||
})
|
||||
|
||||
it("debounces model refreshes triggered by base URL edits", async () => {
|
||||
vi.useFakeTimers()
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
expect(mocks.refreshOpenAiModels).toHaveBeenCalledTimes(1)
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue("http://localhost:1234/v1"), {
|
||||
target: { value: "http://localhost:5678/v1" },
|
||||
})
|
||||
expect(mocks.refreshOpenAiModels).toHaveBeenCalledTimes(1)
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(499)
|
||||
})
|
||||
expect(mocks.refreshOpenAiModels).toHaveBeenCalledTimes(1)
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(1)
|
||||
})
|
||||
expect(mocks.refreshOpenAiModels).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("ignores a stale model-list response", async () => {
|
||||
vi.useFakeTimers()
|
||||
const oldRequest = deferred<{ values: string[] }>()
|
||||
const newRequest = deferred<{ values: string[] }>()
|
||||
mocks.refreshOpenAiModels.mockReturnValueOnce(oldRequest.promise).mockReturnValueOnce(newRequest.promise)
|
||||
renderProvider()
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue("http://localhost:1234/v1"), {
|
||||
target: { value: "http://localhost:5678/v1" },
|
||||
})
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(500)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
newRequest.resolve({ values: ["new-model"] })
|
||||
})
|
||||
expect(screen.getByRole("option", { name: "new-model" })).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
oldRequest.resolve({ values: ["stale-model"] })
|
||||
})
|
||||
expect(screen.queryByRole("option", { name: "stale-model" })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole("option", { name: "new-model" })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("cancels a pending debounced refresh when unmounted", async () => {
|
||||
vi.useFakeTimers()
|
||||
const view = renderProvider()
|
||||
await act(async () => {})
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue("http://localhost:1234/v1"), {
|
||||
target: { value: "http://localhost:5678/v1" },
|
||||
})
|
||||
view.unmount()
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(500)
|
||||
})
|
||||
|
||||
expect(mocks.refreshOpenAiModels).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("restores Azure settings and remote-config locks", async () => {
|
||||
mocks.useExtensionState.mockReturnValue({
|
||||
apiConfiguration: { azureApiVersion: "2025-04-01-preview", azureIdentity: true },
|
||||
remoteConfigSettings: {
|
||||
azureApiVersion: "2025-04-01-preview",
|
||||
openAiBaseUrl: "https://managed.example/v1",
|
||||
openAiHeaders: { "x-managed": "true" },
|
||||
},
|
||||
})
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
|
||||
expect(screen.getByDisplayValue("http://localhost:1234/v1")).toBeDisabled()
|
||||
expect(screen.getByRole("button", { name: "Add Header" })).toBeDisabled()
|
||||
expect(screen.getByLabelText("Set Azure API version")).toBeDisabled()
|
||||
expect(screen.getByRole("checkbox", { name: "Use Azure Identity Authentication" })).toBeChecked()
|
||||
})
|
||||
|
||||
it("writes editable Azure settings through the legacy handlers", async () => {
|
||||
renderProvider()
|
||||
await act(async () => {})
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Set Azure API version"), { target: { value: "2026-01-01" } })
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: "Use Azure Identity Authentication" }))
|
||||
|
||||
expect(mocks.handleFieldChange).toHaveBeenCalledWith("azureApiVersion", "2026-01-01")
|
||||
expect(mocks.handleFieldChange).toHaveBeenCalledWith("azureIdentity", true)
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,6 @@
|
||||
import { TooltipContent, TooltipTrigger } from "@radix-ui/react-tooltip"
|
||||
import {
|
||||
azureOpenAiDefaultApiVersion,
|
||||
type ModelInfo,
|
||||
type OpenAiCompatibleModelInfo,
|
||||
openAiModelInfoSafeDefaults,
|
||||
} from "@shared/api"
|
||||
import { OpenAiModelsRequest } from "@shared/proto/cline/models"
|
||||
import { azureOpenAiDefaultApiVersion, type OpenAiCompatibleModelInfo, openAiModelInfoSafeDefaults } from "@shared/api"
|
||||
import { ApiFormat, OpenAiModelsRequest } from "@shared/proto/cline/models"
|
||||
import { fromProtobufModelInfo } from "@shared/proto-conversions/models/typeConversion"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
@@ -13,7 +8,7 @@ import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { Tooltip } from "@/components/ui/tooltip"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useDynamicProviderSelection } from "@/hooks/useDynamicProviderSelection"
|
||||
import { useProviderConfig } from "@/hooks/useProviderConfig"
|
||||
import { fromProtobufProviderModelOverrides, type ProviderModelOverrides, useProviderConfig } from "@/hooks/useProviderConfig"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
@@ -21,7 +16,6 @@ import { BaseUrlField } from "../common/BaseUrlField"
|
||||
import { DebouncedTextField } from "../common/DebouncedTextField"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import ReasoningEffortSelector from "../ReasoningEffortSelector"
|
||||
import { parsePrice } from "../utils/pricingUtils"
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
import { useProviderApiKeyField } from "../utils/useProviderApiKeyField"
|
||||
|
||||
@@ -53,6 +47,7 @@ export const OpenAICompatibleProvider = ({
|
||||
const [availableOpenAiModels, setAvailableOpenAiModels] = useState<string[]>([])
|
||||
const [isRefreshingOpenAiModels, setIsRefreshingOpenAiModels] = useState(false)
|
||||
const [openAiModelsError, setOpenAiModelsError] = useState<string | undefined>(undefined)
|
||||
const [modelFieldErrors, setModelFieldErrors] = useState<Partial<Record<NumericModelOverrideKey, string>>>({})
|
||||
// Only the built-in "openai" provider stores its API key in the legacy
|
||||
// ApiConfiguration field; custom providers keep it in their per-provider
|
||||
// config (available only as a masked length), so there is no plaintext key
|
||||
@@ -92,34 +87,100 @@ export const OpenAICompatibleProvider = ({
|
||||
// The Model Configuration section reads/writes the resolved model info.
|
||||
// OpenAiCompatibleModelInfo only adds optional fields over ModelInfo, so a
|
||||
// resolved ModelInfo satisfies it structurally.
|
||||
const openAiModelInfo: OpenAiCompatibleModelInfo = selectedModelInfo
|
||||
const openAiModelInfo: OpenAiCompatibleModelInfo = selectedModelInfo ?? openAiModelInfoSafeDefaults
|
||||
const selectedModelOverrides = fromProtobufProviderModelOverrides(committedSelection?.overrides) ?? {}
|
||||
const selectedModelOverridesRef = useRef<{ modelId: string | undefined; overrides: ProviderModelOverrides }>({
|
||||
modelId: selectedModelId,
|
||||
overrides: selectedModelOverrides,
|
||||
})
|
||||
|
||||
// Counts commits whose commit+read-back round-trip has not finished yet.
|
||||
const pendingCommitsRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
// Do not reseed the pending-override accumulator from server state
|
||||
// while commits are in flight: an earlier commit's read-back can land
|
||||
// after a later local edit, and reseeding from that stale snapshot
|
||||
// would silently drop the already-committed newer field.
|
||||
if (pendingCommitsRef.current > 0) {
|
||||
return
|
||||
}
|
||||
selectedModelOverridesRef.current = { modelId: selectedModelId, overrides: selectedModelOverrides }
|
||||
}, [committedSelection?.overrides, selectedModelId])
|
||||
|
||||
const commitOpenAiSelection = useCallback(
|
||||
(modelId: string, modelInfo = openAiModelInfo ?? openAiModelInfoSafeDefaults) => {
|
||||
(modelId: string, overrides?: ProviderModelOverrides) => {
|
||||
if (!modelId.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
pendingCommitsRef.current += 1
|
||||
void commitSelection(currentMode, {
|
||||
providerId,
|
||||
modelId,
|
||||
modelInfo: {
|
||||
...modelInfo,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache ?? openAiModelInfoSafeDefaults.supportsPromptCache,
|
||||
},
|
||||
}).catch((error) => handleProviderConfigWriteError("model selection", error))
|
||||
...(overrides !== undefined ? { overrides } : {}),
|
||||
})
|
||||
.catch((error) => handleProviderConfigWriteError("model selection", error))
|
||||
.finally(() => {
|
||||
pendingCommitsRef.current -= 1
|
||||
})
|
||||
},
|
||||
[commitSelection, currentMode, handleProviderConfigWriteError, openAiModelInfo],
|
||||
[commitSelection, currentMode, handleProviderConfigWriteError, providerId],
|
||||
)
|
||||
|
||||
const handleOpenAiModelInfoChange = useCallback(
|
||||
(modelInfo: typeof openAiModelInfoSafeDefaults) => {
|
||||
if (isOpenAiProvider) {
|
||||
handleModeFieldChange({ plan: "planModeOpenAiModelInfo", act: "actModeOpenAiModelInfo" }, modelInfo, currentMode)
|
||||
const updateModelOverride = useCallback(
|
||||
<K extends keyof ProviderModelOverrides>(key: K, value: ProviderModelOverrides[K] | undefined) => {
|
||||
const modelId = selectedModelId?.trim()
|
||||
if (!modelId) {
|
||||
return
|
||||
}
|
||||
commitOpenAiSelection(selectedModelId || "", modelInfo)
|
||||
|
||||
const currentOverrides =
|
||||
selectedModelOverridesRef.current.modelId === modelId ? selectedModelOverridesRef.current.overrides : {}
|
||||
const nextOverrides = { ...currentOverrides }
|
||||
if (value === undefined) {
|
||||
delete nextOverrides[key]
|
||||
} else {
|
||||
Object.assign(nextOverrides, { [key]: value })
|
||||
}
|
||||
selectedModelOverridesRef.current = { modelId, overrides: nextOverrides }
|
||||
commitOpenAiSelection(modelId, nextOverrides)
|
||||
},
|
||||
[commitOpenAiSelection, currentMode, handleModeFieldChange, isOpenAiProvider, selectedModelId],
|
||||
[commitOpenAiSelection, selectedModelId],
|
||||
)
|
||||
|
||||
const updateNumericModelOverride = useCallback(
|
||||
(key: NumericModelOverrideKey, label: string, value: string) => {
|
||||
const parsed = parseOptionalFiniteNumber(value)
|
||||
if (!parsed.valid) {
|
||||
setModelFieldErrors((current) => ({ ...current, [key]: `${label} must be a valid number.` }))
|
||||
return
|
||||
}
|
||||
setModelFieldErrors((current) => {
|
||||
const next = { ...current }
|
||||
delete next[key]
|
||||
return next
|
||||
})
|
||||
// Debounced fields fire with their initial value on mount and on
|
||||
// model/mode switches; committing that echo would persist resolved
|
||||
// catalog values as user overrides. Only commit actual edits.
|
||||
// Compare against the pending override when one is in flight so a
|
||||
// quick revert during a commit round-trip is not mistaken for an
|
||||
// echo of the (stale) displayed value.
|
||||
const pendingOverrides =
|
||||
selectedModelOverridesRef.current.modelId === selectedModelId?.trim()
|
||||
? selectedModelOverridesRef.current.overrides
|
||||
: undefined
|
||||
const effectiveValue =
|
||||
pendingOverrides && Object.hasOwn(pendingOverrides, key)
|
||||
? displayedModelNumber(pendingOverrides[key] as number | undefined)
|
||||
: displayedModelNumber(openAiModelInfo?.[key])
|
||||
if (parsed.value === effectiveValue) {
|
||||
return
|
||||
}
|
||||
updateModelOverride(key, parsed.value)
|
||||
},
|
||||
[updateModelOverride, openAiModelInfo, selectedModelId],
|
||||
)
|
||||
|
||||
// Debounced function to refresh OpenAI models (prevents excessive API calls while typing)
|
||||
@@ -130,6 +191,7 @@ export const OpenAICompatibleProvider = ({
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current)
|
||||
}
|
||||
openAiModelsRequestRef.current += 1
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -196,22 +258,14 @@ export const OpenAICompatibleProvider = ({
|
||||
void refreshOpenAiModels(config?.baseUrl, latestOpenAiApiKeyRef.current)
|
||||
}, [config?.baseUrl, refreshOpenAiModels])
|
||||
|
||||
const toOpenAiModelInfo = useCallback(
|
||||
(modelId: string): ModelInfo => ({
|
||||
...openAiModelInfoSafeDefaults,
|
||||
name: modelId,
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
const handleOpenAiModelSelection = useCallback(
|
||||
(modelId: string, modelInfo = toOpenAiModelInfo(modelId)) => {
|
||||
(modelId: string) => {
|
||||
if (isOpenAiProvider) {
|
||||
handleModeFieldChange({ plan: "planModeOpenAiModelId", act: "actModeOpenAiModelId" }, modelId, currentMode)
|
||||
}
|
||||
commitOpenAiSelection(modelId, modelInfo)
|
||||
commitOpenAiSelection(modelId)
|
||||
},
|
||||
[commitOpenAiSelection, currentMode, handleModeFieldChange, isOpenAiProvider, toOpenAiModelInfo],
|
||||
[commitOpenAiSelection, currentMode, handleModeFieldChange, isOpenAiProvider],
|
||||
)
|
||||
|
||||
const { savedApiKeyMask, handleApiKeyChange } = useProviderApiKeyField({
|
||||
@@ -261,7 +315,10 @@ export const OpenAICompatibleProvider = ({
|
||||
|
||||
<ApiKeyField initialValue={savedApiKeyMask} onChange={handleApiKeyChange} providerName="OpenAI Compatible" />
|
||||
|
||||
{isRefreshingOpenAiModels && <div role="status">Loading models…</div>}
|
||||
<label htmlFor="openai-compatible-model-picker">
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
{isRefreshingOpenAiModels && <span> Loading models…</span>}
|
||||
</label>
|
||||
{openAiModelsError && <div role="alert">{openAiModelsError}</div>}
|
||||
{availableOpenAiModels.length > 0 ? (
|
||||
<div
|
||||
@@ -271,9 +328,6 @@ export const OpenAICompatibleProvider = ({
|
||||
gap: 8,
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
<label htmlFor="openai-compatible-model-picker">
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</label>
|
||||
<select
|
||||
aria-label="Model ID"
|
||||
id="openai-compatible-model-picker"
|
||||
@@ -316,9 +370,8 @@ export const OpenAICompatibleProvider = ({
|
||||
initialValue={selectedModelId || ""}
|
||||
onChange={(value) => handleOpenAiModelSelection(value)}
|
||||
placeholder={"Enter Model ID..."}
|
||||
style={{ width: "100%", marginBottom: 10 }}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</DebouncedTextField>
|
||||
style={{ width: "100%", marginBottom: 10 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* OpenAI Compatible Custom Headers */}
|
||||
@@ -475,105 +528,67 @@ export const OpenAICompatibleProvider = ({
|
||||
<>
|
||||
<VSCodeCheckbox
|
||||
checked={!!openAiModelInfo?.supportsImages}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
const modelInfo = openAiModelInfo ? { ...openAiModelInfo } : { ...openAiModelInfoSafeDefaults }
|
||||
modelInfo.supportsImages = isChecked
|
||||
handleOpenAiModelInfoChange(modelInfo)
|
||||
}}>
|
||||
onChange={(e: any) => updateModelOverride("supportsVision", e.target.checked === true)}>
|
||||
Supports Images
|
||||
</VSCodeCheckbox>
|
||||
|
||||
<VSCodeCheckbox
|
||||
checked={!!openAiModelInfo?.isR1FormatRequired}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
let modelInfo = openAiModelInfo ? { ...openAiModelInfo } : { ...openAiModelInfoSafeDefaults }
|
||||
modelInfo = { ...modelInfo, isR1FormatRequired: isChecked }
|
||||
|
||||
handleOpenAiModelInfoChange(modelInfo)
|
||||
}}>
|
||||
checked={selectedModelOverrides.isR1FormatRequired ?? openAiModelInfo.apiFormat === ApiFormat.R1_CHAT}
|
||||
onChange={(e: any) => updateModelOverride("isR1FormatRequired", e.target.checked === true)}>
|
||||
Enable R1 messages format
|
||||
</VSCodeCheckbox>
|
||||
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<DebouncedTextField
|
||||
initialValue={
|
||||
openAiModelInfo?.contextWindow
|
||||
? openAiModelInfo.contextWindow.toString()
|
||||
: (openAiModelInfoSafeDefaults.contextWindow?.toString() ?? "")
|
||||
}
|
||||
onChange={(value) => {
|
||||
const modelInfo = openAiModelInfo ? { ...openAiModelInfo } : { ...openAiModelInfoSafeDefaults }
|
||||
modelInfo.contextWindow = Number(value)
|
||||
handleOpenAiModelInfoChange(modelInfo)
|
||||
}}
|
||||
style={{ flex: 1 }}>
|
||||
<span style={{ fontWeight: 500 }}>Context Window Size</span>
|
||||
</DebouncedTextField>
|
||||
<div style={{ flex: 1 }}>
|
||||
<DebouncedTextField
|
||||
initialValue={formatOptionalModelNumber(openAiModelInfo?.contextWindow)}
|
||||
onChange={(value) => updateNumericModelOverride("contextWindow", "Context Window Size", value)}>
|
||||
<span style={{ fontWeight: 500 }}>Context Window Size</span>
|
||||
</DebouncedTextField>
|
||||
{modelFieldErrors.contextWindow && <div role="alert">{modelFieldErrors.contextWindow}</div>}
|
||||
</div>
|
||||
|
||||
<DebouncedTextField
|
||||
initialValue={
|
||||
openAiModelInfo?.maxTokens
|
||||
? openAiModelInfo.maxTokens.toString()
|
||||
: (openAiModelInfoSafeDefaults.maxTokens?.toString() ?? "")
|
||||
}
|
||||
onChange={(value) => {
|
||||
const modelInfo = openAiModelInfo ? { ...openAiModelInfo } : { ...openAiModelInfoSafeDefaults }
|
||||
modelInfo.maxTokens = Number(value)
|
||||
handleOpenAiModelInfoChange(modelInfo)
|
||||
}}
|
||||
style={{ flex: 1 }}>
|
||||
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
|
||||
</DebouncedTextField>
|
||||
<div style={{ flex: 1 }}>
|
||||
<DebouncedTextField
|
||||
initialValue={formatOptionalModelNumber(openAiModelInfo?.maxTokens)}
|
||||
onChange={(value) => updateNumericModelOverride("maxTokens", "Max Output Tokens", value)}
|
||||
placeholder="not set">
|
||||
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
|
||||
</DebouncedTextField>
|
||||
{modelFieldErrors.maxTokens && <div role="alert">{modelFieldErrors.maxTokens}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<DebouncedTextField
|
||||
initialValue={
|
||||
openAiModelInfo?.inputPrice
|
||||
? openAiModelInfo.inputPrice.toString()
|
||||
: (openAiModelInfoSafeDefaults.inputPrice?.toString() ?? "")
|
||||
}
|
||||
onChange={(value) => {
|
||||
const modelInfo = openAiModelInfo ? { ...openAiModelInfo } : { ...openAiModelInfoSafeDefaults }
|
||||
modelInfo.inputPrice = parsePrice(value, openAiModelInfoSafeDefaults.inputPrice ?? 0)
|
||||
handleOpenAiModelInfoChange(modelInfo)
|
||||
}}
|
||||
style={{ flex: 1 }}>
|
||||
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
|
||||
</DebouncedTextField>
|
||||
<div style={{ flex: 1 }}>
|
||||
<DebouncedTextField
|
||||
initialValue={formatOptionalModelNumber(openAiModelInfo?.inputPrice)}
|
||||
onChange={(value) => updateNumericModelOverride("inputPrice", "Input Price", value)}>
|
||||
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
|
||||
</DebouncedTextField>
|
||||
{modelFieldErrors.inputPrice && <div role="alert">{modelFieldErrors.inputPrice}</div>}
|
||||
</div>
|
||||
|
||||
<DebouncedTextField
|
||||
initialValue={
|
||||
openAiModelInfo?.outputPrice
|
||||
? openAiModelInfo.outputPrice.toString()
|
||||
: (openAiModelInfoSafeDefaults.outputPrice?.toString() ?? "")
|
||||
}
|
||||
onChange={(value) => {
|
||||
const modelInfo = openAiModelInfo ? { ...openAiModelInfo } : { ...openAiModelInfoSafeDefaults }
|
||||
modelInfo.outputPrice = parsePrice(value, openAiModelInfoSafeDefaults.outputPrice ?? 0)
|
||||
handleOpenAiModelInfoChange(modelInfo)
|
||||
}}
|
||||
style={{ flex: 1 }}>
|
||||
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
|
||||
</DebouncedTextField>
|
||||
<div style={{ flex: 1 }}>
|
||||
<DebouncedTextField
|
||||
initialValue={formatOptionalModelNumber(openAiModelInfo?.outputPrice)}
|
||||
onChange={(value) => updateNumericModelOverride("outputPrice", "Output Price", value)}>
|
||||
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
|
||||
</DebouncedTextField>
|
||||
{modelFieldErrors.outputPrice && <div role="alert">{modelFieldErrors.outputPrice}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<DebouncedTextField
|
||||
initialValue={
|
||||
openAiModelInfo?.temperature
|
||||
? openAiModelInfo.temperature.toString()
|
||||
: (openAiModelInfoSafeDefaults.temperature?.toString() ?? "")
|
||||
}
|
||||
onChange={(value) => {
|
||||
const modelInfo = openAiModelInfo ? { ...openAiModelInfo } : { ...openAiModelInfoSafeDefaults }
|
||||
modelInfo.temperature = parsePrice(value, openAiModelInfoSafeDefaults.temperature ?? 0)
|
||||
handleOpenAiModelInfoChange(modelInfo)
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Temperature</span>
|
||||
</DebouncedTextField>
|
||||
<div>
|
||||
<DebouncedTextField
|
||||
initialValue={formatOptionalModelNumber(openAiModelInfo?.temperature)}
|
||||
onChange={(value) => updateNumericModelOverride("temperature", "Temperature", value)}
|
||||
placeholder="not set">
|
||||
<span style={{ fontWeight: 500 }}>Temperature</span>
|
||||
</DebouncedTextField>
|
||||
{modelFieldErrors.temperature && <div role="alert">{modelFieldErrors.temperature}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -610,3 +625,25 @@ export const OpenAICompatibleProvider = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type NumericModelOverrideKey = "contextWindow" | "maxTokens" | "inputPrice" | "outputPrice" | "temperature"
|
||||
|
||||
type ParsedOptionalNumber = { valid: true; value: number | undefined } | { valid: false }
|
||||
|
||||
// -1 is the legacy UI sentinel for "not set"; it renders (and compares) as unset.
|
||||
function displayedModelNumber(value: number | undefined): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value !== -1 ? value : undefined
|
||||
}
|
||||
|
||||
function formatOptionalModelNumber(value: number | undefined): string {
|
||||
return displayedModelNumber(value)?.toString() ?? ""
|
||||
}
|
||||
|
||||
function parseOptionalFiniteNumber(value: string): ParsedOptionalNumber {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
return { valid: true, value: undefined }
|
||||
}
|
||||
const parsed = Number(trimmed)
|
||||
return Number.isFinite(parsed) ? { valid: true, value: parsed } : { valid: false }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { openAiModelInfoSafeDefaults } from "@shared/api"
|
||||
import { fromProtobufModelInfo } from "@shared/proto-conversions/models/typeConversion"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
@@ -35,7 +34,6 @@ export const OpenAiCodexProvider = ({ showModelOptions, isPopup, currentMode }:
|
||||
const { config, commitSelection } = useProviderConfig(OPENAI_CODEX_PROVIDER_ID)
|
||||
const {
|
||||
models,
|
||||
defaultModelId,
|
||||
selectedModelId: legacySelectedModelId,
|
||||
selectedModelInfo: legacySelectedModelInfo,
|
||||
hideUsageCost,
|
||||
@@ -53,13 +51,9 @@ export const OpenAiCodexProvider = ({ showModelOptions, isPopup, currentMode }:
|
||||
return
|
||||
}
|
||||
|
||||
const fallbackModelId = defaultModelId || Object.keys(models)[0] || modelId
|
||||
const modelInfo = models[modelId] ?? models[fallbackModelId] ?? selectedModelInfo ?? openAiModelInfoSafeDefaults
|
||||
|
||||
void commitSelection(currentMode, {
|
||||
providerId: OPENAI_CODEX_PROVIDER_ID,
|
||||
modelId,
|
||||
modelInfo,
|
||||
}).catch((err) => console.error("Failed to commit OpenAI Codex model selection:", err))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { openAiModelInfoSafeDefaults } from "@shared/api"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { parseVsCodeLmModelSelector, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
|
||||
@@ -66,8 +65,7 @@ export const VSCodeLmProvider = ({ currentMode }: VSCodeLmProviderProps) => {
|
||||
void commitSelection(currentMode, {
|
||||
providerId: "vscode-lm",
|
||||
modelId,
|
||||
modelInfo: {
|
||||
...openAiModelInfoSafeDefaults,
|
||||
overrides: {
|
||||
name: [selector.vendor, selector.family].filter(Boolean).join(" - ") || modelId,
|
||||
},
|
||||
}).catch((err) => console.error("Failed to commit VS Code LM model selection:", err))
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { AuthState, UserOrganizationsResponse } from "@shared/proto/cline/account"
|
||||
import { act, render, screen } from "@testing-library/react"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { ClineAuthProvider, useClineAuth } from "./ClineAuthContext"
|
||||
|
||||
type AuthStatusCallbacks = {
|
||||
onResponse: (response: AuthState) => void
|
||||
}
|
||||
|
||||
const grpcMocks = vi.hoisted(() => ({
|
||||
getUserOrganizations: vi.fn(),
|
||||
subscribeToAuthStatusUpdate: vi.fn(),
|
||||
authStatusCallbacks: undefined as AuthStatusCallbacks | undefined,
|
||||
}))
|
||||
|
||||
vi.mock("@/services/grpc-client", () => ({
|
||||
AccountServiceClient: {
|
||||
getUserOrganizations: grpcMocks.getUserOrganizations,
|
||||
subscribeToAuthStatusUpdate: grpcMocks.subscribeToAuthStatusUpdate,
|
||||
},
|
||||
}))
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve: (value: T) => void = () => {}
|
||||
const promise = new Promise<T>((promiseResolve) => {
|
||||
resolve = promiseResolve
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
function AuthStateProbe() {
|
||||
const { clineUser, organizations } = useClineAuth()
|
||||
return (
|
||||
<>
|
||||
<div data-testid="user-state">{clineUser?.uid ?? "signed-out"}</div>
|
||||
<div data-testid="organizations-state">
|
||||
{organizations?.map((organization) => organization.organizationId).join(",") ?? "none"}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
describe("ClineAuthProvider", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
grpcMocks.authStatusCallbacks = undefined
|
||||
grpcMocks.subscribeToAuthStatusUpdate.mockImplementation((_request, callbacks: AuthStatusCallbacks) => {
|
||||
grpcMocks.authStatusCallbacks = callbacks
|
||||
return vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
it("does not restore organizations when an in-flight request resolves after sign-out", async () => {
|
||||
const organizationsRequest = createDeferred<UserOrganizationsResponse>()
|
||||
grpcMocks.getUserOrganizations.mockReturnValue(organizationsRequest.promise)
|
||||
|
||||
render(
|
||||
<ClineAuthProvider>
|
||||
<AuthStateProbe />
|
||||
</ClineAuthProvider>,
|
||||
)
|
||||
|
||||
act(() => {
|
||||
grpcMocks.authStatusCallbacks?.onResponse({ user: { uid: "user-1" } })
|
||||
})
|
||||
expect(grpcMocks.getUserOrganizations).toHaveBeenCalledTimes(1)
|
||||
|
||||
act(() => {
|
||||
grpcMocks.authStatusCallbacks?.onResponse({})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
organizationsRequest.resolve({
|
||||
organizations: [
|
||||
{ organizationId: "stale-org", active: true, memberId: "member-1", name: "Stale Org", roles: [] },
|
||||
],
|
||||
})
|
||||
await organizationsRequest.promise
|
||||
})
|
||||
|
||||
expect(screen.getByTestId("user-state")).toHaveTextContent("signed-out")
|
||||
expect(screen.getByTestId("organizations-state")).toHaveTextContent("none")
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { UserOrganization } from "@shared/proto/cline/account"
|
||||
import type { AuthState, UserOrganization } from "@shared/proto/cline/account"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import type React from "react"
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
|
||||
// Define User type (you may need to adjust this based on your actual User type)
|
||||
@@ -25,10 +25,15 @@ export const ClineAuthContext = createContext<ClineAuthContextType | undefined>(
|
||||
export const ClineAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<ClineUser | null>(null)
|
||||
const [userOrganizations, setUserOrganizations] = useState<UserOrganization[] | null>(null)
|
||||
const organizationsRequestIdRef = useRef(0)
|
||||
|
||||
const getUserOrganizations = useCallback(async () => {
|
||||
const requestId = ++organizationsRequestIdRef.current
|
||||
try {
|
||||
const response = await AccountServiceClient.getUserOrganizations(EmptyRequest.create())
|
||||
if (requestId !== organizationsRequestIdRef.current) {
|
||||
return
|
||||
}
|
||||
setUserOrganizations((old) => {
|
||||
if (!deepEqual(response.organizations, old)) {
|
||||
return response.organizations
|
||||
@@ -52,22 +57,23 @@ export const ClineAuthProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||
// Handle auth status update events
|
||||
useEffect(() => {
|
||||
const cancelSubscription = AccountServiceClient.subscribeToAuthStatusUpdate(EmptyRequest.create(), {
|
||||
onResponse: async (response: any) => {
|
||||
setUser((oldUser) => {
|
||||
if (!response?.user?.uid) {
|
||||
return null
|
||||
}
|
||||
onResponse: (response: AuthState) => {
|
||||
const responseUser = response.user
|
||||
if (!responseUser?.uid) {
|
||||
organizationsRequestIdRef.current++
|
||||
setUser(null)
|
||||
setUserOrganizations(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (response?.user && oldUser?.uid !== response.user.uid) {
|
||||
// Once we have a new user, fetch organizations that
|
||||
// allow us to display the active account in account view UI
|
||||
// and fetch the correct credit balance to display on mount
|
||||
getUserOrganizations()
|
||||
return response.user
|
||||
}
|
||||
// Refresh organizations on every auth status update, not just user
|
||||
// changes. Switching organizations doesn't change the uid, so gating
|
||||
// this on uid changes leaves stale `active` flags — which reset the
|
||||
// account view's org dropdown on remount. The deepEqual guard in
|
||||
// getUserOrganizations prevents no-op re-renders.
|
||||
getUserOrganizations()
|
||||
|
||||
return oldUser
|
||||
})
|
||||
setUser((oldUser) => (oldUser?.uid !== responseUser.uid ? responseUser : oldUser))
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Error in auth callback subscription:", error)
|
||||
@@ -79,6 +85,7 @@ export const ClineAuthProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||
|
||||
// Cleanup function to cancel subscription when component unmounts
|
||||
return () => {
|
||||
organizationsRequestIdRef.current++
|
||||
cancelSubscription()
|
||||
}
|
||||
}, [getUserOrganizations])
|
||||
|
||||
@@ -311,6 +311,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
remoteConfigSettings: {},
|
||||
backgroundCommandRunning: false,
|
||||
backgroundCommandTaskId: undefined,
|
||||
foregroundCommandRunning: false,
|
||||
lastDismissedCliBannerVersion: 0,
|
||||
backgroundEditEnabled: false,
|
||||
showFeatureTips: true,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { ApiFormat, ProviderConfigResponse } from "@shared/proto/cline/models"
|
||||
import { ApiFormat, ModelOverrides, ProviderConfigResponse } from "@shared/proto/cline/models"
|
||||
import { act, renderHook, waitFor } from "@testing-library/react"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { useProviderConfig } from "./useProviderConfig"
|
||||
import { fromProtobufProviderModelOverrides, toProtobufProviderModelOverrides, useProviderConfig } from "./useProviderConfig"
|
||||
|
||||
vi.mock("@/services/grpc-client", () => ({
|
||||
ModelsServiceClient: {
|
||||
@@ -57,7 +57,7 @@ describe("useProviderConfig", () => {
|
||||
expect(result.current.config?.baseUrl).toBe("https://custom.example/v1")
|
||||
})
|
||||
|
||||
it("commitSelection sends the full selection envelope and refreshes config", async () => {
|
||||
it("commitSelection sends model settings and refreshes config", async () => {
|
||||
vi.mocked(ModelsServiceClient.readProviderConfig)
|
||||
.mockResolvedValueOnce(config())
|
||||
.mockResolvedValueOnce(
|
||||
@@ -76,6 +76,11 @@ describe("useProviderConfig", () => {
|
||||
apiFormat: ApiFormat.OPENAI_CHAT,
|
||||
tiers: [],
|
||||
},
|
||||
overrides: ModelOverrides.create({
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["tools", "streaming"],
|
||||
temperature: 0.4,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -87,7 +92,7 @@ describe("useProviderConfig", () => {
|
||||
await result.current.commitSelection("act", {
|
||||
providerId: "deepseek",
|
||||
modelId: "deepseek-v4-flash",
|
||||
modelInfo: { name: "DeepSeek V4 Flash", supportsPromptCache: true, apiFormat: ApiFormat.OPENAI_CHAT },
|
||||
overrides: { name: "DeepSeek V4 Flash", capabilities: ["prompt-cache"] },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -96,15 +101,64 @@ describe("useProviderConfig", () => {
|
||||
providerId: "deepseek",
|
||||
mode: "act",
|
||||
modelId: "deepseek-v4-flash",
|
||||
modelInfo: expect.objectContaining({
|
||||
overrides: expect.objectContaining({
|
||||
name: "DeepSeek V4 Flash",
|
||||
supportsPromptCache: true,
|
||||
apiFormat: ApiFormat.OPENAI_CHAT,
|
||||
capabilities: ["prompt-cache"],
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(ModelsServiceClient.readProviderConfig).toHaveBeenCalledTimes(2)
|
||||
expect(result.current.config?.actSelection?.modelId).toBe("deepseek-v4-flash")
|
||||
expect(fromProtobufProviderModelOverrides(result.current.config?.actSelection?.overrides)).toEqual({
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["tools", "streaming"],
|
||||
temperature: 0.4,
|
||||
})
|
||||
})
|
||||
|
||||
it("converts every domain override field explicitly and preserves an empty override message", () => {
|
||||
const overrides = {
|
||||
name: "Custom",
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 128_000,
|
||||
maxInputTokens: 120_000,
|
||||
capabilities: ["tools", "streaming"],
|
||||
supportsVision: false,
|
||||
supportsAttachments: true,
|
||||
supportsReasoning: false,
|
||||
inputPrice: 1,
|
||||
outputPrice: 2,
|
||||
cacheReadsPrice: 3,
|
||||
cacheWritesPrice: 4,
|
||||
temperature: 0.2,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
isR1FormatRequired: false,
|
||||
}
|
||||
|
||||
expect(fromProtobufProviderModelOverrides(toProtobufProviderModelOverrides(overrides))).toEqual(overrides)
|
||||
expect(toProtobufProviderModelOverrides({})).toEqual(ModelOverrides.create({}))
|
||||
})
|
||||
|
||||
it("sends an explicit empty override message so the host can clear stored overrides", async () => {
|
||||
vi.mocked(ModelsServiceClient.readProviderConfig).mockResolvedValue(config())
|
||||
vi.mocked(ModelsServiceClient.commitModelSelection).mockResolvedValue({})
|
||||
const { result } = renderHook(() => useProviderConfig("deepseek"))
|
||||
await waitFor(() => expect(result.current.config).toBeDefined())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.commitSelection("act", {
|
||||
providerId: "deepseek",
|
||||
modelId: "custom-model",
|
||||
overrides: {},
|
||||
})
|
||||
})
|
||||
|
||||
expect(ModelsServiceClient.commitModelSelection).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelId: "custom-model",
|
||||
overrides: ModelOverrides.create({}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("commitSelection rejects mismatched provider ids without calling RPC", async () => {
|
||||
@@ -116,7 +170,6 @@ describe("useProviderConfig", () => {
|
||||
result.current.commitSelection("act", {
|
||||
providerId: "openrouter",
|
||||
modelId: "deepseek-v4-flash",
|
||||
modelInfo: { supportsPromptCache: true },
|
||||
}),
|
||||
).rejects.toThrow("selection providerId openrouter does not match hook providerId deepseek")
|
||||
expect(ModelsServiceClient.commitModelSelection).not.toHaveBeenCalled()
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
WriteProviderConfigPatch,
|
||||
WriteProviderConfigRequest,
|
||||
} from "@shared/proto/cline/models"
|
||||
import { toProtobufModelInfo } from "@shared/proto-conversions/models/typeConversion"
|
||||
import {
|
||||
type ProviderModelOverrides,
|
||||
toProtobufModelOverrides as toProtobufProviderModelOverrides,
|
||||
} from "@shared/proto-conversions/models/modelOverrides"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import type { ProviderId } from "@/context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import type { ModelInfo } from "../../../src/shared/api"
|
||||
|
||||
export type ProviderConfigWritePatch = Partial<Omit<WriteProviderConfigPatch, "headers" | "aws" | "gcp">> & {
|
||||
headers?: Record<string, string>
|
||||
@@ -19,10 +21,23 @@ export type ProviderConfigWritePatch = Partial<Omit<WriteProviderConfigPatch, "h
|
||||
gcp?: Partial<GcpProviderConfig>
|
||||
}
|
||||
|
||||
// The overrides domain type and its proto conversions are shared with the
|
||||
// host; see the tri-state and normalization semantics documented there.
|
||||
export {
|
||||
fromProtobufModelOverrides as fromProtobufProviderModelOverrides,
|
||||
toProtobufModelOverrides as toProtobufProviderModelOverrides,
|
||||
} from "@shared/proto-conversions/models/modelOverrides"
|
||||
export type { ProviderModelOverrides }
|
||||
|
||||
export interface ProviderModelSelection {
|
||||
providerId: ProviderId
|
||||
modelId: string
|
||||
modelInfo: ModelInfo
|
||||
/**
|
||||
* Tri-state: `undefined` preserves the model's stored overrides, an
|
||||
* explicitly empty object clears them, and a non-empty object replaces
|
||||
* them wholesale.
|
||||
*/
|
||||
overrides?: ProviderModelOverrides
|
||||
}
|
||||
|
||||
function toWriteProviderConfigPatch(patch: ProviderConfigWritePatch): WriteProviderConfigPatch {
|
||||
@@ -74,7 +89,8 @@ export function useProviderConfig(providerId: ProviderId) {
|
||||
providerId,
|
||||
mode,
|
||||
modelId: selection.modelId,
|
||||
modelInfo: toProtobufModelInfo(selection.modelInfo),
|
||||
overrides:
|
||||
selection.overrides !== undefined ? toProtobufProviderModelOverrides(selection.overrides) : undefined,
|
||||
}),
|
||||
)
|
||||
await read()
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ApiFormat } from "@shared/proto/cline/models"
|
||||
import { act, renderHook } from "@testing-library/react"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { useProviderModelSelection } from "./useProviderModelSelection"
|
||||
|
||||
describe("useProviderModelSelection", () => {
|
||||
it("does not turn custom fallback model info into persisted overrides", async () => {
|
||||
const commitSelection = vi.fn(async () => undefined)
|
||||
const customModelInfo = {
|
||||
name: "Custom model",
|
||||
contextWindow: 128_000,
|
||||
maxTokens: -1,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
temperature: 0,
|
||||
}
|
||||
const { result } = renderHook(() =>
|
||||
useProviderModelSelection("custom-provider", "act", {
|
||||
models: {},
|
||||
commitSelection,
|
||||
customModelInfo: () => customModelInfo,
|
||||
}),
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.commitModelSelection({ modelId: "custom-model", modelInfo: customModelInfo })
|
||||
})
|
||||
|
||||
expect(commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-provider",
|
||||
modelId: "custom-model",
|
||||
})
|
||||
})
|
||||
|
||||
it("forwards only explicitly supplied overrides", async () => {
|
||||
const commitSelection = vi.fn(async () => undefined)
|
||||
const { result } = renderHook(() =>
|
||||
useProviderModelSelection("custom-provider", "act", {
|
||||
models: {},
|
||||
commitSelection,
|
||||
}),
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.commitModelSelection({
|
||||
modelId: "custom-model",
|
||||
modelInfo: { contextWindow: 128_000, maxTokens: -1, temperature: -1 },
|
||||
overrides: {
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["tools", "streaming"],
|
||||
temperature: 0.2,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
expect(commitSelection).toHaveBeenCalledWith("act", {
|
||||
providerId: "custom-provider",
|
||||
modelId: "custom-model",
|
||||
overrides: {
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
capabilities: ["tools", "streaming"],
|
||||
temperature: 0.2,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,14 @@ import { useCallback } from "react"
|
||||
import type { ProviderId } from "@/context/ExtensionStateContext"
|
||||
import type { ProviderModelSelection } from "./useProviderConfig"
|
||||
|
||||
type ProviderModelSelectionInput =
|
||||
| (Omit<ProviderModelSelection, "providerId"> & { modelInfo?: ModelInfo })
|
||||
| (ProviderModelSelection & { modelInfo?: ModelInfo })
|
||||
|
||||
interface DisplayProviderModelSelection extends ProviderModelSelection {
|
||||
modelInfo: ModelInfo
|
||||
}
|
||||
|
||||
interface UseProviderModelSelectionOptions {
|
||||
models: Record<string, ModelInfo>
|
||||
defaultModelId?: string
|
||||
@@ -36,17 +44,18 @@ export function useProviderModelSelection(
|
||||
(selectedModelId && customModelInfo ? customModelInfo(selectedModelId) : undefined) ??
|
||||
fallbackModelInfo)
|
||||
|
||||
const selectedModel: ProviderModelSelection = {
|
||||
const selectedModel: DisplayProviderModelSelection = {
|
||||
providerId,
|
||||
modelId: selectedModelId,
|
||||
modelInfo: selectedModelInfo,
|
||||
}
|
||||
|
||||
const commitModelSelection = useCallback(
|
||||
(selection: Omit<ProviderModelSelection, "providerId"> | ProviderModelSelection) => {
|
||||
(selection: ProviderModelSelectionInput) => {
|
||||
return commitSelection(currentMode, {
|
||||
...selection,
|
||||
providerId,
|
||||
modelId: selection.modelId,
|
||||
...(selection.overrides !== undefined ? { overrides: selection.overrides } : {}),
|
||||
})
|
||||
},
|
||||
[commitSelection, currentMode, providerId],
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"apps/cli": {
|
||||
"name": "@cline/cli",
|
||||
"version": "3.0.40",
|
||||
"version": "3.0.42",
|
||||
"bin": {
|
||||
"cline": "src/index.ts",
|
||||
},
|
||||
@@ -491,6 +491,17 @@
|
||||
"vitest": "^4.0.17",
|
||||
},
|
||||
},
|
||||
"apps/vscode-rollout": {
|
||||
"name": "@cline/vscode-rollout",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "20.x",
|
||||
"@types/vscode": "1.84.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"typescript": "^5.4.5",
|
||||
},
|
||||
},
|
||||
"apps/vscode/testing-platform": {
|
||||
"name": "testing-infra",
|
||||
"version": "0.1.0",
|
||||
@@ -621,7 +632,7 @@
|
||||
},
|
||||
"sdk/packages/agents": {
|
||||
"name": "@cline/agents",
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.63",
|
||||
"dependencies": {
|
||||
"@cline/llms": "workspace:*",
|
||||
"@cline/shared": "workspace:*",
|
||||
@@ -630,7 +641,7 @@
|
||||
},
|
||||
"sdk/packages/core": {
|
||||
"name": "@cline/core",
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.63",
|
||||
"dependencies": {
|
||||
"@cline/agents": "workspace:*",
|
||||
"@cline/llms": "workspace:*",
|
||||
@@ -668,7 +679,7 @@
|
||||
},
|
||||
"sdk/packages/llms": {
|
||||
"name": "@cline/llms",
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.63",
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "^4.0.89",
|
||||
"@ai-sdk/anthropic": "^3.0.68",
|
||||
@@ -686,6 +697,7 @@
|
||||
"@opentelemetry/sdk-trace-node": "^2.6.1",
|
||||
"@streamparser/json": "^0.0.21",
|
||||
"ai": "^6.0.144",
|
||||
"ai-sdk-ollama": "^3.8.8",
|
||||
"ai-sdk-provider-claude-code": "^3.4.3",
|
||||
"ai-sdk-provider-codex-cli": "^1.1.0",
|
||||
"ai-sdk-provider-opencode-sdk": "^3.0.1",
|
||||
@@ -702,14 +714,14 @@
|
||||
},
|
||||
"sdk/packages/sdk": {
|
||||
"name": "@cline/sdk",
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.63",
|
||||
"dependencies": {
|
||||
"@cline/core": "workspace:*",
|
||||
},
|
||||
},
|
||||
"sdk/packages/shared": {
|
||||
"name": "@cline/shared",
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.63",
|
||||
"dependencies": {
|
||||
"aws4fetch": "^1.0.20",
|
||||
"jsonrepair": "^3.13.2",
|
||||
@@ -756,27 +768,27 @@
|
||||
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.131", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.95", "@ai-sdk/openai": "3.0.82", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-UrbM28zGFJV6xTn7wpv/uCsp/wMKb79MCuZC3Ff1a59PGjLr+iMN+Nlul7cWV04pjE5u5kLP5SybtGZtDP2EkQ=="],
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.132", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.96", "@ai-sdk/openai": "3.0.83", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ngtWAA4BHhtRrC0Vx1by9+KwcYtXbKcKjEoKbKhqJxNes8w6JNtdp5nQfK3ZE9Wbcdq8i6a8s08xXFm3pHe3iA=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Q7NhioTX6m0hKni14Ip9EO6WedbIYcldQ/PsGB7gVAveRNog39FfX31f+9HYoEUrfm9L7QxIcB5aAzJV/hmNRg=="],
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.96", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6VQzaXQdm5FkX6NWOyKzV5GB11C8IqkgsKZE91lg/bdwyvnQJLDwal2qkE0+fC8CCGeW5d+VV8Mw/+H+OcDC1A=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.145", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cqSQ+I0Bjj2W9g1oFyE1O1mSowsWXb+U1wK9vtg5kRQqB95iWVIKbtdr14gGf46cueJSiBlKfPTT24gDDVuFmw=="],
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.146", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-QK922LzOfGeHdZ9QGvghDizQx/tPOolTQSHvMlnUPeaTW5qpiIqpfbLwYiyxEt5YOGLLM/0t232Dut+95udo/Q=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.90", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nn2bSLFZDV5Xhl2oh+C3ckpBUM849zrHLoLe6B9DVu2DcgtIvxKYO0pKLO/vB9XQVKON5XORp7uV7P+60L5XUQ=="],
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.91", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-d/ho+sDjArFjreE2002t9jE4LXX3wde97dN2HCLCX1l41gaJV3wf/c/19axjUNfIf/4uUq+nJmhBO0lW/dg3yw=="],
|
||||
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.158", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.95", "@ai-sdk/google": "3.0.90", "@ai-sdk/openai-compatible": "2.0.58", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Z7KPZ2+M7DFnXPEDgElwazDQxDxYqd9HQdFLuCMSpc0No/1Dr0TKRT+Q5pJwstHJfIQeAMApFvalzGOfTe/ShQ=="],
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.159", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.96", "@ai-sdk/google": "3.0.91", "@ai-sdk/openai-compatible": "2.0.59", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AvHvV3Nw+LaLjTBveP96hBbKFXHoQBMeQa1fd9SV1UnXCvmCfl7cQempb+pZnho12rbTFHwgdX419nFTMNmATw=="],
|
||||
|
||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A/ov/CTrQ0rDztrvgYo9ql4u6tlyfTrwMN4u76zqLM0JqUWy82T82Y8HzP4fCQOs7gZggrruHaNbWnqnE9IwKA=="],
|
||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SOZMjV48dyAn1rsiZSN7emeO0KYKnr9/SqMFPpJYUddPcnLSjac9GGWVKn+LnSzD7Woh3lYbYbJWdbJOQ2U2sQ=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.82", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Gn1YliuNMneXoBmuLX1kH/e5SR/VnU9FXLvJ8WyiV61Noo+wPdE4nuzxRGt3lfV6rla1wyCb1syV4jU0A310ew=="],
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.83", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gYsQPmBQYgWc8+sFdmO4lSbNJoBdt9wgx1wZ1YOi0wX8X0d/K3FYOnNS0/TGymiDk8kh+wKj6PLC6UyA8bwVFA=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0SXA0xVt18F4ki7ttVshqaM0oLXSB475ACOU0/2RK3OZS3UYqrmKF+DJwYBYUZmqCq2nZ2vkNZEXpWP2wfGsrQ=="],
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.59", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CFsUizO+jL+jSlN13rW2nQE9EbWx+8PSFBdB3TtD0UGYOxtefCVa5hsrNo5NUOJJGX5f4xHiXE1i2m5nQ80QPg=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="],
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VG4tpVXCuzm21U9xjg05BCMZnjZOazC72+MxBkLAa7hCKsnqNt542GYWUUqwmHSczJwgbSXN8UvaNgSerUaKdw=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
||||
|
||||
"@ai-sdk/react": ["@ai-sdk/react@3.0.223", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.37", "ai": "6.0.221", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-b7Ri+wLOR9pZkKlEKii3ZuXi79Rh3rC5rYUEFDpXUOJLazztIE4MPM4dQRXCojYgOuPa61gVO2BFjuRVLGsOfw=="],
|
||||
"@ai-sdk/react": ["@ai-sdk/react@3.0.224", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.38", "ai": "6.0.222", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-bfCVv6uTNS+gwMLLpsJQUBZqedCphqNiFkMVjL4kzunlgpC0uFyyYRYd99884NuYZJAF9AKKROK9t+/v1N9tow=="],
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
@@ -784,61 +796,59 @@
|
||||
|
||||
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.204", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.204", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.204", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.204", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.204", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.204", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.204", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.204", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.204" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-nicn6OrlmUIuk4cdIkKQWKpHbkz3rL0EkYIsil6m1QejxD5gVDnVdgHuIUuwhcZB0PriBou9VcYLa3vRoyVFiQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.206", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.206", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.206", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.206", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.206", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.206", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.206", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.206", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.206" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-KljDh9Pg4YCYpoXS8dnWoVSsOHtU4yLCW268K2iOruSxFXE8/Tay6DPvmJzYuqjP5YLNYfj05ZGykwZSUn6GXA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.204", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TYeNVAaeALaTMSLy00n7tUnUCUZEE/rJb7krx3RH5XvWSf/7jGiSDSAvq6aFts1/J7KSefhlctLfA5fPEdmlMQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.206", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FL2+NKcMMN47vcnCW2Fkt3AOgeRRlQxrisbPNaxrxqPJFzhUKs17x5j0XzLefd0xRbDAr74hd0PK/tnp6PHM6w=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.204", "", { "os": "darwin", "cpu": "x64" }, "sha512-azilRc19MvLajTBGR1fqVzK+j1xabPESjrrnoEGU0Ugrgjm+SHxQDI6itXTKwj6TjjLr6sCxEtDTkV5lUzHelg=="],
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.206", "", { "os": "darwin", "cpu": "x64" }, "sha512-mRW8PPMfQN15EunLwpdmcVzk3XuM4DXQUM8DOzaeA1Hr1yxYPaVVBLr9hkdBKOqr8XTl3ueoTR6RZAy6a/n7OA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.204", "", { "os": "linux", "cpu": "arm64" }, "sha512-hwlaYrtJDs0Hu+M56QlgAzSSRCOd4JrJfg+nirKKD4dKA/A0J9RzNW7DaaXZu2RWL6+pauOIem72E95LfcLmFw=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.206", "", { "os": "linux", "cpu": "arm64" }, "sha512-Id6H8l6EsGb7849EAZDOB4Ic+FQpbzt4D5HGOwp59CTW3o/1c4etjQA/Kl1K+DSEWn3FGo6D5UMVgc/rwhBj8g=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.204", "", { "os": "linux", "cpu": "arm64" }, "sha512-btK8EygEeizBFKJxI6xYiJ2EmFqkDcr2l1YPBoOJTg0dPVS6mzH7BUt4Jk6F9c/1UNhzdEWzNq+j2/xUN77W+g=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.206", "", { "os": "linux", "cpu": "arm64" }, "sha512-aMZe1Kl+kYv5QlA15W9Ae25MAzBsA9FA40f5TOtJebA+M/xliF0r2LLb2NdyBviiZCDllcE31l0zgYE0rQqFQw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.204", "", { "os": "linux", "cpu": "x64" }, "sha512-0sVBc2IbXYc4E8U9feYSuP83G9Z+U+/2VhaLl+qea3RAByR1JzOHjjPa4bXNWHDJWBhMF1t8g7a3QsBi1l21DQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.206", "", { "os": "linux", "cpu": "x64" }, "sha512-egZhOC1RlEVhZyq6Oa1b04AF7hh4hO+8oCsJCZ3gOifJQuHih1oW3lZ8fOUxU85jlT2ytAnt5kRp4uQr6PJgbg=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.204", "", { "os": "linux", "cpu": "x64" }, "sha512-ZB2Tp9h1OMjcrEdY0lqSc0wE5Jj1qctwmBk2qaaxbhT0FLAocl4/aUisTOg737aXDnP6R1q7S+7kYTkhox5NLQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.206", "", { "os": "linux", "cpu": "x64" }, "sha512-xQBOBhlcmTNc7YeYT5qLZOikpSq3WHlsJ/t6i7kJqUWrcXlOPaAoSMdKp5xO/V0CjAdzdJoWB88I55UAh8mclQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.204", "", { "os": "win32", "cpu": "arm64" }, "sha512-Wf2dNtscF6MWYeMaKIPDrIZ8302f9rFjtKDYDAWFbV59Fq+xDbWf4AM6B4hC7ln+NgjwlCf6RY0M8a36hTq0rw=="],
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.206", "", { "os": "win32", "cpu": "arm64" }, "sha512-oRQk23bFXSz4QRhOxqnnvlLWq/KiF2PtSBYXrMg1AQrCOHJd2k66aOAS7AJ4ZSzejiyV4N6sS6UThfmF6ipHBQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.204", "", { "os": "win32", "cpu": "x64" }, "sha512-EJXAEiJzeIwpjxqsNW904YbwkpqjQstX6wvWzvryN47/aUQ7aFKPJJgNT/VVRl9gi1r62PIQrpN5c0Kwdv+m5g=="],
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.206", "", { "os": "win32", "cpu": "x64" }, "sha512-BdjKmDojZjc5RjN+8Q6K7Yqf1WYhel6I3DkMXc9zdxS/xIuN42sx7zgQpdMGY73pm7ROPLqo3LieOeMhVH161w=="],
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.37.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw=="],
|
||||
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1081.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/credential-provider-node": "^3.972.64", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-ipDgvZ3Hy8kaFrkwj62yuoghKkZi5WrWTiJnaqGc/ntbuAuijIkRWhzSb8d1MGEE+qb+vFFTrWVFwGSqt76TCA=="],
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.975.1", "", { "dependencies": { "@aws-sdk/types": "^3.974.0", "@aws-sdk/xml-builder": "^3.972.34", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.2", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.974.29", "", { "dependencies": { "@aws-sdk/types": "^3.973.15", "@aws-sdk/xml-builder": "^3.972.33", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.0", "@smithy/signature-v4": "^5.6.1", "@smithy/types": "^4.15.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-yqKcltLbtRh1ubzhRSldIs8jFHNZlyMlgoIccCC0aDVbrB99nXaBdmfr89mK7obWX/NVg4rAMpCpZ6dCDiVBtA=="],
|
||||
"@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.56", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-nedaZz6Wz2FGUBMsWhlvBPGIkTMfRuMJ99YDHJI4CaC31JS8WPyTavEAio74cfd5nGhi0A8XASB4fZA5VqTdOQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.54", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.29", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-RSHJ0Fh03mfbKGDyDWasEZB4FQTJDUcSr0wmDzoswuz8FsPWMHUNl/6KP1+XGnY8wgvBOyVdu2TUuIZotm5l6A=="],
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.57", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-Ah36tYkqyaVnaHkx7VseoTYrHUmwgBps3V+wnrC1idhIIMGlviH0FtrX9EIPdAlVHvXC7FQZLhmHBRz+pLaiWg=="],
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.59", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/fetch-http-handler": "^5.6.4", "@smithy/node-http-handler": "^4.9.4", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.57", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-/vp6i5YEliJqRm5k/BDmYjAyRAMTdkjW6UciVRk9oh/0OfDCWeb/ih7hqte4lFvKXkIbsqe9AdK9LQK6NGardw=="],
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.1", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/credential-provider-env": "^3.972.57", "@aws-sdk/credential-provider-http": "^3.972.59", "@aws-sdk/credential-provider-login": "^3.972.63", "@aws-sdk/credential-provider-process": "^3.972.57", "@aws-sdk/credential-provider-sso": "^3.973.1", "@aws-sdk/credential-provider-web-identity": "^3.972.63", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.62", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/credential-provider-env": "^3.972.55", "@aws-sdk/credential-provider-http": "^3.972.57", "@aws-sdk/credential-provider-login": "^3.972.61", "@aws-sdk/credential-provider-process": "^3.972.55", "@aws-sdk/credential-provider-sso": "^3.972.61", "@aws-sdk/credential-provider-web-identity": "^3.972.61", "@aws-sdk/nested-clients": "^3.997.29", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/credential-provider-imds": "^4.4.5", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-pQIRiQQs+MUlVnJdWJ7/6KS0WxcLRVfut57OFgwC3cnM1F8mXw3Kh4gAVwj6AtvD6CWx8x6+po4ENRcqe64XrQ=="],
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.63", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.61", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/nested-clients": "^3.997.29", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-jtrxWwC7slqxh7DnAWHrwsA3UwCsnlypdYtavGT7EX5p791wxWQys7QzkCZ7JvOMAyylDtPoxyV+ic0zg3rV9g=="],
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.66", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.57", "@aws-sdk/credential-provider-http": "^3.972.59", "@aws-sdk/credential-provider-ini": "^3.973.1", "@aws-sdk/credential-provider-process": "^3.972.57", "@aws-sdk/credential-provider-sso": "^3.973.1", "@aws-sdk/credential-provider-web-identity": "^3.972.63", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-f+qjRXZpz7sgzbc4QB+6nLKfyKFgRRXzWdXbsKPv/VhVRyHsDyq4yBWC/B75BAJpFIcUeI2XR/3gdWJ677zB4A=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.64", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.55", "@aws-sdk/credential-provider-http": "^3.972.57", "@aws-sdk/credential-provider-ini": "^3.972.62", "@aws-sdk/credential-provider-process": "^3.972.55", "@aws-sdk/credential-provider-sso": "^3.972.61", "@aws-sdk/credential-provider-web-identity": "^3.972.61", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/credential-provider-imds": "^4.4.5", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-zyKVYDyMR9VQL/kPi03ygN2vtD9uLMuWRLoJ77KxgZZaS1VlJloI+SzleF9Zg4HWUI+AIu+ZRs8zsJFNqbrxsw=="],
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.57", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-x0XjjF0l1WGRtK2vEhTZqCguQuAIZLep9l2+eeEmuxQQjjD3BlGQXY5xADR+l3t576UX+dxRkRtTjEu40l81Vw=="],
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.1", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/token-providers": "3.1083.0", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.61", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/nested-clients": "^3.997.29", "@aws-sdk/token-providers": "3.1081.0", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-d/V0VRsz73i+PHhbult/tx0Y1+de1SNQVsXkcQCmpfeBq7uODy/RTxNsOLpT9ZVHxcRNzbQFuywLKC33fUMIxA=="],
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.63", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.61", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/nested-clients": "^3.997.29", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-Bv4n3NOI6hPy+rmr6Bw9R6LnBVRkcp3ncj2E2IKSYJG+0UkysSitWMvbgndNvMxDw7gE1pQ/ErwkNceuKwj7zQ=="],
|
||||
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1084.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/credential-provider-cognito-identity": "^3.972.56", "@aws-sdk/credential-provider-env": "^3.972.57", "@aws-sdk/credential-provider-http": "^3.972.59", "@aws-sdk/credential-provider-ini": "^3.973.1", "@aws-sdk/credential-provider-login": "^3.972.63", "@aws-sdk/credential-provider-node": "^3.972.66", "@aws-sdk/credential-provider-process": "^3.972.57", "@aws-sdk/credential-provider-sso": "^3.973.1", "@aws-sdk/credential-provider-web-identity": "^3.972.63", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-6MT9xigrduBftpOEq2HKzGqp2Up0Jwe8dK5W3GyBpePywzgMK6029XEYXkCC5wRYwXwhSPzpjnjRlWUA7K/KNQ=="],
|
||||
|
||||
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1081.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1081.0", "@aws-sdk/core": "^3.974.29", "@aws-sdk/credential-provider-cognito-identity": "^3.972.54", "@aws-sdk/credential-provider-env": "^3.972.55", "@aws-sdk/credential-provider-http": "^3.972.57", "@aws-sdk/credential-provider-ini": "^3.972.62", "@aws-sdk/credential-provider-login": "^3.972.61", "@aws-sdk/credential-provider-node": "^3.972.64", "@aws-sdk/credential-provider-process": "^3.972.55", "@aws-sdk/credential-provider-sso": "^3.972.61", "@aws-sdk/credential-provider-web-identity": "^3.972.61", "@aws-sdk/nested-clients": "^3.997.29", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/credential-provider-imds": "^4.4.5", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-UHWvxd1F5nfBXBRXtQaXWoNT8CYKXZovLQOyz6XZlgFTGc2mWzzPGspOfnh49jwvB2qzsdH046i9jnnmND+64w=="],
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.31", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/signature-v4-multi-region": "^3.996.39", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/fetch-http-handler": "^5.6.4", "@smithy/node-http-handler": "^4.9.4", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.29", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/signature-v4-multi-region": "^3.996.38", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-ot6v8J5W8P0w6ryyuIkXP1bHZHTlvwtn83mVCYaBE0GJ6tJX4vPSBx7M98w9O4wmmDruFsDBUMjhEHA+OosUFQ=="],
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.39", "", { "dependencies": { "@aws-sdk/types": "^3.974.0", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA=="],
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.38", "", { "dependencies": { "@aws-sdk/types": "^3.973.15", "@smithy/signature-v4": "^5.6.1", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-C379Sk+MiFZCfWZphKlMyLHKxV22OjoGM5KJjj5IJNJcOCWL4IGIpnEGzv1FQiRwhYXfq55SJMfxlqPE08JJ9g=="],
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1083.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1081.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.29", "@aws-sdk/nested-clients": "^3.997.29", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-kduAeI6cL+zqwj3gjPh9LhuX7kBZ83msYxutavaR+UPm5K8J7iThJBvNRAsFNyWTji92CSU8dogUgvi9T0BehA=="],
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.974.0", "", { "dependencies": { "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.973.15", "", { "dependencies": { "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ=="],
|
||||
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.33", "", { "dependencies": { "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-ezbwz9WpuLctm6o7P2t2naDhVVPI5jFGrVefVybhcKGjU57VIyT46pQVO0RI2RYkUdhdj2Z9uSIlAzGZE9NW9A=="],
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.34", "", { "dependencies": { "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA=="],
|
||||
|
||||
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
|
||||
|
||||
@@ -1024,6 +1034,8 @@
|
||||
|
||||
"@cline/vscode": ["@cline/vscode@workspace:apps/examples/vscode"],
|
||||
|
||||
"@cline/vscode-rollout": ["@cline/vscode-rollout@workspace:apps/vscode-rollout"],
|
||||
|
||||
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
@@ -1668,7 +1680,7 @@
|
||||
|
||||
"@openai/codex-win32-x64": ["@openai/codex@0.130.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-FzMznm7fr5/nbjZgOujZ9Y9AbdGm7ji1FOoWiY3U+srqauvZaTgn6o6aCheSL7kuymu7nTLOO/cAyWV6NuesqQ=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.17.15", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-gWRQOEggHTELJ9+BtelxnuAczk9qutCXVZenPgRPaT8oVxePf52jfWbqfhyFjnrN8Vlp8tCCTdkEpFW5pZAuEA=="],
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.17.18", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-c/C9PhY8PrbcxDY+JIYtOZsrmMD0KzoVvxq+RGUrZ6LQp57SuVBbT4lfwA2G8Se5RNC1N5JtYjiuaXeECnF2SQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
@@ -1728,7 +1740,7 @@
|
||||
|
||||
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.9.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.9.0", "@opentelemetry/core": "2.9.0", "@opentelemetry/sdk-trace-base": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.42.0", "", {}, "sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw=="],
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
||||
|
||||
"@opentui-ui/dialog": ["@opentui-ui/dialog@0.1.2", "", { "peerDependencies": { "@opentui/core": "^0.1.69", "@opentui/react": "^0.1.69", "@opentui/solid": "^0.1.69" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-EZ4FG5u5sxU75+6pcsJsLzsD5JqO05So/1ceZUKUu7nxZ9IF7gcZEi+MU4HnYC9cb2Q7w6hM9y3/iW+jE1C53w=="],
|
||||
|
||||
@@ -1760,9 +1772,9 @@
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="],
|
||||
|
||||
"@posthog/core": ["@posthog/core@1.39.6", "", { "dependencies": { "@posthog/types": "^1.392.0" } }, "sha512-o6ajIwN5zXoNP0D4H/QPmOyibNTUkSyOR6ya7AG5U2ywXx4awo72L2KnCoiZPQM5x/bXv6jPBdimH8M18Ax0aw=="],
|
||||
"@posthog/core": ["@posthog/core@1.40.1", "", { "dependencies": { "@posthog/types": "^1.393.0" } }, "sha512-jXuMtZCwA7AMpYlo1wjm6GlC58YBlz/ZxpDIsF9hroUfqYoPPDmQbsQb4iiZAS43+o/kwloxdKJIlE0IiwQ5HQ=="],
|
||||
|
||||
"@posthog/types": ["@posthog/types@1.392.1", "", {}, "sha512-Qg6Gl7/1vlr8+gPtBi5gwnLgAgiyFoKOVmTvTtDcvya9cpTwZfna7rQmkGQ4B63CunUYNNbOlqcwiUwUDyTK6w=="],
|
||||
"@posthog/types": ["@posthog/types@1.393.0", "", {}, "sha512-vzWeEJZ7ERQhFRoQYaP5jzN1JvIu46UJyHXsuv+dTGW2r3sMgREOhNxXLZjmFHwZ8/FOHQoyqqQmXTCXZSfMSg=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
@@ -1784,7 +1796,7 @@
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
|
||||
|
||||
"@puppeteer/browsers": ["@puppeteer/browsers@2.6.1", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg=="],
|
||||
"@puppeteer/browsers": ["@puppeteer/browsers@2.13.2", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw=="],
|
||||
|
||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||
|
||||
@@ -1808,7 +1820,7 @@
|
||||
|
||||
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
@@ -1830,7 +1842,7 @@
|
||||
|
||||
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
|
||||
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
||||
|
||||
@@ -1852,7 +1864,7 @@
|
||||
|
||||
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="],
|
||||
|
||||
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.8", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA=="],
|
||||
|
||||
@@ -2248,7 +2260,7 @@
|
||||
|
||||
"@simple-git/argv-parser": ["@simple-git/argv-parser@1.1.1", "", { "dependencies": { "@simple-git/args-pathspec": "^1.0.3" } }, "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.50", "", {}, "sha512-ydBWw0G6WFwWHzh9RK4B5c690UkreOG0llq0r+DaI7LgKgxigf8mhHzIPI3S0850g1BPkq/zpuCfrq4QFgUlTQ=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
@@ -2544,7 +2556,7 @@
|
||||
|
||||
"@types/get-folder-size": ["@types/get-folder-size@3.0.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-tSf/k7Undx6jKRwpChR9tl+0ZPf0BVwkjBRtJ5qSnz6iWm2ZRYMAS2MktC2u7YaTAFHmxpL/LBxI85M7ioJCSg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
"@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
|
||||
|
||||
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
||||
|
||||
@@ -2742,7 +2754,9 @@
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ai": ["ai@6.0.221", "", { "dependencies": { "@ai-sdk/gateway": "3.0.145", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cB7qJbNTMuD5spdJEo+guejX0rkjhSQpc4PHITNB+iBFBnGYHLUZOM+uSeIkY4mS4sVVKBKm3kSQQoI5cUwU3g=="],
|
||||
"ai": ["ai@6.0.222", "", { "dependencies": { "@ai-sdk/gateway": "3.0.146", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kUzBaIHIfaDgBX7X22V4tE48MyYyS2IgCuj/zfANx0JfDAwgKwzKjl9pZUpYSnYPChI7i7yRGRoeTkebAFx1VQ=="],
|
||||
|
||||
"ai-sdk-ollama": ["ai-sdk-ollama@3.8.8", "", { "dependencies": { "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30", "jsonrepair": "^3.14.0", "ollama": "^0.6.3" }, "peerDependencies": { "ai": "^6.0.197" } }, "sha512-peWelPf6sVsRULQyYhfyu1dMZhwewRszsbQVfbuhNLflh+ncRXn6pe1BRE3NAcSsWd7JMRZAV5RzcuR3R9ZfaQ=="],
|
||||
|
||||
"ai-sdk-provider-claude-code": ["ai-sdk-provider-claude-code@3.5.1", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@anthropic-ai/claude-agent-sdk": "^0.3.170" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-DxbOp3qIQTAhdvtynhW3Eq+NqAuU8UKVRVzBSdmVnCVMke8xC372Hy/j7FDalC5PkOOdiv7jI9yUzIk6vN1l6g=="],
|
||||
|
||||
@@ -3278,7 +3292,7 @@
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw=="],
|
||||
|
||||
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
|
||||
|
||||
@@ -3388,7 +3402,7 @@
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
|
||||
"fast-equals": ["fast-equals@5.4.1", "", {}, "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
|
||||
@@ -4182,7 +4196,7 @@
|
||||
|
||||
"node-pty": ["node-pty@1.2.0-beta.11", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-THcUyu1WwdgoIyUvgXOZ70EOMXzheGa0q3tbEb5kUIfKgcpBJ+AJ9Q1kq0bKtYmQzr77usXiTORZTLmAUQlnoQ=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="],
|
||||
"node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="],
|
||||
|
||||
"node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="],
|
||||
|
||||
@@ -4208,6 +4222,8 @@
|
||||
|
||||
"obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="],
|
||||
|
||||
"ollama": ["ollama@0.6.3", "", { "dependencies": { "whatwg-fetch": "^3.6.20" } }, "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg=="],
|
||||
|
||||
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
||||
|
||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||
@@ -4228,7 +4244,7 @@
|
||||
|
||||
"open-graph-scraper": ["open-graph-scraper@6.12.0", "", { "dependencies": { "chardet": "^2.2.0", "cheerio": "^1.2.0", "iconv-lite": "^0.7.2", "undici": "^7.28.0" } }, "sha512-x0fS3eHxdCox+rFBhQSVe+qBznSPn1pspp8A4BoaVEkiECZEwagEb8z06swLfaFFE2gefj1BvEBeJmdeGTDnYw=="],
|
||||
|
||||
"openai": ["openai@6.45.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw=="],
|
||||
"openai": ["openai@6.46.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-DFg6jEPT2RO+oAyXtddeUJU8zkGy1OQ1AjGzNIJUMQG03TTqvCpy9tBpQ+2VVVnvrl3E56F8GEin2JYtWpITtA=="],
|
||||
|
||||
"opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="],
|
||||
|
||||
@@ -4360,13 +4376,13 @@
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"posthog-js": ["posthog-js@1.398.6", "", { "dependencies": { "@posthog/core": "^1.39.6", "@posthog/types": "^1.392.1", "core-js": "^3.49.0", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.29.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } }, "sha512-T86lyZ4Eqn0d0a1CJ9dTc+c1eVNw5qQb1JeomvTueYJ23JXYLEva4m9K/epFRoMtD3iqZ7tDD1jzRoaRH7wycA=="],
|
||||
"posthog-js": ["posthog-js@1.399.1", "", { "dependencies": { "@posthog/core": "^1.40.1", "@posthog/types": "^1.393.0", "core-js": "^3.49.0", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.29.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } }, "sha512-xuDe1ZnWgpW0vPs9lvEEWeyMSookjkjUYzdNsMmdbVaBuiRm/bVLvuN72mezqNBf07P+Rjg+5FkNif3VysRL6g=="],
|
||||
|
||||
"posthog-node": ["posthog-node@5.40.0", "", { "dependencies": { "@posthog/core": "^1.39.6" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-DrLfHuauO0W6qruF80iqr5JdmLysef74XzOB4eh36oRLRhxCySLraTqsi2Pj161LZnp9/JNdRDxwT8ei8VK2YA=="],
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
"preact": ["preact@10.29.6", "", {}, "sha512-/UzLXnc1jrAS2uHi89XsSECqXVHYzV1VUL1L3esxrmPRmDvKFWtmbabE2a4QQ5a3bLgBivubRCEOt4GGmncPBQ=="],
|
||||
"preact": ["preact@10.29.7", "", { "peerDependencies": { "preact-render-to-string": ">=5" }, "optionalPeers": ["preact-render-to-string"] }, "sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q=="],
|
||||
|
||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||
|
||||
@@ -5030,7 +5046,7 @@
|
||||
|
||||
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
|
||||
|
||||
"vite": ["vite@8.1.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.16", "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA=="],
|
||||
"vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
|
||||
|
||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||
|
||||
@@ -5064,6 +5080,8 @@
|
||||
|
||||
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
||||
|
||||
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
|
||||
@@ -5176,6 +5194,12 @@
|
||||
|
||||
"@cline/code/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@cline/vscode-rollout/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
||||
|
||||
"@cline/vscode-rollout/@types/vscode": ["@types/vscode@1.84.0", "", {}, "sha512-lCGOSrhT3cL+foUEqc8G1PVZxoDbiMmxgnUZZTEnHF4mC47eKAUtBGAuMLY6o6Ua8PAuNCoKXbqPmJd1JYnQfg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@discordjs/builders/discord-api-types": ["discord-api-types@0.38.49", "", {}, "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg=="],
|
||||
@@ -5430,30 +5454,46 @@
|
||||
|
||||
"@radix-ui/react-accessible-icon/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw=="],
|
||||
|
||||
"@radix-ui/react-accordion/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-accordion/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-accordion/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-alert-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="],
|
||||
"@radix-ui/react-aspect-ratio/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
|
||||
"@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
|
||||
|
||||
"@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
|
||||
"@radix-ui/react-avatar/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-checkbox/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-checkbox/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-collapsible/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-collapsible/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-collapsible/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-collapsible/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
@@ -5462,48 +5502,68 @@
|
||||
|
||||
"@radix-ui/react-context-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-focus-scope/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-form/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="],
|
||||
|
||||
"@radix-ui/react-form/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
|
||||
"@radix-ui/react-form/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="],
|
||||
|
||||
"@radix-ui/react-form/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ=="],
|
||||
|
||||
"@radix-ui/react-form/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="],
|
||||
"@radix-ui/react-hover-card/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-hover-card/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-hover-card/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
|
||||
"@radix-ui/react-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-menubar/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-menubar/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-menubar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-menubar/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-navigation-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-navigation-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-navigation-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
@@ -5516,30 +5576,24 @@
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A=="],
|
||||
|
||||
"@radix-ui/react-password-toggle-field/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="],
|
||||
|
||||
"@radix-ui/react-password-toggle-field/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
|
||||
"@radix-ui/react-password-toggle-field/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="],
|
||||
|
||||
"@radix-ui/react-password-toggle-field/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"@radix-ui/react-password-toggle-field/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="],
|
||||
|
||||
"@radix-ui/react-password-toggle-field/@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A=="],
|
||||
|
||||
"@radix-ui/react-popover/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
@@ -5548,6 +5602,8 @@
|
||||
|
||||
"@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||
|
||||
"@radix-ui/react-popper/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
@@ -5556,22 +5612,40 @@
|
||||
|
||||
"@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
|
||||
"@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
|
||||
|
||||
"@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
|
||||
"@radix-ui/react-radio-group/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-radio-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-radio-group/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-roving-focus/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-scroll-area/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-scroll-area/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-select/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-select/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
@@ -5580,20 +5654,32 @@
|
||||
|
||||
"@radix-ui/react-select/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
|
||||
"@radix-ui/react-slider/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-slider/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-slider/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-slider/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-switch/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-switch/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-tabs/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-tabs/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-toast/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-toast/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-toast/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
@@ -5614,14 +5700,16 @@
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-toggle": "1.1.14", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gIC5Q+Xljg7lmUdzSuDoy0t97yZn1sZl00Ra37ZvKrYdWnQLU6sWLd09yG8cIB9jUAlQfHgJ2ACAG00MFwsqSQ=="],
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
@@ -5646,6 +5734,8 @@
|
||||
|
||||
"@react-aria/menu/@react-stately/collections": ["@react-stately/collections@3.13.1", "", { "dependencies": { "@swc/helpers": "^0.5.0", "react-stately": "^3.46.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-o1QSrtHyR7ODTdPyna87pZlgzvxBFOR8nI8XB+tQLIW2AMhE76pLYu4TN9CrZVy6nSAtE06IntyBV4toJIhorA=="],
|
||||
|
||||
"@react-aria/selection/@react-types/shared": ["@react-types/shared@3.36.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ=="],
|
||||
|
||||
"@react-aria/table/@react-stately/collections": ["@react-stately/collections@3.13.1", "", { "dependencies": { "@swc/helpers": "^0.5.0", "react-stately": "^3.46.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-o1QSrtHyR7ODTdPyna87pZlgzvxBFOR8nI8XB+tQLIW2AMhE76pLYu4TN9CrZVy6nSAtE06IntyBV4toJIhorA=="],
|
||||
|
||||
"@react-aria/tabs/@react-aria/selection": ["@react-aria/selection@3.28.1", "", { "dependencies": { "@swc/helpers": "^0.5.0", "react-aria": "^3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-gGa9HkRnWsKxRhrtVASvecNyetMtP9fNF/Vcsy9Z+6NigjGUSN4SU0bhfglZ706B4t/NSMoVhcBJXlyFiG0hQw=="],
|
||||
@@ -5696,6 +5786,8 @@
|
||||
|
||||
"@streamdown/code/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
|
||||
|
||||
"@tailwindcss/node/enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
@@ -5802,7 +5894,7 @@
|
||||
|
||||
"claude-dev/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"cli-truncate/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
|
||||
"cli-truncate/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
@@ -6024,6 +6116,8 @@
|
||||
|
||||
"proxy-agent/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"puppeteer-core/@puppeteer/browsers": ["@puppeteer/browsers@2.6.1", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg=="],
|
||||
|
||||
"radix-ui/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collapsible": "1.1.16", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-BpZJNmetujnGgUI6OX0jEhEmlA46WPqgub8Rv09Kyquwd0cc1ndMKpiPYCjmBU6KSSRPAMtgLpEoZSG/tdNIWQ=="],
|
||||
@@ -6040,8 +6134,6 @@
|
||||
|
||||
"radix-ui/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-menu": "2.1.20", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-PS+gKE0z2prJ74Y0sM+brAGK4mYOHIR7TlcV5EJgUQ6E0xMvyswkK2X4yRqyganrzsRL+WCSKAPu0NQITICRWg=="],
|
||||
@@ -6076,8 +6168,6 @@
|
||||
|
||||
"radix-ui/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.7", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.12", "", { "dependencies": { "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZPHyI0JyzoH/rP0tq2uRaIZTj/4s8+kAbqPz+e2N8+ejHvwPJ889dHhqn+vh7PNvNeq+boAoH9yzqeoShzwF2w=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-roving-focus": "1.1.15", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WwZFjWV4s3aC1QtR3k04R+oANHtX2q6fgKlc7MCEiDNlnTxCZ3H8k3mHtEgVlOejystwk1WQgarQhNOQZ2bK1g=="],
|
||||
@@ -6216,6 +6306,8 @@
|
||||
|
||||
"unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg=="],
|
||||
|
||||
"verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||
|
||||
"vite-node/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
@@ -6282,6 +6374,60 @@
|
||||
|
||||
"@cline/cline-hub-webview/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"@cline/vscode-rollout/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@cline/vscode-rollout/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
@@ -6422,7 +6568,7 @@
|
||||
|
||||
"@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-EXIEYmFgybnFMijVgqx1mq/diWwSQcd0JWVksytAVQEnAiaDvP45WuncEVQkFIAC0gVxa2+Xr8wL5pF5jCVKbg=="],
|
||||
|
||||
"@radix-ui/react-accessible-icon/@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.7", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ=="],
|
||||
"@radix-ui/react-accordion/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
@@ -6430,8 +6576,6 @@
|
||||
|
||||
"@radix-ui/react-accordion/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
|
||||
"@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-checkbox/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
@@ -6448,12 +6592,16 @@
|
||||
|
||||
"@radix-ui/react-context-menu/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
@@ -6462,14 +6610,16 @@
|
||||
|
||||
"@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-form/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
|
||||
"@radix-ui/react-hover-card/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-hover-card/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
|
||||
"@radix-ui/react-hover-card/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-menu/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-menubar/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-menubar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-menubar/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
@@ -6482,13 +6632,9 @@
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-roving-focus/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"@radix-ui/react-one-time-password-field/@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
|
||||
|
||||
"@radix-ui/react-password-toggle-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
"@radix-ui/react-popover/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-popover/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
|
||||
@@ -6504,6 +6650,8 @@
|
||||
|
||||
"@radix-ui/react-radio-group/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-roving-focus/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
@@ -6524,6 +6672,8 @@
|
||||
|
||||
"@radix-ui/react-switch/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-tabs/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-tabs/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
@@ -6546,20 +6696,16 @@
|
||||
|
||||
"@radix-ui/react-toggle/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-roving-focus/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-roving-focus/@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-toggle-group/@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QI/hB65XKWACA66P64A+aHxtLUgHJeJLkaQa+awUNXT6T3swndtY5DojeHA+vldrTspMTtFBd7HfZ9QGbM1Qrw=="],
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
@@ -6850,40 +6996,20 @@
|
||||
|
||||
"pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-accordion/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-checkbox/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-collapsible/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-dropdown-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-menubar/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-navigation-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-navigation-menu/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-radio-group/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-roving-focus/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-scroll-area/@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-select/@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-select/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-select/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-slider/@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="],
|
||||
@@ -6892,10 +7018,6 @@
|
||||
|
||||
"radix-ui/@radix-ui/react-switch/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-tabs/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"radix-ui/@radix-ui/react-tooltip/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"react-remark/remark-parse/mdast-util-from-markdown": ["mdast-util-from-markdown@0.8.5", "", { "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-to-string": "^2.0.0", "micromark": "~2.11.0", "parse-entities": "^2.0.0", "unist-util-stringify-position": "^2.0.0" } }, "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ=="],
|
||||
|
||||
"react-remark/remark-rehype/mdast-util-to-hast": ["mdast-util-to-hast@10.2.0", "", { "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", "mdast-util-definitions": "^4.0.0", "mdurl": "^1.0.0", "unist-builder": "^2.0.0", "unist-util-generated": "^1.0.0", "unist-util-position": "^3.0.0", "unist-util-visit": "^2.0.0" } }, "sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ=="],
|
||||
@@ -6994,6 +7116,22 @@
|
||||
|
||||
"unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.7", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
|
||||
"webview-ui/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"webview-ui/@vitejs/plugin-react-swc/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
@@ -7088,14 +7226,20 @@
|
||||
|
||||
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
|
||||
|
||||
"@radix-ui/react-accessible-icon/@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
"@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-toggle-group/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-toolbar/@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
|
||||
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@so-ric/colorspace/color/color-convert/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="],
|
||||
|
||||
"@so-ric/colorspace/color/color-string/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="],
|
||||
@@ -7240,6 +7384,10 @@
|
||||
|
||||
"test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
|
||||
|
||||
"webview-ui/vitest/@vitest/utils/loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||
|
||||
"webview-ui/vitest/chai/check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
|
||||
@@ -7268,8 +7416,6 @@
|
||||
|
||||
"@microsoft/tui-test/jest-diff/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="],
|
||||
|
||||
"@radix-ui/react-accessible-icon/@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
|
||||
"@storybook/react-vite/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],
|
||||
|
||||
"@vscode/test-cli/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
+1
-1
@@ -766,7 +766,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/cline-cli/acp-editor-integrations",
|
||||
"destination": "/cli/acp-editor-integrations"
|
||||
"destination": "/cli/cli-reference"
|
||||
},
|
||||
{
|
||||
"source": "/cline-cli/samples/github-issue-rca",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# Cline SDK Changelog
|
||||
|
||||
## 0.0.63
|
||||
|
||||
- The session runtime now emits `task.mistake_limit_reached` telemetry when the consecutive-mistake limit is hit, so every host (CLI, VS Code extension, hub daemon) captures it — including auto-stops when no host prompt is configured
|
||||
|
||||
## 0.0.62
|
||||
|
||||
- Fixed Ollama native API routing so context window and timeout settings work again
|
||||
- Telemetry is no longer attached to hub tool contexts
|
||||
|
||||
## 0.0.61
|
||||
|
||||
- Context compaction now reports progress status while it runs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cline/agents",
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.63",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cline/core",
|
||||
"description": "Cline Core SDK for Node Runtime",
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.63",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline",
|
||||
|
||||
@@ -30,6 +30,7 @@ export type DelegatedAgentConnectionConfig = Pick<
|
||||
| "reasoningEffort"
|
||||
| "thinkingBudgetTokens"
|
||||
| "maxTokensPerTurn"
|
||||
| "temperature"
|
||||
>;
|
||||
|
||||
export interface DelegatedAgentRuntimeConfig
|
||||
@@ -93,6 +94,7 @@ export function createDelegatedAgentConfigProvider(
|
||||
reasoningEffort: runtimeConfig.reasoningEffort,
|
||||
thinkingBudgetTokens: runtimeConfig.thinkingBudgetTokens,
|
||||
maxTokensPerTurn: runtimeConfig.maxTokensPerTurn,
|
||||
temperature: runtimeConfig.temperature,
|
||||
}),
|
||||
updateConnectionDefaults: (overrides) => {
|
||||
runtimeConfig = {
|
||||
|
||||
@@ -340,6 +340,7 @@ describe("createSpawnAgentTool", () => {
|
||||
providerId: "cline",
|
||||
modelId: "stale-model",
|
||||
apiKey: "oauth-access-old",
|
||||
temperature: 0.3,
|
||||
});
|
||||
const updateConnectionDefaults = vi.spyOn(
|
||||
configProvider,
|
||||
@@ -372,6 +373,7 @@ describe("createSpawnAgentTool", () => {
|
||||
expect.objectContaining({
|
||||
apiKey: "oauth-access-new",
|
||||
modelId: "updated-model",
|
||||
temperature: 0.3,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CLINE_INTERNAL_TELEMETRY_METADATA_KEY } from "../../services/telemetry/tool-context";
|
||||
import { handleCapabilityProgress } from "./handlers/capability-handlers";
|
||||
import type { HubTransportContext } from "./handlers/context";
|
||||
import {
|
||||
@@ -110,6 +111,58 @@ describe("handleCapabilityProgress", () => {
|
||||
});
|
||||
|
||||
describe("hub client runtime capabilities", () => {
|
||||
it("omits process-local telemetry from proxied tool contexts", async () => {
|
||||
const telemetry: Record<string, unknown> = {};
|
||||
telemetry.self = telemetry;
|
||||
const request: ClientContributionRequest = vi.fn(
|
||||
async (_sessionId, _capabilityName, payload) => {
|
||||
expect(() => JSON.stringify(payload)).not.toThrow();
|
||||
return { result: "ok" };
|
||||
},
|
||||
);
|
||||
const runtime = createHubClientContributionRuntime({
|
||||
sessionId: "session-1",
|
||||
targetClientId: "client-1",
|
||||
contributions: [
|
||||
{
|
||||
kind: "toolExecutor",
|
||||
executor: "askQuestion",
|
||||
capabilityName: "tool_executor.askQuestion",
|
||||
},
|
||||
{
|
||||
kind: "tool",
|
||||
name: "switch_to_act_mode",
|
||||
description: "Switch to act mode.",
|
||||
inputSchema: { type: "object" },
|
||||
capabilityName: "custom_tool.switch_to_act_mode",
|
||||
},
|
||||
],
|
||||
requestCapability: request,
|
||||
});
|
||||
const context = {
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 2,
|
||||
metadata: {
|
||||
modelSupportsImages: true,
|
||||
[CLINE_INTERNAL_TELEMETRY_METADATA_KEY]: telemetry,
|
||||
},
|
||||
};
|
||||
|
||||
await runtime.toolExecutors?.askQuestion?.("Continue?", ["Yes"], context);
|
||||
await runtime.localRuntime.extraTools?.[0]?.execute({}, context);
|
||||
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
for (const [, , payload] of vi.mocked(request).mock.calls) {
|
||||
expect(payload.context).toEqual({
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 2,
|
||||
metadata: { modelSupportsImages: true },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("proxies lifecycle hooks through capability requests", async () => {
|
||||
const request = vi.fn(async () => ({
|
||||
control: { context: "extra context" },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user