merge conflicts

This commit is contained in:
Evan 2025-02-06 18:05:29 -08:00
commit 3866ae3449
50 changed files with 1951 additions and 214 deletions

View file

@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add Alibaba qwen models plus/max/coder-plus/turbo

View file

@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add automatic retry for rate limited requests

View file

@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Update README.md to include Getting Started

View file

@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix a bug where we were not properly checking for changesets in check-changeset git action

View file

@ -21,6 +21,7 @@
- [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
- [ ] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
- [ ] I have created a changeset using `npm run changeset` (required for user-facing changes)
- [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
### Screenshots

View file

@ -0,0 +1,103 @@
"""
This script updates a specific version's release notes section in CHANGELOG.md with new content
or reformats existing content.
The script:
1. Takes a version number, changelog path, and optionally new content as input from environment variables
2. Finds the section in the changelog for the specified version
3. Either:
a) Replaces the content with new content if provided, or
b) Reformats existing content by:
- Removing the first two lines of the changeset format
- Ensuring version numbers are wrapped in square brackets
4. Writes the updated changelog back to the file
Environment Variables:
CHANGELOG_PATH: Path to the changelog file (defaults to 'CHANGELOG.md')
VERSION: The version number to update/format
PREV_VERSION: The previous version number (used to locate section boundaries)
NEW_CONTENT: Optional new content to insert for this version
"""
#!/usr/bin/env python3
import os
import sys
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
VERSION = os.environ['VERSION']
PREV_VERSION = os.environ.get("PREV_VERSION", "")
NEW_CONTENT = os.environ.get("NEW_CONTENT", "")
def overwrite_changelog_section(changelog_text: str, new_content: str):
# Find the section for the specified version
version_pattern = f"## {VERSION}\n"
bracketed_version_pattern = f"## [{VERSION}]\n"
prev_version_pattern = f"## [{PREV_VERSION}]\n"
print(f"latest version: {VERSION}")
print(f"prev_version: {PREV_VERSION}")
# Try both unbracketed and bracketed version patterns
version_index = changelog_text.find(version_pattern)
if version_index == -1:
version_index = changelog_text.find(bracketed_version_pattern)
if version_index == -1:
# If version not found, add it at the top (after the first line)
first_newline = changelog_text.find('\n')
if first_newline == -1:
# If no newline found, just prepend
return f"## [{VERSION}]\n\n{changelog_text}"
return f"{changelog_text[:first_newline + 1]}## [{VERSION}]\n\n{changelog_text[first_newline + 1:]}"
else:
# Using bracketed version
version_pattern = bracketed_version_pattern
notes_start_index = version_index + len(version_pattern)
notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and prev_version_pattern in changelog_text else len(changelog_text)
if new_content:
return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:]
else:
changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n")
# Ensure we have at least 2 lines before removing them
if len(changeset_lines) < 2:
print("Warning: Changeset content has fewer than 2 lines")
parsed_lines = "\n".join(changeset_lines)
else:
# Remove the first two lines from the regular changeset format, ex: \n### Patch Changes
parsed_lines = "\n".join(changeset_lines[2:])
updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:]
# Ensure version number is bracketed
updated_changelog = updated_changelog.replace(f"## {VERSION}", f"## [{VERSION}]")
return updated_changelog
try:
print(f"Reading changelog from: {CHANGELOG_PATH}")
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
print(f"Changelog content length: {len(changelog_content)} characters")
print("First 200 characters of changelog:")
print(changelog_content[:200])
print("----------------------------------------------------------------------------------")
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
print("New changelog content:")
print("----------------------------------------------------------------------------------")
print(new_changelog)
print("----------------------------------------------------------------------------------")
print(f"Writing updated changelog back to: {CHANGELOG_PATH}")
with open(CHANGELOG_PATH, 'w') as f:
f.write(new_changelog)
print(f"{CHANGELOG_PATH} updated successfully!")
except FileNotFoundError:
print(f"Error: Changelog file not found at {CHANGELOG_PATH}")
sys.exit(1)
except Exception as e:
print(f"Error updating changelog: {str(e)}")
print(f"Current working directory: {os.getcwd()}")
sys.exit(1)

162
.github/workflows/changeset-release.yml vendored Normal file
View file

@ -0,0 +1,162 @@
name: Changeset Release
run-name: Changeset Release ${{ github.actor != 'cline-bot' && '- Create PR' || '- Update Changelog' }}
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
pull_request:
types: [closed, opened, labeled]
env:
REPO_PATH: ${{ github.repository }}
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
jobs:
# Job 1: Create version bump PR when changesets are merged to main
changeset-pr-version-bump:
if: >
( github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
github.actor != 'cline-bot' ) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Git Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
ref: ${{ env.GIT_REF }}
- name: Setup Node.js
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4
with:
node-version: 20
cache: "npm"
- name: Install Dependencies
run: npm run install:all
# Check if there are any new changesets to process
- name: Check for changesets
id: check-changesets
run: |
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
echo "Changesets diff with previous version: $NEW_CHANGESETS"
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
# Create version bump PR using changesets/action if there are new changesets
- name: Changeset Pull Request
if: steps.check-changesets.outputs.new_changesets != '0'
id: changesets
uses: changesets/action@e9cc34b540dd3ad1b030c57fd97269e8f6ad905a # v1
with:
commit: "changeset version bump"
title: "Changeset version bump"
version: npm run version-packages # This performs the changeset version bump
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Job 2: Process version bump PR created by cline-bot
changeset-pr-edit-approve:
name: Auto approve and merge Bump version PRs
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: >
github.event_name == 'pull_request' &&
github.event.pull_request.base.ref == 'main' &&
github.actor == 'cline-bot' &&
contains(github.event.pull_request.title, 'Changeset version bump')
steps:
- name: Determine checkout ref
id: checkout-ref
run: |
echo "Event action: ${{ github.event.action }}"
echo "Actor: ${{ github.actor }}"
echo "Head ref: ${{ github.head_ref }}"
echo "PR SHA: ${{ github.event.pull_request.head.sha }}"
if [[ "${{ github.event.action }}" == "opened" && "${{ github.actor }}" == "cline-bot" ]]; then
echo "Using branch ref: ${{ github.head_ref }}"
echo "git_ref=${{ github.head_ref }}" >> $GITHUB_OUTPUT
else
echo "Using SHA ref: ${{ github.event.pull_request.head.sha }}"
echo "git_ref=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
fi
- name: Checkout Repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ steps.checkout-ref.outputs.git_ref }}
# Get current and previous versions to edit changelog entry
- name: Get version
id: get_version
run: |
VERSION=$(git show HEAD:package.json | jq -r '.version')
echo "version=$VERSION" >> $GITHUB_OUTPUT
PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION"
echo "prev_version=$PREV_VERSION"
# Update CHANGELOG.md with proper format
- name: Update Changelog Format
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
env:
VERSION: ${{ steps.get_version.outputs.version }}
PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
run: python .github/scripts/overwrite_changeset_changelog.py
# Commit and push changelog updates
- name: Push Changelog updates
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
run: |
git config user.name "cline-bot"
git config user.email github-actions@github.com
echo "Running git add and commit..."
git add CHANGELOG.md
git commit -m "Updating CHANGELOG.md format"
git status
echo "--------------------------------------------------------------------------------"
echo "Pushing to remote..."
echo "--------------------------------------------------------------------------------"
git push
# Add label to indicate changelog has been formatted
- name: Add changelog-ready label
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['changelog-ready']
});
# Auto-approve PR only after it has been labeled
- name: Auto approve PR
if: contains(github.event.pull_request.labels.*.name, 'changelog-ready')
uses: hmarr/auto-approve-action@de8bf34d0402c38aa2c8346973342b2cb02c4435 # v4
with:
review-message: "I'm approving since it's a bump version PR"
# Auto-merge PR
- name: Automerge on PR
if: false # Needs enablePullRequestAutoMerge in repo settings to work contains(github.event.pull_request.labels.*.name, 'changelog-ready')
run: gh pr merge --auto --merge ${{ github.event.pull_request.number }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

117
.github/workflows/check-changeset.yml vendored Normal file
View file

@ -0,0 +1,117 @@
name: Check Changeset
run-name: Check for Changeset in PR
permissions:
contents: read
pull-requests: write
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
check-changeset:
# Skip draft PRs and dependabot PRs
if: github.event.pull_request.draft == false && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Check for changeset
id: check-changeset
run: |
# Debug info
echo "Current directory: $(pwd)"
echo "PR Base Ref: ${{ github.event.pull_request.base.ref }}"
echo "PR Head Ref: ${{ github.event.pull_request.head.ref }}"
echo "PR Head SHA: ${{ github.event.pull_request.head.sha }}"
echo "Git status:"
git status
# Get list of changed files
git fetch origin ${{ github.event.pull_request.base.ref }}
CHANGED_FILES=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} HEAD)
echo "Changed files:"
echo "$CHANGED_FILES"
# Check if any of the changed files are in docs/ or .github/
echo "Checking if changes are docs-only..."
DOCS_ONLY=true
while IFS= read -r file; do
if [[ ! "$file" =~ ^(docs/|.github/) ]]; then
echo "Found non-docs change: $file"
DOCS_ONLY=false
break
fi
done <<< "$CHANGED_FILES"
# If changes are docs-only, skip changeset check
if [ "$DOCS_ONLY" = true ]; then
echo "All changes are in docs/ or .github/, skipping changeset check"
exit 0
else
echo "Changes include non-docs files, checking for changeset..."
fi
# Check if any changeset files are in the changed files
echo "Checking for changeset files in changed files..."
CHANGESET_IN_PR=false
while IFS= read -r file; do
if [[ "$file" =~ ^\.changeset/.*\.md$ && "$file" != ".changeset/README.md" && "$file" != ".changeset/config.json" ]]; then
echo "Found changeset file in PR: $file"
CHANGESET_IN_PR=true
break
fi
done <<< "$CHANGED_FILES"
if [ "$CHANGESET_IN_PR" = false ]; then
echo "No changeset files found in changed files. Changed files in .changeset/:"
echo "$CHANGED_FILES" | grep "^\.changeset/" || true
echo "::error::No changeset file found in PR changes. Please run 'npm run changeset' to create one."
exit 1
fi
- name: Comment on PR
if: failure()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
const message = `This PR requires a changeset since it includes user-facing changes. Please:
1. Run \`npm run changeset\` locally
2. Choose the appropriate version bump:
- \`major\` for breaking changes (1.0.0 → 2.0.0)
- \`minor\` for new features (1.0.0 → 1.1.0)
- \`patch\` for bug fixes (1.0.0 → 1.0.1)
3. Write a clear description of your changes
4. Commit the generated changeset file
Note: Documentation-only changes do not require a changeset.`;
// Get existing comments
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
// Check if we already commented
const botComment = comments.data.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('This PR requires a changeset')
);
if (!botComment) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
}

View file

@ -1,5 +1,20 @@
# Changelog
## [3.2.13]
- Add new gemini models gemini-2.0-flash-lite-preview-02-05 and gemini-2.0-flash-001
- Add all available Mistral API models (thanks @ViezeVingertjes!)
- Add LiteLLM API provider support (thanks @him0!)
## [3.2.12]
- Fix command chaining for Windows users
- Fix reasoning_content error for OpenAI providers
## [3.2.11]
- Add OpenAI o3-mini model
## [3.2.10]
- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct

View file

@ -56,20 +56,30 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
- Update existing tests if your changes affect them
- Include both unit tests and integration tests where appropriate
4. **Commit Guidelines**
4. **Version Management with Changesets**
- Create a changeset for any user-facing changes using `npm run changeset`
- Choose the appropriate version bump:
- `major` for breaking changes (1.0.0 → 2.0.0)
- `minor` for new features (1.0.0 → 1.1.0)
- `patch` for bug fixes (1.0.0 → 1.0.1)
- Write clear, descriptive changeset messages that explain the impact
- Documentation-only changes don't require changesets
5. **Commit Guidelines**
- Write clear, descriptive commit messages
- Use conventional commit format (e.g., "feat:", "fix:", "docs:")
- Reference relevant issues in commits using #issue-number
5. **Before Submitting**
6. **Before Submitting**
- Rebase your branch on the latest main
- Ensure your branch builds successfully
- Double-check all tests are passing
- Review your changes for any debugging code or console logs
6. **Pull Request Description**
7. **Pull Request Description**
- Clearly describe what your changes do
- Include steps to test the changes
- List any breaking changes

View file

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Cline Bot Inc.
Copyright 2025 Cline Bot Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -198,4 +198,4 @@
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.

View file

@ -24,7 +24,7 @@ English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
</td>
<td align="center">
<a href="https://cline.bot/join-us" target="_blank"><strong>We're Hiring!</strong></a>
<a href="https://docs.cline.bot/getting-started/getting-started-new-coders" target="_blank"><strong>Getting Started</strong></a>
</td>
</tbody>
</table>
@ -187,4 +187,4 @@ To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.m
## License
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)

View file

@ -102,4 +102,4 @@ The **Problems** section in VS Code shows any errors or warnings in your code. Y
## Next Steps
After installing these tools, you'll be ready to start coding! Return to the [Getting Started with Cline for New Coders](getting-started-new-coders.md) guide to continue your journey.
After installing these tools, you'll be ready to start coding! Return to the [Getting Started with Cline for New Coders](../getting-started-new-coders/README.md) guide to continue your journey.

View file

@ -38,7 +38,7 @@ STOP! Before proceeding, you MUST verify these requirements:
<img src="https://github.com/user-attachments/assets/abf908b1-be98-4894-8dc7-ef3d27943a47" alt="MCP Server Panel" width="400" />
1. The MCP settings files should be display in a tab in VS Code.
1. Replce the file's contents with this code:
1. Replace the file's contents with this code:
For Windows:
@ -96,7 +96,7 @@ You should witness Cline:
1. Update the mcp setting json file
1. Start the server and start the server
The mcp seetings file should now look like this:
The mcp settings file should now look like this:
_For a Windows machine:_

55
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.2.9",
"version": "3.2.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.2.9",
"version": "3.2.12",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
@ -36,7 +36,7 @@
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"openai": "^4.61.0",
"openai": "^4.82.0",
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
@ -5583,12 +5583,6 @@
"integrity": "sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg==",
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
"integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
"license": "MIT"
},
"node_modules/@types/should": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@types/should/-/should-11.2.0.tgz",
@ -6479,6 +6473,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@ -7029,6 +7024,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@ -7410,6 +7406,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
@ -7422,6 +7419,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -8306,6 +8304,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -8453,6 +8452,7 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -8678,6 +8678,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
@ -8735,6 +8736,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@ -8747,6 +8749,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -8759,6 +8762,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -8793,6 +8797,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@ -10548,6 +10553,7 @@
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -10611,28 +10617,30 @@
}
},
"node_modules/openai": {
"version": "4.61.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.61.0.tgz",
"integrity": "sha512-xkygRBRLIUumxzKGb1ug05pWmJROQsHkGuj/N6Jiw2dj0dI19JvbFpErSZKmJ/DA+0IvpcugZqCAyk8iLpyM6Q==",
"version": "4.82.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.82.0.tgz",
"integrity": "sha512-1bTxOVGZuVGsKKUWbh3BEwX1QxIXUftJv+9COhhGGVDTFwiaOd4gWsMynF2ewj1mg6by3/O+U8+EEHpWRdPaJg==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"@types/qs": "^6.9.15",
"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",
"qs": "^6.10.3"
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
@ -11329,21 +11337,6 @@
"node": ">=18"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -11793,6 +11786,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
@ -11940,6 +11934,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",

View file

@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.2.10",
"version": "3.2.13",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@ -187,7 +187,8 @@
"publish:marketplace": "vsce publish && ovsx publish",
"publish:marketplace:prerelease": "vsce publish --pre-release && ovsx publish --pre-release",
"prepare": "husky",
"changeset": "changeset"
"changeset": "changeset",
"version-packages": "changeset version"
},
"devDependencies": {
"@changesets/cli": "^2.27.12",
@ -238,7 +239,7 @@
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"openai": "^4.61.0",
"openai": "^4.82.0",
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",

View file

@ -11,8 +11,10 @@ import { GeminiHandler } from "./providers/gemini"
import { OpenAiNativeHandler } from "./providers/openai-native"
import { ApiStream } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
import { QwenHandler } from "./providers/qwen"
import { MistralHandler } from "./providers/mistral"
import { VsCodeLmHandler } from "./providers/vscode-lm"
import { LiteLlmHandler } from "./providers/litellm"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@ -46,10 +48,14 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new OpenAiNativeHandler(options)
case "deepseek":
return new DeepSeekHandler(options)
case "qwen":
return new QwenHandler(options)
case "mistral":
return new MistralHandler(options)
case "vscode-lm":
return new VsCodeLmHandler(options)
case "litellm":
return new LiteLlmHandler(options)
default:
return new AnthropicHandler(options)
}

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { withRetry } from "../retry"
import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../../shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
@ -16,6 +17,7 @@ export class AnthropicHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
let stream: AnthropicStream<Anthropic.Beta.PromptCaching.Messages.RawPromptCachingBetaMessageStreamEvent>

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, DeepSeekModelId, ModelInfo, deepSeekDefaultModelId, deepSeekModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
@ -18,6 +19,7 @@ export class DeepSeekHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
@ -51,7 +53,7 @@ export class DeepSeekHandler implements ApiHandler {
}
}
if ("reasoning_content" in delta && delta.reasoning_content) {
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { GoogleGenerativeAI } from "@google/generative-ai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "../../shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
@ -17,6 +18,7 @@ export class GeminiHandler implements ApiHandler {
this.client = new GoogleGenerativeAI(options.geminiApiKey)
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.client.getGenerativeModel({
model: this.getModel().id,

View file

@ -0,0 +1,60 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from ".."
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: "not-needed",
})
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
content: systemPrompt,
}
const stream = await this.client.chat.completions.create({
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
messages: [systemMessage, ...formattedMessages],
temperature: 0,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
getModel() {
return {
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
info: liteLlmModelInfoSaneDefaults,
}
}
}

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
@ -21,11 +22,12 @@ export class MistralHandler implements ApiHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Mistral({
serverURL: "https://codestral.mistral.ai",
serverURL: "https://api.mistral.ai",
apiKey: this.options.mistralApiKey,
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.chat.stream({
model: this.getModel().id,

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
@ -22,6 +23,7 @@ export class OpenAiNativeHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
switch (this.getModel().id) {
case "o1":
@ -43,6 +45,31 @@ export class OpenAiNativeHandler implements ApiHandler {
}
break
}
case "o3-mini": {
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
break
}
default: {
const stream = await this.client.chat.completions.create({
model: this.getModel().id,

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI, { AzureOpenAI } from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
@ -27,6 +28,7 @@ export class OpenAiHandler implements ApiHandler {
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
@ -56,7 +58,7 @@ export class OpenAiHandler implements ApiHandler {
}
}
if ("reasoning_content" in delta && delta.reasoning_content) {
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",

View file

@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import delay from "delay"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
@ -24,6 +25,7 @@ export class OpenRouterHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()

76
src/api/providers/qwen.ts Normal file
View file

@ -0,0 +1,76 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, QwenModelId, ModelInfo, qwenDefaultModelId, qwenModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
export class QwenHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: this.options.qwenApiLine || "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
}
getModel(): { id: QwenModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in qwenModels) {
const id = modelId as QwenModelId
return { id, info: qwenModels[id] }
}
return {
id: qwenDefaultModelId,
info: qwenModels[qwenDefaultModelId],
}
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
// @ts-ignore-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
}
}
}
}

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
@ -18,6 +19,7 @@ export class VertexHandler implements ApiHandler {
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.messages.create({
model: this.getModel().id,

216
src/api/retry.test.ts Normal file
View file

@ -0,0 +1,216 @@
import { describe, it } from "mocha"
import "should"
import { withRetry } from "./retry"
describe("Retry Decorator", () => {
describe("withRetry", () => {
it("should not retry on success", async () => {
let callCount = 0
class TestClass {
@withRetry()
async *successMethod() {
callCount++
yield "success"
}
}
const test = new TestClass()
const result = []
for await (const value of test.successMethod()) {
result.push(value)
}
callCount.should.equal(1)
result.should.deepEqual(["success"])
})
it("should retry on rate limit (429) error", async () => {
let callCount = 0
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 10, maxDelay: 100 })
async *failMethod() {
callCount++
if (callCount === 1) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
throw error
}
yield "success after retry"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
callCount.should.equal(2)
result.should.deepEqual(["success after retry"])
})
it("should not retry on non-rate-limit errors", async () => {
let callCount = 0
class TestClass {
@withRetry()
async *failMethod() {
callCount++
throw new Error("Regular error")
}
}
const test = new TestClass()
try {
for await (const _ of test.failMethod()) {
// Should not reach here
}
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.equal("Regular error")
callCount.should.equal(1)
}
})
it("should respect retry-after header with delta seconds", async () => {
let callCount = 0
const startTime = Date.now()
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence
async *failMethod() {
callCount++
if (callCount === 1) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
error.headers = { "retry-after": "0.01" } // 10ms delay
throw error
}
yield "success after retry"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
const duration = Date.now() - startTime
duration.should.be.approximately(10, 10) // Allow 10ms variance
callCount.should.equal(2)
result.should.deepEqual(["success after retry"])
})
it("should respect retry-after header with Unix timestamp", async () => {
let callCount = 0
const startTime = Date.now()
const retryTimestamp = Math.floor(Date.now() / 1000) + 0.01 // 10ms in the future
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence
async *failMethod() {
callCount++
if (callCount === 1) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
error.headers = { "retry-after": retryTimestamp.toString() }
throw error
}
yield "success after retry"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
const duration = Date.now() - startTime
duration.should.be.approximately(10, 10) // Allow 10ms variance
callCount.should.equal(2)
result.should.deepEqual(["success after retry"])
})
it("should use exponential backoff when no retry-after header", async () => {
let callCount = 0
const startTime = Date.now()
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 10, maxDelay: 100 })
async *failMethod() {
callCount++
if (callCount === 1) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
throw error
}
yield "success after retry"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
const duration = Date.now() - startTime
// First retry should be after baseDelay (10ms)
duration.should.be.approximately(10, 10)
callCount.should.equal(2)
result.should.deepEqual(["success after retry"])
})
it("should respect maxDelay", async () => {
let callCount = 0
const startTime = Date.now()
class TestClass {
@withRetry({ maxRetries: 3, baseDelay: 50, maxDelay: 10 })
async *failMethod() {
callCount++
if (callCount < 3) {
const error: any = new Error("Rate limit exceeded")
error.status = 429
throw error
}
yield "success after retries"
}
}
const test = new TestClass()
const result = []
for await (const value of test.failMethod()) {
result.push(value)
}
const duration = Date.now() - startTime
// Both retries should be capped at maxDelay (10ms each)
duration.should.be.approximately(20, 20)
callCount.should.equal(3)
result.should.deepEqual(["success after retries"])
})
it("should throw after maxRetries attempts", async () => {
let callCount = 0
class TestClass {
@withRetry({ maxRetries: 2, baseDelay: 10 })
async *failMethod() {
callCount++
const error: any = new Error("Rate limit exceeded")
error.status = 429
throw error
}
}
const test = new TestClass()
try {
for await (const _ of test.failMethod()) {
// Should not reach here
}
throw new Error("Should have thrown")
} catch (error: any) {
error.message.should.equal("Rate limit exceeded")
callCount.should.equal(2) // Initial attempt + 1 retry
}
})
})
})

62
src/api/retry.ts Normal file
View file

@ -0,0 +1,62 @@
interface RetryOptions {
maxRetries?: number
baseDelay?: number
maxDelay?: number
}
const DEFAULT_OPTIONS: Required<RetryOptions> = {
maxRetries: 3,
baseDelay: 1_000,
maxDelay: 10_000,
}
export function withRetry(options: RetryOptions = {}) {
const { maxRetries, baseDelay, maxDelay } = { ...DEFAULT_OPTIONS, ...options }
return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = async function* (...args: any[]) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
yield* originalMethod.apply(this, args)
return
} catch (error: any) {
const isRateLimit = error?.status === 429
const isLastAttempt = attempt === maxRetries - 1
if (!isRateLimit || isLastAttempt) {
throw error
}
// Get retry delay from header or calculate exponential backoff
// Check various rate limit headers
const retryAfter =
error.headers?.["retry-after"] ||
error.headers?.["x-ratelimit-reset"] ||
error.headers?.["ratelimit-reset"]
let delay: number
if (retryAfter) {
// Handle both delta-seconds and Unix timestamp formats
const retryValue = parseInt(retryAfter, 10)
if (retryValue > Date.now() / 1000) {
// Unix timestamp
delay = retryValue * 1000 - Date.now()
} else {
// Delta seconds
delay = retryValue * 1000
}
} else {
// Use exponential backoff if no header
delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt))
}
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
return descriptor
}
}

View file

@ -60,7 +60,6 @@ import { SYSTEM_PROMPT } from "./prompts/system"
import { addUserInstructions } from "./prompts/system"
import { OpenAiHandler } from "../api/providers/openai"
import { ApiStream } from "../api/transform/stream"
import { Logger } from "../services/logging/Logger"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
@ -221,7 +220,7 @@ export class Cline {
private async addToClineMessages(message: ClineMessage) {
// these values allow us to reconstruct the conversation history at the time this cline message was created
// it's important that apiConversationHistory is initialized before we add cline messages
message.conversationHistoryIndex = this.apiConversationHistory.length - 1 // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when reseting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to
message.conversationHistoryIndex = this.apiConversationHistory.length - 1 // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when resetting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to
message.conversationHistoryDeletedRange = this.conversationHistoryDeletedRange
this.clineMessages.push(message)
await this.saveClineMessages()
@ -1203,6 +1202,14 @@ export class Cline {
return false
}
private formatErrorWithStatusCode(error: any): string {
const statusCode = error.status || error.statusCode || (error.response && error.response.status)
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
// Only prepend the statusCode if it's not already part of the message
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
@ -1310,10 +1317,9 @@ export class Cline {
} else {
// request failed after retrying automatically once, ask user if they want to retry again
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
const { response } = await this.ask(
"api_req_failed",
error.message ?? JSON.stringify(serializeError(error), null, 2),
)
const errorMessage = this.formatErrorWithStatusCode(error)
const { response } = await this.ask("api_req_failed", errorMessage)
if (response !== "yesButtonClicked") {
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
throw new Error("API request failed")
@ -1401,7 +1407,7 @@ export class Cline {
if (!block.partial) {
// Some models add code block artifacts (around the tool calls) which show up at the end of text content
// matches ``` with atleast one char after the last backtick, at the end of the string
// matches ``` with at least one char after the last backtick, at the end of the string
const match = content?.trimEnd().match(/```[a-zA-Z0-9_-]+$/)
if (match) {
const matchLength = match[0].length
@ -2087,7 +2093,6 @@ export class Cline {
filePattern,
this.llmFileAccessController,
)
// Logger.log(results)
const completeMessage = JSON.stringify({
...sharedMessageProps,
@ -2837,7 +2842,7 @@ export class Cline {
if (!block.partial || this.didRejectTool || this.didAlreadyUseTool) {
// block is finished streaming and executing
if (this.currentStreamingContentIndex === this.assistantMessageContent.length - 1) {
// its okay that we increment if !didCompleteReadingStream, it'll just return bc out of bounds and as streaming continues it will call presentAssitantMessage if a new block is ready. if streaming is finished then we set userMessageContentReady to true when out of bounds. This gracefully allows the stream to continue on and all potential content blocks be presented.
// its okay that we increment if !didCompleteReadingStream, it'll just return bc out of bounds and as streaming continues it will call presentAssistantMessage if a new block is ready. if streaming is finished then we set userMessageContentReady to true when out of bounds. This gracefully allows the stream to continue on and all potential content blocks be presented.
// last block is complete and it is finished executing
this.userMessageContentReady = true // will allow pwaitfor to continue
}
@ -3103,7 +3108,9 @@ export class Cline {
// abandoned happens when extension is no longer waiting for the cline instance to finish aborting (error is thrown here when any function in the for loop throws due to this.abort)
if (!this.abandoned) {
this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
await abortStream("streaming_failed", error.message ?? JSON.stringify(serializeError(error), null, 2))
const errorMessage = this.formatErrorWithStatusCode(error)
await abortStream("streaming_failed", errorMessage)
const history = await this.providerRef.deref()?.getTaskWithId(this.taskId)
if (history) {
await this.providerRef.deref()?.initClineWithHistoryItem(history.historyItem)

View file

@ -2,7 +2,6 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as path from "path"
import * as diff from "diff"
import { LLMFileAccessController } from "../../services/llm-access-control/LLMFileAccessController"
import { Logger } from "../../services/logging/Logger"
export const formatResponse = {
toolDenied: () => `The user denied this operation.`,

View file

@ -1,4 +1,4 @@
import defaultShell from "default-shell"
import { getShell } from "../../utils/shell"
import os from "os"
import osName from "os-name"
import { McpHub } from "../../services/mcp/McpHub"
@ -38,7 +38,7 @@ Always adhere to this format for the tool use to ensure proper parsing and execu
# Tools
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd.toPosix()}
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd.toPosix()}
Parameters:
- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
- requires_approval: (required) A boolean indicating whether this command requires explicit user approval before execution in case the user has auto-approve mode enabled. Set to 'true' for potentially impactful operations like installing/uninstalling packages, deleting/overwriting files, system configuration changes, network operations, or any commands that could have unintended side effects. Set to 'false' for safe operations like reading files/directories, running development servers, building projects, and other non-destructive operations.
@ -941,7 +941,7 @@ ${
SYSTEM INFORMATION
Operating System: ${osName()}
Default Shell: ${defaultShell}
Default Shell: ${getShell()}
Home Directory: ${os.homedir().toPosix()}
Current Working Directory: ${cwd.toPosix()}

View file

@ -73,7 +73,7 @@ export function getNextTruncationRange(
let rangeEndIndex = startOfRest + messagesToRemove - 1
// Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure.
// NOTE: anthropic format messages are always user-assitant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
// NOTE: anthropic format messages are always user-assistant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
if (messages[rangeEndIndex].role !== "user") {
rangeEndIndex -= 1
}

View file

@ -45,6 +45,7 @@ type SecretKey =
| "geminiApiKey"
| "openAiNativeApiKey"
| "deepSeekApiKey"
| "qwenApiKey"
| "mistralApiKey"
| "authToken"
| "authNonce"
@ -76,6 +77,9 @@ type GlobalStateKey =
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
| "qwenApiLine"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@ -174,7 +178,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
webviewView.webview.html = this.getHtmlContent(webviewView.webview)
// Sets up an event listener to listen for messages passed from the webview view context
// and executes code based on the message that is recieved
// and executes code based on the message that is received
this.setWebviewMessageListener(webviewView.webview)
// Logs show up in bottom panel > Debug Console
@ -245,7 +249,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async initClineWithTask(task?: string, images?: string[]) {
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
await this.getState()
this.cline = new Cline(
@ -359,7 +363,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
/**
* Sets up an event listener to listen for messages passed from the webview context and
* executes code based on the message that is recieved.
* executes code based on the message that is received.
*
* @param webview A reference to the extension webview
*/
@ -438,11 +442,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
qwenApiLine,
} = message.apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
@ -466,11 +474,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.storeSecret("geminiApiKey", geminiApiKey)
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
await this.storeSecret("qwenApiKey", qwenApiKey)
await this.storeSecret("mistralApiKey", mistralApiKey)
await this.updateGlobalState("azureApiVersion", azureApiVersion)
await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl)
await this.updateGlobalState("liteLlmModelId", liteLlmModelId)
await this.updateGlobalState("qwenApiLine", qwenApiLine)
if (this.cline) {
this.cline.api = buildApiHandler(message.apiConfiguration)
}
@ -535,6 +547,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "lmstudio":
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId)
break
}
// Restore the model used in previous mode
@ -563,6 +578,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "lmstudio":
await this.updateGlobalState("lmStudioModelId", newModelId)
break
case "litellm":
await this.updateGlobalState("liteLlmModelId", newModelId)
break
}
if (this.cline) {
@ -1289,7 +1307,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
Now that we use retainContextWhenHidden, we don't have to store a cache of cline messages in the user's state, but we could to reduce memory footprint in long conversations.
- We have to be careful of what state is shared between ClineProvider instances since there could be multiple instances of the extension running at once. For example when we cached cline messages using the same key, two instances of the extension could end up using the same key and overwriting each other's messages.
- Some state does need to be shared between the instances, i.e. the API key--however there doesn't seem to be a good way to notfy the other instances that the API key has changed.
- Some state does need to be shared between the instances, i.e. the API key--however there doesn't seem to be a good way to notify the other instances that the API key has changed.
We need to use a unique identifier for each ClineProvider instance's message cache since we could be running several instances of the extension outside of just the sidebar i.e. in editor panels.
@ -1353,6 +1371,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
@ -1364,11 +1383,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
browserSettings,
chatSettings,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
userInfo,
authToken,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
qwenApiLine,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@ -1392,6 +1414,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getSecret("geminiApiKey") as Promise<string | undefined>,
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
this.getSecret("qwenApiKey") as Promise<string | undefined>,
this.getSecret("mistralApiKey") as Promise<string | undefined>,
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
@ -1403,11 +1426,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("browserSettings") as Promise<BrowserSettings | undefined>,
this.getGlobalState("chatSettings") as Promise<ChatSettings | undefined>,
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
this.getGlobalState("liteLlmBaseUrl") as Promise<string | undefined>,
this.getGlobalState("liteLlmModelId") as Promise<string | undefined>,
this.getGlobalState("userInfo") as Promise<UserInfo | undefined>,
this.getSecret("authToken") as Promise<string | undefined>,
this.getGlobalState("previousModeApiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("previousModeModelId") as Promise<string | undefined>,
this.getGlobalState("previousModeModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("qwenApiLine") as Promise<string | undefined>,
])
let apiProvider: ApiProvider
@ -1448,11 +1474,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
qwenApiKey,
qwenApiLine,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
},
lastShownAnnouncementId,
customInstructions,
@ -1541,6 +1571,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
"geminiApiKey",
"openAiNativeApiKey",
"deepSeekApiKey",
"qwenApiKey",
"mistralApiKey",
"authToken",
]

View file

@ -2,7 +2,6 @@ import { globby, Options } from "globby"
import os from "os"
import * as path from "path"
import { arePathsEqual, pathExists } from "../../utils/path"
import { Logger } from "../logging/Logger"
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
const absolutePath = path.resolve(dirPath)

View file

@ -4,7 +4,6 @@ import * as path from "path"
import * as readline from "readline"
import { pathExists } from "../../utils/path"
import { LLMFileAccessController } from "../llm-access-control/LLMFileAccessController"
import { Logger } from "../logging/Logger"
/*
This file provides functionality to perform regex searches on files using ripgrep.

View file

@ -9,12 +9,16 @@ export type ApiProvider =
| "gemini"
| "openai-native"
| "deepseek"
| "qwen"
| "mistral"
| "vscode-lm"
| "litellm"
export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
liteLlmBaseUrl?: string
liteLlmModelId?: string
anthropicBaseUrl?: string
openRouterApiKey?: string
openRouterModelId?: string
@ -36,9 +40,11 @@ export interface ApiHandlerOptions {
geminiApiKey?: string
openAiNativeApiKey?: string
deepSeekApiKey?: string
qwenApiKey?: string
mistralApiKey?: string
azureApiVersion?: string
vsCodeLmModelSelector?: any
qwenApiLine?: string
}
export type ApiConfiguration = ApiHandlerOptions & {
@ -241,10 +247,34 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
// Gemini
// https://ai.google.dev/gemini-api/docs/models/gemini
export type GeminiModelId = keyof typeof geminiModels
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219"
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001"
export const geminiModels = {
"gemini-2.0-flash-001": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-lite-preview-02-05": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-pro-exp-02-05": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-thinking-exp-01-21": {
maxTokens: 65536,
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
@ -267,14 +297,6 @@ export const geminiModels = {
inputPrice: 0,
outputPrice: 0,
},
"gemini-exp-1206": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-1.5-flash-002": {
maxTokens: 8192,
contextWindow: 1_048_576,
@ -315,6 +337,14 @@ export const geminiModels = {
inputPrice: 0,
outputPrice: 0,
},
"gemini-exp-1206": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
} as const satisfies Record<string, ModelInfo>
// OpenAI Native
@ -322,6 +352,14 @@ export const geminiModels = {
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o"
export const openAiNativeModels = {
"o3-mini": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.1,
outputPrice: 4.4,
},
// don't support tool use yet
o1: {
maxTokens: 100_000,
@ -344,16 +382,16 @@ export const openAiNativeModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3,
outputPrice: 12,
inputPrice: 1.1,
outputPrice: 4.4,
},
"gpt-4o": {
maxTokens: 4_096,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5,
outputPrice: 15,
inputPrice: 2.5,
outputPrice: 10,
},
"gpt-4o-mini": {
maxTokens: 16_384,
@ -397,13 +435,164 @@ export const deepSeekModels = {
},
} as const satisfies Record<string, ModelInfo>
// Qwen
// https://bailian.console.aliyun.com/
export type QwenModelId = keyof typeof qwenModels
export const qwenDefaultModelId: QwenModelId = "qwen-coder-plus-latest"
export const qwenModels = {
"qwen-coder-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0035,
outputPrice: 0.007,
cacheWritesPrice: 0.0035,
cacheReadsPrice: 0.007,
},
"qwen-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0008,
outputPrice: 0.002,
cacheWritesPrice: 0.0004,
cacheReadsPrice: 0.001,
},
"qwen-turbo-latest": {
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0003,
outputPrice: 0.0006,
cacheWritesPrice: 0.00015,
cacheReadsPrice: 0.0003,
},
"qwen-max-latest": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0112,
outputPrice: 0.0448,
cacheWritesPrice: 0.0056,
cacheReadsPrice: 0.0224,
},
"qwen-coder-plus": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0035,
outputPrice: 0.007,
cacheWritesPrice: 0.0035,
cacheReadsPrice: 0.007,
},
"qwen-plus": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0008,
outputPrice: 0.002,
cacheWritesPrice: 0.0004,
cacheReadsPrice: 0.001,
},
"qwen-turbo": {
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0003,
outputPrice: 0.0006,
cacheWritesPrice: 0.00015,
cacheReadsPrice: 0.0003,
},
"qwen-max": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0112,
outputPrice: 0.0448,
cacheWritesPrice: 0.0056,
cacheReadsPrice: 0.0224,
},
} as const satisfies Record<string, ModelInfo>
// Mistral
// https://docs.mistral.ai/getting-started/models/models_overview/
export type MistralModelId = keyof typeof mistralModels
export const mistralDefaultModelId: MistralModelId = "codestral-latest"
export const mistralDefaultModelId: MistralModelId = "codestral-2501"
export const mistralModels = {
"codestral-latest": {
maxTokens: 32_768,
"mistral-large-2411": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 6.0,
},
"pixtral-large-2411": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 6.0,
},
"ministral-3b-2410": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.04,
outputPrice: 0.04,
},
"ministral-8b-2410": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.1,
},
"mistral-small-2501": {
maxTokens: 32_000,
contextWindow: 32_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.3,
},
"pixtral-12b-2409": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.15,
},
"open-mistral-nemo-2407": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.15,
},
"open-codestral-mamba": {
maxTokens: 256_000,
contextWindow: 256_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.15,
},
"codestral-2501": {
maxTokens: 256_000,
contextWindow: 256_000,
supportsImages: false,
supportsPromptCache: false,
@ -411,3 +600,16 @@ export const mistralModels = {
outputPrice: 0.9,
},
} as const satisfies Record<string, ModelInfo>
// LiteLLM
// https://docs.litellm.ai/docs/
export type LiteLLMModelId = string
export const liteLlmDefaultModelId = "gpt-3.5-turbo"
export const liteLlmModelInfoSaneDefaults: ModelInfo = {
maxTokens: 4096,
contextWindow: 8192,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
}

240
src/test/shell.test.ts Normal file
View file

@ -0,0 +1,240 @@
import { describe, it, beforeEach, afterEach } from "mocha"
import { expect } from "chai"
import { getShell } from "../utils/shell"
import * as vscode from "vscode"
import { userInfo } from "os"
describe("Shell Detection Tests", () => {
let originalPlatform: string
let originalEnv: NodeJS.ProcessEnv
let originalGetConfig: any
let originalUserInfo: any
// Helper to mock VS Code configuration
function mockVsCodeConfig(platformKey: string, defaultProfileName: string | null, profiles: Record<string, any>) {
vscode.workspace.getConfiguration = () =>
({
get: (key: string) => {
if (key === `defaultProfile.${platformKey}`) {
return defaultProfileName
}
if (key === `profiles.${platformKey}`) {
return profiles
}
return undefined
},
}) as any
}
beforeEach(() => {
// Store original references
originalPlatform = process.platform
originalEnv = { ...process.env }
originalGetConfig = vscode.workspace.getConfiguration
originalUserInfo = userInfo
// Clear environment variables for a clean test
delete process.env.SHELL
delete process.env.COMSPEC
// Default userInfo() mock
;(userInfo as any) = () => ({ shell: null })
})
afterEach(() => {
// Restore everything
Object.defineProperty(process, "platform", { value: originalPlatform })
process.env = originalEnv
vscode.workspace.getConfiguration = originalGetConfig
;(userInfo as any) = originalUserInfo
})
// --------------------------------------------------------------------------
// Windows Shell Detection
// --------------------------------------------------------------------------
describe("Windows Shell Detection", () => {
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "win32" })
})
it("uses explicit PowerShell 7 path from VS Code config (profile path)", () => {
mockVsCodeConfig("windows", "PowerShell", {
PowerShell: { path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe" },
})
expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
})
it("uses PowerShell 7 path if source is 'PowerShell' but no explicit path", () => {
mockVsCodeConfig("windows", "PowerShell", {
PowerShell: { source: "PowerShell" },
})
expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
})
it("falls back to legacy PowerShell if profile includes 'powershell' but no path/source", () => {
mockVsCodeConfig("windows", "PowerShell", {
PowerShell: {},
})
expect(getShell()).to.equal("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
})
it("handles undefined shell profile gracefully", () => {
mockVsCodeConfig("windows", "NonExistentProfile", {})
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
})
it("uses WSL bash when profile indicates WSL source", () => {
mockVsCodeConfig("windows", "WSL", {
WSL: { source: "WSL" },
})
expect(getShell()).to.equal("/bin/bash")
})
it("uses WSL bash when profile name includes 'wsl'", () => {
mockVsCodeConfig("windows", "Ubuntu WSL", {
"Ubuntu WSL": {},
})
expect(getShell()).to.equal("/bin/bash")
})
it("defaults to cmd.exe if no special profile is matched", () => {
mockVsCodeConfig("windows", "CommandPrompt", {
CommandPrompt: {},
})
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
})
it("respects userInfo() if no VS Code config is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => ({ shell: "C:\\Custom\\PowerShell.exe" })
expect(getShell()).to.equal("C:\\Custom\\PowerShell.exe")
})
it("respects an odd COMSPEC if no userInfo shell is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
process.env.COMSPEC = "D:\\CustomCmd\\cmd.exe"
expect(getShell()).to.equal("D:\\CustomCmd\\cmd.exe")
})
})
// --------------------------------------------------------------------------
// macOS Shell Detection
// --------------------------------------------------------------------------
describe("macOS Shell Detection", () => {
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "darwin" })
})
it("uses VS Code profile path if available", () => {
mockVsCodeConfig("osx", "MyCustomShell", {
MyCustomShell: { path: "/usr/local/bin/fish" },
})
expect(getShell()).to.equal("/usr/local/bin/fish")
})
it("falls back to userInfo().shell if no VS Code config is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => ({ shell: "/opt/homebrew/bin/zsh" })
expect(getShell()).to.equal("/opt/homebrew/bin/zsh")
})
it("falls back to SHELL env var if no userInfo shell is found", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
process.env.SHELL = "/usr/local/bin/zsh"
expect(getShell()).to.equal("/usr/local/bin/zsh")
})
it("falls back to /bin/zsh if no config, userInfo, or env variable is set", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
// userInfo => null, SHELL => undefined
expect(getShell()).to.equal("/bin/zsh")
})
})
// --------------------------------------------------------------------------
// Linux Shell Detection
// --------------------------------------------------------------------------
describe("Linux Shell Detection", () => {
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "linux" })
})
it("uses VS Code profile path if available", () => {
mockVsCodeConfig("linux", "CustomProfile", {
CustomProfile: { path: "/usr/bin/fish" },
})
expect(getShell()).to.equal("/usr/bin/fish")
})
it("falls back to userInfo().shell if no VS Code config is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => ({ shell: "/usr/bin/zsh" })
expect(getShell()).to.equal("/usr/bin/zsh")
})
it("falls back to SHELL env var if no userInfo shell is found", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
process.env.SHELL = "/usr/bin/fish"
expect(getShell()).to.equal("/usr/bin/fish")
})
it("falls back to /bin/bash if nothing is set", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
// userInfo => null, SHELL => undefined
expect(getShell()).to.equal("/bin/bash")
})
})
// --------------------------------------------------------------------------
// Unknown Platform & Error Handling
// --------------------------------------------------------------------------
describe("Unknown Platform / Error Handling", () => {
it("falls back to /bin/sh for unknown platforms", () => {
Object.defineProperty(process, "platform", { value: "sunos" })
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
expect(getShell()).to.equal("/bin/sh")
})
it("handles VS Code config errors gracefully, falling back to userInfo shell if present", () => {
Object.defineProperty(process, "platform", { value: "linux" })
vscode.workspace.getConfiguration = () => {
throw new Error("Configuration error")
}
;(userInfo as any) = () => ({ shell: "/bin/bash" })
expect(getShell()).to.equal("/bin/bash")
})
it("handles userInfo errors gracefully, falling back to environment variable if present", () => {
Object.defineProperty(process, "platform", { value: "darwin" })
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => {
throw new Error("userInfo error")
}
process.env.SHELL = "/bin/zsh"
expect(getShell()).to.equal("/bin/zsh")
})
it("falls back fully to default shell paths if everything fails", () => {
Object.defineProperty(process, "platform", { value: "linux" })
vscode.workspace.getConfiguration = () => {
throw new Error("Configuration error")
}
;(userInfo as any) = () => {
throw new Error("userInfo error")
}
// No SHELL in env
delete process.env.SHELL
expect(getShell()).to.equal("/bin/bash")
})
})
})

227
src/utils/shell.ts Normal file
View file

@ -0,0 +1,227 @@
import * as vscode from "vscode"
import { userInfo } from "os"
const SHELL_PATHS = {
// Windows paths
POWERSHELL_7: "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
POWERSHELL_LEGACY: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
CMD: "C:\\Windows\\System32\\cmd.exe",
WSL_BASH: "/bin/bash",
// Unix paths
MAC_DEFAULT: "/bin/zsh",
LINUX_DEFAULT: "/bin/bash",
CSH: "/bin/csh",
BASH: "/bin/bash",
KSH: "/bin/ksh",
SH: "/bin/sh",
ZSH: "/bin/zsh",
DASH: "/bin/dash",
TCSH: "/bin/tcsh",
FALLBACK: "/bin/sh",
} as const
interface MacTerminalProfile {
path?: string
}
type MacTerminalProfiles = Record<string, MacTerminalProfile>
interface WindowsTerminalProfile {
path?: string
source?: "PowerShell" | "WSL"
}
type WindowsTerminalProfiles = Record<string, WindowsTerminalProfile>
interface LinuxTerminalProfile {
path?: string
}
type LinuxTerminalProfiles = Record<string, LinuxTerminalProfile>
// -----------------------------------------------------
// 1) VS Code Terminal Configuration Helpers
// -----------------------------------------------------
function getWindowsTerminalConfig() {
try {
const config = vscode.workspace.getConfiguration("terminal.integrated")
const defaultProfileName = config.get<string>("defaultProfile.windows")
const profiles = config.get<WindowsTerminalProfiles>("profiles.windows") || {}
return { defaultProfileName, profiles }
} catch {
return { defaultProfileName: null, profiles: {} as WindowsTerminalProfiles }
}
}
function getMacTerminalConfig() {
try {
const config = vscode.workspace.getConfiguration("terminal.integrated")
const defaultProfileName = config.get<string>("defaultProfile.osx")
const profiles = config.get<MacTerminalProfiles>("profiles.osx") || {}
return { defaultProfileName, profiles }
} catch {
return { defaultProfileName: null, profiles: {} as MacTerminalProfiles }
}
}
function getLinuxTerminalConfig() {
try {
const config = vscode.workspace.getConfiguration("terminal.integrated")
const defaultProfileName = config.get<string>("defaultProfile.linux")
const profiles = config.get<LinuxTerminalProfiles>("profiles.linux") || {}
return { defaultProfileName, profiles }
} catch {
return { defaultProfileName: null, profiles: {} as LinuxTerminalProfiles }
}
}
// -----------------------------------------------------
// 2) Platform-Specific VS Code Shell Retrieval
// -----------------------------------------------------
/** Attempts to retrieve a shell path from VS Code config on Windows. */
function getWindowsShellFromVSCode(): string | null {
const { defaultProfileName, profiles } = getWindowsTerminalConfig()
if (!defaultProfileName) {
return null
}
const profile = profiles[defaultProfileName]
// If the profile name indicates PowerShell, do version-based detection.
// In testing it was found these typically do not have a path, and this
// implementation manages to deductively get the corect version of PowerShell
if (defaultProfileName.toLowerCase().includes("powershell")) {
if (profile?.path) {
// If there's an explicit PowerShell path, return that
return profile.path
} else if (profile?.source === "PowerShell") {
// If the profile is sourced from PowerShell, assume the newest
return SHELL_PATHS.POWERSHELL_7
}
// Otherwise, assume legacy Windows PowerShell
return SHELL_PATHS.POWERSHELL_LEGACY
}
// If there's a specific path, return that immediately
if (profile?.path) {
return profile.path
}
// If the profile indicates WSL
if (profile?.source === "WSL" || defaultProfileName.toLowerCase().includes("wsl")) {
return SHELL_PATHS.WSL_BASH
}
// If nothing special detected, we assume cmd
return SHELL_PATHS.CMD
}
/** Attempts to retrieve a shell path from VS Code config on macOS. */
function getMacShellFromVSCode(): string | null {
const { defaultProfileName, profiles } = getMacTerminalConfig()
if (!defaultProfileName) {
return null
}
const profile = profiles[defaultProfileName]
return profile?.path || null
}
/** Attempts to retrieve a shell path from VS Code config on Linux. */
function getLinuxShellFromVSCode(): string | null {
const { defaultProfileName, profiles } = getLinuxTerminalConfig()
if (!defaultProfileName) {
return null
}
const profile = profiles[defaultProfileName]
return profile?.path || null
}
// -----------------------------------------------------
// 3) General Fallback Helpers
// -----------------------------------------------------
/**
* Tries to get a users shell from os.userInfo() (works on Unix if the
* underlying system call is supported). Returns null on error or if not found.
*/
function getShellFromUserInfo(): string | null {
try {
const { shell } = userInfo()
return shell || null
} catch {
return null
}
}
/** Returns the environment-based shell variable, or null if not set. */
function getShellFromEnv(): string | null {
const { env } = process
if (process.platform === "win32") {
// On Windows, COMSPEC typically holds cmd.exe
return env.COMSPEC || "C:\\Windows\\System32\\cmd.exe"
}
if (process.platform === "darwin") {
// On macOS/Linux, SHELL is commonly the environment variable
return env.SHELL || "/bin/zsh"
}
if (process.platform === "linux") {
// On Linux, SHELL is commonly the environment variable
return env.SHELL || "/bin/bash"
}
return null
}
// -----------------------------------------------------
// 4) Publicly Exposed Shell Getter
// -----------------------------------------------------
export function getShell(): string {
// 1. Check VS Code config first.
if (process.platform === "win32") {
// Special logic for Windows
const windowsShell = getWindowsShellFromVSCode()
if (windowsShell) {
return windowsShell
}
} else if (process.platform === "darwin") {
// macOS from VS Code
const macShell = getMacShellFromVSCode()
if (macShell) {
return macShell
}
} else if (process.platform === "linux") {
// Linux from VS Code
const linuxShell = getLinuxShellFromVSCode()
if (linuxShell) {
return linuxShell
}
}
// 2. If no shell from VS Code, try userInfo()
const userInfoShell = getShellFromUserInfo()
if (userInfoShell) {
return userInfoShell
}
// 3. If still nothing, try environment variable
const envShell = getShellFromEnv()
if (envShell) {
return envShell
}
// 4. Finally, fall back to a default
if (process.platform === "win32") {
// On Windows, if we got here, we have no config, no COMSPEC, and one very messed up operating system.
// Use CMD as a last resort
return SHELL_PATHS.CMD
}
// On macOS/Linux, fallback to a POSIX shell - This is the behavior of our old shell detection method.
return SHELL_PATHS.FALLBACK
}

View file

@ -36,7 +36,7 @@
"@types/react-dom": "^18.3.0",
"@types/vscode-webview": "^1.57.5",
"jsdom": "^25.0.1",
"vitest": "^2.1.8"
"vitest": "^2.1.9"
}
},
"node_modules/@adobe/css-tools": {
@ -3860,9 +3860,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz",
"integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.2.tgz",
"integrity": "sha512-6Fyg9yQbwJR+ykVdT9sid1oc2ewejS6h4wzQltmJfSW53N60G/ah9pngXGANdy9/aaE/TcUFpWosdm7JXS1WTQ==",
"cpu": [
"arm"
],
@ -3874,9 +3874,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz",
"integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.2.tgz",
"integrity": "sha512-K5GfWe+vtQ3kyEbihrimM38UgX57UqHp+oME7X/EX9Im6suwZfa7Hsr8AtzbJvukTpwMGs+4s29YMSO3rwWtsw==",
"cpu": [
"arm64"
],
@ -3888,9 +3888,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz",
"integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.2.tgz",
"integrity": "sha512-PSN58XG/V/tzqDb9kDGutUruycgylMlUE59f40ny6QIRNsTEIZsrNQTJKUN2keMMSmlzgunMFqyaGLmly39sug==",
"cpu": [
"arm64"
],
@ -3902,9 +3902,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.1.tgz",
"integrity": "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.2.tgz",
"integrity": "sha512-gQhK788rQJm9pzmXyfBB84VHViDERhAhzGafw+E5mUpnGKuxZGkMVDa3wgDFKT6ukLC5V7QTifzsUKdNVxp5qQ==",
"cpu": [
"x64"
],
@ -3916,9 +3916,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz",
"integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.2.tgz",
"integrity": "sha512-eiaHgQwGPpxLC3+zTAcdKl4VsBl3r0AiJOd1Um/ArEzAjN/dbPK1nROHrVkdnoE6p7Svvn04w3f/jEZSTVHunA==",
"cpu": [
"arm64"
],
@ -3930,9 +3930,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz",
"integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.2.tgz",
"integrity": "sha512-lhdiwQ+jf8pewYOTG4bag0Qd68Jn1v2gO1i0mTuiD+Qkt5vNfHVK/jrT7uVvycV8ZchlzXp5HDVmhpzjC6mh0g==",
"cpu": [
"x64"
],
@ -3944,9 +3944,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz",
"integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.2.tgz",
"integrity": "sha512-lfqTpWjSvbgQP1vqGTXdv+/kxIznKXZlI109WkIFPbud41bjigjNmOAAKoazmRGx+k9e3rtIdbq2pQZPV1pMig==",
"cpu": [
"arm"
],
@ -3958,9 +3958,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz",
"integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.2.tgz",
"integrity": "sha512-RGjqULqIurqqv+NJTyuPgdZhka8ImMLB32YwUle2BPTDqDoXNgwFjdjQC59FbSk08z0IqlRJjrJ0AvDQ5W5lpw==",
"cpu": [
"arm"
],
@ -3972,9 +3972,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz",
"integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.2.tgz",
"integrity": "sha512-ZvkPiheyXtXlFqHpsdgscx+tZ7hoR59vOettvArinEspq5fxSDSgfF+L5wqqJ9R4t+n53nyn0sKxeXlik7AY9Q==",
"cpu": [
"arm64"
],
@ -3986,9 +3986,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz",
"integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.2.tgz",
"integrity": "sha512-UlFk+E46TZEoxD9ufLKDBzfSG7Ki03fo6hsNRRRHF+KuvNZ5vd1RRVQm8YZlGsjcJG8R252XFK0xNPay+4WV7w==",
"cpu": [
"arm64"
],
@ -4000,9 +4000,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz",
"integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.2.tgz",
"integrity": "sha512-hJhfsD9ykx59jZuuoQgYT1GEcNNi3RCoEmbo5OGfG8RlHOiVS7iVNev9rhLKh7UBYq409f4uEw0cclTXx8nh8Q==",
"cpu": [
"loong64"
],
@ -4014,9 +4014,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz",
"integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.2.tgz",
"integrity": "sha512-g/O5IpgtrQqPegvqopvmdCF9vneLE7eqYfdPWW8yjPS8f63DNam3U4ARL1PNNB64XHZDHKpvO2Giftf43puB8Q==",
"cpu": [
"ppc64"
],
@ -4028,9 +4028,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz",
"integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.2.tgz",
"integrity": "sha512-bSQijDC96M6PuooOuXHpvXUYiIwsnDmqGU8+br2U7iPoykNi9JtMUpN7K6xml29e0evK0/g0D1qbAUzWZFHY5Q==",
"cpu": [
"riscv64"
],
@ -4042,9 +4042,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz",
"integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.2.tgz",
"integrity": "sha512-49TtdeVAsdRuiUHXPrFVucaP4SivazetGUVH8CIxVsNsaPHV4PFkpLmH9LeqU/R4Nbgky9lzX5Xe1NrzLyraVA==",
"cpu": [
"s390x"
],
@ -4056,9 +4056,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz",
"integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.2.tgz",
"integrity": "sha512-j+jFdfOycLIQ7FWKka9Zd3qvsIyugg5LeZuHF6kFlXo6MSOc6R1w37YUVy8VpAKd81LMWGi5g9J25P09M0SSIw==",
"cpu": [
"x64"
],
@ -4070,9 +4070,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz",
"integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.2.tgz",
"integrity": "sha512-aDPHyM/D2SpXfSNCVWCxyHmOqN9qb7SWkY1+vaXqMNMXslZYnwh9V/UCudl6psyG0v6Ukj7pXanIpfZwCOEMUg==",
"cpu": [
"x64"
],
@ -4084,9 +4084,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz",
"integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.2.tgz",
"integrity": "sha512-LQRkCyUBnAo7r8dbEdtNU08EKLCJMgAk2oP5H3R7BnUlKLqgR3dUjrLBVirmc1RK6U6qhtDw29Dimeer8d5hzQ==",
"cpu": [
"arm64"
],
@ -4098,9 +4098,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz",
"integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.2.tgz",
"integrity": "sha512-wt8OhpQUi6JuPFkm1wbVi1BByeag87LDFzeKSXzIdGcX4bMLqORTtKxLoCbV57BHYNSUSOKlSL4BYYUghainYA==",
"cpu": [
"ia32"
],
@ -4112,9 +4112,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz",
"integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.2.tgz",
"integrity": "sha512-rUrqINax0TvrPBXrFKg0YbQx18NpPN3NNrgmaao9xRNbTwek7lOXObhx8tQy8gelmQ/gLaGy1WptpU2eKJZImg==",
"cpu": [
"x64"
],
@ -5209,14 +5209,14 @@
"license": "ISC"
},
"node_modules/@vitest/expect": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz",
"integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
"integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.8",
"@vitest/utils": "2.1.8",
"@vitest/spy": "2.1.9",
"@vitest/utils": "2.1.9",
"chai": "^5.1.2",
"tinyrainbow": "^1.2.0"
},
@ -5225,13 +5225,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz",
"integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
"integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.8",
"@vitest/spy": "2.1.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.12"
},
@ -5272,9 +5272,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz",
"integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
"integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -5285,13 +5285,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz",
"integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
"integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "2.1.8",
"@vitest/utils": "2.1.9",
"pathe": "^1.1.2"
},
"funding": {
@ -5299,13 +5299,13 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz",
"integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
"integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.8",
"@vitest/pretty-format": "2.1.9",
"magic-string": "^0.30.12",
"pathe": "^1.1.2"
},
@ -5324,9 +5324,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz",
"integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
"integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -5337,13 +5337,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz",
"integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
"integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.8",
"@vitest/pretty-format": "2.1.9",
"loupe": "^3.1.2",
"tinyrainbow": "^1.2.0"
},
@ -19405,9 +19405,9 @@
}
},
"node_modules/vite-node": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz",
"integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
"integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -19457,9 +19457,9 @@
}
},
"node_modules/vite/node_modules/rollup": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz",
"integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.2.tgz",
"integrity": "sha512-sBDUoxZEaqLu9QeNalL8v3jw6WjPku4wfZGyTU7l7m1oC+rpRihXc/n/H+4148ZkGz5Xli8CHMns//fFGKvpIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -19473,42 +19473,42 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.32.1",
"@rollup/rollup-android-arm64": "4.32.1",
"@rollup/rollup-darwin-arm64": "4.32.1",
"@rollup/rollup-darwin-x64": "4.32.1",
"@rollup/rollup-freebsd-arm64": "4.32.1",
"@rollup/rollup-freebsd-x64": "4.32.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.32.1",
"@rollup/rollup-linux-arm-musleabihf": "4.32.1",
"@rollup/rollup-linux-arm64-gnu": "4.32.1",
"@rollup/rollup-linux-arm64-musl": "4.32.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.32.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.32.1",
"@rollup/rollup-linux-riscv64-gnu": "4.32.1",
"@rollup/rollup-linux-s390x-gnu": "4.32.1",
"@rollup/rollup-linux-x64-gnu": "4.32.1",
"@rollup/rollup-linux-x64-musl": "4.32.1",
"@rollup/rollup-win32-arm64-msvc": "4.32.1",
"@rollup/rollup-win32-ia32-msvc": "4.32.1",
"@rollup/rollup-win32-x64-msvc": "4.32.1",
"@rollup/rollup-android-arm-eabi": "4.34.2",
"@rollup/rollup-android-arm64": "4.34.2",
"@rollup/rollup-darwin-arm64": "4.34.2",
"@rollup/rollup-darwin-x64": "4.34.2",
"@rollup/rollup-freebsd-arm64": "4.34.2",
"@rollup/rollup-freebsd-x64": "4.34.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.2",
"@rollup/rollup-linux-arm-musleabihf": "4.34.2",
"@rollup/rollup-linux-arm64-gnu": "4.34.2",
"@rollup/rollup-linux-arm64-musl": "4.34.2",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.2",
"@rollup/rollup-linux-riscv64-gnu": "4.34.2",
"@rollup/rollup-linux-s390x-gnu": "4.34.2",
"@rollup/rollup-linux-x64-gnu": "4.34.2",
"@rollup/rollup-linux-x64-musl": "4.34.2",
"@rollup/rollup-win32-arm64-msvc": "4.34.2",
"@rollup/rollup-win32-ia32-msvc": "4.34.2",
"@rollup/rollup-win32-x64-msvc": "4.34.2",
"fsevents": "~2.3.2"
}
},
"node_modules/vitest": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz",
"integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
"integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "2.1.8",
"@vitest/mocker": "2.1.8",
"@vitest/pretty-format": "^2.1.8",
"@vitest/runner": "2.1.8",
"@vitest/snapshot": "2.1.8",
"@vitest/spy": "2.1.8",
"@vitest/utils": "2.1.8",
"@vitest/expect": "2.1.9",
"@vitest/mocker": "2.1.9",
"@vitest/pretty-format": "^2.1.9",
"@vitest/runner": "2.1.9",
"@vitest/snapshot": "2.1.9",
"@vitest/spy": "2.1.9",
"@vitest/utils": "2.1.9",
"chai": "^5.1.2",
"debug": "^4.3.7",
"expect-type": "^1.1.0",
@ -19520,7 +19520,7 @@
"tinypool": "^1.0.1",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "2.1.8",
"vite-node": "2.1.9",
"why-is-node-running": "^2.3.0"
},
"bin": {
@ -19535,8 +19535,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "2.1.8",
"@vitest/ui": "2.1.8",
"@vitest/browser": "2.1.9",
"@vitest/ui": "2.1.9",
"happy-dom": "*",
"jsdom": "*"
},

View file

@ -59,6 +59,6 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"jsdom": "^25.0.1",
"vitest": "^2.1.8"
"vitest": "^2.1.9"
}
}

View file

@ -169,7 +169,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
// }}
onClick={(e) => {
/*
vscode web toolkit bug: when changing the value of a vscodecheckbox programatically, it will call its onChange with stale state. This led to updateEnabled being called with an old vesion of autoApprovalSettings, effectively undoing the state change that was triggered by the last action being unchecked. A simple workaround is to just not use onChange and intead use onClick. We are lucky this is a checkbox and the newvalue is simply opposite of current state.
vscode web toolkit bug: when changing the value of a vscodecheckbox programmatically, it will call its onChange with stale state. This led to updateEnabled being called with an old version of autoApprovalSettings, effectively undoing the state change that was triggered by the last action being unchecked. A simple workaround is to just not use onChange and instead use onClick. We are lucky this is a checkbox and the newvalue is simply opposite of current state.
*/
if (!hasEnabledActions) return
e.stopPropagation() // stops click from bubbling up to the parent, in this case stopping the expanding/collapsing

View file

@ -751,7 +751,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
}}>
{icon}
{title}
{/* Need to render this everytime since it affects height of row by 2px */}
{/* Need to render this every time since it affects height of row by 2px */}
<VSCodeBadge
style={{
opacity: cost != null && cost > 0 ? 1 : 0,

View file

@ -377,7 +377,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
charBeforeCursor === " " || charBeforeCursor === "\n" || charBeforeCursor === "\r\n"
const charAfterIsWhitespace =
charAfterCursor === " " || charAfterCursor === "\n" || charAfterCursor === "\r\n"
// checks if char before cusor is whitespace after a mention
// checks if char before cursor is whitespace after a mention
if (
charBeforeIsWhitespace &&
inputValue.slice(0, cursorPosition - 1).match(new RegExp(mentionRegex.source + "$")) // "$" is added to ensure the match occurs at the end of the string

View file

@ -433,7 +433,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break
}
}
// textAreaRef.current is not explicitly required here since react gaurantees that ref will be stable across re-renders, and we're not using its value but its reference.
// textAreaRef.current is not explicitly required here since react guarantees that ref will be stable across re-renders, and we're not using its value but its reference.
},
[isHidden, textAreaDisabled, enableButtons, handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick],
)

View file

@ -120,7 +120,7 @@ const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
if (!node.lang) {
node.lang = "javascript"
} else if (node.lang.includes(".")) {
// if the langauge is a file, get the extension
// if the language is a file, get the extension
node.lang = node.lang.split(".").slice(-1)[0]
}
})

View file

@ -50,6 +50,44 @@ const remarkUrlToLink = () => {
}
}
/**
* Custom remark plugin that prevents filenames with extensions from being parsed as bold text
* For example: __init__.py should not be rendered as bold "init" followed by ".py"
* Solves https://github.com/cline/cline/issues/1028
*/
const remarkPreventBoldFilenames = () => {
return (tree: any) => {
visit(tree, "strong", (node: any, index: number | undefined, parent: any) => {
// Only process if there's a next node (potential file extension)
if (!parent || typeof index === "undefined" || index === parent.children.length - 1) return
const nextNode = parent.children[index + 1]
// Check if next node is text and starts with . followed by extension
if (nextNode.type !== "text" || !nextNode.value.match(/^\.[a-zA-Z0-9]+/)) return
// If the strong node has multiple children, something weird is happening
if (node.children?.length !== 1) return
// Get the text content from inside the strong node
const strongContent = node.children?.[0]?.value
if (!strongContent || typeof strongContent !== "string") return
// Validate that the strong content is a valid filename
if (!strongContent.match(/^[a-zA-Z0-9_-]+$/)) return
// Combine into a single text node
const newNode = {
type: "text",
value: `__${strongContent}__${nextNode.value}`,
}
// Replace both nodes with the combined text node
parent.children.splice(index, 2, newNode)
})
}
}
const StyledMarkdown = styled.div`
pre {
background-color: ${CODE_BLOCK_BG_COLOR};
@ -160,6 +198,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
const { theme } = useExtensionState()
const [reactContent, setMarkdown] = useRemark({
remarkPlugins: [
remarkPreventBoldFilenames,
remarkUrlToLink,
() => {
return (tree) => {

View file

@ -20,6 +20,8 @@ import {
bedrockModels,
deepSeekDefaultModelId,
deepSeekModels,
qwenDefaultModelId,
qwenModels,
geminiDefaultModelId,
geminiModels,
mistralDefaultModelId,
@ -133,7 +135,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
*/
@ -179,6 +181,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
<VSCodeOption value="qwen">Qwen</VSCodeOption>
<VSCodeOption value="mistral">Mistral</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
@ -187,6 +190,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption>
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
@ -309,6 +313,64 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "qwen" && (
<div>
<DropdownContainer className="dropdown-container">
<label htmlFor="qwen-line-provider">
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
</label>
<VSCodeDropdown
id="qwen-line-provider"
value={apiConfiguration?.qwenApiLine || ""}
onChange={handleInputChange("qwenApiLine")}
style={{
minWidth: 130,
position: "relative",
}}>
<VSCodeOption value="https://dashscope.aliyuncs.com/compatible-mode/v1">China API</VSCodeOption>
<VSCodeOption value="https://dashscope-intl.aliyuncs.com/compatible-mode/v1">
International API
</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Please select the appropriate API interface based on your location. If you are in China, choose the China
API interface. Otherwise, choose the International API interface.
</p>
<VSCodeTextField
value={apiConfiguration?.qwenApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("qwenApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Qwen API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.qwenApiKey && (
<VSCodeLink
href="https://bailian.console.aliyun.com/"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a Qwen API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)}
{selectedProvider === "mistral" && (
<div>
<VSCodeTextField
@ -739,6 +801,38 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "litellm" && (
<div>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("liteLlmBaseUrl")}
placeholder={"Default: http://localhost:4000"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("liteLlmModelId")}
placeholder={"e.g. gpt-4"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LiteLLM provides a unified interface to access various LLM providers' models. See their{" "}
<VSCodeLink href="https://docs.litellm.ai/docs/" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide
</VSCodeLink>{" "}
for more information.
</p>
</div>
)}
{selectedProvider === "ollama" && (
<div>
<VSCodeTextField
@ -828,6 +922,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
{selectedProvider === "qwen" && createDropdown(qwenModels)}
{selectedProvider === "mistral" && createDropdown(mistralModels)}
</DropdownContainer>
@ -1035,6 +1130,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
case "deepseek":
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "qwen":
return getProviderData(qwenModels, qwenDefaultModelId)
case "mistral":
return getProviderData(mistralModels, mistralDefaultModelId)
case "openrouter":
@ -1072,6 +1169,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
supportsImages: false, // VSCode LM API currently doesn't support images
},
}
case "litellm":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.liteLlmModelId || "",
selectedModelInfo: openAiModelInfoSaneDefaults,
}
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}

View file

@ -67,6 +67,7 @@ export const ExtensionStateContextProvider: React.FC<{
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.qwenApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
].some((key) => key !== undefined)

View file

@ -38,6 +38,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
return "You must provide a valid API key or choose a different provider."
}
break
case "qwen":
if (!apiConfiguration.qwenApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
case "mistral":
if (!apiConfiguration.mistralApiKey) {
return "You must provide a valid API key or choose a different provider."