mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
212 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a2679fc30 | |||
| b0446f7bac | |||
| f8e4bfdbeb | |||
| 9b30efefbe | |||
| 82f1f79b3e | |||
| 00506c0fcf | |||
| 45e1f58bbc | |||
| cd3f3e2150 | |||
| ab8376fee4 | |||
| ff7e5c8866 | |||
| 26be9469c4 | |||
| a925e672ed | |||
| 2491b469e7 | |||
| 3fd1f16acb | |||
| 0a14f0bad5 | |||
| d72127550d | |||
| 97fa8cffb9 | |||
| d6f51b4525 | |||
| c846bc4455 | |||
| efaa0d057e | |||
| ceeb33fdc5 | |||
| f938c40539 | |||
| 65ac7ec3f7 | |||
| f80552f92d | |||
| ac4ca51ced | |||
| 04f8c294c2 | |||
| a6e109718e | |||
| f9025426b8 | |||
| 97abdc500b | |||
| 3d64ab298d | |||
| ccdbe3a4a9 | |||
| 000b4af458 | |||
| a01471f469 | |||
| d7788c2fd6 | |||
| d6be431cb8 | |||
| 4c844cbafa | |||
| 9289d73a81 | |||
| 19e5edbc25 | |||
| a20a2444b3 | |||
| cb02277b45 | |||
| 220603e530 | |||
| e40e6ddbcd | |||
| ec3bd56903 | |||
| c126804bb0 | |||
| ee50e9710a | |||
| 95acb1e95c | |||
| e587115d62 | |||
| 9c840fc89c | |||
| b7519a3669 | |||
| 691cf91a4d | |||
| 24d3bfbccb | |||
| 8927879d93 | |||
| 5f57415785 | |||
| c3f7fa3386 | |||
| e95cb30ebd | |||
| 15b269c8f2 | |||
| 31443f2a86 | |||
| 2d05dfd986 | |||
| 7722234761 | |||
| 09c4202cb2 | |||
| 31eb212bee | |||
| cc166f4144 | |||
| 5dea79c65f | |||
| 217162ccba | |||
| a98269f6d5 | |||
| aab26e9a38 | |||
| 796eac558c | |||
| 3873908efb | |||
| a6e812824c | |||
| a4d1ae1af4 | |||
| fa2797051d | |||
| 10315663b2 | |||
| 338116fbd4 | |||
| 89603d3e92 | |||
| a3dc4471e3 | |||
| 510bb72431 | |||
| 141a302521 | |||
| faae5f22a3 | |||
| 0cbd6b321f | |||
| c134291ba0 | |||
| 9b66eac9fe | |||
| c756470c51 | |||
| b059b3ac1e | |||
| 7ae437fb94 | |||
| d67c7e36da | |||
| 584af64334 | |||
| e3b4b2c816 | |||
| 267b60c09e | |||
| ee06c0811c | |||
| b8024497f2 | |||
| e3cfb405fb | |||
| 8e12bdb00d | |||
| 7d67000d1f | |||
| 283d7d6928 | |||
| 83a28001be | |||
| 66d5a320b5 | |||
| 95e1555361 | |||
| a99648884a | |||
| 0434b5c772 | |||
| e534c3dbc7 | |||
| add2f85053 | |||
| 69ff71b051 | |||
| f096d73813 | |||
| 96540ddf1e | |||
| 7da74bd9d8 | |||
| 2a894d8cf1 | |||
| 8793c2c6c5 | |||
| 9e76cfd44a | |||
| 9c8254cf92 | |||
| 57ae7c05d6 | |||
| e8a2e88fa0 | |||
| 7b6a3d25e5 | |||
| 82eba4d668 | |||
| 9812dd40c4 | |||
| c29816c310 | |||
| 8938e02d8e | |||
| fce7f1bea3 | |||
| fbd249d387 | |||
| 43200007cf | |||
| 4d406fd989 | |||
| 68ab0cff25 | |||
| 407788ee64 | |||
| 41b2ad2b71 | |||
| 1cc1c26585 | |||
| 18e51dff21 | |||
| 02e45fed53 | |||
| fa05f94dfa | |||
| c1a7276867 | |||
| 887456ead8 | |||
| 134020d51b | |||
| 19c56c6ec4 | |||
| acd84e53a8 | |||
| 427e1edcfd | |||
| db3b356bb0 | |||
| 4449b51e2c | |||
| 84f017c98e | |||
| b37f688755 | |||
| 5585c684e9 | |||
| 075e3171a8 | |||
| 53604c9413 | |||
| 8ae39e24be | |||
| 88c75a06f2 | |||
| 4f2d5b15bf | |||
| f9e310f338 | |||
| 7f292bc995 | |||
| 88a07de5fb | |||
| 076b1e39e6 | |||
| 99bba4af27 | |||
| 3cacd57949 | |||
| fc60f6e55e | |||
| 9bddd9a846 | |||
| 6cfdd4fb90 | |||
| a31b394282 | |||
| 3866ae3449 | |||
| 2a0558de80 | |||
| 10b8ee4f54 | |||
| 548338d39e | |||
| c88b5c67d5 | |||
| 1935aab2d5 | |||
| bd5eb8fcae | |||
| 0795b046e1 | |||
| b2e4623559 | |||
| 0b7fac0c9d | |||
| 3fd0106816 | |||
| b4dff4ec14 | |||
| 9640a5f496 | |||
| f108f20466 | |||
| 905327db4e | |||
| 4d311ffb43 | |||
| 012e073fb9 | |||
| 6937823b69 | |||
| 1415c8ce7b | |||
| fd676ef047 | |||
| 9fbd8318f0 | |||
| 15c147cf93 | |||
| 2cc0fa906c | |||
| 42c6dc7e94 | |||
| 5db9ad2585 | |||
| 36d40afb4e | |||
| 01b1c8c0dd | |||
| 180fbd5995 | |||
| 42924c971f | |||
| e41ff78770 | |||
| 6508549584 | |||
| fa67db75e0 | |||
| 989be56f4b | |||
| 05093529bb | |||
| c97fb242a9 | |||
| fb613ef5c8 | |||
| 466b1982f2 | |||
| f6e64ccb93 | |||
| 162cd3b9c5 | |||
| 5fd60b7000 | |||
| a087f5e583 | |||
| 8e4ab703c8 | |||
| 5eb8086b42 | |||
| ef8d49c768 | |||
| 2a078fee77 | |||
| 3df5e533b7 | |||
| 03f07b7626 | |||
| 6500b7c210 | |||
| 50f3791b98 | |||
| 68ac266463 | |||
| 57bb43bb6f | |||
| 3b5d39a21b | |||
| 4a8f3339fb | |||
| 64718667ff | |||
| 4f93437797 | |||
| 1cd2ff10c7 | |||
| 6bd6c830fe | |||
| 1ce57f2203 | |||
| c798df215e |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: make AWS Bedrock authentication predictable
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add translation to CODE_OF_CONDUCT, CONTRIBUTING and README to Arabic ar-sa.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
A new MCP Marketplace display setting has been added to VSCode settings. (It is defaulted to "true")
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -70,8 +70,10 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Git Tag
|
||||
id: create_tag
|
||||
run: |
|
||||
VERSION=v${{ steps.get_version.outputs.version }}
|
||||
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Tagging with $VERSION"
|
||||
git tag "$VERSION"
|
||||
git push origin "$VERSION"
|
||||
@@ -104,6 +106,7 @@ jobs:
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.create_tag.outputs.tag }}
|
||||
files: "*.vsix"
|
||||
# body: ${{ steps.changelog.outputs.content }}
|
||||
generate_release_notes: true
|
||||
|
||||
+184
-130
@@ -1,344 +1,398 @@
|
||||
# Changelog
|
||||
|
||||
## [3.4.0]
|
||||
|
||||
- Introducing MCP Marketplace! You can now discover and install the best MCP servers right from within the extension, with new servers added regularly
|
||||
- Add mermaid diagram support in Plan mode! You can now see visual representations of mermaid code blocks in chat, and click on them to see an expanded view
|
||||
- Use more visual checkpoints indicators after editing files & running commands
|
||||
- Create a checkpoint at the beginning of each task to easily revert to the initial state
|
||||
- Add 'Terminal' context mention to reference the active terminal's contents
|
||||
- Add 'Git Commits' context mention to reference current working changes or specific commits (thanks @mrubens!)
|
||||
- Send current textfield contents as additional feedback when toggling from Plan to Act Mode, or when hitting 'Approve' button
|
||||
- Add advanced configuration options for OpenAI Compatible (context window, max output, pricing, etc.)
|
||||
- Add Alibaba Qwen 2.5 coder models, VL models, and DeepSeek-R1/V3 support
|
||||
- Improve support for AWS Bedrock Profiles
|
||||
- Fix Mistral provider support for non-codestral models
|
||||
- Add advanced setting to disable browser tool
|
||||
- Add advanced setting to set chromium executable path for browser tool
|
||||
|
||||
## [3.3.2]
|
||||
|
||||
- Fix bug where OpenRouter requests would periodically not return cost/token stats, leading to context window limit errors
|
||||
- Make checkpoints more visible and keep track of restored checkpoints
|
||||
|
||||
## [3.3.0]
|
||||
|
||||
- Add .clineignore to block Cline from accessing specified file patterns
|
||||
- Add keyboard shortcut + tooltips for Plan/Act toggle
|
||||
- Fix bug where new files won't show up in files dropdown
|
||||
- Add automatic retry for rate limited requests (thanks @ViezeVingertjes!)
|
||||
- Adding reasoning_effort support for o3-mini in Advanced Settings
|
||||
- Added support for AWS provider profiles using the AWS CLI to make the profile, enabling long lived connections to AWS bedrock
|
||||
- Adding Requesty API provider
|
||||
- Add Together API provider
|
||||
- Add Alibaba Qwen API provider (thanks @aicccode!)
|
||||
|
||||
## [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 (thanks @Szpadel!)
|
||||
- Show Reasoning tokens for models that support it
|
||||
- Fix issues with switching models between Plan/Act modes
|
||||
|
||||
## [3.2.6]
|
||||
|
||||
- Save last used API/model when switching between Plan and Act, for users that like to use different models for each mode
|
||||
- New Context Window progress bar in the task header to understand increased cost/generation degradation as the context increases
|
||||
- Localize READMEs and add language selector for English, Spanish, German, Chinese, and Japanese
|
||||
- Add Advanced Settings to remove MCP prompts from requests to save tokens, enable/disable checkpoints for users that don't use git (more coming soon!)
|
||||
- Add Gemini 2.0 Flash Thinking experimental model
|
||||
- Allow new users to subscribe to mailing list to get notified when new Accounts option is available
|
||||
- Save last used API/model when switching between Plan and Act, for users that like to use different models for each mode
|
||||
- New Context Window progress bar in the task header to understand increased cost/generation degradation as the context increases
|
||||
- Localize READMEs and add language selector for English, Spanish, German, Chinese, and Japanese
|
||||
- Add Advanced Settings to remove MCP prompts from requests to save tokens, enable/disable checkpoints for users that don't use git (more coming soon!)
|
||||
- Add Gemini 2.0 Flash Thinking experimental model
|
||||
- Allow new users to subscribe to mailing list to get notified when new Accounts option is available
|
||||
|
||||
## [3.2.5]
|
||||
|
||||
- Use yellow textfield outline in Plan mode to better distinguish from Act mode
|
||||
- Use yellow textfield outline in Plan mode to better distinguish from Act mode
|
||||
|
||||
## [3.2.3]
|
||||
|
||||
- Add DeepSeek-R1 (deepseek-reasoner) model support with proper parameter handling (thanks @slavakurilyak!)
|
||||
- Add DeepSeek-R1 (deepseek-reasoner) model support with proper parameter handling (thanks @slavakurilyak!)
|
||||
|
||||
## [3.2.0]
|
||||
|
||||
- Add Plan/Act mode toggle to let you plan tasks with Cline before letting him get to work
|
||||
- Easily switch between API providers and models using a new popup menu under the chat field
|
||||
- Add VS Code LM API provider to run models provided by other VS Code extensions (e.g. GitHub Copilot). Shoutout to @julesmons, @RaySinner, and @MrUbens for putting this together!
|
||||
- Add on/off toggle for MCP servers to disable them when not in use. Thanks @MrUbens!
|
||||
- Add Auto-approve option for individual tools in MCP servers. Thanks @MrUbens!
|
||||
- Add Plan/Act mode toggle to let you plan tasks with Cline before letting him get to work
|
||||
- Easily switch between API providers and models using a new popup menu under the chat field
|
||||
- Add VS Code LM API provider to run models provided by other VS Code extensions (e.g. GitHub Copilot). Shoutout to @julesmons, @RaySinner, and @MrUbens for putting this together!
|
||||
- Add on/off toggle for MCP servers to disable them when not in use. Thanks @MrUbens!
|
||||
- Add Auto-approve option for individual tools in MCP servers. Thanks @MrUbens!
|
||||
|
||||
## [3.1.10]
|
||||
|
||||
- New icon!
|
||||
- New icon!
|
||||
|
||||
## [3.1.9]
|
||||
|
||||
- Add Mistral API provider with codestral-latest model
|
||||
- Add Mistral API provider with codestral-latest model
|
||||
|
||||
## [3.1.7]
|
||||
|
||||
- Add ability to change viewport size and headless mode when Cline asks to launch the browser
|
||||
- Add ability to change viewport size and headless mode when Cline asks to launch the browser
|
||||
|
||||
## [3.1.6]
|
||||
|
||||
- Fix bug where filepaths with Chinese characters would not show up in context mention menu (thanks @chi-chat!)
|
||||
- Update Anthropic model prices (thanks @timoteostewart!)
|
||||
- Fix bug where filepaths with Chinese characters would not show up in context mention menu (thanks @chi-chat!)
|
||||
- Update Anthropic model prices (thanks @timoteostewart!)
|
||||
|
||||
## [3.1.5]
|
||||
|
||||
- Fix bug where Cline couldn't read "@/" import path aliases from tool results
|
||||
- Fix bug where Cline couldn't read "@/" import path aliases from tool results
|
||||
|
||||
## [3.1.4]
|
||||
|
||||
- Fix issue where checkpoints would not work for users with git commit signing enabled globally
|
||||
- Fix issue where checkpoints would not work for users with git commit signing enabled globally
|
||||
|
||||
## [3.1.2]
|
||||
|
||||
- Fix issue where LFS files would be not be ignored when creating checkpoints
|
||||
- Fix issue where LFS files would be not be ignored when creating checkpoints
|
||||
|
||||
## [3.1.0]
|
||||
|
||||
- Added checkpoints: Snapshots of workspace are automatically created whenever Cline uses a tool
|
||||
- Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
|
||||
- Restore options: Choose to restore just the task state, just the workspace files, or both
|
||||
- New 'See new changes' button appears after task completion, providing an overview of all workspace changes
|
||||
- Task header now shows disk space usage with a delete button to help manage snapshot storage
|
||||
- Added checkpoints: Snapshots of workspace are automatically created whenever Cline uses a tool
|
||||
- Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
|
||||
- Restore options: Choose to restore just the task state, just the workspace files, or both
|
||||
- New 'See new changes' button appears after task completion, providing an overview of all workspace changes
|
||||
- Task header now shows disk space usage with a delete button to help manage snapshot storage
|
||||
|
||||
## [3.0.12]
|
||||
|
||||
- Fix DeepSeek API cost reporting (input price is 0 since it's all either a cache read or write, different than how Anthropic reports cache usage)
|
||||
- Fix DeepSeek API cost reporting (input price is 0 since it's all either a cache read or write, different than how Anthropic reports cache usage)
|
||||
|
||||
## [3.0.11]
|
||||
|
||||
- Emphasize auto-formatting done by the editor in file edit responses for more reliable diff editing
|
||||
- Emphasize auto-formatting done by the editor in file edit responses for more reliable diff editing
|
||||
|
||||
## [3.0.10]
|
||||
|
||||
- Add DeepSeek provider to API Provider options
|
||||
- Fix context window limit errors for DeepSeek v3
|
||||
- Add DeepSeek provider to API Provider options
|
||||
- Fix context window limit errors for DeepSeek v3
|
||||
|
||||
## [3.0.9]
|
||||
|
||||
- Fix bug where DeepSeek v3 would incorrectly escape HTML entities in diff edits
|
||||
- Fix bug where DeepSeek v3 would incorrectly escape HTML entities in diff edits
|
||||
|
||||
## [3.0.8]
|
||||
|
||||
- Mitigate DeepSeek v3 diff edit errors by adding 'auto-formatting considerations' to system prompt, encouraging model to use updated file contents as reference point for SEARCH blocks
|
||||
- Mitigate DeepSeek v3 diff edit errors by adding 'auto-formatting considerations' to system prompt, encouraging model to use updated file contents as reference point for SEARCH blocks
|
||||
|
||||
## [3.0.7]
|
||||
|
||||
- Revert to using batched file watcher to fix crash when many files would be created at once
|
||||
- Revert to using batched file watcher to fix crash when many files would be created at once
|
||||
|
||||
## [3.0.6]
|
||||
|
||||
- Fix bug where some files would be missing in the `@` context mention menu
|
||||
- Add Bedrock support in additional regions
|
||||
- Diff edit improvements
|
||||
- Add OpenRouter's middle-out transform for models that don't use prompt caching (prevents context window limit errors, but cannot be applied to models like Claude since it would continuously break the cache)
|
||||
- Fix bug where some files would be missing in the `@` context mention menu
|
||||
- Add Bedrock support in additional regions
|
||||
- Diff edit improvements
|
||||
- Add OpenRouter's middle-out transform for models that don't use prompt caching (prevents context window limit errors, but cannot be applied to models like Claude since it would continuously break the cache)
|
||||
|
||||
## [3.0.4]
|
||||
|
||||
- Fix bug where gemini models would add code block artifacts to the end of text content
|
||||
- Fix context mention menu visual issues on light themes
|
||||
- Fix bug where gemini models would add code block artifacts to the end of text content
|
||||
- Fix context mention menu visual issues on light themes
|
||||
|
||||
## [3.0.2]
|
||||
|
||||
- Adds block anchor matching for more reliable diff edits (if 3+ lines, first and last line are used as anchors to search for)
|
||||
- Add instruction to system prompt to use complete lines in diff edits to work properly with fallback strategies
|
||||
- Improves diff edit error handling
|
||||
- Adds new Gemini models
|
||||
- Adds block anchor matching for more reliable diff edits (if 3+ lines, first and last line are used as anchors to search for)
|
||||
- Add instruction to system prompt to use complete lines in diff edits to work properly with fallback strategies
|
||||
- Improves diff edit error handling
|
||||
- Adds new Gemini models
|
||||
|
||||
## [3.0.0]
|
||||
|
||||
- Cline now uses a search & replace diff based approach when editing large files to prevent code deletion issues.
|
||||
- Adds support for a more comprehensive auto-approve configuration, allowing you to specify which tools require approval and which don't.
|
||||
- Adds ability to enable system notifications for when Cline needs approval or completes a task.
|
||||
- Adds support for a root-level `.clinerules` file that can be used to specify custom instructions for the project.
|
||||
- Cline now uses a search & replace diff based approach when editing large files to prevent code deletion issues.
|
||||
- Adds support for a more comprehensive auto-approve configuration, allowing you to specify which tools require approval and which don't.
|
||||
- Adds ability to enable system notifications for when Cline needs approval or completes a task.
|
||||
- Adds support for a root-level `.clinerules` file that can be used to specify custom instructions for the project.
|
||||
|
||||
## [2.2.0]
|
||||
|
||||
- Add support for Model Context Protocol (MCP), enabling Cline to use custom tools like web-search tool or GitHub tool
|
||||
- Add MCP server management tab accessible via the server icon in the menu bar
|
||||
- Add ability for Cline to dynamically create new MCP servers based on user requests (e.g., "add a tool that gets the latest npm docs")
|
||||
- Add support for Model Context Protocol (MCP), enabling Cline to use custom tools like web-search tool or GitHub tool
|
||||
- Add MCP server management tab accessible via the server icon in the menu bar
|
||||
- Add ability for Cline to dynamically create new MCP servers based on user requests (e.g., "add a tool that gets the latest npm docs")
|
||||
|
||||
## [2.1.6]
|
||||
|
||||
- Add LM Studio as an API provider option (make sure to start the LM Studio server to use it with the extension!)
|
||||
- Add LM Studio as an API provider option (make sure to start the LM Studio server to use it with the extension!)
|
||||
|
||||
## [2.1.5]
|
||||
|
||||
- Add support for prompt caching for new Claude model IDs on OpenRouter (e.g. `anthropic/claude-3.5-sonnet-20240620`)
|
||||
- Add support for prompt caching for new Claude model IDs on OpenRouter (e.g. `anthropic/claude-3.5-sonnet-20240620`)
|
||||
|
||||
## [2.1.4]
|
||||
|
||||
- AWS Bedrock fixes (add missing regions, support for cross-region inference, and older Sonnet model for regions where new model is not available)
|
||||
- AWS Bedrock fixes (add missing regions, support for cross-region inference, and older Sonnet model for regions where new model is not available)
|
||||
|
||||
## [2.1.3]
|
||||
|
||||
- Add support for Claude 3.5 Haiku, 66% cheaper than Sonnet with similar intelligence
|
||||
- Add support for Claude 3.5 Haiku, 66% cheaper than Sonnet with similar intelligence
|
||||
|
||||
## [2.1.2]
|
||||
|
||||
- Misc. bug fixes
|
||||
- Update README with new browser feature
|
||||
- Misc. bug fixes
|
||||
- Update README with new browser feature
|
||||
|
||||
## [2.1.1]
|
||||
|
||||
- Add stricter prompt to prevent Cline from editing files during a browser session without first closing the browser
|
||||
- Add stricter prompt to prevent Cline from editing files during a browser session without first closing the browser
|
||||
|
||||
## [2.1.0]
|
||||
|
||||
- Cline now uses Anthropic's new "Computer Use" feature to launch a browser, click, type, and scroll. This gives him more autonomy in runtime debugging, end-to-end testing, and even general web use. Try asking "Look up the weather in Colorado" to see it in action! (Available with Claude 3.5 Sonnet v2)
|
||||
- Cline now uses Anthropic's new "Computer Use" feature to launch a browser, click, type, and scroll. This gives him more autonomy in runtime debugging, end-to-end testing, and even general web use. Try asking "Look up the weather in Colorado" to see it in action! (Available with Claude 3.5 Sonnet v2)
|
||||
|
||||
## [2.0.19]
|
||||
|
||||
- Fix model info for Claude 3.5 Sonnet v1 on OpenRouter
|
||||
- Fix model info for Claude 3.5 Sonnet v1 on OpenRouter
|
||||
|
||||
## [2.0.18]
|
||||
|
||||
- Add support for both v1 and v2 of Claude 3.5 Sonnet for GCP Vertex and AWS Bedrock (for cases where the new model is not enabled yet or unavailable in your region)
|
||||
- Add support for both v1 and v2 of Claude 3.5 Sonnet for GCP Vertex and AWS Bedrock (for cases where the new model is not enabled yet or unavailable in your region)
|
||||
|
||||
## [2.0.17]
|
||||
|
||||
- Update Anthropic model IDs
|
||||
- Update Anthropic model IDs
|
||||
|
||||
## [2.0.16]
|
||||
|
||||
- Adjustments to system prompt
|
||||
- Adjustments to system prompt
|
||||
|
||||
## [2.0.15]
|
||||
|
||||
- Fix bug where modifying Cline's edits would lead him to try to re-apply the edits
|
||||
- Fix bug where weaker models would display file contents before using the write_to_file tool
|
||||
- Fix o1-mini and o1-preview errors when using OpenAI native
|
||||
- Fix bug where modifying Cline's edits would lead him to try to re-apply the edits
|
||||
- Fix bug where weaker models would display file contents before using the write_to_file tool
|
||||
- Fix o1-mini and o1-preview errors when using OpenAI native
|
||||
|
||||
## [2.0.14]
|
||||
|
||||
- Gracefully cancel requests while stream could be hanging
|
||||
- Gracefully cancel requests while stream could be hanging
|
||||
|
||||
## [2.0.13]
|
||||
|
||||
- Detect code omission and show warning with troubleshooting link
|
||||
- Detect code omission and show warning with troubleshooting link
|
||||
|
||||
## [2.0.12]
|
||||
|
||||
- Keep cursor out of the way during file edit streaming animation
|
||||
- Keep cursor out of the way during file edit streaming animation
|
||||
|
||||
## [2.0.11]
|
||||
|
||||
- Adjust prompts around read_file to prevent re-reading files unnecessarily
|
||||
- Adjust prompts around read_file to prevent re-reading files unnecessarily
|
||||
|
||||
## [2.0.10]
|
||||
|
||||
- More adjustments to system prompt to prevent lazy coding
|
||||
- More adjustments to system prompt to prevent lazy coding
|
||||
|
||||
## [2.0.9]
|
||||
|
||||
- Update system prompt to try to prevent Cline from lazy coding (`// rest of code here...`)
|
||||
- Update system prompt to try to prevent Cline from lazy coding (`// rest of code here...`)
|
||||
|
||||
## [2.0.8]
|
||||
|
||||
- Fix o1-mini and o1-preview for OpenAI
|
||||
- Fix diff editor not opening sometimes in slow environments like project idx
|
||||
- Fix o1-mini and o1-preview for OpenAI
|
||||
- Fix diff editor not opening sometimes in slow environments like project idx
|
||||
|
||||
## [2.0.7]
|
||||
|
||||
- Misc. bug fixes
|
||||
- Misc. bug fixes
|
||||
|
||||
## [2.0.6]
|
||||
|
||||
- Update URLs to https://github.com/cline/cline
|
||||
- Update URLs to https://github.com/cline/cline
|
||||
|
||||
## [2.0.5]
|
||||
|
||||
- Fixed bug where Cline's edits would stream into the active tab when switching tabs during a write_to_file
|
||||
- Added explanation in task continuation prompt that an interrupted write_to_file reverts the file to its original contents, preventing unnecessary re-reads
|
||||
- Fixed non-first chunk error handling in case stream fails mid-way through
|
||||
- Fixed bug where Cline's edits would stream into the active tab when switching tabs during a write_to_file
|
||||
- Added explanation in task continuation prompt that an interrupted write_to_file reverts the file to its original contents, preventing unnecessary re-reads
|
||||
- Fixed non-first chunk error handling in case stream fails mid-way through
|
||||
|
||||
## [2.0.0]
|
||||
|
||||
- New name! Meet Cline, an AI assistant that can use your CLI and Editor
|
||||
- Responses are now streamed with a yellow text decoration animation to keep track of Cline's progress as he edits files
|
||||
- New Cancel button to give Cline feedback if he goes off in the wrong direction, giving you more control over tasks
|
||||
- Re-imagined tool calling prompt resulting in ~40% fewer requests to accomplish tasks + better performance with other models
|
||||
- Search and use any model with OpenRouter
|
||||
- New name! Meet Cline, an AI assistant that can use your CLI and Editor
|
||||
- Responses are now streamed with a yellow text decoration animation to keep track of Cline's progress as he edits files
|
||||
- New Cancel button to give Cline feedback if he goes off in the wrong direction, giving you more control over tasks
|
||||
- Re-imagined tool calling prompt resulting in ~40% fewer requests to accomplish tasks + better performance with other models
|
||||
- Search and use any model with OpenRouter
|
||||
|
||||
## [1.9.7]
|
||||
|
||||
- Only auto-include error diagnostics after file edits, removed warnings to keep Claude from getting distracted in projects with strict linting rules
|
||||
- Only auto-include error diagnostics after file edits, removed warnings to keep Claude from getting distracted in projects with strict linting rules
|
||||
|
||||
## [1.9.6]
|
||||
|
||||
- Added support for new Google Gemini models `gemini-1.5-flash-002` and `gemini-1.5-pro-002`
|
||||
- Updated system prompt to be more lenient when terminal output doesn't stream back properly
|
||||
- Adjusted system prompt to prevent overuse of the inspect_site tool
|
||||
- Increased global line height for improved readability
|
||||
- Added support for new Google Gemini models `gemini-1.5-flash-002` and `gemini-1.5-pro-002`
|
||||
- Updated system prompt to be more lenient when terminal output doesn't stream back properly
|
||||
- Adjusted system prompt to prevent overuse of the inspect_site tool
|
||||
- Increased global line height for improved readability
|
||||
|
||||
## [1.9.0]
|
||||
|
||||
- Claude can now use a browser! This update adds a new `inspect_site` tool that captures screenshots and console logs from websites (including localhost), making it easier for Claude to troubleshoot issues on his own.
|
||||
- Improved automatic linter/compiler debugging by only sending Claude new errors that result from his edits, rather than reporting all workspace problems.
|
||||
- Claude can now use a browser! This update adds a new `inspect_site` tool that captures screenshots and console logs from websites (including localhost), making it easier for Claude to troubleshoot issues on his own.
|
||||
- Improved automatic linter/compiler debugging by only sending Claude new errors that result from his edits, rather than reporting all workspace problems.
|
||||
|
||||
## [1.8.0]
|
||||
|
||||
- You can now use '@' in the textarea to add context!
|
||||
- @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs!
|
||||
- @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging
|
||||
- @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
- @folder: Adds folder's files all at once to speed up your workflow even more
|
||||
- You can now use '@' in the textarea to add context!
|
||||
- @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs!
|
||||
- @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging
|
||||
- @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
- @folder: Adds folder's files all at once to speed up your workflow even more
|
||||
|
||||
## [1.7.0]
|
||||
|
||||
- Adds problems monitoring to keep Claude updated on linter/compiler/build issues, letting him proactively fix errors on his own! (adding missing imports, fixing type errors, etc.)
|
||||
- Adds problems monitoring to keep Claude updated on linter/compiler/build issues, letting him proactively fix errors on his own! (adding missing imports, fixing type errors, etc.)
|
||||
|
||||
## [1.6.5]
|
||||
|
||||
- Adds support for OpenAI o1, Azure OpenAI, and Google Gemini (free for up to 15 requests per minute!)
|
||||
- Task header can now be collapsed to provide more space for viewing conversations
|
||||
- Adds fuzzy search and sorting to Task History, making it easier to find specific tasks
|
||||
- Adds support for OpenAI o1, Azure OpenAI, and Google Gemini (free for up to 15 requests per minute!)
|
||||
- Task header can now be collapsed to provide more space for viewing conversations
|
||||
- Adds fuzzy search and sorting to Task History, making it easier to find specific tasks
|
||||
|
||||
## [1.6.0]
|
||||
|
||||
- Commands now run directly in your terminal thanks to VSCode 1.93's new shell integration updates! Plus a new 'Proceed While Running' button to let Claude continue working while commands run, sending him new output along the way (i.e. letting him react to server errors as he edits files)
|
||||
- Commands now run directly in your terminal thanks to VSCode 1.93's new shell integration updates! Plus a new 'Proceed While Running' button to let Claude continue working while commands run, sending him new output along the way (i.e. letting him react to server errors as he edits files)
|
||||
|
||||
## [1.5.27]
|
||||
|
||||
- Claude's changes now appear in your file's Timeline, allowing you to easily view a diff of each edit. This is especially helpful if you want to revert to a previous version. No need for git—everything is tracked by VSCode's local history!
|
||||
- Updated system prompt to keep Claude from re-reading files unnecessarily
|
||||
- Claude's changes now appear in your file's Timeline, allowing you to easily view a diff of each edit. This is especially helpful if you want to revert to a previous version. No need for git—everything is tracked by VSCode's local history!
|
||||
- Updated system prompt to keep Claude from re-reading files unnecessarily
|
||||
|
||||
## [1.5.19]
|
||||
|
||||
- Adds support for OpenAI compatible API providers (e.g. Ollama!)
|
||||
- Adds support for OpenAI compatible API providers (e.g. Ollama!)
|
||||
|
||||
## [1.5.13]
|
||||
|
||||
- New terminal emulator! When Claude runs commands, you can now type directly in the terminal (+ support for Python environments)
|
||||
- Adds search to Task History
|
||||
- New terminal emulator! When Claude runs commands, you can now type directly in the terminal (+ support for Python environments)
|
||||
- Adds search to Task History
|
||||
|
||||
## [1.5.6]
|
||||
|
||||
- You can now edit Claude's changes before accepting! When he edits or creates a file, you can modify his changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in the center to undo `// rest of code here` shenanigans)
|
||||
- You can now edit Claude's changes before accepting! When he edits or creates a file, you can modify his changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in the center to undo `// rest of code here` shenanigans)
|
||||
|
||||
## [1.5.4]
|
||||
|
||||
- Adds support for reading .pdf and .docx files (try "turn my business_plan.docx into a company website")
|
||||
- Adds support for reading .pdf and .docx files (try "turn my business_plan.docx into a company website")
|
||||
|
||||
## [1.5.0]
|
||||
|
||||
- Adds new `search_files` tool that lets Claude perform regex searches in your project, making it easy for him to refactor code, address TODOs and FIXMEs, remove dead code, and more!
|
||||
- Adds new `search_files` tool that lets Claude perform regex searches in your project, making it easy for him to refactor code, address TODOs and FIXMEs, remove dead code, and more!
|
||||
|
||||
## [1.4.0]
|
||||
|
||||
- Adds "Always allow read-only operations" setting to let Claude read files and view directories without needing approval (off by default)
|
||||
- Implement sliding window context management to keep tasks going past 200k tokens
|
||||
- Adds Google Cloud Vertex AI support and updates Claude 3.5 Sonnet max output to 8192 tokens for all providers.
|
||||
- Improves system prompt to gaurd against lazy edits (less "//rest of code here")
|
||||
- Adds "Always allow read-only operations" setting to let Claude read files and view directories without needing approval (off by default)
|
||||
- Implement sliding window context management to keep tasks going past 200k tokens
|
||||
- Adds Google Cloud Vertex AI support and updates Claude 3.5 Sonnet max output to 8192 tokens for all providers.
|
||||
- Improves system prompt to gaurd against lazy edits (less "//rest of code here")
|
||||
|
||||
## [1.3.0]
|
||||
|
||||
- Adds task history
|
||||
- Adds task history
|
||||
|
||||
## [1.2.0]
|
||||
|
||||
- Adds support for Prompt Caching to significantly reduce costs and response times (currently only available through Anthropic API for Claude 3.5 Sonnet and Claude 3.0 Haiku)
|
||||
- Adds support for Prompt Caching to significantly reduce costs and response times (currently only available through Anthropic API for Claude 3.5 Sonnet and Claude 3.0 Haiku)
|
||||
|
||||
## [1.1.1]
|
||||
|
||||
- Adds option to choose other Claude models (+ GPT-4o, DeepSeek, and Mistral if you use OpenRouter)
|
||||
- Adds option to add custom instructions to the end of the system prompt
|
||||
- Adds option to choose other Claude models (+ GPT-4o, DeepSeek, and Mistral if you use OpenRouter)
|
||||
- Adds option to add custom instructions to the end of the system prompt
|
||||
|
||||
## [1.1.0]
|
||||
|
||||
- Paste images in chat to use Claude's vision capabilities and turn mockups into fully functional applications or fix bugs with screenshots
|
||||
- Paste images in chat to use Claude's vision capabilities and turn mockups into fully functional applications or fix bugs with screenshots
|
||||
|
||||
## [1.0.9]
|
||||
|
||||
- Add support for OpenRouter and AWS Bedrock
|
||||
- Add support for OpenRouter and AWS Bedrock
|
||||
|
||||
## [1.0.8]
|
||||
|
||||
- Shows diff view of new or edited files right in the editor
|
||||
- Shows diff view of new or edited files right in the editor
|
||||
|
||||
## [1.0.7]
|
||||
|
||||
- Replace `list_files` and `analyze_project` with more explicit `list_files_top_level`, `list_files_recursive`, and `view_source_code_definitions_top_level` to get source code definitions only for files relevant to the task
|
||||
- Replace `list_files` and `analyze_project` with more explicit `list_files_top_level`, `list_files_recursive`, and `view_source_code_definitions_top_level` to get source code definitions only for files relevant to the task
|
||||
|
||||
## [1.0.6]
|
||||
|
||||
- Interact with CLI commands by sending messages to stdin and terminating long-running processes like servers
|
||||
- Export tasks to markdown files (useful as context for future tasks)
|
||||
- Interact with CLI commands by sending messages to stdin and terminating long-running processes like servers
|
||||
- Export tasks to markdown files (useful as context for future tasks)
|
||||
|
||||
## [1.0.5]
|
||||
|
||||
- Claude now has context about vscode's visible editors and opened tabs
|
||||
- Claude now has context about vscode's visible editors and opened tabs
|
||||
|
||||
## [1.0.4]
|
||||
|
||||
- Open in the editor (using menu bar or `Claude Dev: Open In New Tab` in command palette) to see how Claude updates your workspace more clearly
|
||||
- New `analyze_project` tool to help Claude get a comprehensive overview of your project's source code definitions and file structure
|
||||
- Provide feedback to tool use like terminal commands and file edits
|
||||
- Updated max output tokens to 8192 so less lazy coding (`// rest of code here...`)
|
||||
- Added ability to retry failed API requests (helpful for rate limits)
|
||||
- Quality of life improvements like markdown rendering, memory optimizations, better theme support
|
||||
- Open in the editor (using menu bar or `Claude Dev: Open In New Tab` in command palette) to see how Claude updates your workspace more clearly
|
||||
- New `analyze_project` tool to help Claude get a comprehensive overview of your project's source code definitions and file structure
|
||||
- Provide feedback to tool use like terminal commands and file edits
|
||||
- Updated max output tokens to 8192 so less lazy coding (`// rest of code here...`)
|
||||
- Added ability to retry failed API requests (helpful for rate limits)
|
||||
- Quality of life improvements like markdown rendering, memory optimizations, better theme support
|
||||
|
||||
## [0.0.6]
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
|
||||
+13
-3
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ Welcome to the Cline documentation - your comprehensive guide to using and exten
|
||||
|
||||
- **Interested in contributing?** We welcome your input:
|
||||
- Feel free to submit a pull request
|
||||
- [Contribution Guidelines](CONTRIBUTING.md)
|
||||
- [Contribution Guidelines](../CONTRIBUTING.md)
|
||||
|
||||
## Additional Resources
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
### .clineignore Support
|
||||
|
||||
To give you more control over which files are accessible to Cline, we've implemented `.clineignore` functionality, similar to `.gitignore`. This allows you to specify files and directories that Cline should **not** access or process. This is useful for:
|
||||
|
||||
* **Privacy:** Preventing Cline from accessing sensitive or private files in your workspace.
|
||||
* **Performance:** Excluding large directories or files that are irrelevant to your tasks, potentially improving the efficiency of Cline.
|
||||
* **Context Management:** Focusing Cline's attention on the relevant parts of your project.
|
||||
|
||||
**How to use `.clineignore`**
|
||||
|
||||
1. **Create a `.clineignore` file:** In the root directory of your workspace (the same level as your `.vscode` folder, or the top level folder you opened in VS Code), create a new file named `.clineignore`.
|
||||
|
||||
2. **Define ignore patterns:** Open the `.clineignore` file and specify the patterns for files and directories you want Cline to ignore. The syntax is the same as `.gitignore`:
|
||||
|
||||
* Each line in the file represents a pattern.
|
||||
* **Standard glob patterns are supported:**
|
||||
* `*` matches zero or more characters
|
||||
* `?` matches one character
|
||||
* `[]` matches a character range
|
||||
* `**` matches any number of directories and subdirectories.
|
||||
|
||||
* **Directory patterns:** Append `/` to the end of a pattern to specify a directory.
|
||||
* **Negation patterns:** Start a pattern with `!` to negate (un-ignore) a previously ignored pattern.
|
||||
* **Comments:** Start a line with `#` to add comments.
|
||||
|
||||
**Example `.clineignore` file:**
|
||||
|
||||
```
|
||||
# Ignore log files
|
||||
*.log
|
||||
|
||||
# Ignore the entire 'node_modules' directory
|
||||
node_modules/
|
||||
|
||||
# Ignore all files in the 'temp' directory and its subdirectories
|
||||
temp/**
|
||||
|
||||
# But DO NOT ignore 'important.log' even if it's in the root
|
||||
!important.log
|
||||
|
||||
# Ignore any file named 'secret.txt' in any subdirectory
|
||||
**/secret.txt
|
||||
```
|
||||
|
||||
3. **Cline respects your `.clineignore`:** Once you save the `.clineignore` file, Cline will automatically recognize and apply these rules.
|
||||
|
||||
* **File Access Control:** Cline will not be able to read the content of ignored files using tools like `read_file`. If you attempt to use a tool on an ignored file, Cline will inform you that access is blocked due to `.clineignore` settings.
|
||||
* **File Listing:** When you ask Cline to list files in a directory (e.g., using `list_files`), ignored files and directories will still be listed, but they will be marked with a **🔒** symbol next to their name to indicate that they are ignored. This helps you understand which files Cline can and cannot interact with.
|
||||
|
||||
4. **Dynamic Updates:** Cline monitors your `.clineignore` file for changes. If you modify, create, or delete your `.clineignore` file, Cline will automatically update its ignore rules without needing to restart VS Code or the extension.
|
||||
|
||||
**In Summary**
|
||||
|
||||
The `.clineignore` file provides a powerful and flexible way to control Cline's access to your workspace files, enhancing privacy, performance, and context management. By leveraging familiar `.gitignore` syntax, you can easily tailor Cline's focus to the most relevant parts of your projects.
|
||||
@@ -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.
|
||||
|
||||
@@ -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:_
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# Cline's Memory Bank
|
||||
|
||||
I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
|
||||
|
||||
## Memory Bank Structure
|
||||
|
||||
The Memory Bank consists of required core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
PB[projectbrief.md] --> PC[productContext.md]
|
||||
PB --> SP[systemPatterns.md]
|
||||
PB --> TC[techContext.md]
|
||||
|
||||
PC --> AC[activeContext.md]
|
||||
SP --> AC
|
||||
TC --> AC
|
||||
|
||||
AC --> P[progress.md]
|
||||
```
|
||||
|
||||
### Core Files (Required)
|
||||
1. `projectbrief.md`
|
||||
- Foundation document that shapes all other files
|
||||
- Created at project start if it doesn't exist
|
||||
- Defines core requirements and goals
|
||||
- Source of truth for project scope
|
||||
|
||||
2. `productContext.md`
|
||||
- Why this project exists
|
||||
- Problems it solves
|
||||
- How it should work
|
||||
- User experience goals
|
||||
|
||||
3. `activeContext.md`
|
||||
- Current work focus
|
||||
- Recent changes
|
||||
- Next steps
|
||||
- Active decisions and considerations
|
||||
|
||||
4. `systemPatterns.md`
|
||||
- System architecture
|
||||
- Key technical decisions
|
||||
- Design patterns in use
|
||||
- Component relationships
|
||||
|
||||
5. `techContext.md`
|
||||
- Technologies used
|
||||
- Development setup
|
||||
- Technical constraints
|
||||
- Dependencies
|
||||
|
||||
6. `progress.md`
|
||||
- What works
|
||||
- What's left to build
|
||||
- Current status
|
||||
- Known issues
|
||||
|
||||
### Additional Context
|
||||
Create additional files/folders within memory-bank/ when they help organize:
|
||||
- Complex feature documentation
|
||||
- Integration specifications
|
||||
- API documentation
|
||||
- Testing strategies
|
||||
- Deployment procedures
|
||||
|
||||
## Core Workflows
|
||||
|
||||
### Plan Mode
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Start] --> ReadFiles[Read Memory Bank]
|
||||
ReadFiles --> CheckFiles{Files Complete?}
|
||||
|
||||
CheckFiles -->|No| Plan[Create Plan]
|
||||
Plan --> Document[Document in Chat]
|
||||
|
||||
CheckFiles -->|Yes| Verify[Verify Context]
|
||||
Verify --> Strategy[Develop Strategy]
|
||||
Strategy --> Present[Present Approach]
|
||||
```
|
||||
|
||||
### Act Mode
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Start] --> Context[Check Memory Bank]
|
||||
Context --> Update[Update Documentation]
|
||||
Update --> Rules[Update .clinerules if needed]
|
||||
Rules --> Execute[Execute Task]
|
||||
Execute --> Document[Document Changes]
|
||||
```
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
Memory Bank updates occur when:
|
||||
1. Discovering new project patterns
|
||||
2. After implementing significant changes
|
||||
3. When user requests with **update memory bank** (MUST review ALL files)
|
||||
4. When context needs clarification
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Update Process]
|
||||
|
||||
subgraph Process
|
||||
P1[Review ALL Files]
|
||||
P2[Document Current State]
|
||||
P3[Clarify Next Steps]
|
||||
P4[Update .clinerules]
|
||||
|
||||
P1 --> P2 --> P3 --> P4
|
||||
end
|
||||
|
||||
Start --> Process
|
||||
```
|
||||
|
||||
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
|
||||
|
||||
## Project Intelligence (.clinerules)
|
||||
|
||||
The .clinerules file is my learning journal for each project. It captures important patterns, preferences, and project intelligence that help me work more effectively. As I work with you and the project, I'll discover and document key insights that aren't obvious from the code alone.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start{Discover New Pattern}
|
||||
|
||||
subgraph Learn [Learning Process]
|
||||
D1[Identify Pattern]
|
||||
D2[Validate with User]
|
||||
D3[Document in .clinerules]
|
||||
end
|
||||
|
||||
subgraph Apply [Usage]
|
||||
A1[Read .clinerules]
|
||||
A2[Apply Learned Patterns]
|
||||
A3[Improve Future Work]
|
||||
end
|
||||
|
||||
Start --> Learn
|
||||
Learn --> Apply
|
||||
```
|
||||
|
||||
### What to Capture
|
||||
- Critical implementation paths
|
||||
- User preferences and workflow
|
||||
- Project-specific patterns
|
||||
- Known challenges
|
||||
- Evolution of project decisions
|
||||
- Tool usage patterns
|
||||
|
||||
The format is flexible - focus on capturing valuable insights that help me work more effectively with you and the project. Think of .clinerules as a living document that grows smarter as we work together.
|
||||
|
||||
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
||||
@@ -0,0 +1,47 @@
|
||||
# ميثاق المساهمين
|
||||
|
||||
## تعهدنا
|
||||
|
||||
نحن المساهمون والقائمون على هذا المشروع، نتعهد بتوفير بيئة مفتوحة ومرحبة، ونجعل المشاركة في مشروعنا ومجتمعنا تجربة خالية من التحرش للجميع، بغض النظر عن العمر، أو حجم الجسم، أو الإعاقة، أو العرق، أو الخصائص الجنسية، أو الهوية الجنسية والتعبير عنها، أو مستوى الخبرة، أو التعليم، أو الوضع الاجتماعي والاقتصادي، أو الجنسية، أو المظهر الشخصي، أو الدين، أو الهوية الجنسية والتوجه الجنسي.
|
||||
|
||||
## معاييرنا
|
||||
|
||||
أمثلة على السلوك الذي يساهم في خلق بيئة إيجابية تشمل:
|
||||
|
||||
- استخدام لغة ترحيبية وشاملة
|
||||
- احترام وجهات النظر والخبرات المختلفة
|
||||
- تقبل النقد البناء برحابة صدر
|
||||
- التركيز على ما هو الأفضل للمجتمع
|
||||
- إظهار التعاطف تجاه أعضاء المجتمع الآخرين
|
||||
|
||||
أمثلة على السلوك غير المقبول من قبل المشاركين تشمل:
|
||||
|
||||
- استخدام لغة أو صور جنسية والاهتمام الجنسي غير المرغوب فيه أو التحرش الجنسي
|
||||
- التصيد، والتعليقات المهينة/المسيئة، والهجمات الشخصية أو السياسية
|
||||
- التحرش العلني أو الخاص
|
||||
- نشر معلومات الآخرين الخاصة، مثل العنوان الفعلي أو الإلكتروني، دون إذن صريح
|
||||
- أي سلوك آخر يمكن اعتباره غير لائق في بيئة مهنية
|
||||
|
||||
## مسؤولياتنا
|
||||
|
||||
يتحمل القائمون على المشروع مسؤولية توضيح معايير السلوك المقبول، ومن المتوقع أن يتخذوا إجراءات تصحيحية مناسبة وعادلة استجابة لأي حالات سلوك غير مقبول.
|
||||
|
||||
يحق للقائمين على المشروع إزالة أو تعديل أو رفض التعليقات والالتزامات والتعليمات البرمجية وتعديلات wiki والمشكلات والمساهمات الأخرى التي لا تتماشى مع مدونة قواعد السلوك هذه، أو حظر أي مساهم بشكل مؤقت أو دائم بسبب سلوكيات أخرى يعتبرونها غير لائقة أو مهددة أو مسيئة أو ضارة، كما أنهم يتحملون مسؤولية ذلك.
|
||||
|
||||
## النطاق
|
||||
|
||||
تنطبق مدونة قواعد السلوك هذه داخل مساحات المشروع وفي الأماكن العامة عندما يمثل الفرد المشروع أو مجتمعه. تتضمن أمثلة تمثيل مشروع أو مجتمع استخدام عنوان بريد إلكتروني رسمي للمشروع، أو النشر عبر حساب رسمي على وسائل التواصل الاجتماعي، أو العمل كممثل معين في حدث عبر الإنترنت أو خارجه. يمكن للقائمين على المشروع تحديد وتوضيح تمثيل المشروع بشكل أكبر.
|
||||
|
||||
## التنفيذ
|
||||
|
||||
يمكن الإبلاغ عن حالات السلوك المسيء أو التحرش أو السلوك غير المقبول عن طريق الاتصال بفريق المشروع على hi@cline.bot. ستتم مراجعة جميع الشكاوى والتحقيق فيها وستؤدي إلى استجابة تعتبر ضرورية ومناسبة للظروف. يلتزم فريق المشروع بالحفاظ على السرية فيما يتعلق بالمبلغ عن الحادث. يمكن نشر مزيد من التفاصيل حول سياسات التنفيذ المحددة بشكل منفصل.
|
||||
|
||||
قد يواجه القائمون على المشروع الذين لا يتبعون أو يفرضون مدونة قواعد السلوك بحسن نية تداعيات مؤقتة أو دائمة على النحو الذي يحدده الأعضاء الآخرون في قيادة المشروع.
|
||||
|
||||
## الإسناد
|
||||
|
||||
تم اقتباس مدونة قواعد السلوك هذه من [تعهد المساهم][homepage]، الإصدار 1.4، متاح على https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
للحصول على إجابات للأسئلة الشائعة حول مدونة قواعد السلوك هذه، راجع https://www.contributor-covenant.org/faq
|
||||
@@ -0,0 +1,93 @@
|
||||
# المساهمة في Cline
|
||||
|
||||
نحن سعداء لاهتمامك بالمساهمة في Cline. سواء كنت تصلح خطأً أو تضيف ميزة أو تحسن الوثائق لدينا، فإن كل مساهمة تجعل Cline أذكى! للحفاظ على مجتمعنا نابضًا بالحياة وترحيبيًا، يجب على جميع الأعضاء الالتزام بـ [مدونة قواعد السلوك](CODE_OF_CONDUCT.md) لدينا.
|
||||
|
||||
## الإبلاغ عن الأخطاء أو المشكلات
|
||||
|
||||
تساعد تقارير الأخطاء على جعل Cline أفضل للجميع! قبل إنشاء مشكلة جديدة، يرجى [البحث عن المشكلات الموجودة](https://github.com/cline/cline/issues) لتجنب الازدواجية. عندما تكون جاهزًا للإبلاغ عن خطأ، انتقل إلى [صفحة المشكلات](https://github.com/cline/cline/issues/new/choose) حيث ستجد قالبًا لمساعدتك في ملء المعلومات ذات الصلة.
|
||||
|
||||
<blockquote class='warning-note'>
|
||||
🔐 <b>مهم:</b> إذا اكتشفت ثغرة أمنية، فيرجى استخدام <a href="https://github.com/cline/cline/security/advisories/new">أداة الأمان على Github للإبلاغ عنها بشكل خاص</a>.
|
||||
</blockquote>
|
||||
|
||||
## تحديد ما يجب العمل عليه
|
||||
|
||||
تبحث عن مساهمة أولى جيدة؟ تحقق من المشكلات المميزة بـ ["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue) أو ["help wanted"](https://github.com/cline/cline/labels/help%20wanted). تم تحديد هذه المشكلات خصيصًا للمساهمين الجدد والمجالات التي نرحب فيها بالمساعدة!
|
||||
|
||||
نرحب أيضًا بالمساهمات في [الوثائق](https://github.com/cline/cline/tree/main/docs) لدينا! سواء كان تصحيح أخطاء إملائية، أو تحسين الأدلة الحالية، أو إنشاء محتوى تعليمي جديد - نود بناء مستودع موارد مدفوع من المجتمع يساعد الجميع على الاستفادة القصوى من Cline. يمكنك البدء بالغوص في `/docs` والبحث عن مجالات تحتاج إلى تحسين.
|
||||
|
||||
إذا كنت تخطط للعمل على ميزة أكبر، فيرجى إنشاء [طلب ميزة](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) أولاً حتى نتمكن من مناقشة ما إذا كان ذلك يتماشى مع رؤية Cline.
|
||||
|
||||
## إعداد التطوير
|
||||
|
||||
1. **إضافات VS Code**
|
||||
|
||||
- عند فتح المشروع، سيطالبك VS Code بتثبيت الإضافات الموصى بها
|
||||
- هذه الإضافات مطلوبة للتطوير - يرجى قبول جميع مطالبات التثبيت
|
||||
- إذا تجاهلت المطالبات، يمكنك تثبيتها يدويًا من لوحة الإضافات
|
||||
|
||||
2. **التطوير المحلي**
|
||||
- قم بتشغيل `npm run install:all` لتثبيت التبعيات
|
||||
- قم بتشغيل `npm run test` لتشغيل الاختبارات محليًا
|
||||
- قبل تقديم طلب السحب، قم بتشغيل `npm run format:fix` لتنسيق التعليمات البرمجية الخاصة بك
|
||||
|
||||
## كتابة وتقديم التعليمات البرمجية
|
||||
|
||||
يمكن لأي شخص المساهمة بالتعليمات البرمجية في Cline، لكننا نطلب منك اتباع هذه الإرشادات لضمان دمج مساهماتك بسلاسة:
|
||||
|
||||
1. **احتفظ بطلبات السحب مركزة**
|
||||
|
||||
- قيد طلبات السحب بميزة واحدة أو إصلاح خطأ
|
||||
- قسم التغييرات الأكبر إلى طلبات سحب أصغر ومتصلة
|
||||
- قسم التغييرات إلى التزامات منطقية يمكن مراجعتها بشكل مستقل
|
||||
|
||||
2. **جودة التعليمات البرمجية**
|
||||
|
||||
- قم بتشغيل `npm run lint` للتحقق من نمط التعليمات البرمجية
|
||||
- قم بتشغيل `npm run format` لتنسيق التعليمات البرمجية تلقائيًا
|
||||
- يجب أن تجتاز جميع طلبات السحب عمليات التحقق المستمر التي تشمل كلاً من التنضيد والتنسيق
|
||||
- تعامل مع أي تحذيرات أو أخطاء ESLint قبل التقديم
|
||||
- اتبع أفضل ممارسات TypeScript والحفاظ على سلامة النوع
|
||||
|
||||
3. **الاختبار**
|
||||
|
||||
- أضف اختبارات للميزات الجديدة
|
||||
- قم بتشغيل `npm test` للتأكد من اجتياز جميع الاختبارات
|
||||
- قم بتحديث الاختبارات الحالية إذا كانت تغييراتك تؤثر عليها
|
||||
- تضمين كل من اختبارات الوحدة واختبارات التكامل حيثما كان ذلك مناسبًا
|
||||
|
||||
4. **إدارة الإصدار مع Changesets**
|
||||
|
||||
- أنشئ changeset لأي تغييرات واجهة المستخدم باستخدام `npm run changeset`
|
||||
- اختر زيادة الإصدار المناسبة:
|
||||
- `major` للتغييرات الكبيرة (1.0.0 → 2.0.0)
|
||||
- `minor` للميزات الجديدة (1.0.0 → 1.1.0)
|
||||
- `patch` لإصلاحات الأخطاء (1.0.0 → 1.0.1)
|
||||
- اكتب رسائل changeset واضحة ووصفية تشرح التأثير
|
||||
- لا تتطلب التغييرات في الوثائق فقط changesets
|
||||
|
||||
5. **إرشادات الالتزام (Commit Guidelines)**
|
||||
|
||||
- اكتب رسائل التزام واضحة وواصفة
|
||||
- استخدم تنسيق الالتزام التقليدي (مثل: "feat:", "fix:", "docs:")
|
||||
- أشر إلى القضايا ذات الصلة في الالتزامات باستخدام #رقم-القضية
|
||||
|
||||
6. **قبل الإرسال**
|
||||
|
||||
- قم بإعادة دمج فرعك مع أحدث إصدار من الفرع الرئيسي
|
||||
- تأكد من أن الفرع الخاص بك يُبنى بنجاح
|
||||
- تحقق من اجتياز جميع الاختبارات
|
||||
- راجع التغييرات الخاصة بك للتأكد من عدم وجود تعليمات تصحيح الأخطاء أو سجلات وحدة التحكم
|
||||
|
||||
7. **وصف طلب السحب (Pull Request Description)**
|
||||
|
||||
- صف بوضوح ما تقوم به التغييرات
|
||||
- قم بتضمين خطوات لاختبار التغييرات
|
||||
- أدرج أي تغييرات غير متوافقة
|
||||
- أضف لقطات شاشة للتغييرات في واجهة المستخدم
|
||||
|
||||
## اتفاقية المساهمة
|
||||
|
||||
من خلال إرسال طلب سحب، فإنك توافق على أن مساهماتك سيتم ترخيصها بنفس ترخيص المشروع ([Apache 2.0](LICENSE)).
|
||||
|
||||
تذكر: المساهمة في Cline لا تقتصر فقط على كتابة الكود - إنها تتعلق بأن تكون جزءًا من مجتمع يُشكل مستقبل التطوير بمساعدة الذكاء الاصطناعي. لنبنِ شيئًا رائعًا معًا! 🚀
|
||||
@@ -0,0 +1,189 @@
|
||||
<div align="center"><sub>
|
||||
العربية | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">الإسبانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">الألمانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">اليابانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">الصينية المبسطة</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">الصينية التقليدية</a> | <a href="https://github.com/cline/cline/blob/main/locales/pt-BR/README.md" target="_blank">البرتغالية</a>
|
||||
</sub></div>
|
||||
|
||||
# Cline – \#1 على OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tbody>
|
||||
<td align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>تنزيل من متجر VS</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<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>طلبات الميزات</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.cline.bot/getting-started/getting-started-new-coders" target="_blank"><strong>البدء</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
التقى Cline، مساعد الذكاء الاصطناعي الذي يمكنه استخدام **سطر الأوامر** و **محرر النصوص** الخاص بك.
|
||||
|
||||
بفضل [قدرات Claude 3.5 Sonnet على التعليمات البرمجية الوكيلة](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf)، يمكن لـ Cline التعامل مع مهام تطوير البرامج المعقدة خطوة بخطوة. مع الأدوات التي تسمح له بإنشاء وتعديل الملفات، واستكشاف المشاريع الكبيرة، واستخدام المتصفح، وتنفيذ أوامر الطرفية (بعد منحك الإذن)، يمكنه مساعدتك بطرق تتجاوز إكمال الكود أو الدعم الفني. يمكن لـ Cline أيضًا استخدام بروتوكول سياق النموذج (MCP) لإنشاء أدوات جديدة وتوسيع قدراته الخاصة. في حين تعمل النصوص البرمجية الآلية المستقلة تقليديًا في بيئات محاصرة، توفر هذه الإضافة واجهة رسومية لموافقة المستخدم على كل تغيير في الملف وأمر طرفية، مما يوفر طريقة آمنة وسهلة الاستخدام لاستكشاف إمكانات الذكاء الاصطناعي الوكيل.
|
||||
|
||||
1. أدخل مهمتك وأضف الصور لتحويل المحاكاة إلى تطبيقات وظيفية أو إصلاح الأخطاء مع لقطات الشاشة.
|
||||
2. يبدأ Cline بتحليل هيكل الملفات الخاصة بك وشجرة التعريف المصدرية، وإجراء عمليات بحث regex، وقراءة الملفات ذات الصلة للاطلاع على المشاريع الحالية. من خلال إدارة المعلومات التي يتم إضافتها إلى السياق بعناية، يمكن لـ Cline تقديم مساعدة قيمة حتى للمشاريع الكبيرة والمعقدة دون إرهاق نافذة السياق.
|
||||
3. بمجرد حصول Cline على المعلومات التي يحتاجها، يمكنه:
|
||||
- إنشاء وتعديل الملفات + مراقبة أخطاء Linter/Compiler أثناء السير، مما يسمح له بإصلاح المشكلات مثل الواردات المفقودة وأخطاء البناء النحوي بمفرده.
|
||||
- تنفيذ الأوامر مباشرة في الطرفية الخاصة بك ومراقبة إخراجها أثناء العمل، مما يسمح له على سبيل المثال بالاستجابة لمشكلات خادم التطوير بعد تعديل ملف.
|
||||
- بالنسبة لمهام تطوير الويب، يمكن لـ Cline إطلاق الموقع في متصفح بلا رأس، والنقر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة + سجلات وحدة التحكم، مما يسمح له بإصلاح أخطاء وقت التشغيل والأخطاء البصرية.
|
||||
4. عند اكتمال المهمة، سيقدم Cline النتيجة لك مع أمر طرفية مثل `open -a "Google Chrome" index.html`، والذي تقوم بتشغيله بنقرة زر.
|
||||
|
||||
> [!TIP]
|
||||
> استخدم اختصار `CMD/CTRL + Shift + P` لفتح لوحة الأوامر واكتب "Cline: Open In New Tab" لفتح الإضافة كعلامة تبويب في محرر النصوص الخاص بك. يتيح لك هذا استخدام Cline جنبًا إلى جنب مع مستكشف الملفات الخاص بك، ورؤية كيف يغير مساحة العمل الخاصة بك بوضوح أكبر.
|
||||
|
||||
---
|
||||
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
||||
|
||||
### استخدم أي واجهة برمجة تطبيقات ونموذج
|
||||
|
||||
يدعم Cline مقدمي واجهات برمجة التطبيقات مثل OpenRouter و Anthropic و OpenAI و Google Gemini و AWS Bedrock و Azure و GCP Vertex. يمكنك أيضًا تكوين أي واجهة برمجة تطبيقات متوافقة مع OpenAI، أو استخدام نموذج محلي من خلال LM Studio/Ollama. إذا كنت تستخدم OpenRouter، فستقوم الإضافة بجلب قائمة النماذج الأحدث الخاصة بهم، مما يسمح لك باستخدام أحدث النماذج بمجرد توفرها.
|
||||
|
||||
تتتبع الإضافة أيضًا إجمالي الرموز والاستخدام الخاص بواجهة برمجة التطبيقات لدورة المهمة بأكملها وطلبات فردية، مما يبقيك على اطلاع بالإنفاق في كل خطوة.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
|
||||
|
||||
### تشغيل الأوامر في الطرفية
|
||||
|
||||
بفضل [تحديثات تكامل الشل الجديدة في VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api)، يمكن لـ Cline تنفيذ الأوامر مباشرة في الطرفية الخاصة بك وتلقي الإخراج. يسمح له هذا بأداء مجموعة واسعة من المهام، من تثبيت الحزم وتشغيل سكربتات البناء إلى نشر التطبيقات، وإدارة قواعد البيانات، وتنفيذ الاختبارات، وذلك بالتكيف مع بيئة التطوير الخاصة بك وسلسلة الأدوات للقيام بالعمل على النحو الصحيح.
|
||||
|
||||
بالنسبة للعمليات الطويلة المدى مثل خوادم التطوير، استخدم زر "المتابعة أثناء التشغيل" للسماح لـ Cline بالاستمرار في المهمة بينما يعمل الأمر في الخلفية. أثناء عمل Cline، سيتم إخباره بأي إخراج طرفية جديد على الطريق، مما يسمح له بالاستجابة للمشكلات التي قد تنشأ، مثل أخطاء وقت الإنشاء عند تعديل الملفات.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
|
||||
|
||||
### إنشاء وتعديل الملفات
|
||||
|
||||
يمكن لـ Cline إنشاء وتعديل الملفات مباشرة في محرر النصوص الخاص بك، وعرض الاختلافات. يمكنك تعديل أو إلغاء تغييرات Cline مباشرة في محرر الاختلافات، أو تقديم ملاحظات في الدردشة حتى تكون راضيًا عن النتيجة. يراقب Cline أيضًا أخطاء Linter/Compiler (الواردات المفقودة، أخطاء البناء النحوي، إلخ) حتى يتمكن من إصلاح المشكلات التي تنشأ أثناء السير بمفرده.
|
||||
|
||||
يتم تسجيل جميع التغييرات التي أجراها Cline في جدول زمني للملف، مما يوفر طريقة سهلة لتتبع وإلغاء التعديلات إذا لزم الأمر.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
|
||||
|
||||
### استخدم المتصفح
|
||||
|
||||
مع قدرة [استخدام الكمبيوتر](https://www.anthropic.com/news/3-5-models-and-computer-use) الجديدة لـ Claude 3.5 Sonnet، يمكن لـ Cline إطلاق متصفح، والنقر على العناصر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة وسجلات وحدة التحكم في كل خطوة. يسمح له هذا بالتصحيح التفاعلي، واختبار نهاية إلى نهاية، وحتى الاستخدام العام للويب! يمنحه هذا الاستقلالية لإصلاح الأخطاء البصرية وأخطاء وقت التشغيل دون الحاجة إلى نسخ ولصق سجلات الأخطاء بنفسك.
|
||||
|
||||
حاول طلب من Cline "اختبار التطبيق"، وشاهده يشغل أمرًا مثل `npm run dev`، ويطلق خادم التطوير المحلي في متصفح، ويجري سلسلة من الاختبارات للتأكد من أن كل شيء يعمل. [شاهد عرضًا توضيحيًا هنا.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
|
||||
|
||||
### "إضافة أداة التي..."
|
||||
|
||||
شكراً لـ [بروتوكول سياق النموذج](https://github.com/modelcontextprotocol)، يمكن لـ Cline توسيع قدراته من خلال الأدوات المخصصة. بينما يمكنك استخدام [الخوادم التي أنشأها المجتمع](https://github.com/modelcontextprotocol/servers)، يمكن لـ Cline بدلاً من ذلك إنشاء أدوات وتثبيتها مصممة خصيصًا لتناسب سير عملك. ما عليك سوى أن تطلب من Cline "إضافة أداة"، وسيتولى كل شيء، من إنشاء خادم MCP جديد إلى تثبيته في الامتداد. تصبح هذه الأدوات المخصصة بعد ذلك جزءًا من مجموعة أدوات Cline، جاهزة للاستخدام في المهام المستقبلية.
|
||||
|
||||
- **"أضف أداة تجلب تذاكر Jira"**: استرجع تذاكر AC وقم بتشغيل Cline
|
||||
- **"أضف أداة تدير AWS EC2s"**: تحقق من مقاييس الخادم وقم بتوسيع أو تقليص عدد الحالات
|
||||
- **"أضف أداة تجلب أحدث حوادث PagerDuty"**: استرجع التفاصيل واطلب من Cline إصلاح الأخطاء
|
||||
|
||||
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
|
||||
|
||||
### إضافة السياق
|
||||
|
||||
**`@url`**: الصق رابط URL ليقوم الامتداد بجلبه وتحويله إلى Markdown، مفيد عندما تريد تزويد Cline بأحدث الوثائق
|
||||
|
||||
**`@problems`**: أضف أخطاء وتحذيرات بيئة العمل ('لوحة المشكلات') ليتمكن Cline من إصلاحها
|
||||
|
||||
**`@file`**: يضيف محتويات ملف حتى لا تضطر إلى إهدار طلبات API بالموافقة على قراءة الملف (+ البحث في الملفات)
|
||||
|
||||
**`@folder`**: يضيف جميع ملفات المجلد دفعة واحدة لتسريع سير العمل بشكل أكبر
|
||||
|
||||
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
|
||||
|
||||
### نقاط التحقق: المقارنة والاستعادة
|
||||
|
||||
أثناء عمل Cline على مهمة، يأخذ الامتداد لقطة من بيئة العمل في كل خطوة. يمكنك استخدام زر "Compare" لرؤية الفرق بين اللقطة وبيئة العمل الحالية، وزر "Restore" للعودة إلى تلك النقطة.
|
||||
|
||||
على سبيل المثال، عند العمل مع خادم ويب محلي، يمكنك استخدام "استعادة بيئة العمل فقط" لاختبار إصدارات مختلفة من تطبيقك بسرعة، ثم استخدام "استعادة المهمة وبيئة العمل" عندما تجد الإصدار الذي تريد المتابعة منه. يتيح لك ذلك استكشاف أساليب مختلفة بأمان دون فقدان التقدم.
|
||||
|
||||
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## المساهمة
|
||||
|
||||
للمساهمة في المشروع، ابدأ بـ [دليل المساهمة](CONTRIBUTING.md) لتعلم الأساسيات. يمكنك أيضًا الانضمام إلى [خادم Discord](https://discord.gg/cline) للدردشة مع المساهمين الآخرين في قناة `#contributors`. إذا كنت تبحث عن عمل بدوام كامل، تحقق من الوظائف المتاحة على [صفحة التوظيف](https://cline.bot/join-us)!
|
||||
|
||||
<details>
|
||||
<summary>تعليمات التطوير المحلي</summary>
|
||||
|
||||
1. استنساخ المستودع _(يتطلب [git-lfs](https://git-lfs.com/))_:
|
||||
```bash
|
||||
git clone https://github.com/cline/cline.git
|
||||
```
|
||||
2. افتح المشروع في VSCode:
|
||||
```bash
|
||||
code cline
|
||||
```
|
||||
3. قم بتثبيت التبعيات اللازمة للامتداد وواجهة الويب:
|
||||
```bash
|
||||
npm run install:all
|
||||
```
|
||||
4. قم بالتشغيل بالضغط على `F5` (أو من `Run` -> `Start Debugging`) لفتح نافذة VSCode جديدة مع تحميل الامتداد. (قد تحتاج إلى تثبيت [إضافة esbuild problem matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) إذا واجهت مشكلات في بناء المشروع.)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>إنشاء طلب سحب (Pull Request)</summary>
|
||||
|
||||
1. قبل إنشاء PR، قم بإنشاء إدخال للتغييرات:
|
||||
```bash
|
||||
npm run changeset
|
||||
```
|
||||
سيطلب منك تحديد:
|
||||
- نوع التغيير (رئيسي، ثانوي، إصلاح)
|
||||
- `رئيسي` → تغييرات غير متوافقة (1.0.0 → 2.0.0)
|
||||
- `ثانوي` → ميزات جديدة (1.0.0 → 1.1.0)
|
||||
- `إصلاح` → إصلاحات للأخطاء (1.0.0 → 1.0.1)
|
||||
- وصف التغييرات التي قمت بها
|
||||
|
||||
2. قم بحفظ التغييرات وملف `.changeset` الذي تم إنشاؤه
|
||||
|
||||
3. ادفع فرعك وأنشئ PR على GitHub. سيقوم CI بـ:
|
||||
- تشغيل الاختبارات والفحوصات
|
||||
- سيقوم Changesetbot بإنشاء تعليق يوضح تأثير الإصدار
|
||||
- عند الدمج مع الفرع الرئيسي، سيقوم Changesetbot بإنشاء PR لحزم الإصدار
|
||||
- عند دمج PR لحزم الإصدار، سيتم نشر إصدار جديد
|
||||
|
||||
</details>
|
||||
|
||||
## الرخصة
|
||||
|
||||
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
|
||||
@@ -0,0 +1,51 @@
|
||||
# Código de Conduta para Contribuidores
|
||||
|
||||
## Nosso Compromisso
|
||||
|
||||
|
||||
Com o objetivo de promover um ambiente aberto e acolhedor, nós, como contribuidores e mantenedores, nos comprometemos a tornar a participação em nosso projeto e comunidade uma experiência livre de assédio para todos, independentemente de idade, tamanho corporal, deficiência, etnia, características sexuais, identidade e expressão de gênero, nível de experiência, educação, status socioeconômico, nacionalidade, aparência pessoal, raça, religião ou orientação sexual.
|
||||
|
||||
## Nossos Padrões
|
||||
|
||||
Exemplos de comportamentos que contribuem para criar um ambiente positivo incluem:
|
||||
|
||||
- Uso de linguagem acolhedora e inclusiva
|
||||
- Respeito por diferentes pontos de vista e experiências
|
||||
- Aceitar críticas de maneira construtiva
|
||||
- Foco no que é melhor para a comunidade
|
||||
- Ser empático com outros membros da comunidade
|
||||
|
||||
|
||||
Exemplos de comportamentos inaceitáveis por parte dos participantes incluem:
|
||||
|
||||
- Uso de linguagem ou imagens sexualizadas e atenção ou avanços sexuais indesejados
|
||||
- Trollar, insultar, fazer comentários depreciativos, ataques pessoais ou políticos
|
||||
- Assédio público ou privado
|
||||
- Divulgar informações privadas sem autorização, como endereços físicos ou eletrônicos, sem permissão explícita
|
||||
- Outras condutas que poderiam ser consideradas inadequadas em um ambiente profissional
|
||||
|
||||
## Nossas Responsabilidades
|
||||
|
||||
Os mantenedores do projeto são responsáveis por esclarecer os padrões de comportamento aceitáveis e devem tomar ações corretivas apropriadas e justas em resposta a qualquer instância de comportamento inaceitável.
|
||||
|
||||
Os mantenedores têm o direito e a responsabilidade de remover, editar ou rejeitar comentários, commits, códigos, edições no wiki, issues e outras contribuições que não estejam alinhadas com este Código de Conduta. Também podem banir temporária ou permanentemente qualquer colaborador cujo comportamento seja considerado inapropriado, ameaçador, ofensivo ou prejudicial.
|
||||
|
||||
## Escopo
|
||||
|
||||
Este Código de Conduta se aplica tanto aos espaços do projeto quanto aos espaços públicos
|
||||
quando uma pessoa representa o projeto ou sua comunidade. Exemplos de
|
||||
representação de um projeto ou comunidade incluem o uso de um endereço de e-mail oficial do projeto,
|
||||
publicar em uma conta oficial de mídia social ou atuar como representante designado
|
||||
em um evento online ou offline. A representação de um projeto pode
|
||||
ser mais especificamente definido e esclarecido pelos mantenedores do projeto.
|
||||
|
||||
## Aplicação
|
||||
|
||||
Casos de comportamento abusivo, assediador ou inaceitáveis podem ser reportados entrando em contato com a equipe do projeto pelo email hi@cline.bot. Todas as queixas serão revisadas e investigadas confidencialmente. Mais detalhes sobre políticas específicas podem ser publicados separadamente.
|
||||
|
||||
Os mantenedores que não seguirem ou aplicarem este Código de Conduta de boa fé podem enfrentar repercussões temporárias ou permanentes determinadas por outros membros da liderança do projeto.
|
||||
|
||||
## Atribuição
|
||||
|
||||
Este Código de Conduta é adaptado do [Contributor Covenant](https://www.contributor-covenant.org), versão 1.4, disponível em https://www.contributor-covenant.org/version/1/4/code-of-conduct.html.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Contribuir para o Cline
|
||||
|
||||
Estamos felizes por você estar interessado em contribuir com o Cline. Seja corrigindo um erro, adicionando uma funcionalidade ou melhorando nossa documentação, cada contribuição torna o Cline mais inteligente! Para manter nossa comunidade viva e acolhedora, todos os membros devem cumprir nosso Código de Conduta [Código de Conduta](CODE_OF_CONDUCT.md).
|
||||
|
||||
## Relatar erros ou problemas
|
||||
|
||||
Relatar erros ajuda a melhorar o Cline para todos! Antes de criar um novo issue, revise as [issues existentes](https://github.com/cline/cline/issues) para evitar duplicações. Quando estiver pronto para relatar um erro, vá até nossa [página de Issues](https://github.com/cline/cline/issues/new/choose), onde você encontrará um modelo que ajudará a preencher as informações relevantes.
|
||||
|
||||
<blockquote class='warning-note'>
|
||||
🔐 <b>Importante:</b> Se você descobrir uma vulnerabilidade de segurança, utilize a <a href="https://github.com/cline/cline/security/advisories/new">ferramenta de segurança do GitHub</a> para relatá-la de forma privada.
|
||||
</blockquote>
|
||||
|
||||
## Escolher no que trabalhar
|
||||
|
||||
Procurando uma boa primeira contribuição? Consulte os problemas marcados com ["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue) ou ["help wanted"](https://github.com/cline/cline/labels/help%20wanted). Estes foram especialmente selecionados para novos colaboradores e são áreas em que adoraríamos receber ajuda!
|
||||
|
||||
Também damos boas-vindas a contribuições para nossa [documentação](https://github.com/cline/cline/tree/main/docs). Seja corrigindo erros de digitação, melhorando guias existentes ou criando novos conteúdos educativos, queremos construir um repositório de recursos gerido pela comunidade que ajude todos a tirar o máximo proveito do Cline. Você pode começar explorando `/docs` e procurando áreas que precisam de melhorias.
|
||||
|
||||
Se planeja trabalhar em uma funcionalidade maior, crie primeiro uma [solicitação de funcionalidade](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) para que possamos discutir se ela se alinha à visão do Cline.
|
||||
|
||||
## Configurar o ambiente de desenvolvimento
|
||||
|
||||
1. **Extensões do VS Code**
|
||||
|
||||
- Ao abrir o projeto, o VS Code solicitará que você instale as extensões recomendadas.
|
||||
- Essas extensões são necessárias para o desenvolvimento – aceite todas as solicitações de instalação.
|
||||
- Caso tenha rejeitado as solicitações, você pode instalá-las manualmente na seção de extensões.
|
||||
|
||||
2. **Desenvolvimento local**
|
||||
- Execute `npm run install:all` para instalar as dependências.
|
||||
- Execute `npm run test` para rodar os testes localmente.
|
||||
- Antes de enviar um PR, execute `npm run format:fix` para formatar seu código.
|
||||
|
||||
## Escrever e enviar código
|
||||
|
||||
Qualquer pessoa pode contribuir com código para o Cline, mas pedimos que siga estas diretrizes para garantir que suas contribuições sejam integradas sem problemas:
|
||||
|
||||
1. **Mantenha os Pull Requests focados**
|
||||
|
||||
- Limite os PRs a uma única funcionalidade ou correção de erro.
|
||||
- Divida alterações maiores em PRs menores e coerentes.
|
||||
- Divida as alterações em commits lógicos que possam ser revisados independentemente.
|
||||
|
||||
2. **Qualidade do código**
|
||||
|
||||
- Execute `npm run lint` para verificar o estilo do código.
|
||||
- Execute `npm run format` para formatar automaticamente o código.
|
||||
- Todos os PRs devem passar nas verificações do CI, que incluem linting e formatação.
|
||||
- Resolva todos os avisos ou erros do ESLint antes de enviar.
|
||||
- Siga as melhores práticas para TypeScript e mantenha a segurança dos tipos.
|
||||
|
||||
3. **Testes**
|
||||
|
||||
- Adicione testes para novas funcionalidades.
|
||||
- Execute `npm test` para garantir que todos os testes passem.
|
||||
- Atualize testes existentes caso suas alterações os afetem.
|
||||
- Inclua tanto testes unitários quanto de integração onde for apropriado.
|
||||
|
||||
4. **Diretrizes de commits**
|
||||
|
||||
- Escreva mensagens de commit claras e descritivas.
|
||||
- Use o formato convencional (por exemplo, "feat:", "fix:", "docs:").
|
||||
- Faça referência aos issues relevantes nos commits usando #número-do-issue.
|
||||
|
||||
5. **Antes de enviar**
|
||||
|
||||
- Faça rebase com sua branch com a última versão da branch principal (main).
|
||||
- Certifique-se de que sua branch seja construída corretamente.
|
||||
- Verifique se todos os testes passam.
|
||||
- Revise suas alterações para remover qualquer código de depuração ou logs desnecessários.
|
||||
|
||||
6. **Descrição do Pull Request**
|
||||
- Descreva claramente o que suas alterações fazem.
|
||||
- Inclua passos para testar as alterações.
|
||||
- Liste quaisquer mudanças importantes.
|
||||
- Adicione capturas de tela para mudanças na interface do usuário.
|
||||
|
||||
## Acordo de contribuição
|
||||
|
||||
Ao enviar um Pull Request, você concorda que suas contribuições serão licenciadas sob a mesma licença do projeto ([Apache 2.0](LICENSE)).
|
||||
|
||||
Lembre-se: Contribuir com o Cline não é apenas escrever código – é fazer parte de uma comunidade que está moldando o futuro do desenvolvimento assistido por IA. Vamos criar algo incrível juntos! 🚀
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# Cline – #1 no OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tbody>
|
||||
<td align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Baixar no VS Marketplace</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<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>Solicitação de Funcionalidades</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://cline.bot/join-us" target="_blank"><strong>Estamos Contratando!</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Conheça o Cline: um assistente de IA que pode usar seu **CLI** e **Editor**.
|
||||
|
||||
Graças às [habilidades avançadas do Claude 3.5 Sonnet](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf), o Cline pode lidar com tarefas complexas de desenvolvimento de software passo a passo. Com ferramentas que permitem criar e editar arquivos, explorar grandes projetos, usar o navegador e executar comandos no terminal (com sua aprovação), ele pode ajudar você de maneiras que vão além da inclusão de código ou suporte técnico. O Cline pode é capaz inclusive de usar o Model Context Protocol (MCP) para criar novas ferramentas e expandir seus próprios recursos. Embora os scripts de IA autônomas tradicionalmente sejam executados em ambientes isolados, esta extensão oferece uma GUI com um humano no circuito para aprovar cada alteração de arquivo e comando de terminal, fornecendo uma maneira segura e acessível de explorar todo o potencial da IA.
|
||||
|
||||
1. Insira sua tarefa e adicione imagens para transformar mockups em aplicativos funcionais ou corrigir erros através de capturas de tela.
|
||||
|
||||
2. O Cline começará analisando a estrutura do seu arquivo e os ASTs do código-fonte, fazendo pesquisas com Regex e lendo arquivos relevantes para se orientar em projetos existentes. Ao gerenciar cuidadosamente as informações agregadas, o Cline pode fornecer assistência valiosa mesmo em projetos grandes e complexos, sem sobrecarregar a janela de contexto.
|
||||
3. Assim que ele tiver as informações necessárias, o Cline poderá:
|
||||
- Criar e editar arquivos + monitorar erros de Linter/Compilador, para que você possa corrigir proativamente problemas como importações ausentes e erros de sintaxe.
|
||||
- Executar comandos diretamente no terminal e monitorar o resultado, para que você possa responder a problemas do servidor de desenvolvimento após editar um arquivo.
|
||||
- Para tarefas de desenvolvimento web, o Cline pode iniciar o site em um navegador headless, clicar, digitar, fazer scroll e capturar capturas de tela + registros de console, para que você possa corrigir erros em tempo de execução e erros visuais.
|
||||
|
||||
> [!TIP]
|
||||
> Use o atalho de teclado `CMD/CTRL + Shift + P` para abrir a lista de comandos possiveis e digite "Cline: Abrir em nova aba" para abrir a extensão como uma aba no seu editor. Dessa forma, você pode usar o Cline junto com seu explorador de arquivos e ver mais claramente como seu espaço de trabalho muda.
|
||||
|
||||
---
|
||||
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
||||
|
||||
### Use qualquer API ou modelo
|
||||
|
||||
O Cline oferece suporte a provedores de API como OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure e GCP Vertex. Você também pode configurar qualquer API compatível com OpenAI ou usar um modelo local via LM Studio/Ollama. Se você usar o OpenRouter, a extensão recuperará sua lista de modelos mais recentes, para que você possa usar os modelos mais novos assim que estiverem disponíveis.
|
||||
|
||||
A extensão também rastreia o uso total de tokens e os custos da API para todo o ciclo de tarefas e solicitações individuais, para que você seja informado sobre as despesas em cada etapa.
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
|
||||
|
||||
### Executar comandos no terminal
|
||||
|
||||
Graças às novas [atualizações de integração do Shell no VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), o Cline pode executar comandos diretamente no seu terminal e receber o resultado. Isso permite que você execute uma variedade de tarefas, desde instalar pacotes e executar build scripts para fazer deploy de aplicações, gerenciar bancos de dados e executar testes, adaptando-se ao seu ambiente de desenvolvimento e ferramentas para fazer o trabalho corretamente.
|
||||
|
||||
Para processos de longa duração, como servidores de desenvolvimento, use o botão "Continuar durante a execução" para permitir que o Cline continue a tarefa enquanto o comando é executado em segundo plano. Enquanto Cline trabalha, você será notificado sobre novas saídas do terminal, para que possa responder a problemas que possam surgir, como erros de compilação ao editar arquivos.
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
|
||||
|
||||
### Criar e editar arquivos
|
||||
|
||||
Cline pode criar e editar arquivos diretamente no seu editor, apresentando um diff com as alterações. Você pode editar ou reverter as alterações do Cline diretamente no editor de diff ou fornecer feedback no chat até ficar satisfeito com o resultado. Cline também monitora erros de linter/compilador (importações ausentes, erros de sintaxe, etc.) para que possa corrigir problemas que surgem ao longo do caminho por conta própria.
|
||||
|
||||
Todas as alterações feitas pelo Cline são registradas na Linha do tempo do arquivo, fornecendo uma maneira fácil de rastrear e reverter modificações, caso seja necessário.
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
|
||||
|
||||
### Uso do navegador
|
||||
|
||||
Com a nova habilidade de [uso de computador](https://www.anthropic.com/news/3-5-models-and-computer-use) do Claude Sonnet 3.5, Cline pode abrir um navegador, clicar em elementos, digitar texto e rolar, capturando a tela e logs de console. Isso permite depurar de maneira interativa, testes end-to-end e até mesmo uso geral da web. Isso lhe dá autonomia para solucionar erros visuais e problemas em tempo de execução sem precisar copiar e colar logs dos erros.
|
||||
|
||||
Tente pedir a Cline para "testar o aplicativo" e observe enquanto o Cline executa um comando como `npm run dev`, inicia seu servidor de desenvolvimento local em um navegador e executa uma série de testes para confirmar se tudo funciona. [Veja uma demonstração aqui.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
|
||||
|
||||
### "adicione uma ferramenta que..."
|
||||
|
||||
Graças ao [Model Context Protocol](https://github.com/modelcontextprotocol), o Cline pode expandir seus recursos por meio de ferramentas personalizadas. Embora você possa usar [servidores criados pela comunidade](https://github.com/modelcontextprotocol/servers), Cline pode criar e instalar ferramentas especificamente para seu fluxo de trabalho. Basta pedir ao Cline para "adicionar uma ferramenta" e ele cuidará de tudo, desde a criação de um novo servidor MCP até a instalação na extensão. Essas ferramentas personalizadas se tornam parte do conjunto de ferramentas da Cline e estão prontas para serem usadas em tarefas futuras.
|
||||
|
||||
- "adicione uma ferramenta que recupere tickets do Jira": Recupere ACs de tickets e coloque Cline para trabalhar
|
||||
- "adicione uma ferramenta que gerencie AWS EC2s": verifique as métricas do servidor e aumente ou diminua as instâncias
|
||||
- "adicione uma ferramenta para recuperar os últimos incidentes do PagerDuty": Recupere detalhes e peça ao Cline para corrigir erros
|
||||
|
||||
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
|
||||
|
||||
### Adicione contexto
|
||||
|
||||
**`@url`:** Insira uma URL para a extensão recuperar e converter para Markdown, que é útil quando você deseja fornecer ao Cline documentos mais recentes
|
||||
|
||||
**`@problems`:** Adicionar erros e avisos do espaço de trabalho (painel 'Problemas') que o Cline deve corrigir
|
||||
|
||||
**`@file`:** Adicione o conteúdo de um arquivo para que você não precise desperdiçar solicitações de API para aprovar a leitura do arquivo (+ para pesquisar arquivos)
|
||||
|
||||
**`@folder`:** Adicione arquivos de uma pasta por vez para acelerar ainda mais seu fluxo de trabalho
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
|
||||
|
||||
### Checkpoints: Comparar e Restaurar
|
||||
|
||||
Enquanto Cline trabalha em uma tarefa, a extensão cria um instantâneo de seu espaço de trabalho em cada etapa. Você pode usar o botão "Comparar" para ver a diferença entre o instantâneo e seu espaço de trabalho atual, e o botão "Restaurar" para retornar a esse ponto.
|
||||
|
||||
Por exemplo, se estiver trabalhando com um servidor web local, você pode usar 'Restaurar somente o espaço de trabalho' para testar rapidamente diferentes versões do seu aplicativo e, em seguida, 'Restaurar tarefa e espaço de trabalho' quando encontrar a versão na qual deseja continuar trabalhando. Isso permite que você explore diferentes abordagens com segurança sem perder o progresso.
|
||||
|
||||
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Contribuições
|
||||
|
||||
Para contribuir com o projeto, comece com nosso [Guia de Contribuição](CONTRIBUTING.md) para aprender o básico. Você também pode entrar no nosso [Discord](https://discord.gg/cline) para bater papo com outros colaboradores no canal `#contributors`. Se você está procurando um emprego de período integral, confira nossas vagas em aberto na nossa [página de carreiras](https://cline.bot/join-us).
|
||||
|
||||
<details>
|
||||
<summary>Instruções para desenvolvimento local</summary>
|
||||
|
||||
1. Clone o repositório _(Necessário [git-lfs](https://git-lfs.com/))_:
|
||||
```bash
|
||||
git clone https://github.com/cline/cline.git
|
||||
```
|
||||
2. Abra o projeto no VSCode:
|
||||
```bash
|
||||
code cline
|
||||
```
|
||||
3. Instale as dependências necessárias para a extensão e webview-gui:
|
||||
```bash
|
||||
npm run install:all
|
||||
```
|
||||
4. Inicie pressionando `F5` (ou `Executar`->`Iniciar Depuração`) para abrir uma nova janela do VSCode com a extensão carregada. (Pode ser necessário instalar a [extensão esbuild problem matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) se você encontrar problemas ao compilar seu projeto.)
|
||||
|
||||
</details>
|
||||
|
||||
## Licença
|
||||
|
||||
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
|
||||
Generated
+553
-74
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.2.6",
|
||||
"version": "3.4.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.2.6",
|
||||
"version": "3.4.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
"@anthropic-ai/sdk": "^0.26.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.4.1",
|
||||
"@google/generative-ai": "^0.18.0",
|
||||
"@mistralai/mistralai": "^1.3.6",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
@@ -32,10 +32,11 @@
|
||||
"firebase": "^11.2.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"ignore": "^7.0.3",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"openai": "^4.61.0",
|
||||
"openai": "^4.83.0",
|
||||
"os-name": "^6.0.0",
|
||||
"p-wait-for": "^5.0.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
@@ -47,7 +48,7 @@
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.12",
|
||||
@@ -62,7 +63,7 @@
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"chai": "^4.3.10",
|
||||
"esbuild": "^0.21.5",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^8.57.0",
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -2532,10 +2533,78 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2546,7 +2615,347 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
|
||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
|
||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
|
||||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
|
||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
@@ -2610,6 +3019,15 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -3660,6 +4078,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@manypkg/get-packages/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@manypkg/get-packages/node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
@@ -3671,13 +4098,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mistralai/mistralai": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.4.0.tgz",
|
||||
"integrity": "sha512-xA3DAtIDh4Qgr1EoSuiGVE+2ABNrxpcTeC0kSXYbkDNUGdthalLAH7DgbG0fkKZ7TN8xdWXQq2WiIghp/O96Eg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz",
|
||||
"integrity": "sha512-AIn8pwAwA/fDvEUvmkt+40zH1ZmfaG3Q7oUWl17GUEC1tU7ZPwYz8Cv9P59lyS1SisHdDSu81oknO7f1ywkz8Q==",
|
||||
"dependencies": {
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": ">= 3"
|
||||
}
|
||||
},
|
||||
"node_modules/@mistralai/mistralai/node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
|
||||
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mixmark-io/domino": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
||||
@@ -5564,12 +6003,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",
|
||||
@@ -5634,6 +6067,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
|
||||
@@ -5772,6 +6214,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
@@ -6442,6 +6893,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",
|
||||
@@ -6618,6 +7070,15 @@
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi/node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
@@ -6992,6 +7453,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",
|
||||
@@ -7373,6 +7835,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"
|
||||
@@ -7385,6 +7848,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"
|
||||
@@ -7437,9 +7901,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -7447,32 +7911,34 @@
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
"@esbuild/aix-ppc64": "0.25.0",
|
||||
"@esbuild/android-arm": "0.25.0",
|
||||
"@esbuild/android-arm64": "0.25.0",
|
||||
"@esbuild/android-x64": "0.25.0",
|
||||
"@esbuild/darwin-arm64": "0.25.0",
|
||||
"@esbuild/darwin-x64": "0.25.0",
|
||||
"@esbuild/freebsd-arm64": "0.25.0",
|
||||
"@esbuild/freebsd-x64": "0.25.0",
|
||||
"@esbuild/linux-arm": "0.25.0",
|
||||
"@esbuild/linux-arm64": "0.25.0",
|
||||
"@esbuild/linux-ia32": "0.25.0",
|
||||
"@esbuild/linux-loong64": "0.25.0",
|
||||
"@esbuild/linux-mips64el": "0.25.0",
|
||||
"@esbuild/linux-ppc64": "0.25.0",
|
||||
"@esbuild/linux-riscv64": "0.25.0",
|
||||
"@esbuild/linux-s390x": "0.25.0",
|
||||
"@esbuild/linux-x64": "0.25.0",
|
||||
"@esbuild/netbsd-arm64": "0.25.0",
|
||||
"@esbuild/netbsd-x64": "0.25.0",
|
||||
"@esbuild/openbsd-arm64": "0.25.0",
|
||||
"@esbuild/openbsd-x64": "0.25.0",
|
||||
"@esbuild/sunos-x64": "0.25.0",
|
||||
"@esbuild/win32-arm64": "0.25.0",
|
||||
"@esbuild/win32-ia32": "0.25.0",
|
||||
"@esbuild/win32-x64": "0.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@@ -7628,6 +8094,15 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -8260,6 +8735,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"
|
||||
@@ -8407,6 +8883,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",
|
||||
@@ -8604,6 +9081,14 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globby/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "9.14.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.0.tgz",
|
||||
@@ -8624,6 +9109,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"
|
||||
@@ -8681,6 +9167,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"
|
||||
@@ -8693,6 +9180,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"
|
||||
@@ -8705,6 +9193,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"
|
||||
@@ -8739,6 +9228,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"
|
||||
@@ -8915,10 +9405,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
||||
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
|
||||
"license": "MIT",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz",
|
||||
"integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
@@ -10495,6 +10984,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"
|
||||
@@ -10558,28 +11048,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.83.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.83.0.tgz",
|
||||
"integrity": "sha512-fmTsqud0uTtRKsPC7L8Lu55dkaTwYucqncDHzVvO64DKOpNTuiYwjbR/nVgpapXuYy8xSnhQQPUm+3jQaxICgw==",
|
||||
"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
|
||||
}
|
||||
@@ -11276,21 +11768,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",
|
||||
@@ -11740,6 +12217,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",
|
||||
@@ -11887,6 +12365,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",
|
||||
@@ -13340,9 +13819,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
+34
-7
@@ -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.7",
|
||||
"version": "3.4.5",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
@@ -73,7 +73,7 @@
|
||||
{
|
||||
"command": "cline.mcpButtonClicked",
|
||||
"title": "MCP Servers",
|
||||
"icon": "$(server)"
|
||||
"icon": "$(extensions)"
|
||||
},
|
||||
{
|
||||
"command": "cline.historyButtonClicked",
|
||||
@@ -161,6 +161,31 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enables extension to save checkpoints of workspace throughout the task."
|
||||
},
|
||||
"cline.disableBrowserTool": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Disables extension from spawning browser session."
|
||||
},
|
||||
"cline.modelSettings.o3Mini.reasoningEffort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"default": "medium",
|
||||
"description": "Controls the reasoning effort when using the o3-mini model. Higher values may result in more thorough but slower responses."
|
||||
},
|
||||
"cline.chromeExecutablePath": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
|
||||
},
|
||||
"cline.mcpMarketplace.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Controls whether the MCP Marketplace is enabled."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +212,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",
|
||||
@@ -202,7 +228,7 @@
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"chai": "^4.3.10",
|
||||
"esbuild": "^0.21.5",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^8.57.0",
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -215,7 +241,7 @@
|
||||
"@anthropic-ai/sdk": "^0.26.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.4.1",
|
||||
"@google/generative-ai": "^0.18.0",
|
||||
"@mistralai/mistralai": "^1.3.6",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
@@ -234,10 +260,11 @@
|
||||
"firebase": "^11.2.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"ignore": "^7.0.3",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"openai": "^4.61.0",
|
||||
"openai": "^4.83.0",
|
||||
"os-name": "^6.0.0",
|
||||
"p-wait-for": "^5.0.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
@@ -249,6 +276,6 @@
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,12 @@ import { GeminiHandler } from "./providers/gemini"
|
||||
import { OpenAiNativeHandler } from "./providers/openai-native"
|
||||
import { ApiStream } from "./transform/stream"
|
||||
import { DeepSeekHandler } from "./providers/deepseek"
|
||||
import { RequestyHandler } from "./providers/requesty"
|
||||
import { TogetherHandler } from "./providers/together"
|
||||
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 +50,18 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new OpenAiNativeHandler(options)
|
||||
case "deepseek":
|
||||
return new DeepSeekHandler(options)
|
||||
case "requesty":
|
||||
return new RequestyHandler(options)
|
||||
case "together":
|
||||
return new TogetherHandler(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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,49 +3,25 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
||||
export class AwsBedrockHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: AnthropicBedrock
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new AnthropicBedrock({
|
||||
// Authenticate by either providing the keys below or use the default AWS credential providers, such as
|
||||
// using ~/.aws/credentials or the "AWS_SECRET_ACCESS_KEY" and "AWS_ACCESS_KEY_ID" environment variables.
|
||||
...(this.options.awsAccessKey ? { awsAccessKey: this.options.awsAccessKey } : {}),
|
||||
...(this.options.awsSecretKey ? { awsSecretKey: this.options.awsSecretKey } : {}),
|
||||
...(this.options.awsSessionToken ? { awsSessionToken: this.options.awsSessionToken } : {}),
|
||||
|
||||
// awsRegion changes the aws region to which the request is made. By default, we read AWS_REGION,
|
||||
// and if that's not present, we default to us-east-1. Note that we do not read ~/.aws/config for the region.
|
||||
awsRegion: this.options.awsRegion,
|
||||
})
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
// cross region inference requires prefixing the model id with the region
|
||||
let modelId: string
|
||||
if (this.options.awsUseCrossRegionInference) {
|
||||
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
|
||||
switch (regionPrefix) {
|
||||
case "us-":
|
||||
modelId = `us.${this.getModel().id}`
|
||||
break
|
||||
case "eu-":
|
||||
modelId = `eu.${this.getModel().id}`
|
||||
break
|
||||
default:
|
||||
// cross region inference is not supported in this region, falling back to default model
|
||||
modelId = this.getModel().id
|
||||
break
|
||||
}
|
||||
} else {
|
||||
modelId = this.getModel().id
|
||||
}
|
||||
let modelId = await this.getModelId()
|
||||
|
||||
const stream = await this.client.messages.create({
|
||||
// create anthropic client, using sessions created or renewed after this handler's
|
||||
// initialization, and allowing for session renewal if necessary as well
|
||||
let client = await this.getClient()
|
||||
|
||||
const stream = await client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: this.getModel().info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
@@ -112,4 +88,68 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
info: bedrockModels[bedrockDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
private async getClient(): Promise<AnthropicBedrock> {
|
||||
// Create AWS credentials by executing a an AWS provider chain exactly as the
|
||||
// Anthropic SDK does it, by wrapping the default chain into a temporary process
|
||||
// environment.
|
||||
const providerChain = fromNodeProviderChain()
|
||||
const credentials = await AwsBedrockHandler.withTempEnv(
|
||||
() => {
|
||||
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
|
||||
AwsBedrockHandler.setEnv("AWS_ACCESS_KEY_ID", this.options.awsAccessKey)
|
||||
AwsBedrockHandler.setEnv("AWS_SECRET_ACCESS_KEY", this.options.awsSecretKey)
|
||||
AwsBedrockHandler.setEnv("AWS_SESSION_TOKEN", this.options.awsSessionToken)
|
||||
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
|
||||
},
|
||||
() => providerChain(),
|
||||
)
|
||||
|
||||
// Return an AnthropicBedrock client with the resolved/assumed credentials.
|
||||
//
|
||||
// When AnthropicBedrock creates its AWS client, the chain will execute very
|
||||
// fast as the access/secret keys will already be already provided, and have
|
||||
// a higher precedence than the profiles.
|
||||
return new AnthropicBedrock({
|
||||
awsAccessKey: credentials.accessKeyId,
|
||||
awsSecretKey: credentials.secretAccessKey,
|
||||
awsSessionToken: credentials.sessionToken,
|
||||
awsRegion: this.options.awsRegion || "us-east-1",
|
||||
})
|
||||
}
|
||||
|
||||
private async getModelId(): Promise<string> {
|
||||
if (this.options.awsUseCrossRegionInference) {
|
||||
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
|
||||
switch (regionPrefix) {
|
||||
case "us-":
|
||||
return `us.${this.getModel().id}`
|
||||
case "eu-":
|
||||
return `eu.${this.getModel().id}`
|
||||
break
|
||||
default:
|
||||
// cross region inference is not supported in this region, falling back to default model
|
||||
return this.getModel().id
|
||||
break
|
||||
}
|
||||
}
|
||||
return this.getModel().id
|
||||
}
|
||||
|
||||
private static async withTempEnv<R>(updateEnv: () => void, fn: () => Promise<R>): Promise<R> {
|
||||
const previousEnv = { ...process.env }
|
||||
|
||||
try {
|
||||
updateEnv()
|
||||
return await fn()
|
||||
} finally {
|
||||
process.env = previousEnv
|
||||
}
|
||||
}
|
||||
|
||||
private static async setEnv(key: string, value: string | undefined) {
|
||||
if (key !== "" && value !== undefined) {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
export class DeepSeekHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -17,12 +19,25 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
// Only set temperature for non-reasoner models
|
||||
@@ -38,6 +53,13 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: this.options.liteLlmApiKey || "noop",
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,11 @@ export class MistralHandler implements ApiHandler {
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new Mistral({
|
||||
serverURL: "https://codestral.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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import {
|
||||
ApiHandlerOptions,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
} from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
|
||||
|
||||
export class OpenAiNativeHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -22,6 +24,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
switch (this.getModel().id) {
|
||||
case "o1":
|
||||
@@ -43,6 +46,32 @@ 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 },
|
||||
reasoning_effort: (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium",
|
||||
})
|
||||
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,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
export class OpenAiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -26,13 +28,22 @@ export class OpenAiHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
const modelId = this.options.openAiModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: this.options.openAiModelId ?? "",
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
@@ -46,6 +57,14 @@ export class OpenAiHandler implements ApiHandler {
|
||||
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",
|
||||
@@ -59,7 +78,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.openAiModelId ?? "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
info: this.options.openAiModelInfo ?? openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import delay from "delay"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
export class OpenRouterHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -23,11 +25,12 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
||||
// Convert Anthropic messages to OpenAI format
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
@@ -98,6 +101,15 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
break
|
||||
}
|
||||
|
||||
let temperature = 0
|
||||
let topP: number | undefined = undefined
|
||||
if (this.getModel().id.startsWith("deepseek/deepseek-r1") || this.getModel().id === "perplexity/sonar-reasoning") {
|
||||
// Recommended values from DeepSeek
|
||||
temperature = 0.7
|
||||
topP = 0.95
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
|
||||
let shouldApplyMiddleOutTransform = !model.info.supportsPromptCache
|
||||
// except for deepseek (which we set supportsPromptCache to true for), where because the context window is so small our truncation algo might miss and we should use openrouter's middle-out transform as a fallback to ensure we don't exceed the context window (FIXME: once we have a more robust token estimator we should not rely on this)
|
||||
@@ -109,10 +121,13 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: maxTokens,
|
||||
temperature: 0,
|
||||
temperature: temperature,
|
||||
top_p: topP,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
|
||||
include_reasoning: true,
|
||||
...(model.id === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
|
||||
})
|
||||
|
||||
let genId: string | undefined
|
||||
@@ -136,6 +151,37 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// console.log("reasoning", delta.reasoning)
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
reasoning: delta.reasoning,
|
||||
}
|
||||
|
||||
// if (didStreamThinkTagInReasoning) {
|
||||
// yield {
|
||||
// type: "text",
|
||||
// // @ts-ignore-next-line
|
||||
// text: delta.reasoning,
|
||||
// }
|
||||
// } else {
|
||||
// yield {
|
||||
// type: "reasoning",
|
||||
// // @ts-ignore-next-line
|
||||
// text: delta.reasoning,
|
||||
// }
|
||||
|
||||
// // @ts-ignore-next-line
|
||||
// reasoningResponse += delta.reasoning
|
||||
// if (reasoningResponse.includes("</think>")) {
|
||||
// didStreamThinkTagInReasoning = true
|
||||
// console.log("did hit think tag", reasoningResponse)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// if (chunk.usage) {
|
||||
// yield {
|
||||
// type: "usage",
|
||||
@@ -145,8 +191,31 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// }
|
||||
}
|
||||
|
||||
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
|
||||
if (genId) {
|
||||
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
|
||||
try {
|
||||
const generationIterator = this.fetchGenerationDetails(genId)
|
||||
const generation = (await generationIterator.next()).value
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
yield {
|
||||
type: "usage",
|
||||
// cacheWriteTokens: 0,
|
||||
// cacheReadTokens: 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
|
||||
async *fetchGenerationDetails(genId: string) {
|
||||
// console.log("Fetching generation details for:", genId)
|
||||
try {
|
||||
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
|
||||
headers: {
|
||||
@@ -154,21 +223,11 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
},
|
||||
timeout: 5_000, // this request hangs sometimes
|
||||
})
|
||||
|
||||
const generation = response.data?.data
|
||||
console.log("OpenRouter generation details:", response.data)
|
||||
yield {
|
||||
type: "usage",
|
||||
// cacheWriteTokens: 0,
|
||||
// cacheReadTokens: 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
yield response.data?.data
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,9 +237,6 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
if (modelId && modelInfo) {
|
||||
return { id: modelId, info: modelInfo }
|
||||
}
|
||||
return {
|
||||
id: openRouterDefaultModelId,
|
||||
info: openRouterDefaultModelInfo,
|
||||
}
|
||||
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
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"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
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 === "china"
|
||||
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
: "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()
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-r1")
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...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 },
|
||||
...(model.id === "deepseek-r1" ? {} : { temperature: 0 }),
|
||||
})
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class RequestyHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://router.requesty.ai/v1",
|
||||
apiKey: this.options.requestyApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.options.requestyModelId ?? ""
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
|
||||
})
|
||||
|
||||
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) || "",
|
||||
}
|
||||
}
|
||||
|
||||
// Requesty usage includes an extra field for Anthropic use cases.
|
||||
// Safely cast the prompt token details section to the appropriate structure.
|
||||
interface RequestyUsage extends OpenAI.CompletionUsage {
|
||||
prompt_tokens_details?: {
|
||||
caching_tokens?: number
|
||||
cached_tokens?: number
|
||||
}
|
||||
total_cost?: number
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
const usage = chunk.usage as RequestyUsage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.prompt_tokens || 0,
|
||||
outputTokens: usage.completion_tokens || 0,
|
||||
cacheWriteTokens: usage.prompt_tokens_details?.caching_tokens || undefined,
|
||||
cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined,
|
||||
totalCost: usage.total_cost || undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.requestyModelId ?? "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
export class TogetherHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.together.xyz/v1",
|
||||
apiKey: this.options.togetherApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.options.togetherModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
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 (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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.togetherModelId ?? "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
interface RetryOptions {
|
||||
maxRetries?: number
|
||||
baseDelay?: number
|
||||
maxDelay?: number
|
||||
retryAllErrors?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<RetryOptions> = {
|
||||
maxRetries: 3,
|
||||
baseDelay: 1_000,
|
||||
maxDelay: 10_000,
|
||||
retryAllErrors: false,
|
||||
}
|
||||
|
||||
export function withRetry(options: RetryOptions = {}) {
|
||||
const { maxRetries, baseDelay, maxDelay, retryAllErrors } = { ...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 && !retryAllErrors) || 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
/**
|
||||
* Converts Anthropic messages to OpenAI format and merges consecutive messages with the same role.
|
||||
* This is required for DeepSeek Reasoner which does not support successive messages with the same role.
|
||||
* DeepSeek highly recommends using 'user' role instead of 'system' role for optimal performance.
|
||||
*
|
||||
* @param messages Array of Anthropic messages
|
||||
* @returns Array of OpenAI messages where consecutive messages with the same role are merged together
|
||||
*/
|
||||
export function convertToR1Format(messages: Anthropic.Messages.MessageParam[]): OpenAI.Chat.ChatCompletionMessageParam[] {
|
||||
return messages.reduce<OpenAI.Chat.ChatCompletionMessageParam[]>((merged, message) => {
|
||||
const lastMessage = merged[merged.length - 1]
|
||||
let messageContent: string | (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] =
|
||||
""
|
||||
let hasImages = false
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
const textParts: string[] = []
|
||||
const imageParts: OpenAI.Chat.ChatCompletionContentPartImage[] = []
|
||||
|
||||
message.content.forEach((part) => {
|
||||
if (part.type === "text") {
|
||||
textParts.push(part.text)
|
||||
}
|
||||
if (part.type === "image") {
|
||||
hasImages = true
|
||||
imageParts.push({
|
||||
type: "image_url",
|
||||
image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (hasImages) {
|
||||
const parts: (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] = []
|
||||
if (textParts.length > 0) {
|
||||
parts.push({ type: "text", text: textParts.join("\n") })
|
||||
}
|
||||
parts.push(...imageParts)
|
||||
messageContent = parts
|
||||
} else {
|
||||
messageContent = textParts.join("\n")
|
||||
}
|
||||
} else {
|
||||
messageContent = message.content
|
||||
}
|
||||
|
||||
// If the last message has the same role, merge the content
|
||||
if (lastMessage?.role === message.role) {
|
||||
if (typeof lastMessage.content === "string" && typeof messageContent === "string") {
|
||||
lastMessage.content += `\n${messageContent}`
|
||||
} else {
|
||||
const lastContent = Array.isArray(lastMessage.content)
|
||||
? lastMessage.content
|
||||
: [{ type: "text" as const, text: lastMessage.content || "" }]
|
||||
|
||||
const newContent = Array.isArray(messageContent)
|
||||
? messageContent
|
||||
: [{ type: "text" as const, text: messageContent }]
|
||||
|
||||
if (message.role === "assistant") {
|
||||
const mergedContent = [
|
||||
...lastContent,
|
||||
...newContent,
|
||||
] as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"]
|
||||
lastMessage.content = mergedContent
|
||||
} else {
|
||||
const mergedContent = [...lastContent, ...newContent] as OpenAI.Chat.ChatCompletionUserMessageParam["content"]
|
||||
lastMessage.content = mergedContent
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Adds new message with the correct type based on role
|
||||
if (message.role === "assistant") {
|
||||
const newMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam = {
|
||||
role: "assistant",
|
||||
content: messageContent as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"],
|
||||
}
|
||||
merged.push(newMessage)
|
||||
} else {
|
||||
const newMessage: OpenAI.Chat.ChatCompletionUserMessageParam = {
|
||||
role: "user",
|
||||
content: messageContent as OpenAI.Chat.ChatCompletionUserMessageParam["content"],
|
||||
}
|
||||
merged.push(newMessage)
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}, [])
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
export type ApiStream = AsyncGenerator<ApiStreamChunk>
|
||||
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk
|
||||
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamReasoningChunk | ApiStreamUsageChunk
|
||||
|
||||
export interface ApiStreamTextChunk {
|
||||
type: "text"
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface ApiStreamReasoningChunk {
|
||||
type: "reasoning"
|
||||
reasoning: string
|
||||
}
|
||||
|
||||
export interface ApiStreamUsageChunk {
|
||||
type: "usage"
|
||||
inputTokens: number
|
||||
|
||||
+338
-163
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,245 @@
|
||||
import { ClineIgnoreController } from "./ClineIgnoreController"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { after, beforeEach, describe, it } from "mocha"
|
||||
import "should"
|
||||
|
||||
describe("ClineIgnoreController", () => {
|
||||
let tempDir: string
|
||||
let controller: ClineIgnoreController
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a temp directory for testing
|
||||
tempDir = path.join(os.tmpdir(), `llm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
await fs.mkdir(tempDir)
|
||||
|
||||
// Create default .clineignore file
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, ".clineignore"),
|
||||
[".env", "*.secret", "private/", "# This is a comment", "", "temp.*", "file-with-space-at-end.* ", "**/.git/**"].join(
|
||||
"\n",
|
||||
),
|
||||
)
|
||||
|
||||
controller = new ClineIgnoreController(tempDir)
|
||||
await controller.initialize()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Clean up temp directory
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("Default Patterns", () => {
|
||||
// it("should block access to common ignored files", async () => {
|
||||
// const results = [
|
||||
// controller.validateAccess(".env"),
|
||||
// controller.validateAccess(".git/config"),
|
||||
// controller.validateAccess("node_modules/package.json"),
|
||||
// ]
|
||||
// results.forEach((result) => result.should.be.false())
|
||||
// })
|
||||
|
||||
it("should allow access to regular files", async () => {
|
||||
const results = [
|
||||
controller.validateAccess("src/index.ts"),
|
||||
controller.validateAccess("README.md"),
|
||||
controller.validateAccess("package.json"),
|
||||
]
|
||||
results.forEach((result) => result.should.be.true())
|
||||
})
|
||||
|
||||
it("should block access to .clineignore file", async () => {
|
||||
const result = controller.validateAccess(".clineignore")
|
||||
result.should.be.false()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Custom Patterns", () => {
|
||||
it("should block access to custom ignored patterns", async () => {
|
||||
const results = [
|
||||
controller.validateAccess("config.secret"),
|
||||
controller.validateAccess("private/data.txt"),
|
||||
controller.validateAccess("temp.json"),
|
||||
controller.validateAccess("nested/deep/file.secret"),
|
||||
controller.validateAccess("private/nested/deep/file.txt"),
|
||||
]
|
||||
results.forEach((result) => result.should.be.false())
|
||||
})
|
||||
|
||||
it("should allow access to non-ignored files", async () => {
|
||||
const results = [
|
||||
controller.validateAccess("public/data.txt"),
|
||||
controller.validateAccess("config.json"),
|
||||
controller.validateAccess("src/temp/file.ts"),
|
||||
controller.validateAccess("nested/deep/file.txt"),
|
||||
controller.validateAccess("not-private/data.txt"),
|
||||
]
|
||||
results.forEach((result) => result.should.be.true())
|
||||
})
|
||||
|
||||
it("should handle pattern edge cases", async () => {
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, ".clineignore"),
|
||||
["*.secret", "private/", "*.tmp", "data-*.json", "temp/*"].join("\n"),
|
||||
)
|
||||
|
||||
controller = new ClineIgnoreController(tempDir)
|
||||
await controller.initialize()
|
||||
|
||||
const results = [
|
||||
controller.validateAccess("data-123.json"), // Should be false (wildcard)
|
||||
controller.validateAccess("data.json"), // Should be true (doesn't match pattern)
|
||||
controller.validateAccess("script.tmp"), // Should be false (extension match)
|
||||
]
|
||||
|
||||
results[0].should.be.false() // data-123.json
|
||||
results[1].should.be.true() // data.json
|
||||
results[2].should.be.false() // script.tmp
|
||||
})
|
||||
|
||||
// ToDo: handle negation patterns successfully
|
||||
|
||||
// it("should handle negation patterns", async () => {
|
||||
// await fs.writeFile(
|
||||
// path.join(tempDir, ".clineignore"),
|
||||
// [
|
||||
// "temp/*", // Ignore everything in temp
|
||||
// "!temp/allowed/*", // But allow files in temp/allowed
|
||||
// "docs/**/*.md", // Ignore all markdown files in docs
|
||||
// "!docs/README.md", // Except README.md
|
||||
// "!docs/CONTRIBUTING.md", // And CONTRIBUTING.md
|
||||
// "assets/", // Ignore all assets
|
||||
// "!assets/public/", // Except public assets
|
||||
// "!assets/public/*.png", // Specifically allow PNGs in public assets
|
||||
// ].join("\n"),
|
||||
// )
|
||||
|
||||
// controller = new ClineIgnoreController(tempDir)
|
||||
|
||||
// const results = [
|
||||
// // Basic negation
|
||||
// controller.validateAccess("temp/file.txt"), // Should be false (in temp/)
|
||||
// controller.validateAccess("temp/allowed/file.txt"), // Should be true (negated)
|
||||
// controller.validateAccess("temp/allowed/nested/file.txt"), // Should be true (negated with nested)
|
||||
|
||||
// // Multiple negations in same path
|
||||
// controller.validateAccess("docs/guide.md"), // Should be false (matches docs/**/*.md)
|
||||
// controller.validateAccess("docs/README.md"), // Should be true (negated)
|
||||
// controller.validateAccess("docs/CONTRIBUTING.md"), // Should be true (negated)
|
||||
// controller.validateAccess("docs/api/guide.md"), // Should be false (nested markdown)
|
||||
|
||||
// // Nested negations
|
||||
// controller.validateAccess("assets/logo.png"), // Should be false (in assets/)
|
||||
// controller.validateAccess("assets/public/logo.png"), // Should be true (negated and matches *.png)
|
||||
// controller.validateAccess("assets/public/data.json"), // Should be true (in negated public/)
|
||||
// ]
|
||||
|
||||
// results[0].should.be.false() // temp/file.txt
|
||||
// results[1].should.be.true() // temp/allowed/file.txt
|
||||
// results[2].should.be.true() // temp/allowed/nested/file.txt
|
||||
// results[3].should.be.false() // docs/guide.md
|
||||
// results[4].should.be.true() // docs/README.md
|
||||
// results[5].should.be.true() // docs/CONTRIBUTING.md
|
||||
// results[6].should.be.false() // docs/api/guide.md
|
||||
// results[7].should.be.false() // assets/logo.png
|
||||
// results[8].should.be.true() // assets/public/logo.png
|
||||
// results[9].should.be.true() // assets/public/data.json
|
||||
// })
|
||||
|
||||
it("should handle comments in .clineignore", async () => {
|
||||
// Create a new .clineignore with comments
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, ".clineignore"),
|
||||
["# Comment line", "*.secret", "private/", "temp.*"].join("\n"),
|
||||
)
|
||||
|
||||
controller = new ClineIgnoreController(tempDir)
|
||||
await controller.initialize()
|
||||
|
||||
const result = controller.validateAccess("test.secret")
|
||||
result.should.be.false()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Path Handling", () => {
|
||||
it("should handle absolute paths and match ignore patterns", async () => {
|
||||
// Test absolute path that should be allowed
|
||||
const allowedPath = path.join(tempDir, "src/file.ts")
|
||||
const allowedResult = controller.validateAccess(allowedPath)
|
||||
allowedResult.should.be.true()
|
||||
|
||||
// Test absolute path that matches an ignore pattern (*.secret)
|
||||
const ignoredPath = path.join(tempDir, "config.secret")
|
||||
const ignoredResult = controller.validateAccess(ignoredPath)
|
||||
ignoredResult.should.be.false()
|
||||
|
||||
// Test absolute path in ignored directory (private/)
|
||||
const ignoredDirPath = path.join(tempDir, "private/data.txt")
|
||||
const ignoredDirResult = controller.validateAccess(ignoredDirPath)
|
||||
ignoredDirResult.should.be.false()
|
||||
})
|
||||
|
||||
it("should handle relative paths and match ignore patterns", async () => {
|
||||
// Test relative path that should be allowed
|
||||
const allowedResult = controller.validateAccess("./src/file.ts")
|
||||
allowedResult.should.be.true()
|
||||
|
||||
// Test relative path that matches an ignore pattern (*.secret)
|
||||
const ignoredResult = controller.validateAccess("./config.secret")
|
||||
ignoredResult.should.be.false()
|
||||
|
||||
// Test relative path in ignored directory (private/)
|
||||
const ignoredDirResult = controller.validateAccess("./private/data.txt")
|
||||
ignoredDirResult.should.be.false()
|
||||
})
|
||||
|
||||
it("should normalize paths with backslashes", async () => {
|
||||
const result = controller.validateAccess("src\\file.ts")
|
||||
result.should.be.true()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Batch Filtering", () => {
|
||||
it("should filter an array of paths", async () => {
|
||||
const paths = ["src/index.ts", ".env", "lib/utils.ts", ".git/config", "dist/bundle.js"]
|
||||
|
||||
const filtered = controller.filterPaths(paths)
|
||||
filtered.should.deepEqual(["src/index.ts", "lib/utils.ts", "dist/bundle.js"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle invalid paths", async () => {
|
||||
// Test with an invalid path containing null byte
|
||||
const result = controller.validateAccess("\0invalid")
|
||||
result.should.be.true()
|
||||
})
|
||||
|
||||
it("should handle missing .clineignore gracefully", async () => {
|
||||
// Create a new controller in a directory without .clineignore
|
||||
const emptyDir = path.join(os.tmpdir(), `llm-test-empty-${Date.now()}`)
|
||||
await fs.mkdir(emptyDir)
|
||||
|
||||
try {
|
||||
const controller = new ClineIgnoreController(emptyDir)
|
||||
await controller.initialize()
|
||||
const result = controller.validateAccess("file.txt")
|
||||
result.should.be.true()
|
||||
} finally {
|
||||
await fs.rm(emptyDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle empty .clineignore", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".clineignore"), "")
|
||||
|
||||
controller = new ClineIgnoreController(tempDir)
|
||||
await controller.initialize()
|
||||
|
||||
const result = controller.validateAccess("regular-file.txt")
|
||||
result.should.be.true()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,189 @@
|
||||
import path from "path"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import ignore, { Ignore } from "ignore"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
export const LOCK_TEXT_SYMBOL = "\u{1F512}"
|
||||
|
||||
/**
|
||||
* Controls LLM access to files by enforcing ignore patterns.
|
||||
* Designed to be instantiated once in Cline.ts and passed to file manipulation services.
|
||||
* Uses the 'ignore' library to support standard .gitignore syntax in .clineignore files.
|
||||
*/
|
||||
export class ClineIgnoreController {
|
||||
private cwd: string
|
||||
private ignoreInstance: Ignore
|
||||
private disposables: vscode.Disposable[] = []
|
||||
clineIgnoreContent: string | undefined
|
||||
|
||||
constructor(cwd: string) {
|
||||
this.cwd = cwd
|
||||
this.ignoreInstance = ignore()
|
||||
this.clineIgnoreContent = undefined
|
||||
// Set up file watcher for .clineignore
|
||||
this.setupFileWatcher()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the controller by loading custom patterns
|
||||
* Must be called after construction and before using the controller
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
await this.loadClineIgnore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the file watcher for .clineignore changes
|
||||
*/
|
||||
private setupFileWatcher(): void {
|
||||
const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore")
|
||||
const fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern)
|
||||
|
||||
// Watch for changes and updates
|
||||
this.disposables.push(
|
||||
fileWatcher.onDidChange(() => {
|
||||
this.loadClineIgnore()
|
||||
}),
|
||||
fileWatcher.onDidCreate(() => {
|
||||
this.loadClineIgnore()
|
||||
}),
|
||||
fileWatcher.onDidDelete(() => {
|
||||
this.loadClineIgnore()
|
||||
}),
|
||||
)
|
||||
|
||||
// Add fileWatcher itself to disposables
|
||||
this.disposables.push(fileWatcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load custom patterns from .clineignore if it exists
|
||||
*/
|
||||
private async loadClineIgnore(): Promise<void> {
|
||||
try {
|
||||
// Reset ignore instance to prevent duplicate patterns
|
||||
this.ignoreInstance = ignore()
|
||||
const ignorePath = path.join(this.cwd, ".clineignore")
|
||||
if (await fileExistsAtPath(ignorePath)) {
|
||||
const content = await fs.readFile(ignorePath, "utf8")
|
||||
this.clineIgnoreContent = content
|
||||
this.ignoreInstance.add(content)
|
||||
this.ignoreInstance.add(".clineignore")
|
||||
} else {
|
||||
this.clineIgnoreContent = undefined
|
||||
}
|
||||
} catch (error) {
|
||||
// Should never happen: reading file failed even though it exists
|
||||
console.error("Unexpected error loading .clineignore:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file should be accessible to the LLM
|
||||
* @param filePath - Path to check (relative to cwd)
|
||||
* @returns true if file is accessible, false if ignored
|
||||
*/
|
||||
validateAccess(filePath: string): boolean {
|
||||
// Always allow access if .clineignore does not exist
|
||||
if (!this.clineIgnoreContent) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
// Normalize path to be relative to cwd and use forward slashes
|
||||
const absolutePath = path.resolve(this.cwd, filePath)
|
||||
const relativePath = path.relative(this.cwd, absolutePath).toPosix()
|
||||
|
||||
// Ignore expects paths to be path.relative()'d
|
||||
return !this.ignoreInstance.ignores(relativePath)
|
||||
} catch (error) {
|
||||
// console.error(`Error validating access for ${filePath}:`, error)
|
||||
// Ignore is designed to work with relative file paths, so will throw error for paths outside cwd. We are allowing access to all files outside cwd.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a terminal command should be allowed to execute based on file access patterns
|
||||
* @param command - Terminal command to validate
|
||||
* @returns path of file that is being accessed if it is being accessed, undefined if command is allowed
|
||||
*/
|
||||
validateCommand(command: string): string | undefined {
|
||||
// Always allow if no .clineignore exists
|
||||
if (!this.clineIgnoreContent) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Split command into parts and get the base command
|
||||
const parts = command.trim().split(/\s+/)
|
||||
const baseCommand = parts[0].toLowerCase()
|
||||
|
||||
// Commands that read file contents
|
||||
const fileReadingCommands = [
|
||||
// Unix commands
|
||||
"cat",
|
||||
"less",
|
||||
"more",
|
||||
"head",
|
||||
"tail",
|
||||
"grep",
|
||||
"awk",
|
||||
"sed",
|
||||
// PowerShell commands and aliases
|
||||
"get-content",
|
||||
"gc",
|
||||
"type",
|
||||
"select-string",
|
||||
"sls",
|
||||
]
|
||||
|
||||
if (fileReadingCommands.includes(baseCommand)) {
|
||||
// Check each argument that could be a file path
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const arg = parts[i]
|
||||
// Skip command flags/options (both Unix and PowerShell style)
|
||||
if (arg.startsWith("-") || arg.startsWith("/")) {
|
||||
continue
|
||||
}
|
||||
// Ignore PowerShell parameter names
|
||||
if (arg.includes(":")) {
|
||||
continue
|
||||
}
|
||||
// Validate file access
|
||||
if (!this.validateAccess(arg)) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter an array of paths, removing those that should be ignored
|
||||
* @param paths - Array of paths to filter (relative to cwd)
|
||||
* @returns Array of allowed paths
|
||||
*/
|
||||
filterPaths(paths: string[]): string[] {
|
||||
try {
|
||||
return paths
|
||||
.map((p) => ({
|
||||
path: p,
|
||||
allowed: this.validateAccess(p),
|
||||
}))
|
||||
.filter((x) => x.allowed)
|
||||
.map((x) => x.path)
|
||||
} catch (error) {
|
||||
console.error("Error filtering paths:", error)
|
||||
return [] // Fail closed for security
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when the controller is no longer needed
|
||||
*/
|
||||
dispose(): void {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.disposables = []
|
||||
}
|
||||
}
|
||||
@@ -7,27 +7,32 @@ import fs from "fs/promises"
|
||||
import { extractTextFromFile } from "../../integrations/misc/extract-text"
|
||||
import { isBinaryFile } from "isbinaryfile"
|
||||
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
|
||||
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
|
||||
import { getCommitInfo } from "../../utils/git"
|
||||
import { getWorkingState } from "../../utils/git"
|
||||
|
||||
export function openMention(mention?: string): void {
|
||||
if (!mention) {
|
||||
return
|
||||
}
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mention.startsWith("/")) {
|
||||
const relPath = mention.slice(1)
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
const absPath = path.resolve(cwd, relPath)
|
||||
if (mention.endsWith("/")) {
|
||||
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
|
||||
// vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window
|
||||
} else {
|
||||
openFile(absPath)
|
||||
}
|
||||
} else if (mention === "problems") {
|
||||
vscode.commands.executeCommand("workbench.actions.view.problems")
|
||||
} else if (mention === "terminal") {
|
||||
vscode.commands.executeCommand("workbench.action.terminal.focus")
|
||||
} else if (mention.startsWith("http")) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(mention))
|
||||
}
|
||||
@@ -46,6 +51,12 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
|
||||
: `'${mentionPath}' (see below for file content)`
|
||||
} else if (mention === "problems") {
|
||||
return `Workspace Problems (see below for diagnostics)`
|
||||
} else if (mention === "terminal") {
|
||||
return `Terminal Output (see below for output)`
|
||||
} else if (mention === "git-changes") {
|
||||
return `Working directory changes (see below for details)`
|
||||
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
|
||||
return `Git commit '${mention}' (see below for commit info)`
|
||||
}
|
||||
return match
|
||||
})
|
||||
@@ -99,6 +110,27 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
|
||||
}
|
||||
} else if (mention === "terminal") {
|
||||
try {
|
||||
const terminalOutput = await getLatestTerminalOutput()
|
||||
parsedText += `\n\n<terminal_output>\n${terminalOutput}\n</terminal_output>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
|
||||
}
|
||||
} else if (mention === "git-changes") {
|
||||
try {
|
||||
const workingState = await getWorkingState(cwd)
|
||||
parsedText += `\n\n<git_working_state>\n${workingState}\n</git_working_state>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_working_state>\nError fetching working state: ${error.message}\n</git_working_state>`
|
||||
}
|
||||
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
|
||||
try {
|
||||
const commitInfo = await getCommitInfo(mention, cwd)
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\n${commitInfo}\n</git_commit>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as path from "path"
|
||||
import * as diff from "diff"
|
||||
import * as path from "path"
|
||||
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
|
||||
|
||||
export const formatResponse = {
|
||||
toolDenied: () => `The user denied this operation.`,
|
||||
|
||||
toolDeniedWithFeedback: (feedback?: string) =>
|
||||
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
|
||||
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
||||
|
||||
clineIgnoreError: (path: string) =>
|
||||
`Access to ${path} is blocked by the .clineignore file settings. You must try to continue in the task without using this file, or ask the user to update the .clineignore file.`,
|
||||
|
||||
noToolsUsed: () =>
|
||||
`[ERROR] You did not use a tool in your previous response! Please retry with a tool use.
|
||||
|
||||
@@ -46,7 +47,12 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
return formatImagesIntoBlocks(images)
|
||||
},
|
||||
|
||||
formatFilesList: (absolutePath: string, files: string[], didHitLimit: boolean): string => {
|
||||
formatFilesList: (
|
||||
absolutePath: string,
|
||||
files: string[],
|
||||
didHitLimit: boolean,
|
||||
clineIgnoreController?: ClineIgnoreController,
|
||||
): string => {
|
||||
const sorted = files
|
||||
.map((file) => {
|
||||
// convert absolute path to relative path
|
||||
@@ -77,14 +83,30 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
// the shorter one comes first
|
||||
return aParts.length - bParts.length
|
||||
})
|
||||
|
||||
const clineIgnoreParsed = clineIgnoreController
|
||||
? sorted.map((filePath) => {
|
||||
// path is relative to absolute path, not cwd
|
||||
// validateAccess expects either path relative to cwd or absolute path
|
||||
// otherwise, for validating against ignore patterns like "assets/icons", we would end up with just "icons", which would result in the path not being ignored.
|
||||
const absoluteFilePath = path.resolve(absolutePath, filePath)
|
||||
const isIgnored = !clineIgnoreController.validateAccess(absoluteFilePath)
|
||||
if (isIgnored) {
|
||||
return LOCK_TEXT_SYMBOL + " " + filePath
|
||||
}
|
||||
|
||||
return filePath
|
||||
})
|
||||
: sorted
|
||||
|
||||
if (didHitLimit) {
|
||||
return `${sorted.join(
|
||||
return `${clineIgnoreParsed.join(
|
||||
"\n",
|
||||
)}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)`
|
||||
} else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) {
|
||||
} else if (clineIgnoreParsed.length === 0 || (clineIgnoreParsed.length === 1 && clineIgnoreParsed[0] === "")) {
|
||||
return "No files found."
|
||||
} else {
|
||||
return sorted.join("\n")
|
||||
return clineIgnoreParsed.join("\n")
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -331,7 +331,24 @@ ${
|
||||
<access_mcp_resource>
|
||||
<server_name>weather-server</server_name>
|
||||
<uri>weather://san-francisco/current</uri>
|
||||
</access_mcp_resource>`
|
||||
</access_mcp_resource>
|
||||
|
||||
## Example 6: Another example of using an MCP tool (where the server name is a unique identifier such as a URL)
|
||||
|
||||
<use_mcp_tool>
|
||||
<server_name>github.com/modelcontextprotocol/servers/tree/main/src/github</server_name>
|
||||
<tool_name>create_issue</tool_name>
|
||||
<arguments>
|
||||
{
|
||||
"owner": "octocat",
|
||||
"repo": "hello-world",
|
||||
"title": "Found a bug",
|
||||
"body": "I'm having a problem with this.",
|
||||
"labels": ["bug", "help wanted"],
|
||||
"assignees": ["octocat"]
|
||||
}
|
||||
</arguments>
|
||||
</use_mcp_tool>`
|
||||
: ""
|
||||
}
|
||||
|
||||
@@ -750,7 +767,7 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
|
||||
|
||||
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
|
||||
|
||||
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section.
|
||||
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section. (Note: If you encounter a 'not connected' error when testing a newly installed mcp server, a common cause is an incorrect build path in your MCP settings configuration. Since compiled JavaScript files are commonly output to either 'dist/' or 'build/' directories, double-check that the build path in your MCP settings matches where your files are actually being compiled. E.g. If you assumed 'build' as the folder, check tsconfig.json to see if it's using 'dist' instead.)
|
||||
|
||||
7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?"
|
||||
|
||||
@@ -865,9 +882,10 @@ In each user message, the environment_details will specify the current mode. The
|
||||
## What is PLAN MODE?
|
||||
|
||||
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task.
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. You may return mermaid diagrams to visually display your understanding.
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Returning mermaid diagrams may be helpful here as well.
|
||||
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
|
||||
- If at any point a mermaid diagram would make your plan clearer to help the user quickly see the structure, you are encouraged to include a Mermaid code block in the response. (Note: if you use colors in your mermaid diagrams, be sure to use high contrast colors so the text is readable.)
|
||||
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
|
||||
|
||||
====
|
||||
@@ -941,7 +959,7 @@ ${
|
||||
SYSTEM INFORMATION
|
||||
|
||||
Operating System: ${osName()}
|
||||
Default Shell: ${defaultShell}
|
||||
Default Shell: ${getShell()}
|
||||
Home Directory: ${os.homedir().toPosix()}
|
||||
Current Working Directory: ${cwd.toPosix()}
|
||||
|
||||
@@ -957,13 +975,20 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
|
||||
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
|
||||
|
||||
export function addUserInstructions(settingsCustomInstructions?: string, clineRulesFileInstructions?: string) {
|
||||
export function addUserInstructions(
|
||||
settingsCustomInstructions?: string,
|
||||
clineRulesFileInstructions?: string,
|
||||
clineIgnoreInstructions?: string,
|
||||
) {
|
||||
let customInstructions = ""
|
||||
if (settingsCustomInstructions) {
|
||||
customInstructions += settingsCustomInstructions + "\n\n"
|
||||
}
|
||||
if (clineRulesFileInstructions) {
|
||||
customInstructions += clineRulesFileInstructions
|
||||
customInstructions += clineRulesFileInstructions + "\n\n"
|
||||
}
|
||||
if (clineIgnoreInstructions) {
|
||||
customInstructions += clineIgnoreInstructions
|
||||
}
|
||||
|
||||
return `
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ import { selectImages } from "../../integrations/misc/process-images"
|
||||
import { getTheme } from "../../integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { McpDownloadResponse, McpMarketplaceCatalog, McpMarketplaceItem, McpServer } from "../../shared/mcp"
|
||||
import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager"
|
||||
import { ApiProvider, ModelInfo } from "../../shared/api"
|
||||
import { findLast } from "../../shared/array"
|
||||
import { ExtensionMessage, ExtensionState } from "../../shared/ExtensionMessage"
|
||||
import { ExtensionMessage, ExtensionState, Platform } from "../../shared/ExtensionMessage"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { ClineCheckpointRestore, WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
@@ -28,6 +29,9 @@ import { getUri } from "./getUri"
|
||||
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
|
||||
import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings"
|
||||
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings"
|
||||
import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"
|
||||
import { searchCommits } from "../../utils/git"
|
||||
import { ChatContent } from "../../shared/ChatContent"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -45,7 +49,11 @@ type SecretKey =
|
||||
| "geminiApiKey"
|
||||
| "openAiNativeApiKey"
|
||||
| "deepSeekApiKey"
|
||||
| "requestyApiKey"
|
||||
| "togetherApiKey"
|
||||
| "qwenApiKey"
|
||||
| "mistralApiKey"
|
||||
| "liteLlmApiKey"
|
||||
| "authToken"
|
||||
| "authNonce"
|
||||
type GlobalStateKey =
|
||||
@@ -53,6 +61,8 @@ type GlobalStateKey =
|
||||
| "apiModelId"
|
||||
| "awsRegion"
|
||||
| "awsUseCrossRegionInference"
|
||||
| "awsProfile"
|
||||
| "awsUseProfile"
|
||||
| "vertexProjectId"
|
||||
| "vertexRegion"
|
||||
| "lastShownAnnouncementId"
|
||||
@@ -60,6 +70,7 @@ type GlobalStateKey =
|
||||
| "taskHistory"
|
||||
| "openAiBaseUrl"
|
||||
| "openAiModelId"
|
||||
| "openAiModelInfo"
|
||||
| "ollamaModelId"
|
||||
| "ollamaBaseUrl"
|
||||
| "lmStudioModelId"
|
||||
@@ -76,6 +87,12 @@ type GlobalStateKey =
|
||||
| "previousModeApiProvider"
|
||||
| "previousModeModelId"
|
||||
| "previousModeModelInfo"
|
||||
| "liteLlmBaseUrl"
|
||||
| "liteLlmModelId"
|
||||
| "qwenApiLine"
|
||||
| "requestyModelId"
|
||||
| "togetherModelId"
|
||||
| "mcpMarketplaceCatalog"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
@@ -92,10 +109,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private view?: vscode.WebviewView | vscode.WebviewPanel
|
||||
private cline?: Cline
|
||||
private workspaceTracker?: WorkspaceTracker
|
||||
workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
private authManager: FirebaseAuthManager
|
||||
private latestAnnouncementId = "jan-20-2025" // update to some unique identifier when we add a new announcement
|
||||
private latestAnnouncementId = "feb-19-2025" // update to some unique identifier when we add a new announcement
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -174,7 +191,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
|
||||
@@ -223,7 +240,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.disposables,
|
||||
)
|
||||
|
||||
// Listen for when color changes
|
||||
// Listen for configuration changes
|
||||
vscode.workspace.onDidChangeConfiguration(
|
||||
async (e) => {
|
||||
if (e && e.affectsConfiguration("workbench.colorTheme")) {
|
||||
@@ -233,6 +250,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
text: JSON.stringify(await getTheme()),
|
||||
})
|
||||
}
|
||||
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
|
||||
// Update state when marketplace tab setting changes
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
},
|
||||
null,
|
||||
this.disposables,
|
||||
@@ -245,7 +266,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(
|
||||
@@ -324,15 +345,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
// Use a nonce to only allow a specific script to be run.
|
||||
/*
|
||||
content security policy of your webview to only allow scripts that have a specific nonce
|
||||
create a content security policy meta tag so that only loading scripts with a nonce is allowed
|
||||
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
||||
content security policy of your webview to only allow scripts that have a specific nonce
|
||||
create a content security policy meta tag so that only loading scripts with a nonce is allowed
|
||||
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
||||
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
|
||||
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
|
||||
|
||||
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
|
||||
*/
|
||||
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
|
||||
*/
|
||||
const nonce = getNonce()
|
||||
|
||||
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
|
||||
@@ -359,7 +380,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
|
||||
*/
|
||||
@@ -369,7 +390,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
switch (message.type) {
|
||||
case "webviewDidLaunch":
|
||||
this.postStateToWebview()
|
||||
this.workspaceTracker?.initializeFilePaths() // don't await
|
||||
this.workspaceTracker?.populateFilePaths() // don't await
|
||||
getTheme().then((theme) =>
|
||||
this.postMessageToWebview({
|
||||
type: "theme",
|
||||
@@ -388,6 +409,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
|
||||
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
|
||||
// (see normalizeApiConfiguration > openrouter)
|
||||
// Prefetch marketplace and OpenRouter models
|
||||
|
||||
this.getGlobalState("mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
|
||||
if (mcpMarketplaceCatalog) {
|
||||
this.postMessageToWebview({
|
||||
type: "mcpMarketplaceCatalog",
|
||||
mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog,
|
||||
})
|
||||
}
|
||||
})
|
||||
this.silentlyRefreshMcpMarketplace()
|
||||
this.refreshOpenRouterModels().then(async (openRouterModels) => {
|
||||
if (openRouterModels) {
|
||||
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
@@ -401,6 +433,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
case "newTask":
|
||||
// Code that should run in response to the hello message command
|
||||
@@ -425,11 +458,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
@@ -438,11 +474,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmApiKey,
|
||||
qwenApiLine,
|
||||
} = message.apiConfiguration
|
||||
await this.updateGlobalState("apiProvider", apiProvider)
|
||||
await this.updateGlobalState("apiModelId", apiModelId)
|
||||
@@ -453,11 +498,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.storeSecret("awsSessionToken", awsSessionToken)
|
||||
await this.updateGlobalState("awsRegion", awsRegion)
|
||||
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
|
||||
await this.updateGlobalState("awsProfile", awsProfile)
|
||||
await this.updateGlobalState("awsUseProfile", awsUseProfile)
|
||||
await this.updateGlobalState("vertexProjectId", vertexProjectId)
|
||||
await this.updateGlobalState("vertexRegion", vertexRegion)
|
||||
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
|
||||
await this.storeSecret("openAiApiKey", openAiApiKey)
|
||||
await this.updateGlobalState("openAiModelId", openAiModelId)
|
||||
await this.updateGlobalState("openAiModelInfo", openAiModelInfo)
|
||||
await this.updateGlobalState("ollamaModelId", ollamaModelId)
|
||||
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
|
||||
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
|
||||
@@ -466,11 +514,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.storeSecret("geminiApiKey", geminiApiKey)
|
||||
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
|
||||
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
|
||||
await this.storeSecret("requestyApiKey", requestyApiKey)
|
||||
await this.storeSecret("togetherApiKey", togetherApiKey)
|
||||
await this.storeSecret("qwenApiKey", qwenApiKey)
|
||||
await this.storeSecret("mistralApiKey", mistralApiKey)
|
||||
await this.storeSecret("liteLlmApiKey", liteLlmApiKey)
|
||||
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)
|
||||
await this.updateGlobalState("requestyModelId", requestyModelId)
|
||||
await this.updateGlobalState("togetherModelId", togetherModelId)
|
||||
if (this.cline) {
|
||||
this.cline.api = buildApiHandler(message.apiConfiguration)
|
||||
}
|
||||
@@ -498,90 +555,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
case "chatSettings":
|
||||
case "togglePlanActMode":
|
||||
if (message.chatSettings) {
|
||||
const didSwitchToActMode = message.chatSettings.mode === "act"
|
||||
|
||||
// Get previous model info that we will revert to after saving current mode api info
|
||||
const {
|
||||
apiConfiguration,
|
||||
previousModeApiProvider: newApiProvider,
|
||||
previousModeModelId: newModelId,
|
||||
previousModeModelInfo: newModelInfo,
|
||||
} = await this.getState()
|
||||
|
||||
// Save the last model used in this mode
|
||||
await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider)
|
||||
switch (apiConfiguration.apiProvider) {
|
||||
case "anthropic":
|
||||
case "bedrock":
|
||||
case "vertex":
|
||||
case "gemini":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
|
||||
break
|
||||
case "openrouter":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
|
||||
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
|
||||
break
|
||||
case "vscode-lm":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
|
||||
break
|
||||
case "openai":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
|
||||
break
|
||||
case "ollama":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
|
||||
break
|
||||
case "lmstudio":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
|
||||
break
|
||||
}
|
||||
|
||||
// Restore the model used in previous mode
|
||||
if (newApiProvider && newModelId) {
|
||||
await this.updateGlobalState("apiProvider", newApiProvider)
|
||||
switch (newApiProvider) {
|
||||
case "anthropic":
|
||||
case "bedrock":
|
||||
case "vertex":
|
||||
case "gemini":
|
||||
await this.updateGlobalState("apiModelId", newModelId)
|
||||
break
|
||||
case "openrouter":
|
||||
await this.updateGlobalState("openRouterModelId", newModelId)
|
||||
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
|
||||
break
|
||||
case "vscode-lm":
|
||||
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
|
||||
break
|
||||
case "openai":
|
||||
await this.updateGlobalState("openAiModelId", newModelId)
|
||||
break
|
||||
case "ollama":
|
||||
await this.updateGlobalState("ollamaModelId", newModelId)
|
||||
break
|
||||
case "lmstudio":
|
||||
await this.updateGlobalState("lmStudioModelId", newModelId)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateGlobalState("chatSettings", message.chatSettings)
|
||||
await this.postStateToWebview()
|
||||
if (this.cline) {
|
||||
this.cline.updateChatSettings(message.chatSettings)
|
||||
if (this.cline.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
this.cline.didRespondToPlanAskBySwitchingMode = true
|
||||
// this is necessary for the webview to update accordingly, but Cline instance will not send text back as feedback message
|
||||
await this.postMessageToWebview({
|
||||
type: "invoke",
|
||||
invoke: "sendMessage",
|
||||
text: "[Proceeding with the task...]",
|
||||
})
|
||||
} else {
|
||||
this.cancelTask()
|
||||
}
|
||||
}
|
||||
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
|
||||
}
|
||||
break
|
||||
// case "relaunchChromeDebugMode":
|
||||
@@ -721,6 +697,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.handleSignOut()
|
||||
break
|
||||
}
|
||||
case "showMcpView": {
|
||||
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
|
||||
break
|
||||
}
|
||||
case "openMcpSettings": {
|
||||
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
|
||||
if (mcpSettingsFilePath) {
|
||||
@@ -728,6 +708,69 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "fetchMcpMarketplace": {
|
||||
await this.fetchMcpMarketplace(message.bool)
|
||||
break
|
||||
}
|
||||
case "downloadMcp": {
|
||||
if (message.mcpId) {
|
||||
// 1. Toggle to act mode if we are in plan mode
|
||||
const { chatSettings } = await this.getStateToPostToWebview()
|
||||
if (chatSettings.mode === "plan") {
|
||||
await this.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
}
|
||||
|
||||
// 2. Enable MCP settings if disabled
|
||||
// Enable MCP mode if disabled
|
||||
const mcpConfig = vscode.workspace.getConfiguration("cline.mcp")
|
||||
if (mcpConfig.get<string>("mode") !== "full") {
|
||||
await mcpConfig.update("mode", "full", true)
|
||||
}
|
||||
|
||||
// 3. download MCP
|
||||
await this.downloadMcp(message.mcpId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "silentlyRefreshMcpMarketplace": {
|
||||
await this.silentlyRefreshMcpMarketplace()
|
||||
break
|
||||
}
|
||||
// case "openMcpMarketplaceServerDetails": {
|
||||
// if (message.text) {
|
||||
// const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`)
|
||||
// const details: McpDownloadResponse = await response.json()
|
||||
|
||||
// if (details.readmeContent) {
|
||||
// // Disable markdown preview markers
|
||||
// const config = vscode.workspace.getConfiguration("markdown")
|
||||
// await config.update("preview.markEditorSelection", false, true)
|
||||
|
||||
// // Create URI with base64 encoded markdown content
|
||||
// const uri = vscode.Uri.parse(
|
||||
// `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`,
|
||||
// )
|
||||
|
||||
// // close existing
|
||||
// const tabs = vscode.window.tabGroups.all
|
||||
// .flatMap((tg) => tg.tabs)
|
||||
// .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview"))
|
||||
// for (const tab of tabs) {
|
||||
// await vscode.window.tabGroups.close(tab)
|
||||
// }
|
||||
|
||||
// // Show only the preview
|
||||
// await vscode.commands.executeCommand("markdown.showPreview", uri, {
|
||||
// sideBySide: true,
|
||||
// preserveFocus: true,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// this.postMessageToWebview({ type: "relinquishControl" })
|
||||
|
||||
// break
|
||||
// }
|
||||
case "toggleMcpServer": {
|
||||
try {
|
||||
await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!)
|
||||
@@ -752,6 +795,31 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "deleteMcpServer": {
|
||||
if (message.serverName) {
|
||||
this.mcpHub?.deleteServer(message.serverName)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "fetchLatestMcpServersFromHub": {
|
||||
this.mcpHub?.sendLatestMcpServers()
|
||||
break
|
||||
}
|
||||
case "searchCommits": {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (cwd) {
|
||||
try {
|
||||
const commits = await searchCommits(message.text || "", cwd)
|
||||
await this.postMessageToWebview({
|
||||
type: "commitSearchResults",
|
||||
commits,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error searching commits: ${JSON.stringify(error)}`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "openExtensionSettings": {
|
||||
const settingsFilter = message.text || ""
|
||||
await vscode.commands.executeCommand(
|
||||
@@ -769,6 +837,106 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
)
|
||||
}
|
||||
|
||||
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent) {
|
||||
const didSwitchToActMode = chatSettings.mode === "act"
|
||||
|
||||
// Get previous model info that we will revert to after saving current mode api info
|
||||
const {
|
||||
apiConfiguration,
|
||||
previousModeApiProvider: newApiProvider,
|
||||
previousModeModelId: newModelId,
|
||||
previousModeModelInfo: newModelInfo,
|
||||
} = await this.getState()
|
||||
|
||||
// Save the last model used in this mode
|
||||
await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider)
|
||||
switch (apiConfiguration.apiProvider) {
|
||||
case "anthropic":
|
||||
case "bedrock":
|
||||
case "vertex":
|
||||
case "gemini":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
|
||||
break
|
||||
case "openrouter":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
|
||||
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
|
||||
break
|
||||
case "vscode-lm":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
|
||||
break
|
||||
case "openai":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
|
||||
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openAiModelInfo)
|
||||
break
|
||||
case "ollama":
|
||||
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
|
||||
break
|
||||
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
|
||||
if (newApiProvider && newModelId) {
|
||||
await this.updateGlobalState("apiProvider", newApiProvider)
|
||||
switch (newApiProvider) {
|
||||
case "anthropic":
|
||||
case "bedrock":
|
||||
case "vertex":
|
||||
case "gemini":
|
||||
await this.updateGlobalState("apiModelId", newModelId)
|
||||
break
|
||||
case "openrouter":
|
||||
await this.updateGlobalState("openRouterModelId", newModelId)
|
||||
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
|
||||
break
|
||||
case "vscode-lm":
|
||||
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
|
||||
break
|
||||
case "openai":
|
||||
await this.updateGlobalState("openAiModelId", newModelId)
|
||||
await this.updateGlobalState("openAiModelInfo", newModelInfo)
|
||||
break
|
||||
case "ollama":
|
||||
await this.updateGlobalState("ollamaModelId", newModelId)
|
||||
break
|
||||
case "lmstudio":
|
||||
await this.updateGlobalState("lmStudioModelId", newModelId)
|
||||
break
|
||||
case "litellm":
|
||||
await this.updateGlobalState("liteLlmModelId", newModelId)
|
||||
break
|
||||
}
|
||||
|
||||
if (this.cline) {
|
||||
const { apiConfiguration: updatedApiConfiguration } = await this.getState()
|
||||
this.cline.api = buildApiHandler(updatedApiConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateGlobalState("chatSettings", chatSettings)
|
||||
await this.postStateToWebview()
|
||||
// console.log("chatSettings", message.chatSettings)
|
||||
if (this.cline) {
|
||||
this.cline.updateChatSettings(chatSettings)
|
||||
if (this.cline.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
this.cline.didRespondToPlanAskBySwitchingMode = true
|
||||
// this is necessary for the webview to update accordingly, but Cline instance will not send text back as feedback message
|
||||
await this.postMessageToWebview({
|
||||
type: "invoke",
|
||||
invoke: "sendMessage",
|
||||
text: chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
|
||||
images: chatContent?.images,
|
||||
})
|
||||
} else {
|
||||
this.cancelTask()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async subscribeEmail(email?: string) {
|
||||
if (!email) {
|
||||
return
|
||||
@@ -951,6 +1119,171 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// MCP Marketplace
|
||||
|
||||
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
|
||||
try {
|
||||
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error("Invalid response from MCP marketplace API")
|
||||
}
|
||||
|
||||
const catalog: McpMarketplaceCatalog = {
|
||||
items: (response.data || []).map((item: any) => ({
|
||||
...item,
|
||||
githubStars: item.githubStars ?? 0,
|
||||
downloadCount: item.downloadCount ?? 0,
|
||||
tags: item.tags ?? [],
|
||||
})),
|
||||
}
|
||||
|
||||
// Store in global state
|
||||
await this.updateGlobalState("mcpMarketplaceCatalog", catalog)
|
||||
return catalog
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch MCP marketplace:", error)
|
||||
if (!silent) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpMarketplaceCatalog",
|
||||
error: errorMessage,
|
||||
})
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async silentlyRefreshMcpMarketplace() {
|
||||
try {
|
||||
const catalog = await this.fetchMcpMarketplaceFromApi(true)
|
||||
if (catalog) {
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpMarketplaceCatalog",
|
||||
mcpMarketplaceCatalog: catalog,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to silently refresh MCP marketplace:", error)
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchMcpMarketplace(forceRefresh: boolean = false) {
|
||||
try {
|
||||
// Check if we have cached data
|
||||
const cachedCatalog = (await this.getGlobalState("mcpMarketplaceCatalog")) as McpMarketplaceCatalog | undefined
|
||||
if (!forceRefresh && cachedCatalog?.items) {
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpMarketplaceCatalog",
|
||||
mcpMarketplaceCatalog: cachedCatalog,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const catalog = await this.fetchMcpMarketplaceFromApi(false)
|
||||
if (catalog) {
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpMarketplaceCatalog",
|
||||
mcpMarketplaceCatalog: catalog,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to handle cached MCP marketplace:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpMarketplaceCatalog",
|
||||
error: errorMessage,
|
||||
})
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadMcp(mcpId: string) {
|
||||
try {
|
||||
// First check if we already have this MCP server installed
|
||||
const servers = this.mcpHub?.getServers() || []
|
||||
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
|
||||
|
||||
if (isInstalled) {
|
||||
throw new Error("This MCP server is already installed")
|
||||
}
|
||||
|
||||
// Fetch server details from marketplace
|
||||
const response = await axios.post<McpDownloadResponse>(
|
||||
"https://api.cline.bot/v1/mcp/download",
|
||||
{ mcpId },
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 10000,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error("Invalid response from MCP marketplace API")
|
||||
}
|
||||
|
||||
console.log("[downloadMcp] Response from download API", { response })
|
||||
|
||||
const mcpDetails = response.data
|
||||
|
||||
// Validate required fields
|
||||
if (!mcpDetails.githubUrl) {
|
||||
throw new Error("Missing GitHub URL in MCP download response")
|
||||
}
|
||||
if (!mcpDetails.readmeContent) {
|
||||
throw new Error("Missing README content in MCP download response")
|
||||
}
|
||||
|
||||
// Send details to webview
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
mcpDownloadDetails: mcpDetails,
|
||||
})
|
||||
|
||||
// Create task with context from README
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl}.
|
||||
Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
|
||||
Once installed, demonstrate the server's capabilities by using one of its tools.
|
||||
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
|
||||
|
||||
// Initialize task and show chat view
|
||||
await this.initClineWithTask(task)
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to download MCP:", error)
|
||||
let errorMessage = "Failed to download MCP"
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === "ECONNABORTED") {
|
||||
errorMessage = "Request timed out. Please try again."
|
||||
} else if (error.response?.status === 404) {
|
||||
errorMessage = "MCP server not found in marketplace."
|
||||
} else if (error.response?.status === 500) {
|
||||
errorMessage = "Internal server error. Please try again later."
|
||||
} else if (!error.response && error.request) {
|
||||
errorMessage = "Network error. Please check your internet connection."
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
// Show error in both notification and marketplace UI
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
await this.postMessageToWebview({
|
||||
type: "mcpDownloadDetails",
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAi
|
||||
|
||||
async getOpenAiModels(baseUrl?: string, apiKey?: string) {
|
||||
@@ -1252,6 +1585,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
chatSettings,
|
||||
userInfo,
|
||||
authToken,
|
||||
mcpMarketplaceEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
return {
|
||||
@@ -1264,11 +1598,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
clineMessages: this.cline?.clineMessages || [],
|
||||
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
|
||||
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
||||
platform: process.platform as Platform,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
isLoggedIn: !!authToken,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1619,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.
|
||||
|
||||
@@ -1334,11 +1670,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
@@ -1347,6 +1686,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
@@ -1358,11 +1702,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
userInfo,
|
||||
authToken,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
previousModeModelInfo,
|
||||
qwenApiLine,
|
||||
liteLlmApiKey,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||
@@ -1373,11 +1721,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.getSecret("awsSessionToken") as Promise<string | undefined>,
|
||||
this.getGlobalState("awsRegion") as Promise<string | undefined>,
|
||||
this.getGlobalState("awsUseCrossRegionInference") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("awsProfile") as Promise<string | undefined>,
|
||||
this.getGlobalState("awsUseProfile") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("vertexProjectId") as Promise<string | undefined>,
|
||||
this.getGlobalState("vertexRegion") as Promise<string | undefined>,
|
||||
this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
|
||||
this.getSecret("openAiApiKey") as Promise<string | undefined>,
|
||||
this.getGlobalState("openAiModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("openAiModelInfo") as Promise<ModelInfo | undefined>,
|
||||
this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
|
||||
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
|
||||
@@ -1386,6 +1737,11 @@ 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("requestyApiKey") as Promise<string | undefined>,
|
||||
this.getGlobalState("requestyModelId") as Promise<string | undefined>,
|
||||
this.getSecret("togetherApiKey") as Promise<string | undefined>,
|
||||
this.getGlobalState("togetherModelId") 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>,
|
||||
@@ -1397,11 +1753,15 @@ 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>,
|
||||
this.getSecret("liteLlmApiKey") as Promise<string | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -1418,6 +1778,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const o3MiniReasoningEffort = vscode.workspace
|
||||
.getConfiguration("cline.modelSettings.o3Mini")
|
||||
.get("reasoningEffort", "medium")
|
||||
|
||||
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
|
||||
|
||||
return {
|
||||
apiConfiguration: {
|
||||
apiProvider,
|
||||
@@ -1429,11 +1795,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
@@ -1442,11 +1811,21 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
qwenApiLine,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
vsCodeLmModelSelector,
|
||||
o3MiniReasoningEffort,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmApiKey,
|
||||
},
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
@@ -1459,6 +1838,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
previousModeModelInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1535,7 +1915,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
"deepSeekApiKey",
|
||||
"requestyApiKey",
|
||||
"togetherApiKey",
|
||||
"qwenApiKey",
|
||||
"mistralApiKey",
|
||||
"liteLlmApiKey",
|
||||
"authToken",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Gets the contents of the active terminal
|
||||
* @returns The terminal contents as a string
|
||||
*/
|
||||
export async function getLatestTerminalOutput(): Promise<string> {
|
||||
// Store original clipboard content to restore later
|
||||
const originalClipboard = await vscode.env.clipboard.readText()
|
||||
|
||||
try {
|
||||
// Select terminal content
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.selectAll")
|
||||
|
||||
// Copy selection to clipboard
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
|
||||
|
||||
// Clear the selection
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
|
||||
|
||||
// Get terminal contents from clipboard
|
||||
let terminalContents = (await vscode.env.clipboard.readText()).trim()
|
||||
|
||||
// Check if there's actually a terminal open
|
||||
if (terminalContents === originalClipboard) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Clean up command separation
|
||||
const lines = terminalContents.split("\n")
|
||||
const lastLine = lines.pop()?.trim()
|
||||
if (lastLine) {
|
||||
let i = lines.length - 1
|
||||
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
|
||||
i--
|
||||
}
|
||||
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
|
||||
}
|
||||
|
||||
return terminalContents
|
||||
} finally {
|
||||
// Restore original clipboard content
|
||||
await vscode.env.clipboard.writeText(originalClipboard)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class WorkspaceTracker {
|
||||
this.registerListeners()
|
||||
}
|
||||
|
||||
async initializeFilePaths() {
|
||||
async populateFilePaths() {
|
||||
// should not auto get filepaths for desktop since it would immediately show permission popup before cline ever creates a file
|
||||
if (!cwd) {
|
||||
return
|
||||
|
||||
@@ -42,11 +42,15 @@ export class BrowserSession {
|
||||
await fs.mkdir(puppeteerDir, { recursive: true })
|
||||
}
|
||||
|
||||
// if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
|
||||
// if it does exist it will return the path to existing chromium
|
||||
const stats: PCRStats = await PCR({
|
||||
downloadPath: puppeteerDir,
|
||||
})
|
||||
const chromeExecutablePath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
|
||||
if (chromeExecutablePath && !(await fileExistsAtPath(chromeExecutablePath))) {
|
||||
throw new Error(`Chrome executable not found at path: ${chromeExecutablePath}`)
|
||||
}
|
||||
const stats: PCRStats = chromeExecutablePath
|
||||
? { puppeteer: require("puppeteer-core"), executablePath: chromeExecutablePath }
|
||||
: // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
|
||||
// if it does exist it will return the path to existing chromium
|
||||
await PCR({ downloadPath: puppeteerDir })
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
@@ -45,9 +45,11 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
|
||||
ignore: recursive ? dirsToIgnore : undefined, // just in case there is no gitignore, we ignore sensible defaults
|
||||
onlyFiles: false, // true by default, false means it will list directories on their own too
|
||||
}
|
||||
|
||||
// * globs all files in one dir, ** globs files in nested directories
|
||||
const files = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)
|
||||
return [files, files.length >= limit]
|
||||
const filePaths = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)
|
||||
|
||||
return [filePaths, filePaths.length >= limit]
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -472,6 +472,10 @@ export class McpHub {
|
||||
})
|
||||
}
|
||||
|
||||
async sendLatestMcpServers() {
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
|
||||
// Using server
|
||||
|
||||
// Public methods for server management
|
||||
@@ -632,6 +636,33 @@ export class McpHub {
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteServer(serverName: string) {
|
||||
try {
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
||||
config.mcpServers = {}
|
||||
}
|
||||
if (config.mcpServers[serverName]) {
|
||||
delete config.mcpServers[serverName]
|
||||
const updatedConfig = {
|
||||
mcpServers: config.mcpServers,
|
||||
}
|
||||
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
|
||||
await this.updateServerConnections(config.mcpServers)
|
||||
vscode.window.showInformationMessage(`Deleted ${serverName} MCP server`)
|
||||
} else {
|
||||
vscode.window.showWarningMessage(`${serverName} not found in MCP configuration`)
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to delete MCP server: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.removeAllFileWatchers()
|
||||
for (const connection of this.connections) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as vscode from "vscode"
|
||||
import * as childProcess from "child_process"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import * as readline from "readline"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController"
|
||||
|
||||
/*
|
||||
This file provides functionality to perform regex searches on files using ripgrep.
|
||||
@@ -50,7 +51,7 @@ const isWindows = /^win/.test(process.platform)
|
||||
const binName = isWindows ? "rg.exe" : "rg"
|
||||
|
||||
interface SearchResult {
|
||||
file: string
|
||||
filePath: string
|
||||
line: number
|
||||
column: number
|
||||
match: string
|
||||
@@ -63,7 +64,7 @@ const MAX_RESULTS = 300
|
||||
async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
|
||||
const checkPath = async (pkgFolder: string) => {
|
||||
const fullPath = path.join(vscodeAppRoot, pkgFolder, binName)
|
||||
return (await pathExists(fullPath)) ? fullPath : undefined
|
||||
return (await fileExistsAtPath(fullPath)) ? fullPath : undefined
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -74,14 +75,6 @@ async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
|
||||
)
|
||||
}
|
||||
|
||||
async function pathExists(path: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
fs.access(path, (err) => {
|
||||
resolve(err === null)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function execRipgrep(bin: string, args: string[]): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rgProcess = childProcess.spawn(bin, args)
|
||||
@@ -122,7 +115,13 @@ async function execRipgrep(bin: string, args: string[]): Promise<string> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function regexSearchFiles(cwd: string, directoryPath: string, regex: string, filePattern?: string): Promise<string> {
|
||||
export async function regexSearchFiles(
|
||||
cwd: string,
|
||||
directoryPath: string,
|
||||
regex: string,
|
||||
filePattern?: string,
|
||||
clineIgnoreController?: ClineIgnoreController,
|
||||
): Promise<string> {
|
||||
const vscodeAppRoot = vscode.env.appRoot
|
||||
const rgPath = await getBinPath(vscodeAppRoot)
|
||||
|
||||
@@ -150,7 +149,7 @@ export async function regexSearchFiles(cwd: string, directoryPath: string, regex
|
||||
results.push(currentResult as SearchResult)
|
||||
}
|
||||
currentResult = {
|
||||
file: parsed.data.path.text,
|
||||
filePath: parsed.data.path.text,
|
||||
line: parsed.data.line_number,
|
||||
column: parsed.data.submatches[0].start,
|
||||
match: parsed.data.lines.text,
|
||||
@@ -174,7 +173,12 @@ export async function regexSearchFiles(cwd: string, directoryPath: string, regex
|
||||
results.push(currentResult as SearchResult)
|
||||
}
|
||||
|
||||
return formatResults(results, cwd)
|
||||
// Filter results using ClineIgnoreController if provided
|
||||
const filteredResults = clineIgnoreController
|
||||
? results.filter((result) => clineIgnoreController.validateAccess(result.filePath))
|
||||
: results
|
||||
|
||||
return formatResults(filteredResults, cwd)
|
||||
}
|
||||
|
||||
function formatResults(results: SearchResult[], cwd: string): string {
|
||||
@@ -189,7 +193,7 @@ function formatResults(results: SearchResult[], cwd: string): string {
|
||||
|
||||
// Group results by file name
|
||||
results.slice(0, MAX_RESULTS).forEach((result) => {
|
||||
const relativeFilePath = path.relative(cwd, result.file)
|
||||
const relativeFilePath = path.relative(cwd, result.filePath)
|
||||
if (!groupedResults[relativeFilePath]) {
|
||||
groupedResults[relativeFilePath] = []
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@ import * as path from "path"
|
||||
import { listFiles } from "../glob/list-files"
|
||||
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController"
|
||||
|
||||
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
|
||||
export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> {
|
||||
export async function parseSourceCodeForDefinitionsTopLevel(
|
||||
dirPath: string,
|
||||
clineIgnoreController?: ClineIgnoreController,
|
||||
): Promise<string> {
|
||||
// check if the path exists
|
||||
const dirExists = await fileExistsAtPath(path.resolve(dirPath))
|
||||
if (!dirExists) {
|
||||
@@ -24,10 +28,14 @@ export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Pr
|
||||
|
||||
// Parse specific files we have language parsers for
|
||||
// const filesWithoutDefinitions: string[] = []
|
||||
for (const file of filesToParse) {
|
||||
const definitions = await parseFile(file, languageParsers)
|
||||
|
||||
// Filter filepaths for access if controller is provided
|
||||
const allowedFilesToParse = clineIgnoreController ? clineIgnoreController.filterPaths(filesToParse) : filesToParse
|
||||
|
||||
for (const filePath of allowedFilesToParse) {
|
||||
const definitions = await parseFile(filePath, languageParsers, clineIgnoreController)
|
||||
if (definitions) {
|
||||
result += `${path.relative(dirPath, file).toPosix()}\n${definitions}\n`
|
||||
result += `${path.relative(dirPath, filePath).toPosix()}\n${definitions}\n`
|
||||
}
|
||||
// else {
|
||||
// filesWithoutDefinitions.push(file)
|
||||
@@ -98,7 +106,14 @@ This approach allows us to focus on the most relevant parts of the code (defined
|
||||
- https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/test/helper.js
|
||||
- https://tree-sitter.github.io/tree-sitter/code-navigation-systems
|
||||
*/
|
||||
async function parseFile(filePath: string, languageParsers: LanguageParser): Promise<string | undefined> {
|
||||
async function parseFile(
|
||||
filePath: string,
|
||||
languageParsers: LanguageParser,
|
||||
clineIgnoreController?: ClineIgnoreController,
|
||||
): Promise<string | null> {
|
||||
if (clineIgnoreController && !clineIgnoreController.validateAccess(filePath)) {
|
||||
return null
|
||||
}
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
const ext = path.extname(filePath).toLowerCase().slice(1)
|
||||
|
||||
@@ -159,5 +174,5 @@ async function parseFile(filePath: string, languageParsers: LanguageParser): Pro
|
||||
if (formattedOutput.length > 0) {
|
||||
return `|----\n${formattedOutput}|----\n`
|
||||
}
|
||||
return undefined
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ChatContent {
|
||||
message?: string
|
||||
images?: string[]
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
// type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello'
|
||||
|
||||
import { GitCommit } from "../utils/git"
|
||||
import { ApiConfiguration, ModelInfo } from "./api"
|
||||
import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { HistoryItem } from "./HistoryItem"
|
||||
import { McpServer } from "./mcp"
|
||||
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
@@ -26,6 +27,9 @@ export interface ExtensionMessage {
|
||||
| "vsCodeLmModels"
|
||||
| "requestVsCodeLmModels"
|
||||
| "emailSubscribed"
|
||||
| "mcpMarketplaceCatalog"
|
||||
| "mcpDownloadDetails"
|
||||
| "commitSearchResults"
|
||||
text?: string
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
@@ -46,8 +50,16 @@ export interface ExtensionMessage {
|
||||
openRouterModels?: Record<string, ModelInfo>
|
||||
openAiModels?: string[]
|
||||
mcpServers?: McpServer[]
|
||||
mcpMarketplaceCatalog?: McpMarketplaceCatalog
|
||||
error?: string
|
||||
mcpDownloadDetails?: McpDownloadResponse
|
||||
commits?: GitCommit[]
|
||||
}
|
||||
|
||||
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"
|
||||
|
||||
export const DEFAULT_PLATFORM = "unknown"
|
||||
|
||||
export interface ExtensionState {
|
||||
version: string
|
||||
apiConfiguration?: ApiConfiguration
|
||||
@@ -62,11 +74,13 @@ export interface ExtensionState {
|
||||
browserSettings: BrowserSettings
|
||||
chatSettings: ChatSettings
|
||||
isLoggedIn: boolean
|
||||
platform: Platform
|
||||
userInfo?: {
|
||||
displayName: string | null
|
||||
email: string | null
|
||||
photoURL: string | null
|
||||
}
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
@@ -75,9 +89,11 @@ export interface ClineMessage {
|
||||
ask?: ClineAsk
|
||||
say?: ClineSay
|
||||
text?: string
|
||||
reasoning?: string
|
||||
images?: string[]
|
||||
partial?: boolean
|
||||
lastCheckpointHash?: string
|
||||
isCheckpointCheckedOut?: boolean
|
||||
conversationHistoryIndex?: number
|
||||
conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
|
||||
}
|
||||
@@ -103,6 +119,7 @@ export type ClineSay =
|
||||
| "api_req_started"
|
||||
| "api_req_finished"
|
||||
| "text"
|
||||
| "reasoning"
|
||||
| "completion_result"
|
||||
| "user_feedback"
|
||||
| "user_feedback_diff"
|
||||
@@ -119,6 +136,8 @@ export type ClineSay =
|
||||
| "use_mcp_server"
|
||||
| "diff_error"
|
||||
| "deleted_api_reqs"
|
||||
| "clineignore_error"
|
||||
| "checkpoint_created"
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ApiConfiguration } from "./api"
|
||||
import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { ChatContent } from "./ChatContent"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
@@ -28,9 +29,10 @@ export interface WebviewMessage {
|
||||
| "refreshOpenAiModels"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserSettings"
|
||||
| "chatSettings"
|
||||
| "togglePlanActMode"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
| "taskCompletionViewChanges"
|
||||
@@ -42,6 +44,12 @@ export interface WebviewMessage {
|
||||
| "accountLoginClicked"
|
||||
| "accountLogoutClicked"
|
||||
| "subscribeEmail"
|
||||
| "fetchMcpMarketplace"
|
||||
| "downloadMcp"
|
||||
| "silentlyRefreshMcpMarketplace"
|
||||
| "searchCommits"
|
||||
| "showMcpView"
|
||||
| "fetchLatestMcpServersFromHub"
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
@@ -53,6 +61,8 @@ export interface WebviewMessage {
|
||||
autoApprovalSettings?: AutoApprovalSettings
|
||||
browserSettings?: BrowserSettings
|
||||
chatSettings?: ChatSettings
|
||||
chatContent?: ChatContent
|
||||
mcpId?: string
|
||||
|
||||
// For toggleToolAutoApprove
|
||||
serverName?: string
|
||||
|
||||
+352
-19
@@ -8,13 +8,20 @@ export type ApiProvider =
|
||||
| "lmstudio"
|
||||
| "gemini"
|
||||
| "openai-native"
|
||||
| "requesty"
|
||||
| "together"
|
||||
| "deepseek"
|
||||
| "qwen"
|
||||
| "mistral"
|
||||
| "vscode-lm"
|
||||
| "litellm"
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
apiKey?: string // anthropic
|
||||
liteLlmBaseUrl?: string
|
||||
liteLlmModelId?: string
|
||||
liteLlmApiKey?: string
|
||||
anthropicBaseUrl?: string
|
||||
openRouterApiKey?: string
|
||||
openRouterModelId?: string
|
||||
@@ -24,11 +31,14 @@ export interface ApiHandlerOptions {
|
||||
awsSessionToken?: string
|
||||
awsRegion?: string
|
||||
awsUseCrossRegionInference?: boolean
|
||||
awsUseProfile?: boolean
|
||||
awsProfile?: string
|
||||
vertexProjectId?: string
|
||||
vertexRegion?: string
|
||||
openAiBaseUrl?: string
|
||||
openAiApiKey?: string
|
||||
openAiModelId?: string
|
||||
openAiModelInfo?: ModelInfo
|
||||
ollamaModelId?: string
|
||||
ollamaBaseUrl?: string
|
||||
lmStudioModelId?: string
|
||||
@@ -36,9 +46,16 @@ export interface ApiHandlerOptions {
|
||||
geminiApiKey?: string
|
||||
openAiNativeApiKey?: string
|
||||
deepSeekApiKey?: string
|
||||
requestyApiKey?: string
|
||||
requestyModelId?: string
|
||||
togetherApiKey?: string
|
||||
togetherModelId?: string
|
||||
qwenApiKey?: string
|
||||
mistralApiKey?: string
|
||||
azureApiVersion?: string
|
||||
vsCodeLmModelSelector?: any
|
||||
o3MiniReasoningEffort?: string
|
||||
qwenApiLine?: string
|
||||
}
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions & {
|
||||
@@ -166,7 +183,7 @@ export const bedrockModels = {
|
||||
|
||||
// OpenRouter
|
||||
// https://openrouter.ai/models?order=newest&supported_parameters=tools
|
||||
export const openRouterDefaultModelId = "anthropic/claude-3.5-sonnet:beta" // will always exist in openRouterModels
|
||||
export const openRouterDefaultModelId = "anthropic/claude-3.5-sonnet" // will always exist in openRouterModels
|
||||
export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -178,7 +195,7 @@ export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
description:
|
||||
"The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._",
|
||||
"The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal",
|
||||
}
|
||||
|
||||
// Vertex AI
|
||||
@@ -241,10 +258,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 +308,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 +348,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 +363,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 +393,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 +446,284 @@ 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 = {
|
||||
"qwen2.5-coder-32b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.002,
|
||||
outputPrice: 0.006,
|
||||
cacheWritesPrice: 0.002,
|
||||
cacheReadsPrice: 0.006,
|
||||
},
|
||||
"qwen2.5-coder-14b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.002,
|
||||
outputPrice: 0.006,
|
||||
cacheWritesPrice: 0.002,
|
||||
cacheReadsPrice: 0.006,
|
||||
},
|
||||
"qwen2.5-coder-7b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.001,
|
||||
outputPrice: 0.002,
|
||||
cacheWritesPrice: 0.001,
|
||||
cacheReadsPrice: 0.002,
|
||||
},
|
||||
"qwen2.5-coder-3b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
cacheWritesPrice: 0.0,
|
||||
cacheReadsPrice: 0.0,
|
||||
},
|
||||
"qwen2.5-coder-1.5b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
cacheWritesPrice: 0.0,
|
||||
cacheReadsPrice: 0.0,
|
||||
},
|
||||
"qwen2.5-coder-0.5b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
cacheWritesPrice: 0.0,
|
||||
cacheReadsPrice: 0.0,
|
||||
},
|
||||
"qwen-coder-plus-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.5,
|
||||
outputPrice: 7,
|
||||
cacheWritesPrice: 3.5,
|
||||
cacheReadsPrice: 7,
|
||||
},
|
||||
"qwen-plus-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2,
|
||||
cacheWritesPrice: 0.8,
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
"qwen-turbo-latest": {
|
||||
maxTokens: 1_000_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2,
|
||||
cacheWritesPrice: 0.8,
|
||||
cacheReadsPrice: 2,
|
||||
},
|
||||
"qwen-max-latest": {
|
||||
maxTokens: 30_720,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.4,
|
||||
outputPrice: 9.6,
|
||||
cacheWritesPrice: 2.4,
|
||||
cacheReadsPrice: 9.6,
|
||||
},
|
||||
"qwen-coder-plus": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.5,
|
||||
outputPrice: 7,
|
||||
cacheWritesPrice: 3.5,
|
||||
cacheReadsPrice: 7,
|
||||
},
|
||||
"qwen-plus": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2,
|
||||
cacheWritesPrice: 0.8,
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
"qwen-turbo": {
|
||||
maxTokens: 1_000_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 0.6,
|
||||
cacheWritesPrice: 0.3,
|
||||
cacheReadsPrice: 0.6,
|
||||
},
|
||||
"qwen-max": {
|
||||
maxTokens: 30_720,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.4,
|
||||
outputPrice: 9.6,
|
||||
cacheWritesPrice: 2.4,
|
||||
cacheReadsPrice: 9.6,
|
||||
},
|
||||
"deepseek-v3": {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0.28,
|
||||
cacheWritesPrice: 0.14,
|
||||
cacheReadsPrice: 0.014,
|
||||
},
|
||||
"deepseek-r1": {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 2.19,
|
||||
cacheWritesPrice: 0.55,
|
||||
cacheReadsPrice: 0.14,
|
||||
},
|
||||
"qwen-vl-max": {
|
||||
maxTokens: 30_720,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3,
|
||||
outputPrice: 9,
|
||||
cacheWritesPrice: 3,
|
||||
cacheReadsPrice: 9,
|
||||
},
|
||||
"qwen-vl-max-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3,
|
||||
outputPrice: 9,
|
||||
cacheWritesPrice: 3,
|
||||
cacheReadsPrice: 9,
|
||||
},
|
||||
"qwen-vl-plus": {
|
||||
maxTokens: 6_000,
|
||||
contextWindow: 8_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.5,
|
||||
outputPrice: 4.5,
|
||||
cacheWritesPrice: 1.5,
|
||||
cacheReadsPrice: 4.5,
|
||||
},
|
||||
"qwen-vl-plus-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.5,
|
||||
outputPrice: 4.5,
|
||||
cacheWritesPrice: 1.5,
|
||||
cacheReadsPrice: 4.5,
|
||||
},
|
||||
} 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 +731,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: -1,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
}
|
||||
|
||||
@@ -7,42 +7,48 @@ Mention regex:
|
||||
|
||||
- **Regex Breakdown**:
|
||||
- `/@`:
|
||||
- **@**: The mention must start with the '@' symbol.
|
||||
- **@**: The mention must start with the '@' symbol.
|
||||
|
||||
- `((?:\/|\w+:\/\/)[^\s]+?|problems\b)`:
|
||||
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
|
||||
- `(?:\/|\w+:\/\/)`:
|
||||
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
|
||||
- `\/`:
|
||||
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
|
||||
- `|`: Logical OR.
|
||||
- `\w+:\/\/`:
|
||||
- **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
|
||||
- `[^\s]+?`:
|
||||
- **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
|
||||
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
|
||||
- `|`: Logical OR.
|
||||
- `problems\b`:
|
||||
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
|
||||
- `(?:\/|\w+:\/\/)`:
|
||||
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
|
||||
- `\/`:
|
||||
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
|
||||
- `|`: Logical OR.
|
||||
- `\w+:\/\/`:
|
||||
- **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
|
||||
- `[^\s]+?`:
|
||||
- **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
|
||||
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
|
||||
- `|`: Logical OR.
|
||||
- `problems\b`:
|
||||
- **Exact Word ('problems')**: Matches the exact word 'problems'.
|
||||
- **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
|
||||
- `terminal\b`:
|
||||
- **Exact Word ('terminal')**: Matches the exact word 'terminal'.
|
||||
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
|
||||
|
||||
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
|
||||
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
|
||||
- `[.,;:!?]?`:
|
||||
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
|
||||
- `(?=[\s\r\n]|$)`:
|
||||
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
|
||||
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
|
||||
- `[.,;:!?]?`:
|
||||
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
|
||||
- `(?=[\s\r\n]|$)`:
|
||||
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
|
||||
|
||||
- **Summary**:
|
||||
- The regex effectively matches:
|
||||
- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
|
||||
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
|
||||
- The exact word 'problems'.
|
||||
- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
|
||||
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
|
||||
- The exact word 'problems'.
|
||||
- The exact word 'terminal'.
|
||||
- The exact word 'git-changes'.
|
||||
- It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text.
|
||||
|
||||
- **Global Regex**:
|
||||
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
|
||||
|
||||
*/
|
||||
export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
|
||||
export const mentionRegex =
|
||||
/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|terminal\b|git-changes\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
|
||||
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
|
||||
|
||||
@@ -66,3 +66,39 @@ export type McpToolCallResponse = {
|
||||
>
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
export interface McpMarketplaceItem {
|
||||
mcpId: string
|
||||
githubUrl: string
|
||||
name: string
|
||||
author: string
|
||||
description: string
|
||||
codiconIcon: string
|
||||
logoUrl: string
|
||||
category: string
|
||||
tags: string[]
|
||||
requiresApiKey: boolean
|
||||
readmeContent?: string
|
||||
llmsInstallationContent?: string
|
||||
isRecommended: boolean
|
||||
githubStars: number
|
||||
downloadCount: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
lastGithubSync: string
|
||||
}
|
||||
|
||||
export interface McpMarketplaceCatalog {
|
||||
items: McpMarketplaceItem[]
|
||||
}
|
||||
|
||||
export interface McpDownloadResponse {
|
||||
mcpId: string
|
||||
githubUrl: string
|
||||
name: string
|
||||
author: string
|
||||
description: string
|
||||
readmeContent: string
|
||||
llmsInstallationContent: string
|
||||
requiresApiKey: boolean
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -34,4 +34,14 @@ describe("Extension Tests", function () {
|
||||
await vscode.commands.executeCommand("cline.historyButtonClicked")
|
||||
// Success if no error thrown
|
||||
})
|
||||
|
||||
it("should handle advanced settings configuration", async () => {
|
||||
// Test browser session setting
|
||||
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", true, true)
|
||||
const updatedConfig = vscode.workspace.getConfiguration("cline")
|
||||
expect(updatedConfig.get("disableBrowserTool")).to.be.true
|
||||
|
||||
// Reset settings
|
||||
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", undefined, true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,7 +28,13 @@ describe("Chat Integration Tests", () => {
|
||||
vscode.postMessage({ type: 'newTask', text: message.text });
|
||||
break;
|
||||
case 'toggleMode':
|
||||
vscode.postMessage({ type: 'chatSettings', chatSettings: { mode: 'act' } });
|
||||
vscode.postMessage({
|
||||
type: 'togglePlanActMode',
|
||||
chatSettings: { mode: 'act' },
|
||||
chatContent: {
|
||||
message: "message test",
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'invoke':
|
||||
if (message.invoke === 'primaryButtonClick') {
|
||||
@@ -78,7 +84,7 @@ describe("Chat Integration Tests", () => {
|
||||
// Set up state change listener
|
||||
const stateChangePromise = new Promise<any>((resolve) => {
|
||||
panel.webview.onDidReceiveMessage((message) => {
|
||||
if (message.type === "chatSettings") {
|
||||
if (message.type === "togglePlanActMode") {
|
||||
resolve(message)
|
||||
}
|
||||
})
|
||||
@@ -92,6 +98,25 @@ describe("Chat Integration Tests", () => {
|
||||
assert.equal(stateChange.chatSettings.mode, "act")
|
||||
})
|
||||
|
||||
it("should toggle between plan and act modes with messages", async () => {
|
||||
// Set up state change listener
|
||||
const stateChangePromise = new Promise<any>((resolve) => {
|
||||
panel.webview.onDidReceiveMessage((message) => {
|
||||
if (message.type === "togglePlanActMode") {
|
||||
resolve(message)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Trigger mode toggle
|
||||
await panel.webview.postMessage({ type: "toggleMode" })
|
||||
|
||||
// Verify mode changed
|
||||
const stateChange = await stateChangePromise
|
||||
assert.equal(stateChange.chatSettings.mode, "act")
|
||||
assert.equal(stateChange.chatContent.message, "message test")
|
||||
})
|
||||
|
||||
it("should handle tool approval flow", async () => {
|
||||
// Set up approval listener
|
||||
const approvalPromise = new Promise<any>((resolve) => {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { exec } from "child_process"
|
||||
import { promisify } from "util"
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
const GIT_OUTPUT_LINE_LIMIT = 500
|
||||
|
||||
export interface GitCommit {
|
||||
hash: string
|
||||
shortHash: string
|
||||
subject: string
|
||||
author: string
|
||||
date: string
|
||||
}
|
||||
|
||||
async function checkGitRepo(cwd: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse --git-dir", { cwd })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGitInstalled(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git --version")
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
console.error("Git is not installed")
|
||||
return []
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
console.error("Not a git repository")
|
||||
return []
|
||||
}
|
||||
|
||||
// Search commits by hash or message, limiting to 10 results
|
||||
const { stdout } = await execAsync(
|
||||
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,
|
||||
{ cwd },
|
||||
)
|
||||
|
||||
let output = stdout
|
||||
if (!output.trim() && /^[a-f0-9]+$/i.test(query)) {
|
||||
// If no results from grep search and query looks like a hash, try searching by hash
|
||||
const { stdout: hashStdout } = await execAsync(
|
||||
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--author-date-order ${query}`,
|
||||
{ cwd },
|
||||
).catch(() => ({ stdout: "" }))
|
||||
|
||||
if (!hashStdout.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
output = hashStdout
|
||||
}
|
||||
|
||||
const commits: GitCommit[] = []
|
||||
const lines = output
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line !== "--")
|
||||
|
||||
for (let i = 0; i < lines.length; i += 5) {
|
||||
commits.push({
|
||||
hash: lines[i],
|
||||
shortHash: lines[i + 1],
|
||||
subject: lines[i + 2],
|
||||
author: lines[i + 3],
|
||||
date: lines[i + 4],
|
||||
})
|
||||
}
|
||||
|
||||
return commits
|
||||
} catch (error) {
|
||||
console.error("Error searching commits:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCommitInfo(hash: string, cwd: string): Promise<string> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
return "Git is not installed"
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
return "Not a git repository"
|
||||
}
|
||||
|
||||
// Get commit info, stats, and diff separately
|
||||
const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {
|
||||
cwd,
|
||||
})
|
||||
const [fullHash, shortHash, subject, author, date, body] = info.trim().split("\n")
|
||||
|
||||
const { stdout: stats } = await execAsync(`git show --stat --format="" ${hash}`, { cwd })
|
||||
|
||||
const { stdout: diff } = await execAsync(`git show --format="" ${hash}`, { cwd })
|
||||
|
||||
const summary = [
|
||||
`Commit: ${shortHash} (${fullHash})`,
|
||||
`Author: ${author}`,
|
||||
`Date: ${date}`,
|
||||
`\nMessage: ${subject}`,
|
||||
body ? `\nDescription:\n${body}` : "",
|
||||
"\nFiles Changed:",
|
||||
stats.trim(),
|
||||
"\nFull Changes:",
|
||||
].join("\n")
|
||||
|
||||
const output = summary + "\n\n" + diff.trim()
|
||||
return truncateOutput(output)
|
||||
} catch (error) {
|
||||
console.error("Error getting commit info:", error)
|
||||
return `Failed to get commit info: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkingState(cwd: string): Promise<string> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
return "Git is not installed"
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
return "Not a git repository"
|
||||
}
|
||||
|
||||
// Get status of working directory
|
||||
const { stdout: status } = await execAsync("git status --short", { cwd })
|
||||
if (!status.trim()) {
|
||||
return "No changes in working directory"
|
||||
}
|
||||
|
||||
// Get all changes (both staged and unstaged) compared to HEAD
|
||||
const { stdout: diff } = await execAsync("git diff HEAD", { cwd })
|
||||
const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()
|
||||
return truncateOutput(output)
|
||||
} catch (error) {
|
||||
console.error("Error getting working state:", error)
|
||||
return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}
|
||||
|
||||
function truncateOutput(content: string): string {
|
||||
if (!GIT_OUTPUT_LINE_LIMIT) {
|
||||
return content
|
||||
}
|
||||
|
||||
const lines = content.split("\n")
|
||||
if (lines.length <= GIT_OUTPUT_LINE_LIMIT) {
|
||||
return content
|
||||
}
|
||||
|
||||
const beforeLimit = Math.floor(GIT_OUTPUT_LINE_LIMIT * 0.2) // 20% of lines before
|
||||
const afterLimit = GIT_OUTPUT_LINE_LIMIT - beforeLimit // remaining 80% after
|
||||
return [
|
||||
...lines.slice(0, beforeLimit),
|
||||
`\n[...${lines.length - GIT_OUTPUT_LINE_LIMIT} lines omitted...]\n`,
|
||||
...lines.slice(-afterLimit),
|
||||
].join("\n")
|
||||
}
|
||||
@@ -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 user’s 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
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// "Official" jest workaround for mocking window.matchMedia()
|
||||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // Deprecated
|
||||
removeListener: vi.fn(), // Deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
})
|
||||
Generated
+3069
-304
File diff suppressed because it is too large
Load Diff
+15
-9
@@ -3,17 +3,13 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.101",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@floating-ui/react": "^0.27.4",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"debounce": "^2.1.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^7.0.0",
|
||||
"fzf": "^0.5.2",
|
||||
"mermaid": "^11.4.1",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -34,7 +30,8 @@
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "node ./scripts/build-react-no-split.js",
|
||||
"test": "react-scripts test",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest dev",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
@@ -56,6 +53,15 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode-webview": "^1.57.5"
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^15.0.6",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^20.x",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/vscode-webview": "^1.57.5",
|
||||
"jsdom": "^25.0.1",
|
||||
"vitest": "^2.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,18 +98,26 @@ config.module.rules[1].oneOf.forEach((rule) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Disable code splitting
|
||||
config.optimization.splitChunks = {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
// Force all code into a single bundle for VS Code webview compatibility.
|
||||
// This is necessary for:
|
||||
// 1. Mermaid.js to work properly (prevents async chunk loading)
|
||||
// 2. Consistent CSP nonce handling (single bundle = single nonce)
|
||||
config.optimization = {
|
||||
...config.optimization,
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
},
|
||||
name: "main", // Forces all chunks (dynamic import() calls, for example those used by Mermaid) into one bundle - this is what actually prevents code splitting
|
||||
},
|
||||
runtimeChunk: false,
|
||||
}
|
||||
|
||||
// Disable code chunks
|
||||
config.optimization.runtimeChunk = false
|
||||
|
||||
// Rename main.{hash}.js to main.js
|
||||
config.output.filename = "static/js/[name].js"
|
||||
// Ensure all chunks are named 'main' to match our CSP nonce setup
|
||||
config.output = {
|
||||
...config.output,
|
||||
filename: "static/js/[name].js",
|
||||
}
|
||||
|
||||
// Rename main.{hash}.css to main.css
|
||||
config.plugins[5].options.filename = "static/css/[name].css"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import "@testing-library/jest-dom"
|
||||
import "./matchMedia"
|
||||
@@ -1,6 +1,7 @@
|
||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo } from "react"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND, VSC_INACTIVE_SELECTION_BACKGROUND } from "../../utils/vscStyles"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
interface AnnouncementProps {
|
||||
version: string
|
||||
@@ -30,29 +31,34 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
</h3>
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
<b>Plan/Act mode toggle:</b> Plan mode turns Cline into an architect that gathers information, asks clarifying
|
||||
questions, and designs a solution. Switch back to Act mode to let him execute the plan!{" "}
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1881761978986934582" style={{ display: "inline" }}>
|
||||
See a demo here.
|
||||
<b>Introducing MCP Marketplace:</b> Discover and install the best MCP servers right from the extension, with
|
||||
new servers added regularly! Get started by going to the{" "}
|
||||
<span className="codicon codicon-extensions" style={{ marginRight: "4px", fontSize: 10 }}></span>
|
||||
<VSCodeLink
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "showMcpView" })
|
||||
}}>
|
||||
MCP Servers tab
|
||||
</VSCodeLink>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
<b>Quick API/model switching</b> with a new popup menu under the chat field
|
||||
<b>Mermaid diagrams in Plan mode!</b> Cline can now visualize his plans using flowcharts, sequences,
|
||||
entity-relationships, and more. When he explains his approach using mermaid, you'll see a diagram right in
|
||||
chat that you can click to expand.
|
||||
</li>
|
||||
<li>
|
||||
<b>VS Code LM API</b> lets you use models from other extensions like GitHub Copilot
|
||||
Use <code>@terminal</code> to reference terminal contents, and <code>@git</code> to reference working changes
|
||||
and commits!
|
||||
</li>
|
||||
<li>
|
||||
<b>MCP server improvements:</b> On/off toggle to disable servers when not in use, and Auto-approve option for
|
||||
individual tools
|
||||
</li>
|
||||
<li>
|
||||
In case you missed it, Cline now supports Checkpoints!{" "}
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1876378124126236949" style={{ display: "inline" }}>
|
||||
See it in action here.
|
||||
</VSCodeLink>
|
||||
New visual indicator for checkpoints after edits & commands, and automatic checkpoint at the start of each
|
||||
task.
|
||||
</li>
|
||||
</ul>
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1892262424881090721" style={{ display: "inline" }}>
|
||||
See a demo of the changes here!
|
||||
</VSCodeLink>
|
||||
{/*<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
||||
@@ -109,9 +115,12 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
}}
|
||||
/>
|
||||
<p style={{ margin: "0" }}>
|
||||
Join our{" "}
|
||||
Join us on{" "}
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://x.com/cline">
|
||||
X,
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://discord.gg/cline">
|
||||
discord
|
||||
discord,
|
||||
</VSCodeLink>{" "}
|
||||
or{" "}
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://www.reddit.com/r/cline/">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useSize } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
|
||||
import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { BrowserSettingsMenu } from "../browser/BrowserSettingsMenu"
|
||||
import { CheckpointControls } from "../common/CheckpointControls"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import { ChatRowContent, ProgressIndicator } from "./ChatRow"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import styled from "styled-components"
|
||||
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
|
||||
import { findLast } from "../../../../src/shared/array"
|
||||
import { BrowserSettingsMenu } from "../browser/BrowserSettingsMenu"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
|
||||
|
||||
interface BrowserSessionRowProps {
|
||||
messages: ClineMessage[]
|
||||
@@ -144,10 +143,10 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
return launchMessage?.say === "browser_action_launch"
|
||||
}, [messages])
|
||||
|
||||
const lastCheckpointMessageTs = useMemo(() => {
|
||||
const lastCheckpointMessage = findLast(messages, (m) => m.lastCheckpointHash !== undefined)
|
||||
return lastCheckpointMessage?.ts
|
||||
}, [messages])
|
||||
// const lastCheckpointMessageTs = useMemo(() => {
|
||||
// const lastCheckpointMessage = findLast(messages, (m) => m.lastCheckpointHash !== undefined)
|
||||
// return lastCheckpointMessage?.ts
|
||||
// }, [messages])
|
||||
|
||||
// Find the latest available URL and screenshot
|
||||
const latestState = useMemo(() => {
|
||||
@@ -231,10 +230,10 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
// Use latest click position while browsing, otherwise use display state
|
||||
const mousePosition = isBrowsing ? latestClickPosition || displayState.mousePosition : displayState.mousePosition
|
||||
|
||||
let shouldShowCheckpoints = true
|
||||
if (isLast) {
|
||||
shouldShowCheckpoints = lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
|
||||
}
|
||||
// let shouldShowCheckpoints = true
|
||||
// if (isLast) {
|
||||
// shouldShowCheckpoints = lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
|
||||
// }
|
||||
|
||||
const shouldShowSettings = useMemo(() => {
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
@@ -423,7 +422,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowCheckpoints && <CheckpointOverlay messageTs={lastCheckpointMessageTs} />}
|
||||
{/* {shouldShowCheckpoints && <CheckpointOverlay messageTs={lastCheckpointMessageTs} />} */}
|
||||
</BrowserSessionRowContainer>,
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "../../../../src/shared/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "../../../../src/shared/combineCommandSequences"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { findMatchingResourceOrTemplate } from "../../utils/mcp"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "../../utils/mcp"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
@@ -24,6 +24,7 @@ import Thumbnails from "../common/Thumbnails"
|
||||
import McpResourceRow from "../mcp/McpResourceRow"
|
||||
import McpToolRow from "../mcp/McpToolRow"
|
||||
import { highlightMentions } from "./TaskHeader"
|
||||
import { CheckmarkControl } from "../common/CheckmarkControl"
|
||||
|
||||
const ChatRowContainer = styled.div`
|
||||
padding: 10px 6px 10px 15px;
|
||||
@@ -59,8 +60,8 @@ const ChatRow = memo(
|
||||
message.ask === "tool" ||
|
||||
message.say === "command" ||
|
||||
message.ask === "command" ||
|
||||
message.say === "completion_result" ||
|
||||
message.ask === "completion_result" ||
|
||||
// message.say === "completion_result" ||
|
||||
// message.ask === "completion_result" ||
|
||||
message.say === "use_mcp_server" ||
|
||||
message.ask === "use_mcp_server")
|
||||
|
||||
@@ -99,7 +100,7 @@ const ChatRow = memo(
|
||||
export default ChatRow
|
||||
|
||||
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
|
||||
const { mcpServers } = useExtensionState()
|
||||
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
|
||||
|
||||
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
|
||||
|
||||
@@ -185,9 +186,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
),
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
||||
{message.type === "ask" ? "Cline wants to execute this command:" : "Cline executed this command:"}
|
||||
</span>,
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>Cline wants to execute this command:</span>,
|
||||
]
|
||||
case "use_mcp_server":
|
||||
const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
|
||||
@@ -202,18 +201,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
),
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
||||
{message.type === "ask" ? (
|
||||
<>
|
||||
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
|
||||
<code>{mcpServerUse.serverName}</code> MCP server:
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Cline {mcpServerUse.type === "use_mcp_tool" ? "used a tool" : "accessed a resource"} on the{" "}
|
||||
<code>{mcpServerUse.serverName}</code> MCP server:
|
||||
</>
|
||||
)}
|
||||
<span style={{ color: normalColor, fontWeight: "bold", wordBreak: "break-word" }}>
|
||||
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
|
||||
<code style={{ wordBreak: "break-all" }}>
|
||||
{getMcpServerDisplayName(mcpServerUse.serverName, mcpMarketplaceCatalog)}
|
||||
</code>{" "}
|
||||
MCP server:
|
||||
</span>,
|
||||
]
|
||||
case "completion_result":
|
||||
@@ -298,16 +291,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
default:
|
||||
return [null, null]
|
||||
}
|
||||
}, [
|
||||
type,
|
||||
cost,
|
||||
apiRequestFailedMessage,
|
||||
isCommandExecuting,
|
||||
apiReqCancelReason,
|
||||
isMcpServerResponding,
|
||||
message.text,
|
||||
message.type,
|
||||
])
|
||||
}, [type, cost, apiRequestFailedMessage, isCommandExecuting, apiReqCancelReason, isMcpServerResponding, message.text])
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
@@ -346,9 +330,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
{toolIcon("edit")}
|
||||
<span style={{ fontWeight: "bold" }}>
|
||||
{message.type === "ask" ? "Cline wants to edit this file:" : "Cline is editing this file:"}
|
||||
</span>
|
||||
<span style={{ fontWeight: "bold" }}>Cline wants to edit this file:</span>
|
||||
</div>
|
||||
<CodeAccordian
|
||||
// isLoading={message.partial}
|
||||
@@ -364,9 +346,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
{toolIcon("new-file")}
|
||||
<span style={{ fontWeight: "bold" }}>
|
||||
{message.type === "ask" ? "Cline wants to create a new file:" : "Cline is creating a new file:"}
|
||||
</span>
|
||||
<span style={{ fontWeight: "bold" }}>Cline wants to create a new file:</span>
|
||||
</div>
|
||||
<CodeAccordian
|
||||
isLoading={message.partial}
|
||||
@@ -383,7 +363,8 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
<div style={headerStyle}>
|
||||
{toolIcon("file-code")}
|
||||
<span style={{ fontWeight: "bold" }}>
|
||||
{message.type === "ask" ? "Cline wants to read this file:" : "Cline read this file:"}
|
||||
{/* {message.type === "ask" ? "" : "Cline read this file:"} */}
|
||||
Cline wants to read this file:
|
||||
</span>
|
||||
</div>
|
||||
{/* <CodeAccordian
|
||||
@@ -505,15 +486,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
<div style={headerStyle}>
|
||||
{toolIcon("search")}
|
||||
<span style={{ fontWeight: "bold" }}>
|
||||
{message.type === "ask" ? (
|
||||
<>
|
||||
Cline wants to search this directory for <code>{tool.regex}</code>:
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Cline searched this directory for <code>{tool.regex}</code>:
|
||||
</>
|
||||
)}
|
||||
Cline wants to search this directory for <code>{tool.regex}</code>:
|
||||
</span>
|
||||
</div>
|
||||
<CodeAccordian
|
||||
@@ -751,7 +724,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,
|
||||
@@ -842,6 +815,62 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
<Markdown markdown={message.text} />
|
||||
</div>
|
||||
)
|
||||
case "reasoning":
|
||||
return (
|
||||
<>
|
||||
{message.text && (
|
||||
<div
|
||||
onClick={onToggleExpand}
|
||||
style={{
|
||||
// marginBottom: 15,
|
||||
cursor: "pointer",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
|
||||
fontStyle: "italic",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
{isExpanded ? (
|
||||
<div style={{ marginTop: -3 }}>
|
||||
<span style={{ fontWeight: "bold", display: "block", marginBottom: "4px" }}>
|
||||
Reasoning
|
||||
<span
|
||||
className="codicon codicon-chevron-down"
|
||||
style={{
|
||||
display: "inline-block",
|
||||
transform: "translateY(3px)",
|
||||
marginLeft: "1.5px",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{message.text}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span style={{ fontWeight: "bold", marginRight: "4px" }}>Reasoning:</span>
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
direction: "rtl",
|
||||
textAlign: "left",
|
||||
flex: 1,
|
||||
}}>
|
||||
{message.text + "\u200E"}
|
||||
</span>
|
||||
<span
|
||||
className="codicon codicon-chevron-right"
|
||||
style={{
|
||||
marginLeft: "4px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
case "user_feedback":
|
||||
return (
|
||||
<div
|
||||
@@ -933,6 +962,53 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "clineignore_error":
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "rgba(255, 191, 0, 0.1)",
|
||||
padding: 8,
|
||||
borderRadius: 3,
|
||||
fontSize: 12,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-error"
|
||||
style={{
|
||||
marginRight: 8,
|
||||
fontSize: 18,
|
||||
color: "#FFA500",
|
||||
}}></i>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: "#FFA500",
|
||||
}}>
|
||||
Access Denied
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Cline tried to access <code>{message.text}</code> which is blocked by the{" "}
|
||||
<code>.clineignore</code>
|
||||
file.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "checkpoint_created":
|
||||
return (
|
||||
<>
|
||||
<CheckmarkControl messageTs={message.ts} isCheckpointCheckedOut={message.isCheckpointCheckedOut} />
|
||||
</>
|
||||
)
|
||||
case "completion_result":
|
||||
const hasChanges = message.text?.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
|
||||
const text = hasChanges ? message.text?.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
||||
import DynamicTextArea from "react-textarea-autosize"
|
||||
import { useClickAway, useWindowSize } from "react-use"
|
||||
import { useClickAway, useEvent, useWindowSize } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import {
|
||||
ContextMenuOptionType,
|
||||
@@ -12,13 +13,16 @@ import {
|
||||
removeMention,
|
||||
shouldShowContextMenu,
|
||||
} from "../../utils/context-mentions"
|
||||
import { useMetaKeyDetection, useShortcut } from "../../utils/hooks"
|
||||
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import Tooltip from "../common/Tooltip"
|
||||
import ApiOptions, { normalizeApiConfiguration } from "../settings/ApiOptions"
|
||||
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
||||
import ContextMenu from "./ContextMenu"
|
||||
import { ChatSettings } from "../../../../src/shared/ChatSettings"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
@@ -210,8 +214,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels } = useExtensionState()
|
||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [gitCommits, setGitCommits] = useState<any[]>([])
|
||||
|
||||
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
|
||||
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
|
||||
const [showContextMenu, setShowContextMenu] = useState(false)
|
||||
@@ -231,13 +237,47 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
const [arrowPosition, setArrowPosition] = useState(0)
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [shownTooltipMode, setShownTooltipMode] = useState<ChatSettings["mode"] | null>(null)
|
||||
|
||||
const [, metaKeyChar] = useMetaKeyDetection(platform)
|
||||
|
||||
// Add a ref to track previous menu state
|
||||
const prevShowModelSelector = useRef(showModelSelector)
|
||||
|
||||
// Fetch git commits when Git is selected or when typing a hash
|
||||
useEffect(() => {
|
||||
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
|
||||
vscode.postMessage({
|
||||
type: "searchCommits",
|
||||
text: searchQuery || "",
|
||||
})
|
||||
}
|
||||
}, [selectedType, searchQuery])
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "commitSearchResults": {
|
||||
const commits =
|
||||
message.commits?.map((commit: any) => ({
|
||||
type: ContextMenuOptionType.Git,
|
||||
value: commit.hash,
|
||||
label: commit.subject,
|
||||
description: `${commit.shortHash} by ${commit.author} on ${commit.date}`,
|
||||
})) || []
|
||||
setGitCommits(commits)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
const queryItems = useMemo(() => {
|
||||
return [
|
||||
{ type: ContextMenuOptionType.Problems, value: "problems" },
|
||||
{ type: ContextMenuOptionType.Terminal, value: "terminal" },
|
||||
...gitCommits,
|
||||
...filePaths
|
||||
.map((file) => "/" + file)
|
||||
.map((path) => ({
|
||||
@@ -245,7 +285,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
value: path,
|
||||
})),
|
||||
]
|
||||
}, [filePaths])
|
||||
}, [filePaths, gitCommits])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -269,7 +309,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
return
|
||||
}
|
||||
|
||||
if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) {
|
||||
if (
|
||||
type === ContextMenuOptionType.File ||
|
||||
type === ContextMenuOptionType.Folder ||
|
||||
type === ContextMenuOptionType.Git
|
||||
) {
|
||||
if (!value) {
|
||||
setSelectedType(type)
|
||||
setSearchQuery("")
|
||||
@@ -288,6 +332,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
insertValue = value || ""
|
||||
} else if (type === ContextMenuOptionType.Problems) {
|
||||
insertValue = "problems"
|
||||
} else if (type === ContextMenuOptionType.Terminal) {
|
||||
insertValue = "terminal"
|
||||
} else if (type === ContextMenuOptionType.Git) {
|
||||
insertValue = value || ""
|
||||
}
|
||||
|
||||
const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue)
|
||||
@@ -377,7 +425,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
|
||||
@@ -597,7 +645,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}, [apiConfiguration, openRouterModels])
|
||||
|
||||
const onModeToggle = useCallback(() => {
|
||||
if (textAreaDisabled) return
|
||||
// if (textAreaDisabled) return
|
||||
let changeModeDelay = 0
|
||||
if (showModelSelector) {
|
||||
// user has model selector open, so we should save it before switching modes
|
||||
@@ -607,17 +655,23 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
setTimeout(() => {
|
||||
const newMode = chatSettings.mode === "plan" ? "act" : "plan"
|
||||
vscode.postMessage({
|
||||
type: "chatSettings",
|
||||
type: "togglePlanActMode",
|
||||
chatSettings: {
|
||||
mode: newMode,
|
||||
},
|
||||
chatContent: {
|
||||
message: inputValue.trim() ? inputValue : undefined,
|
||||
images: selectedImages.length > 0 ? selectedImages : undefined,
|
||||
},
|
||||
})
|
||||
// Focus the textarea after mode toggle with slight delay
|
||||
setTimeout(() => {
|
||||
textAreaRef.current?.focus()
|
||||
}, 100)
|
||||
}, changeModeDelay)
|
||||
}, [chatSettings.mode, textAreaDisabled, showModelSelector, submitApiConfig])
|
||||
}, [chatSettings.mode, showModelSelector, submitApiConfig, inputValue, selectedImages])
|
||||
|
||||
useShortcut("Meta+Shift+a", onModeToggle, { disableTextInputs: false }) // important that we don't disable the text input here
|
||||
|
||||
const handleContextButtonClick = useCallback(() => {
|
||||
if (textAreaDisabled) return
|
||||
@@ -695,6 +749,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
return `openai-compat:${selectedModelId}`
|
||||
case "vscode-lm":
|
||||
return `vscode-lm:${apiConfiguration.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` : unknownModel}`
|
||||
case "together":
|
||||
return `${selectedProvider}:${apiConfiguration.togetherModelId}`
|
||||
case "lmstudio":
|
||||
return `${selectedProvider}:${apiConfiguration.lmStudioModelId}`
|
||||
case "ollama":
|
||||
return `${selectedProvider}:${apiConfiguration.ollamaModelId}`
|
||||
case "litellm":
|
||||
return `${selectedProvider}:${apiConfiguration.liteLlmModelId}`
|
||||
case "requesty":
|
||||
return `${selectedProvider}:${apiConfiguration.requestyModelId}`
|
||||
default:
|
||||
return `${selectedProvider}:${selectedModelId}`
|
||||
}
|
||||
@@ -874,7 +938,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
borderTop: 0,
|
||||
borderColor: "transparent",
|
||||
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
|
||||
padding: "9px 49px 3px 9px",
|
||||
padding: "9px 28px 3px 9px",
|
||||
}}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
@@ -1038,7 +1102,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
<ModelDisplayButton
|
||||
role="button"
|
||||
isActive={showModelSelector}
|
||||
disabled={textAreaDisabled}
|
||||
disabled={false}
|
||||
onClick={handleModelButtonClick}
|
||||
// onKeyDown={(e) => {
|
||||
// if (e.key === "Enter" || e.key === " ") {
|
||||
@@ -1067,12 +1131,26 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
)}
|
||||
</ModelContainer>
|
||||
</ButtonGroup>
|
||||
|
||||
<SwitchContainer data-testid="mode-switch" disabled={textAreaDisabled} onClick={onModeToggle}>
|
||||
<Slider isAct={chatSettings.mode === "act"} isPlan={chatSettings.mode === "plan"} />
|
||||
<SwitchOption isActive={chatSettings.mode === "plan"}>Plan</SwitchOption>
|
||||
<SwitchOption isActive={chatSettings.mode === "act"}>Act</SwitchOption>
|
||||
</SwitchContainer>
|
||||
<Tooltip
|
||||
visible={shownTooltipMode !== null}
|
||||
tipText={`In ${shownTooltipMode === "act" ? "Act" : "Plan"} mode, Cline will ${shownTooltipMode === "act" ? "complete the task immediately" : "gather information to architect a plan"}`}
|
||||
hintText={`Toggle w/ ${metaKeyChar}+Shift+A`}>
|
||||
<SwitchContainer data-testid="mode-switch" disabled={false} onClick={onModeToggle}>
|
||||
<Slider isAct={chatSettings.mode === "act"} isPlan={chatSettings.mode === "plan"} />
|
||||
<SwitchOption
|
||||
isActive={chatSettings.mode === "plan"}
|
||||
onMouseOver={() => setShownTooltipMode("plan")}
|
||||
onMouseLeave={() => setShownTooltipMode(null)}>
|
||||
Plan
|
||||
</SwitchOption>
|
||||
<SwitchOption
|
||||
isActive={chatSettings.mode === "act"}
|
||||
onMouseOver={() => setShownTooltipMode("act")}
|
||||
onMouseLeave={() => setShownTooltipMode(null)}>
|
||||
Act
|
||||
</SwitchOption>
|
||||
</SwitchContainer>
|
||||
</Tooltip>
|
||||
</ControlsContainer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -324,67 +324,99 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
/*
|
||||
This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension.
|
||||
*/
|
||||
const handlePrimaryButtonClick = useCallback(() => {
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "command":
|
||||
case "command_output":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
break
|
||||
case "completion_result":
|
||||
case "resume_completed_task":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
startNewTask()
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
}, [clineAsk, startNewTask])
|
||||
const handlePrimaryButtonClick = useCallback(
|
||||
(text?: string, images?: string[]) => {
|
||||
const trimmedInput = text?.trim()
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "command":
|
||||
case "command_output":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
text: trimmedInput,
|
||||
images: images,
|
||||
})
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
}
|
||||
// Clear input state after sending
|
||||
setInputValue("")
|
||||
setSelectedImages([])
|
||||
break
|
||||
case "completion_result":
|
||||
case "resume_completed_task":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
startNewTask()
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
},
|
||||
[clineAsk, startNewTask],
|
||||
)
|
||||
|
||||
const handleSecondaryButtonClick = useCallback(() => {
|
||||
if (isStreaming) {
|
||||
vscode.postMessage({ type: "cancelTask" })
|
||||
setDidClickCancel(true)
|
||||
return
|
||||
}
|
||||
const handleSecondaryButtonClick = useCallback(
|
||||
(text?: string, images?: string[]) => {
|
||||
const trimmedInput = text?.trim()
|
||||
if (isStreaming) {
|
||||
vscode.postMessage({ type: "cancelTask" })
|
||||
setDidClickCancel(true)
|
||||
return
|
||||
}
|
||||
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
startNewTask()
|
||||
break
|
||||
case "command":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
// responds to the API with a "This operation failed" and lets it try again
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
})
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
}, [clineAsk, startNewTask, isStreaming])
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
startNewTask()
|
||||
break
|
||||
case "command":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
text: trimmedInput,
|
||||
images: images,
|
||||
})
|
||||
} else {
|
||||
// responds to the API with a "This operation failed" and lets it try again
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
})
|
||||
}
|
||||
// Clear input state after sending
|
||||
setInputValue("")
|
||||
setSelectedImages([])
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
},
|
||||
[clineAsk, startNewTask, isStreaming],
|
||||
)
|
||||
|
||||
const handleTaskCloseButtonClick = useCallback(() => {
|
||||
startNewTask()
|
||||
@@ -426,14 +458,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
handleSendMessage(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
case "primaryButtonClick":
|
||||
handlePrimaryButtonClick()
|
||||
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
case "secondaryButtonClick":
|
||||
handleSecondaryButtonClick()
|
||||
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
|
||||
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],
|
||||
)
|
||||
@@ -869,7 +901,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
flex: secondaryButtonText ? 1 : 2,
|
||||
marginRight: secondaryButtonText ? "6px" : "0",
|
||||
}}
|
||||
onClick={handlePrimaryButtonClick}>
|
||||
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
@@ -881,7 +913,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
flex: isStreaming ? 2 : 1,
|
||||
marginLeft: isStreaming ? 0 : "6px",
|
||||
}}
|
||||
onClick={handleSecondaryButtonClick}>
|
||||
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
|
||||
{isStreaming ? "Cancel" : secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
@@ -919,7 +951,7 @@ const ScrollToBottomButton = styled.div`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
height: 25px;
|
||||
|
||||
&:hover {
|
||||
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 90%, transparent);
|
||||
|
||||
@@ -48,10 +48,33 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
switch (option.type) {
|
||||
case ContextMenuOptionType.Problems:
|
||||
return <span>Problems</span>
|
||||
case ContextMenuOptionType.Terminal:
|
||||
return <span>Terminal</span>
|
||||
case ContextMenuOptionType.URL:
|
||||
return <span>Paste URL to fetch contents</span>
|
||||
case ContextMenuOptionType.NoResults:
|
||||
return <span>No results found</span>
|
||||
case ContextMenuOptionType.Git:
|
||||
if (option.value) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||||
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
opacity: 0.7,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
lineHeight: "1.2",
|
||||
}}>
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <span>Git Commits</span>
|
||||
}
|
||||
case ContextMenuOptionType.File:
|
||||
case ContextMenuOptionType.Folder:
|
||||
if (option.value) {
|
||||
@@ -85,8 +108,12 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
return "folder"
|
||||
case ContextMenuOptionType.Problems:
|
||||
return "warning"
|
||||
case ContextMenuOptionType.Terminal:
|
||||
return "terminal"
|
||||
case ContextMenuOptionType.URL:
|
||||
return "link"
|
||||
case ContextMenuOptionType.Git:
|
||||
return "git-commit"
|
||||
case ContextMenuOptionType.NoResults:
|
||||
return "info"
|
||||
default:
|
||||
@@ -161,7 +188,9 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
/>
|
||||
{renderOptionContent(option)}
|
||||
</div>
|
||||
{(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
|
||||
{(option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder ||
|
||||
option.type === ContextMenuOptionType.Git) &&
|
||||
!option.value && (
|
||||
<i
|
||||
className="codicon codicon-chevron-right"
|
||||
@@ -173,7 +202,10 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
/>
|
||||
)}
|
||||
{(option.type === ContextMenuOptionType.Problems ||
|
||||
((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) &&
|
||||
option.type === ContextMenuOptionType.Terminal ||
|
||||
((option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder ||
|
||||
option.type === ContextMenuOptionType.Git) &&
|
||||
option.value)) && (
|
||||
<i
|
||||
className="codicon codicon-add"
|
||||
|
||||
@@ -100,20 +100,26 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
}, [task.text, windowWidth])
|
||||
|
||||
const isCostAvailable = useMemo(() => {
|
||||
const openAiCompatHasPricing =
|
||||
apiConfiguration?.apiProvider === "openai" &&
|
||||
apiConfiguration?.openAiModelInfo?.inputPrice &&
|
||||
apiConfiguration?.openAiModelInfo?.outputPrice
|
||||
if (openAiCompatHasPricing) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
apiConfiguration?.apiProvider !== "openai" &&
|
||||
apiConfiguration?.apiProvider !== "vscode-lm" &&
|
||||
apiConfiguration?.apiProvider !== "ollama" &&
|
||||
apiConfiguration?.apiProvider !== "lmstudio" &&
|
||||
apiConfiguration?.apiProvider !== "gemini"
|
||||
)
|
||||
}, [apiConfiguration?.apiProvider])
|
||||
}, [apiConfiguration?.apiProvider, apiConfiguration?.openAiModelInfo])
|
||||
|
||||
const shouldShowPromptCacheInfo = doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter"
|
||||
|
||||
const ContextWindowComponent = (
|
||||
<>
|
||||
{isTaskExpanded && contextWindow && lastApiReqTotalTokens && (
|
||||
{isTaskExpanded && contextWindow && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -140,7 +146,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
flex: 1,
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
<span>{formatLargeNumber(lastApiReqTotalTokens)}</span>
|
||||
<span>{formatLargeNumber(lastApiReqTotalTokens || 0)}</span>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -158,7 +164,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: `${(lastApiReqTotalTokens / contextWindow) * 100}%`,
|
||||
width: `${((lastApiReqTotalTokens || 0) / contextWindow) * 100}%`,
|
||||
height: "100%",
|
||||
backgroundColor: "var(--vscode-badge-foreground)",
|
||||
borderRadius: "2px",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import Announcement from "../Announcement"
|
||||
|
||||
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
useTheme: () => ({ themeType: "light" }),
|
||||
VSCodeButton: (props: any) => <button {...props}>{props.children}</button>,
|
||||
VSCodeLink: ({ children }: { children: React.ReactNode }) => <a>{children}</a>,
|
||||
}))
|
||||
|
||||
describe("Announcement", () => {
|
||||
const hideAnnouncement = vi.fn()
|
||||
|
||||
it("renders the announcement with the correct version", () => {
|
||||
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
|
||||
expect(screen.getByText(/New in v2.0/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls hideAnnouncement when close button is clicked", () => {
|
||||
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
|
||||
fireEvent.click(screen.getByRole("button"))
|
||||
expect(hideAnnouncement).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("renders the mcp server improvements announcement", () => {
|
||||
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
|
||||
expect(screen.getByText(/MCP server improvements:/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders the 'See new changes' button feature", () => {
|
||||
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
|
||||
expect(screen.getByText(/See it in action here./)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders the demo link", () => {
|
||||
render(<Announcement version="2.0.0" hideAnnouncement={hideAnnouncement} />)
|
||||
expect(screen.getByText(/See a demo here./)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,394 @@
|
||||
import { useCallback, useRef, useState, useEffect } from "react"
|
||||
import { useClickAway, useEvent } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
import { ClineCheckpointRestore } from "../../../../src/shared/WebviewMessage"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { createPortal } from "react-dom"
|
||||
import { useFloating, offset, flip, shift } from "@floating-ui/react"
|
||||
|
||||
interface CheckmarkControlProps {
|
||||
messageTs?: number
|
||||
isCheckpointCheckedOut?: boolean
|
||||
}
|
||||
|
||||
export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: CheckmarkControlProps) => {
|
||||
const [compareDisabled, setCompareDisabled] = useState(false)
|
||||
const [restoreTaskDisabled, setRestoreTaskDisabled] = useState(false)
|
||||
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false)
|
||||
const [restoreBothDisabled, setRestoreBothDisabled] = useState(false)
|
||||
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false)
|
||||
const [hasMouseEntered, setHasMouseEntered] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { refs, floatingStyles, update, placement } = useFloating({
|
||||
placement: "bottom-end",
|
||||
middleware: [
|
||||
offset({
|
||||
mainAxis: 8,
|
||||
crossAxis: 10,
|
||||
}),
|
||||
flip(),
|
||||
shift(),
|
||||
],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
update()
|
||||
}
|
||||
window.addEventListener("scroll", handleScroll, true)
|
||||
return () => window.removeEventListener("scroll", handleScroll, true)
|
||||
}, [update])
|
||||
|
||||
useEffect(() => {
|
||||
if (showRestoreConfirm) {
|
||||
update()
|
||||
}
|
||||
}, [showRestoreConfirm, update])
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
|
||||
if (event.data.type === "relinquishControl") {
|
||||
setCompareDisabled(false)
|
||||
setRestoreTaskDisabled(false)
|
||||
setRestoreWorkspaceDisabled(false)
|
||||
setRestoreBothDisabled(false)
|
||||
setShowRestoreConfirm(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleRestoreTask = () => {
|
||||
setRestoreTaskDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointRestore",
|
||||
number: messageTs,
|
||||
text: "task",
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestoreWorkspace = () => {
|
||||
setRestoreWorkspaceDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointRestore",
|
||||
number: messageTs,
|
||||
text: "workspace",
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestoreBoth = () => {
|
||||
setRestoreBothDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointRestore",
|
||||
number: messageTs,
|
||||
text: "taskAndWorkspace",
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setHasMouseEntered(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (hasMouseEntered) {
|
||||
setShowRestoreConfirm(false)
|
||||
setHasMouseEntered(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleControlsMouseLeave = (e: React.MouseEvent) => {
|
||||
const tooltipElement = tooltipRef.current
|
||||
|
||||
if (tooltipElement && showRestoreConfirm) {
|
||||
const tooltipRect = tooltipElement.getBoundingClientRect()
|
||||
|
||||
if (
|
||||
e.clientY >= tooltipRect.top &&
|
||||
e.clientY <= tooltipRect.bottom &&
|
||||
e.clientX >= tooltipRect.left &&
|
||||
e.clientX <= tooltipRect.right
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setShowRestoreConfirm(false)
|
||||
setHasMouseEntered(false)
|
||||
}
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
return (
|
||||
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
|
||||
<i
|
||||
className="codicon codicon-bookmark"
|
||||
style={{
|
||||
color: isCheckpointCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Label $isCheckedOut={isCheckpointCheckedOut}>
|
||||
{isCheckpointCheckedOut ? "Checkpoint (restored)" : "Checkpoint"}
|
||||
</Label>
|
||||
<DottedLine $isCheckedOut={isCheckpointCheckedOut} />
|
||||
<ButtonGroup>
|
||||
<CustomButton
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
disabled={compareDisabled}
|
||||
style={{ cursor: compareDisabled ? "wait" : "pointer" }}
|
||||
onClick={() => {
|
||||
setCompareDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointDiff",
|
||||
number: messageTs,
|
||||
})
|
||||
}}>
|
||||
Compare
|
||||
</CustomButton>
|
||||
<DottedLine small $isCheckedOut={isCheckpointCheckedOut} />
|
||||
<div ref={refs.setReference} style={{ position: "relative", marginTop: -2 }}>
|
||||
<CustomButton
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
isActive={showRestoreConfirm}
|
||||
onClick={() => setShowRestoreConfirm(true)}>
|
||||
Restore
|
||||
</CustomButton>
|
||||
{showRestoreConfirm &&
|
||||
createPortal(
|
||||
<RestoreConfirmTooltip
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
data-placement={placement}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreWorkspace}
|
||||
disabled={restoreWorkspaceDisabled}
|
||||
style={{
|
||||
cursor: restoreWorkspaceDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files
|
||||
</VSCodeButton>
|
||||
<p>
|
||||
Restores your project's files back to a snapshot taken at this point (use "Compare" to see
|
||||
what will be reverted)
|
||||
</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreTask}
|
||||
disabled={restoreTaskDisabled}
|
||||
style={{
|
||||
cursor: restoreTaskDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Task Only
|
||||
</VSCodeButton>
|
||||
<p>Deletes messages after this point (does not affect workspace files)</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreBoth}
|
||||
disabled={restoreBothDisabled}
|
||||
style={{
|
||||
cursor: restoreBothDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files & Task
|
||||
</VSCodeButton>
|
||||
<p>Restores your project's files and deletes all messages after this point</p>
|
||||
</RestoreOption>
|
||||
</RestoreConfirmTooltip>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
<DottedLine small $isCheckedOut={isCheckpointCheckedOut} />
|
||||
</ButtonGroup>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div<{ isMenuOpen?: boolean; $isCheckedOut?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
margin-top: -10px;
|
||||
margin-bottom: -10px;
|
||||
opacity: ${(props) => (props.$isCheckedOut ? 1 : props.isMenuOpen ? 1 : 0.5)};
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const Label = styled.span<{ $isCheckedOut?: boolean }>`
|
||||
color: ${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
font-size: 9px;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const DottedLine = styled.div<{ small?: boolean; $isCheckedOut?: boolean }>`
|
||||
flex: ${(props) => (props.small ? "0 0 5px" : "1")};
|
||||
min-width: ${(props) => (props.small ? "5px" : "5px")};
|
||||
height: 1px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")} 50%,
|
||||
transparent 50%
|
||||
);
|
||||
background-size: 4px 1px;
|
||||
background-repeat: repeat-x;
|
||||
`
|
||||
|
||||
const ButtonGroup = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const CustomButton = styled.button<{ disabled?: boolean; isActive?: boolean; $isCheckedOut?: boolean }>`
|
||||
background: ${(props) =>
|
||||
props.isActive || props.disabled
|
||||
? props.$isCheckedOut
|
||||
? "var(--vscode-textLink-foreground)"
|
||||
: "var(--vscode-descriptionForeground)"
|
||||
: "transparent"};
|
||||
border: none;
|
||||
color: ${(props) =>
|
||||
props.isActive || props.disabled
|
||||
? "var(--vscode-editor-background)"
|
||||
: props.$isCheckedOut
|
||||
? "var(--vscode-textLink-foreground)"
|
||||
: "var(--vscode-descriptionForeground)"};
|
||||
padding: 2px 6px;
|
||||
font-size: 9px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 1px;
|
||||
background-image: ${(props) =>
|
||||
props.isActive || props.disabled
|
||||
? "none"
|
||||
: `linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%)`};
|
||||
background-size: ${(props) => (props.isActive || props.disabled ? "auto" : `4px 1px, 1px 4px, 4px 1px, 1px 4px`)};
|
||||
background-repeat: repeat-x, repeat-y, repeat-x, repeat-y;
|
||||
background-position:
|
||||
0 0,
|
||||
100% 0,
|
||||
0 100%,
|
||||
0 0;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: ${(props) =>
|
||||
props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"};
|
||||
color: var(--vscode-editor-background);
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`
|
||||
|
||||
const RestoreOption = styled.div`
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--vscode-editorGroup-border);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 2px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
&:last-child p {
|
||||
margin: 0 0 -2px 0;
|
||||
}
|
||||
`
|
||||
|
||||
const RestoreConfirmTooltip = styled.div`
|
||||
position: fixed;
|
||||
background: ${CODE_BLOCK_BG_COLOR};
|
||||
border: 1px solid var(--vscode-editorGroup-border);
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
width: min(calc(100vw - 54px), 600px);
|
||||
z-index: 1000;
|
||||
|
||||
// Add invisible padding to create a safe hover zone
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// Adjust arrow to be above the padding
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: 24px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: ${CODE_BLOCK_BG_COLOR};
|
||||
border-left: 1px solid var(--vscode-editorGroup-border);
|
||||
border-top: 1px solid var(--vscode-editorGroup-border);
|
||||
transform: rotate(45deg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// When menu appears above the button
|
||||
&[data-placement^="top"] {
|
||||
&::before {
|
||||
top: auto;
|
||||
bottom: -8px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: auto;
|
||||
bottom: -6px;
|
||||
right: 24px;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 6px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 12px;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
`
|
||||
@@ -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]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import styled from "styled-components"
|
||||
|
||||
const StyledButton = styled(VSCodeButton)`
|
||||
--danger-button-bg: #c42b2b;
|
||||
--danger-button-hover: #a82424;
|
||||
--danger-button-active: #8f1f1f;
|
||||
|
||||
background-color: var(--danger-button-bg) !important;
|
||||
border-color: var(--danger-button-bg) !important;
|
||||
color: #ffffff !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--danger-button-hover) !important;
|
||||
border-color: var(--danger-button-hover) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--danger-button-active) !important;
|
||||
border-color: var(--danger-button-active) !important;
|
||||
}
|
||||
`
|
||||
|
||||
interface DangerButtonProps extends React.ComponentProps<typeof VSCodeButton> {}
|
||||
|
||||
const DangerButton: React.FC<DangerButtonProps> = (props) => {
|
||||
return <StyledButton {...props} />
|
||||
}
|
||||
|
||||
export default DangerButton
|
||||
@@ -1,10 +1,11 @@
|
||||
import { memo, useEffect } from "react"
|
||||
import React, { memo, useEffect } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import rehypeHighlight, { Options } from "rehype-highlight"
|
||||
import styled from "styled-components"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
import MermaidBlock from "./MermaidBlock"
|
||||
|
||||
interface MarkdownBlockProps {
|
||||
markdown?: string
|
||||
@@ -50,6 +51,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 +199,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
|
||||
const { theme } = useExtensionState()
|
||||
const [reactContent, setMarkdown] = useRemark({
|
||||
remarkPlugins: [
|
||||
remarkPreventBoldFilenames,
|
||||
remarkUrlToLink,
|
||||
() => {
|
||||
return (tree) => {
|
||||
@@ -181,7 +221,27 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
|
||||
],
|
||||
rehypeReactOptions: {
|
||||
components: {
|
||||
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
|
||||
pre: ({ node, children, ...preProps }: any) => {
|
||||
if (Array.isArray(children) && children.length === 1 && React.isValidElement(children[0])) {
|
||||
const child = children[0] as React.ReactElement<{ className?: string }>
|
||||
if (child.props?.className?.includes("language-mermaid")) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return (
|
||||
<StyledPre {...preProps} theme={theme}>
|
||||
{children}
|
||||
</StyledPre>
|
||||
)
|
||||
},
|
||||
code: (props: any) => {
|
||||
const className = props.className || ""
|
||||
if (className.includes("language-mermaid")) {
|
||||
const codeText = String(props.children || "")
|
||||
return <MermaidBlock code={codeText} />
|
||||
}
|
||||
return <code {...props} />
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import mermaid from "mermaid"
|
||||
import { useDebounceEffect } from "../../utils/useDebounceEffect"
|
||||
import styled from "styled-components"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
const MERMAID_THEME = {
|
||||
background: "#1e1e1e", // VS Code dark theme background
|
||||
textColor: "#ffffff", // Main text color
|
||||
mainBkg: "#2d2d2d", // Background for nodes
|
||||
nodeBorder: "#888888", // Border color for nodes
|
||||
lineColor: "#cccccc", // Lines connecting nodes
|
||||
primaryColor: "#3c3c3c", // Primary color for highlights
|
||||
primaryTextColor: "#ffffff", // Text in primary colored elements
|
||||
primaryBorderColor: "#888888",
|
||||
secondaryColor: "#2d2d2d", // Secondary color for alternate elements
|
||||
tertiaryColor: "#454545", // Third color for special elements
|
||||
|
||||
// Class diagram specific
|
||||
classText: "#ffffff",
|
||||
|
||||
// State diagram specific
|
||||
labelColor: "#ffffff",
|
||||
|
||||
// Sequence diagram specific
|
||||
actorLineColor: "#cccccc",
|
||||
actorBkg: "#2d2d2d",
|
||||
actorBorder: "#888888",
|
||||
actorTextColor: "#ffffff",
|
||||
|
||||
// Flow diagram specific
|
||||
fillType0: "#2d2d2d",
|
||||
fillType1: "#3c3c3c",
|
||||
fillType2: "#454545",
|
||||
}
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "loose",
|
||||
theme: "dark",
|
||||
themeVariables: {
|
||||
...MERMAID_THEME,
|
||||
fontSize: "16px",
|
||||
fontFamily: "var(--vscode-font-family, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif)",
|
||||
|
||||
// Additional styling
|
||||
noteTextColor: "#ffffff",
|
||||
noteBkgColor: "#454545",
|
||||
noteBorderColor: "#888888",
|
||||
|
||||
// Improve contrast for special elements
|
||||
critBorderColor: "#ff9580",
|
||||
critBkgColor: "#803d36",
|
||||
|
||||
// Task diagram specific
|
||||
taskTextColor: "#ffffff",
|
||||
taskTextOutsideColor: "#ffffff",
|
||||
taskTextLightColor: "#ffffff",
|
||||
|
||||
// Numbers/sections
|
||||
sectionBkgColor: "#2d2d2d",
|
||||
sectionBkgColor2: "#3c3c3c",
|
||||
|
||||
// Alt sections in sequence diagrams
|
||||
altBackground: "#2d2d2d",
|
||||
|
||||
// Links
|
||||
linkColor: "#6cb6ff",
|
||||
|
||||
// Borders and lines
|
||||
compositeBackground: "#2d2d2d",
|
||||
compositeBorder: "#888888",
|
||||
titleColor: "#ffffff",
|
||||
},
|
||||
})
|
||||
|
||||
interface MermaidBlockProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
export default function MermaidBlock({ code }: MermaidBlockProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 1) Whenever `code` changes, mark that we need to re-render a new chart
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
}, [code])
|
||||
|
||||
// 2) Debounce the actual parse/render
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = ""
|
||||
}
|
||||
mermaid
|
||||
.parse(code, { suppressErrors: true })
|
||||
.then((isValid) => {
|
||||
if (!isValid) {
|
||||
throw new Error("Invalid or incomplete Mermaid code")
|
||||
}
|
||||
const id = `mermaid-${Math.random().toString(36).substring(2)}`
|
||||
return mermaid.render(id, code)
|
||||
})
|
||||
.then(({ svg }) => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = svg
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("Mermaid parse/render failed:", err)
|
||||
containerRef.current!.innerHTML = code.replace(/</g, "<").replace(/>/g, ">")
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
},
|
||||
500, // Delay 500ms
|
||||
[code], // Dependencies for scheduling
|
||||
)
|
||||
|
||||
/**
|
||||
* Called when user clicks the rendered diagram.
|
||||
* Converts the <svg> to a PNG and sends it to the extension.
|
||||
*/
|
||||
const handleClick = async () => {
|
||||
if (!containerRef.current) return
|
||||
const svgEl = containerRef.current.querySelector("svg")
|
||||
if (!svgEl) return
|
||||
|
||||
try {
|
||||
const pngDataUrl = await svgToPng(svgEl)
|
||||
vscode.postMessage({
|
||||
type: "openImage",
|
||||
text: pngDataUrl,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error("Error converting SVG to PNG:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MermaidBlockContainer>
|
||||
{isLoading && <LoadingMessage>Generating mermaid diagram...</LoadingMessage>}
|
||||
|
||||
{/* The container for the final <svg> or raw code. */}
|
||||
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
|
||||
</MermaidBlockContainer>
|
||||
)
|
||||
}
|
||||
|
||||
async function svgToPng(svgEl: SVGElement): Promise<string> {
|
||||
console.log("svgToPng function called")
|
||||
// Clone the SVG to avoid modifying the original
|
||||
const svgClone = svgEl.cloneNode(true) as SVGElement
|
||||
|
||||
// Get the original viewBox
|
||||
const viewBox = svgClone.getAttribute("viewBox")?.split(" ").map(Number) || []
|
||||
const originalWidth = viewBox[2] || svgClone.clientWidth
|
||||
const originalHeight = viewBox[3] || svgClone.clientHeight
|
||||
|
||||
// Calculate the scale factor to fit editor width while maintaining aspect ratio
|
||||
|
||||
// Unless we can find a way to get the actual editor window dimensions through the VS Code API (which might be possible but would require changes to the extension side),
|
||||
// the fixed width seems like a reliable approach.
|
||||
const editorWidth = 3_600
|
||||
|
||||
const scale = editorWidth / originalWidth
|
||||
const scaledHeight = originalHeight * scale
|
||||
|
||||
// Update SVG dimensions
|
||||
svgClone.setAttribute("width", `${editorWidth}`)
|
||||
svgClone.setAttribute("height", `${scaledHeight}`)
|
||||
|
||||
const serializer = new XMLSerializer()
|
||||
const svgString = serializer.serializeToString(svgClone)
|
||||
const svgDataUrl = "data:image/svg+xml;base64," + btoa(decodeURIComponent(encodeURIComponent(svgString)))
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = editorWidth
|
||||
canvas.height = scaledHeight
|
||||
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return reject("Canvas context not available")
|
||||
|
||||
// Fill background with Mermaid's dark theme background color
|
||||
ctx.fillStyle = MERMAID_THEME.background
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = "high"
|
||||
|
||||
ctx.drawImage(img, 0, 0, editorWidth, scaledHeight)
|
||||
resolve(canvas.toDataURL("image/png", 1.0))
|
||||
}
|
||||
img.onerror = reject
|
||||
img.src = svgDataUrl
|
||||
})
|
||||
}
|
||||
|
||||
const MermaidBlockContainer = styled.div`
|
||||
position: relative;
|
||||
margin: 8px 0;
|
||||
`
|
||||
|
||||
const LoadingMessage = styled.div`
|
||||
padding: 8px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
`
|
||||
|
||||
interface SvgContainerProps {
|
||||
$isLoading: boolean
|
||||
}
|
||||
|
||||
const SvgContainer = styled.div<SvgContainerProps>`
|
||||
opacity: ${(props) => (props.$isLoading ? 0.3 : 1)};
|
||||
min-height: 20px;
|
||||
transition: opacity 0.2s ease;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import {
|
||||
getAsVar,
|
||||
VSC_DESCRIPTION_FOREGROUND,
|
||||
VSC_SIDEBAR_BACKGROUND,
|
||||
VSC_INPUT_PLACEHOLDER_FOREGROUND,
|
||||
VSC_INPUT_BORDER,
|
||||
} from "../../utils/vscStyles"
|
||||
|
||||
interface TooltipProps {
|
||||
visible: boolean
|
||||
hintText: string
|
||||
tipText: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
// add styled component for tooltip
|
||||
const TooltipBody = styled.div`
|
||||
position: absolute;
|
||||
background-color: ${getAsVar(VSC_SIDEBAR_BACKGROUND)};
|
||||
color: ${getAsVar(VSC_DESCRIPTION_FOREGROUND)};
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
bottom: 100%;
|
||||
left: -180%;
|
||||
z-index: 10;
|
||||
white-space: wrap;
|
||||
max-width: 200px;
|
||||
border: 1px solid ${getAsVar(VSC_INPUT_BORDER)};
|
||||
pointer-events: none;
|
||||
font-size: 0.9em;
|
||||
`
|
||||
|
||||
const Hint = styled.div`
|
||||
font-size: 0.8em;
|
||||
color: ${getAsVar(VSC_INPUT_PLACEHOLDER_FOREGROUND)};
|
||||
opacity: 0.8;
|
||||
margin-top: 2px;
|
||||
`
|
||||
|
||||
const Tooltip: React.FC<TooltipProps> = ({ visible, tipText, hintText, children }) => {
|
||||
return (
|
||||
<div style={{ position: "relative", display: "inline-block" }}>
|
||||
{children}
|
||||
{visible && (
|
||||
<TooltipBody>
|
||||
{tipText}
|
||||
{hintText && <Hint>{hintText}</Hint>}
|
||||
</TooltipBody>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tooltip
|
||||
@@ -1,78 +1,40 @@
|
||||
import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useState } from "react"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { useEffect, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { McpServer } from "../../../../src/shared/mcp"
|
||||
import McpToolRow from "./McpToolRow"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { getMcpServerDisplayName } from "../../utils/mcp"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import DangerButton from "../common/DangerButton"
|
||||
import McpMarketplaceView from "./marketplace/McpMarketplaceView"
|
||||
import McpResourceRow from "./McpResourceRow"
|
||||
import McpToolRow from "./McpToolRow"
|
||||
|
||||
type McpViewProps = {
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
const McpView = ({ onDone }: McpViewProps) => {
|
||||
const { mcpServers: servers } = useExtensionState()
|
||||
const { mcpServers: servers, mcpMarketplaceEnabled } = useExtensionState()
|
||||
const [activeTab, setActiveTab] = useState(mcpMarketplaceEnabled ? "marketplace" : "installed")
|
||||
|
||||
// const [servers, setServers] = useState<McpServer[]>([
|
||||
// // Add some mock servers for testing
|
||||
// {
|
||||
// name: "local-tools",
|
||||
// config: JSON.stringify({
|
||||
// mcpServers: {
|
||||
// "local-tools": {
|
||||
// command: "npx",
|
||||
// args: ["-y", "@modelcontextprotocol/server-tools"],
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// status: "connected",
|
||||
// tools: [
|
||||
// {
|
||||
// name: "execute_command",
|
||||
// description: "Run a shell command on the local system",
|
||||
// },
|
||||
// {
|
||||
// name: "read_file",
|
||||
// description: "Read contents of a file from the filesystem",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// name: "postgres-db",
|
||||
// config: JSON.stringify({
|
||||
// mcpServers: {
|
||||
// "postgres-db": {
|
||||
// command: "npx",
|
||||
// args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// status: "disconnected",
|
||||
// error: "Failed to connect to database: Connection refused",
|
||||
// },
|
||||
// {
|
||||
// name: "github-tools",
|
||||
// config: JSON.stringify({
|
||||
// mcpServers: {
|
||||
// "github-tools": {
|
||||
// command: "npx",
|
||||
// args: ["-y", "@modelcontextprotocol/server-github"],
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// status: "connecting",
|
||||
// resources: [
|
||||
// {
|
||||
// uri: "github://repo/issues",
|
||||
// name: "Repository Issues",
|
||||
// },
|
||||
// {
|
||||
// uri: "github://repo/pulls",
|
||||
// name: "Pull Requests",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ])
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!mcpMarketplaceEnabled && activeTab === "marketplace") {
|
||||
// If marketplace is disabled and we're on marketplace tab, switch to installed
|
||||
setActiveTab("installed")
|
||||
}
|
||||
}, [mcpMarketplaceEnabled, activeTab])
|
||||
|
||||
useEffect(() => {
|
||||
if (mcpMarketplaceEnabled) {
|
||||
vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" })
|
||||
vscode.postMessage({ type: "fetchLatestMcpServersFromHub" })
|
||||
}
|
||||
}, [mcpMarketplaceEnabled])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -90,86 +52,146 @@ const McpView = ({ onDone }: McpViewProps) => {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "10px 17px 10px 20px",
|
||||
padding: "10px 17px 5px 20px",
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
|
||||
<div style={{ flex: 1, overflow: "auto" }}>
|
||||
{/* Tabs container */}
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "13px",
|
||||
marginBottom: "16px",
|
||||
marginTop: "5px",
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
padding: "0 20px 0 20px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
}}>
|
||||
The{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
|
||||
Model Context Protocol
|
||||
</VSCodeLink>{" "}
|
||||
enables communication with locally running MCP servers that provide additional tools and resources to extend
|
||||
Cline's capabilities. You can use{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||
community-made servers
|
||||
</VSCodeLink>{" "}
|
||||
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest npm docs").{" "}
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
||||
See a demo here.
|
||||
</VSCodeLink>
|
||||
{mcpMarketplaceEnabled && (
|
||||
<TabButton isActive={activeTab === "marketplace"} onClick={() => handleTabChange("marketplace")}>
|
||||
Marketplace
|
||||
</TabButton>
|
||||
)}
|
||||
<TabButton isActive={activeTab === "installed"} onClick={() => handleTabChange("installed")}>
|
||||
Installed
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
{servers.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}>
|
||||
{servers.map((server) => (
|
||||
<ServerRow key={server.name} server={server} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Content container */}
|
||||
<div style={{ width: "100%" }}>
|
||||
{mcpMarketplaceEnabled && activeTab === "marketplace" && <McpMarketplaceView />}
|
||||
{activeTab === "installed" && (
|
||||
<div style={{ padding: "16px 20px" }}>
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "13px",
|
||||
marginBottom: "16px",
|
||||
marginTop: "5px",
|
||||
}}>
|
||||
The{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
|
||||
Model Context Protocol
|
||||
</VSCodeLink>{" "}
|
||||
enables communication with locally running MCP servers that provide additional tools and resources
|
||||
to extend Cline's capabilities. You can use{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||
community-made servers
|
||||
</VSCodeLink>{" "}
|
||||
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest
|
||||
npm docs").{" "}
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
||||
See a demo here.
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
|
||||
{/* Server Configuration Button */}
|
||||
{servers.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}>
|
||||
{servers.map((server) => (
|
||||
<ServerRow key={server.name} server={server} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
No MCP servers installed
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: "10px", width: "100%" }}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{ width: "100%" }}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openMcpSettings" })
|
||||
}}>
|
||||
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
|
||||
Configure MCP Servers
|
||||
</VSCodeButton>
|
||||
{/* Settings Section */}
|
||||
<div style={{ marginBottom: "20px", marginTop: 10 }}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{ width: "100%", marginBottom: "5px" }}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openMcpSettings" })
|
||||
}}>
|
||||
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
|
||||
Configure MCP Servers
|
||||
</VSCodeButton>
|
||||
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<VSCodeLink
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "openExtensionSettings",
|
||||
text: "cline.mcp",
|
||||
})
|
||||
}}
|
||||
style={{ fontSize: "12px" }}>
|
||||
Advanced MCP Settings
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Advanced Settings Link */}
|
||||
<div style={{ textAlign: "center", marginTop: "5px" }}>
|
||||
<VSCodeLink
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "openExtensionSettings",
|
||||
text: "cline.mcp",
|
||||
})
|
||||
}}
|
||||
style={{ fontSize: "12px" }}>
|
||||
Advanced MCP Settings
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
|
||||
{/* Bottom padding */}
|
||||
<div style={{ height: "20px" }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledTabButton = styled.button<{ isActive: boolean }>`
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
|
||||
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
margin-bottom: -1px;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
`
|
||||
|
||||
const TabButton = ({ children, isActive, onClick }: { children: React.ReactNode; isActive: boolean; onClick: () => void }) => (
|
||||
<StyledTabButton isActive={isActive} onClick={onClick}>
|
||||
{children}
|
||||
</StyledTabButton>
|
||||
)
|
||||
|
||||
// Server Row Component
|
||||
const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
const { mcpMarketplaceCatalog } = useExtensionState()
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (server.status) {
|
||||
@@ -195,6 +217,14 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
setIsDeleting(true)
|
||||
vscode.postMessage({
|
||||
type: "deleteMcpServer",
|
||||
serverName: server.name,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
<div
|
||||
@@ -211,7 +241,18 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
{!server.error && (
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} style={{ marginRight: "8px" }} />
|
||||
)}
|
||||
<span style={{ flex: 1 }}>{server.name}</span>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
wordBreak: "break-all",
|
||||
whiteSpace: "normal",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginRight: "4px",
|
||||
}}>
|
||||
{getMcpServerDisplayName(server.name, mcpMarketplaceCatalog)}
|
||||
</span>
|
||||
<div style={{ display: "flex", alignItems: "center", marginRight: "8px" }} onClick={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
role="switch"
|
||||
@@ -379,6 +420,16 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
}}>
|
||||
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
|
||||
</VSCodeButton>
|
||||
|
||||
<DangerButton
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
style={{
|
||||
width: "calc(100% - 14px)",
|
||||
margin: "5px 7px 3px 7px",
|
||||
}}>
|
||||
{isDeleting ? "Deleting..." : "Delete Server"}
|
||||
</DangerButton>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import { useCallback, useState, useRef, useMemo } from "react"
|
||||
import styled from "styled-components"
|
||||
import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp"
|
||||
import { vscode } from "../../../utils/vscode"
|
||||
import { useEvent } from "react-use"
|
||||
|
||||
interface McpMarketplaceCardProps {
|
||||
item: McpMarketplaceItem
|
||||
installedServers: McpServer[]
|
||||
}
|
||||
|
||||
const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) => {
|
||||
const isInstalled = installedServers.some((server) => server.name === item.mcpId)
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const githubLinkRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message = event.data
|
||||
switch (message.type) {
|
||||
case "mcpDownloadDetails":
|
||||
setIsDownloading(false)
|
||||
break
|
||||
case "relinquishControl":
|
||||
setIsLoading(false)
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
const githubAuthorUrl = useMemo(() => {
|
||||
const url = new URL(item.githubUrl)
|
||||
const pathParts = url.pathname.split("/")
|
||||
if (pathParts.length >= 2) {
|
||||
return `${url.origin}/${pathParts[1]}`
|
||||
}
|
||||
return item.githubUrl
|
||||
}, [item.githubUrl])
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
.mcp-card {
|
||||
cursor: pointer;
|
||||
outline: none !important;
|
||||
}
|
||||
.mcp-card:hover {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.mcp-card:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<a
|
||||
href={item.githubUrl}
|
||||
className="mcp-card"
|
||||
style={{
|
||||
padding: "14px 16px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 12,
|
||||
cursor: isLoading ? "wait" : "pointer",
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
}}>
|
||||
{/* Main container with logo and content */}
|
||||
<div style={{ display: "flex", gap: "12px" }}>
|
||||
{/* Logo */}
|
||||
{item.logoUrl && (
|
||||
<img
|
||||
src={item.logoUrl}
|
||||
alt={`${item.name} logo`}
|
||||
style={{
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Content section */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
{/* First row: name and install button */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
}}>
|
||||
<h3
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
{item.name}
|
||||
</h3>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault() // Prevent card click when clicking install
|
||||
e.stopPropagation() // Stop event from bubbling up to parent link
|
||||
if (!isInstalled && !isDownloading) {
|
||||
setIsDownloading(true)
|
||||
vscode.postMessage({
|
||||
type: "downloadMcp",
|
||||
mcpId: item.mcpId,
|
||||
})
|
||||
}
|
||||
}}
|
||||
style={{}}>
|
||||
<StyledInstallButton disabled={isInstalled || isDownloading} $isInstalled={isInstalled}>
|
||||
{isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"}
|
||||
</StyledInstallButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Second row: metadata */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
flexWrap: "wrap",
|
||||
minWidth: 0,
|
||||
rowGap: 0,
|
||||
}}>
|
||||
<a
|
||||
href={githubAuthorUrl}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "var(--vscode-foreground)",
|
||||
minWidth: 0,
|
||||
opacity: 0.7,
|
||||
textDecoration: "none",
|
||||
border: "none !important",
|
||||
}}
|
||||
className="github-link"
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.opacity = "1"
|
||||
e.currentTarget.style.color = "var(--link-active-foreground)"
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.opacity = "0.7"
|
||||
e.currentTarget.style.color = "var(--vscode-foreground)"
|
||||
}}>
|
||||
<div style={{ display: "flex", gap: "4px", alignItems: "center" }} ref={githubLinkRef}>
|
||||
<span className="codicon codicon-github" style={{ fontSize: "14px" }} />
|
||||
<span
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-all",
|
||||
minWidth: 0,
|
||||
}}>
|
||||
{item.author}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
minWidth: 0,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span className="codicon codicon-star-full" />
|
||||
<span style={{ wordBreak: "break-all" }}>{item.githubStars?.toLocaleString() ?? 0}</span>
|
||||
</div>
|
||||
{/* <div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
minWidth: 0,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span className="codicon codicon-cloud-download" />
|
||||
<span style={{ wordBreak: "break-all" }}>{item.downloadCount?.toLocaleString() ?? 0}</span>
|
||||
</div> */}
|
||||
{item.requiresApiKey && (
|
||||
<span className="codicon codicon-key" title="Requires API key" style={{ flexShrink: 0 }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description and tags */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{/* {!item.isRecommended && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-notificationsWarningIcon-foreground)",
|
||||
marginTop: -3,
|
||||
marginBottom: -3,
|
||||
}}>
|
||||
<span className="codicon codicon-warning" style={{ fontSize: "14px" }} />
|
||||
<span>Community Made (use at your own risk)</span>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<p style={{ fontSize: "13px", margin: 0 }}>{item.description}</p>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "6px",
|
||||
flexWrap: "nowrap",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
padding: "1px 4px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
{item.category}
|
||||
</span>
|
||||
{item.tags.map((tag, index) => (
|
||||
<span
|
||||
key={tag}
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
padding: "1px 4px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
whiteSpace: "nowrap",
|
||||
display: "inline-flex",
|
||||
}}>
|
||||
{tag}
|
||||
{index === item.tags.length - 1 ? "" : ""}
|
||||
</span>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: "32px",
|
||||
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledInstallButton = styled.button<{ $isInstalled?: boolean }>`
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: ${(props) =>
|
||||
props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"};
|
||||
color: var(--vscode-button-foreground);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: ${(props) =>
|
||||
props.$isInstalled ? "var(--vscode-button-secondaryHoverBackground)" : "var(--vscode-button-hoverBackground)"};
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: ${(props) =>
|
||||
props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"};
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
`
|
||||
|
||||
export default McpMarketplaceCard
|
||||
@@ -0,0 +1,286 @@
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeProgressRing,
|
||||
VSCodeRadioGroup,
|
||||
VSCodeRadio,
|
||||
VSCodeDropdown,
|
||||
VSCodeOption,
|
||||
VSCodeTextField,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { McpMarketplaceItem } from "../../../../../src/shared/mcp"
|
||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../../utils/vscode"
|
||||
import McpMarketplaceCard from "./McpMarketplaceCard"
|
||||
import McpSubmitCard from "./McpSubmitCard"
|
||||
const McpMarketplaceView = () => {
|
||||
const { mcpServers } = useExtensionState()
|
||||
const [items, setItems] = useState<McpMarketplaceItem[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
||||
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name">("newest")
|
||||
|
||||
const categories = useMemo(() => {
|
||||
const uniqueCategories = new Set(items.map((item) => item.category))
|
||||
return Array.from(uniqueCategories).sort()
|
||||
}, [items])
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
return items
|
||||
.filter((item) => {
|
||||
const matchesSearch =
|
||||
searchQuery === "" ||
|
||||
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
const matchesCategory = !selectedCategory || item.category === selectedCategory
|
||||
return matchesSearch && matchesCategory
|
||||
})
|
||||
.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
// case "downloadCount":
|
||||
// return b.downloadCount - a.downloadCount
|
||||
case "stars":
|
||||
return b.githubStars - a.githubStars
|
||||
case "name":
|
||||
return a.name.localeCompare(b.name)
|
||||
case "newest":
|
||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}, [items, searchQuery, selectedCategory, sortBy])
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "mcpMarketplaceCatalog") {
|
||||
if (message.error) {
|
||||
setError(message.error)
|
||||
} else {
|
||||
setItems(message.mcpMarketplaceCatalog?.items || [])
|
||||
setError(null)
|
||||
}
|
||||
setIsLoading(false)
|
||||
setIsRefreshing(false)
|
||||
} else if (message.type === "mcpDownloadDetails") {
|
||||
if (message.error) {
|
||||
setError(message.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
|
||||
// Fetch marketplace catalog
|
||||
fetchMarketplace()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchMarketplace = (forceRefresh: boolean = false) => {
|
||||
if (forceRefresh) {
|
||||
setIsRefreshing(true)
|
||||
} else {
|
||||
setIsLoading(true)
|
||||
}
|
||||
setError(null)
|
||||
vscode.postMessage({ type: "fetchMcpMarketplace", bool: forceRefresh })
|
||||
}
|
||||
|
||||
if (isLoading || isRefreshing) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
padding: "20px",
|
||||
}}>
|
||||
<VSCodeProgressRing />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
padding: "20px",
|
||||
gap: "12px",
|
||||
}}>
|
||||
<div style={{ color: "var(--vscode-errorForeground)" }}>{error}</div>
|
||||
<VSCodeButton appearance="secondary" onClick={() => fetchMarketplace(true)}>
|
||||
<span className="codicon codicon-refresh" style={{ marginRight: "6px" }} />
|
||||
Retry
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
}}>
|
||||
<div style={{ padding: "20px 20px 5px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
||||
{/* Search row */}
|
||||
<VSCodeTextField
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Search MCPs..."
|
||||
value={searchQuery}
|
||||
onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}>
|
||||
<div
|
||||
slot="start"
|
||||
className="codicon codicon-search"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<div
|
||||
className="codicon codicon-close"
|
||||
aria-label="Clear search"
|
||||
onClick={() => setSearchQuery("")}
|
||||
slot="end"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VSCodeTextField>
|
||||
|
||||
{/* Filter row */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
Filter:
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
zIndex: 2,
|
||||
flex: 1,
|
||||
}}>
|
||||
<VSCodeDropdown
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
value={selectedCategory || ""}
|
||||
onChange={(e) => setSelectedCategory((e.target as HTMLSelectElement).value || null)}>
|
||||
<VSCodeOption value="">All Categories</VSCodeOption>
|
||||
{categories.map((category) => (
|
||||
<VSCodeOption key={category} value={category}>
|
||||
{category}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort row */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
marginTop: "3px",
|
||||
}}>
|
||||
Sort:
|
||||
</span>
|
||||
<VSCodeRadioGroup
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
marginTop: "-2.5px",
|
||||
}}
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy((e.target as HTMLInputElement).value as typeof sortBy)}>
|
||||
{/* <VSCodeRadio value="downloadCount">Most Installs</VSCodeRadio> */}
|
||||
<VSCodeRadio value="newest">Newest</VSCodeRadio>
|
||||
<VSCodeRadio value="stars">GitHub Stars</VSCodeRadio>
|
||||
<VSCodeRadio value="name">Name</VSCodeRadio>
|
||||
</VSCodeRadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
.mcp-search-input,
|
||||
.mcp-select {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mcp-search-input {
|
||||
min-width: 140px;
|
||||
}
|
||||
.mcp-search-input:focus,
|
||||
.mcp-select:focus {
|
||||
border-color: var(--vscode-focusBorder) !important;
|
||||
}
|
||||
.mcp-search-input:hover,
|
||||
.mcp-select:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{filteredItems.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
padding: "20px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
{searchQuery || selectedCategory
|
||||
? "No matching MCP servers found"
|
||||
: "No MCP servers found in the marketplace"}
|
||||
</div>
|
||||
) : (
|
||||
filteredItems.map((item) => <McpMarketplaceCard key={item.mcpId} item={item} installedServers={mcpServers} />)
|
||||
)}
|
||||
<McpSubmitCard />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpMarketplaceView
|
||||
@@ -0,0 +1,45 @@
|
||||
const McpSubmitCard = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
padding: "15px",
|
||||
margin: "20px",
|
||||
backgroundColor: "var(--vscode-textBlockQuote-background)",
|
||||
borderRadius: "6px",
|
||||
}}>
|
||||
{/* Icon */}
|
||||
<i className="codicon codicon-add" style={{ fontSize: "18px" }} />
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
textAlign: "center",
|
||||
maxWidth: "480px",
|
||||
}}>
|
||||
<h3
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "14px",
|
||||
fontWeight: 600,
|
||||
color: "var(--vscode-foreground)",
|
||||
}}>
|
||||
Submit MCP Server
|
||||
</h3>
|
||||
<p style={{ fontSize: "13px", margin: 0, color: "var(--vscode-descriptionForeground)" }}>
|
||||
Help others discover great MCP servers by submitting an issue to{" "}
|
||||
<a href="https://github.com/cline/mcp-marketplace">github.com/cline/mcp-marketplace</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpSubmitCard
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
bedrockModels,
|
||||
deepSeekDefaultModelId,
|
||||
deepSeekModels,
|
||||
qwenDefaultModelId,
|
||||
qwenModels,
|
||||
geminiDefaultModelId,
|
||||
geminiModels,
|
||||
mistralDefaultModelId,
|
||||
@@ -39,6 +41,7 @@ import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterModelPicker"
|
||||
import styled from "styled-components"
|
||||
import * as vscodemodels from "vscode"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
showModelOptions: boolean
|
||||
@@ -78,6 +81,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
|
||||
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
|
||||
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
|
||||
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
|
||||
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
|
||||
@@ -133,7 +137,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.
|
||||
*/
|
||||
@@ -177,16 +181,20 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
}}>
|
||||
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
|
||||
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
||||
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
|
||||
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
|
||||
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
|
||||
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
|
||||
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
|
||||
<VSCodeOption value="mistral">Mistral</VSCodeOption>
|
||||
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
|
||||
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
|
||||
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
|
||||
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
|
||||
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
|
||||
<VSCodeOption value="requesty">Requesty</VSCodeOption>
|
||||
<VSCodeOption value="together">Together</VSCodeOption>
|
||||
<VSCodeOption value="qwen">Alibaba Qwen</VSCodeOption>
|
||||
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
|
||||
<VSCodeOption value="ollama">Ollama</VSCodeOption>
|
||||
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
</DropdownContainer>
|
||||
|
||||
@@ -309,6 +317,62 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "qwen" && (
|
||||
<div>
|
||||
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
|
||||
<label htmlFor="qwen-line-provider">
|
||||
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
id="qwen-line-provider"
|
||||
value={apiConfiguration?.qwenApiLine || "china"}
|
||||
onChange={handleInputChange("qwenApiLine")}
|
||||
style={{
|
||||
minWidth: 130,
|
||||
position: "relative",
|
||||
}}>
|
||||
<VSCodeOption value="china">China API</VSCodeOption>
|
||||
<VSCodeOption value="international">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
|
||||
@@ -382,30 +446,56 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
}}>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsAccessKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("awsAccessKey")}
|
||||
placeholder="Enter Access Key...">
|
||||
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsSecretKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("awsSecretKey")}
|
||||
placeholder="Enter Secret Key...">
|
||||
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsSessionToken || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("awsSessionToken")}
|
||||
placeholder="Enter Session Token...">
|
||||
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeRadioGroup
|
||||
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
|
||||
onChange={(e) => {
|
||||
const value = (e.target as HTMLInputElement)?.value
|
||||
const useProfile = value === "profile"
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
awsUseProfile: useProfile,
|
||||
})
|
||||
}}>
|
||||
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
|
||||
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
|
||||
</VSCodeRadioGroup>
|
||||
|
||||
{apiConfiguration?.awsUseProfile ? (
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsProfile || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("awsProfile")}
|
||||
placeholder="Enter profile name (default if empty)">
|
||||
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
|
||||
</VSCodeTextField>
|
||||
) : (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsAccessKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("awsAccessKey")}
|
||||
placeholder="Enter Access Key...">
|
||||
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsSecretKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("awsSecretKey")}
|
||||
placeholder="Enter Secret Key...">
|
||||
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsSessionToken || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("awsSessionToken")}
|
||||
placeholder="Enter Session Token...">
|
||||
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
|
||||
</VSCodeTextField>
|
||||
</>
|
||||
)}
|
||||
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 1} className="dropdown-container">
|
||||
<label htmlFor="aws-region-dropdown">
|
||||
<span style={{ fontWeight: 500 }}>AWS Region</span>
|
||||
@@ -460,9 +550,18 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Authenticate by either providing the keys above or use the default AWS credential providers, i.e.
|
||||
~/.aws/credentials or environment variables. These credentials are only used locally to make API requests
|
||||
from this extension.
|
||||
{apiConfiguration?.awsUseProfile ? (
|
||||
<>
|
||||
Using AWS Profile credentials from ~/.aws/credentials. Leave profile name empty to use the default
|
||||
profile. These credentials are only used locally to make API requests from this extension.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Authenticate by either providing the keys above or use the default AWS credential providers, i.e.
|
||||
~/.aws/credentials or environment variables. These credentials are only used locally to make API
|
||||
requests from this extension.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -597,6 +696,189 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
|
||||
display: "flex",
|
||||
margin: "10px 0",
|
||||
cursor: "pointer",
|
||||
alignItems: "center",
|
||||
}}
|
||||
onClick={() => setModelConfigurationSelected((val) => !val)}>
|
||||
<span
|
||||
className={`codicon ${modelConfigurationSelected ? "codicon-chevron-down" : "codicon-chevron-right"}`}
|
||||
style={{
|
||||
marginRight: "4px",
|
||||
}}></span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Model Configuration
|
||||
</span>
|
||||
</div>
|
||||
{modelConfigurationSelected && (
|
||||
<>
|
||||
<VSCodeCheckbox
|
||||
checked={apiConfiguration?.openAiModelInfo?.supportsImages}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.supportsImages = isChecked
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
Supports Images
|
||||
</VSCodeCheckbox>
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.contextWindow
|
||||
? apiConfiguration.openAiModelInfo.contextWindow.toString()
|
||||
: openAiModelInfoSaneDefaults.contextWindow?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.contextWindow = Number(input.target.value)
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Context Window Size</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.maxTokens
|
||||
? apiConfiguration.openAiModelInfo.maxTokens.toString()
|
||||
: openAiModelInfoSaneDefaults.maxTokens?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.maxTokens = input.target.value
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.inputPrice
|
||||
? apiConfiguration.openAiModelInfo.inputPrice.toString()
|
||||
: openAiModelInfoSaneDefaults.inputPrice?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.inputPrice = input.target.value
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiModelInfo?.outputPrice
|
||||
? apiConfiguration.openAiModelInfo.outputPrice.toString()
|
||||
: openAiModelInfoSaneDefaults.outputPrice?.toString()
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
onInput={(input: any) => {
|
||||
let modelInfo = apiConfiguration?.openAiModelInfo
|
||||
? apiConfiguration.openAiModelInfo
|
||||
: { ...openAiModelInfoSaneDefaults }
|
||||
modelInfo.outputPrice = input.target.value
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiModelInfo: modelInfo,
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
|
||||
models. Less capable models may not work as expected.)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "requesty" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.requestyApiKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("requestyApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>API Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.requestyModelId || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("requestyModelId")}
|
||||
placeholder={"Enter Model ID..."}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
|
||||
models. Less capable models may not work as expected.)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "together" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.togetherApiKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("togetherApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>API Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.togetherModelId || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("togetherModelId")}
|
||||
placeholder={"Enter Model ID..."}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
@@ -739,6 +1021,46 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "litellm" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.liteLlmApiKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("liteLlmApiKey")}
|
||||
placeholder="Default: noop">
|
||||
<span style={{ fontWeight: 500 }}>API Key</span>
|
||||
</VSCodeTextField>
|
||||
<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 +1150,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 +1358,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 +1397,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)
|
||||
}
|
||||
|
||||
@@ -228,8 +228,8 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
|
||||
If you're unsure which model to choose, Cline works best with{" "}
|
||||
<VSCodeLink
|
||||
style={{ display: "inline", fontSize: "inherit" }}
|
||||
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet:beta")}>
|
||||
anthropic/claude-3.5-sonnet:beta.
|
||||
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet")}>
|
||||
anthropic/claude-3.5-sonnet.
|
||||
</VSCodeLink>
|
||||
You can also try searching "free" for no-cost options currently available.
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import ApiOptions from "../ApiOptions"
|
||||
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...actual,
|
||||
// your mocked methods
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "requesty",
|
||||
requestyApiKey: "",
|
||||
requestyModelId: "",
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("ApiOptions Component", () => {
|
||||
vi.clearAllMocks()
|
||||
const mockPostMessage = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
global.vscode = { postMessage: mockPostMessage } as any
|
||||
})
|
||||
|
||||
it("renders Requesty API Key input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
|
||||
expect(apiKeyInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders Requesty Model ID input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
|
||||
expect(modelIdInput).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...actual,
|
||||
// your mocked methods
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "together",
|
||||
requestyApiKey: "",
|
||||
requestyModelId: "",
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("ApiOptions Component", () => {
|
||||
vi.clearAllMocks()
|
||||
const mockPostMessage = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
global.vscode = { postMessage: mockPostMessage } as any
|
||||
})
|
||||
|
||||
it("renders Together API Key input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
|
||||
expect(apiKeyInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders Together Model ID input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
|
||||
expect(modelIdInput).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...actual,
|
||||
// your mocked methods
|
||||
useExtensionState: vi.fn(() => ({
|
||||
apiConfiguration: {
|
||||
apiProvider: "openai",
|
||||
requestyApiKey: "",
|
||||
requestyModelId: "",
|
||||
},
|
||||
setApiConfiguration: vi.fn(),
|
||||
uriScheme: "vscode",
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("OpenApiInfoOptions", () => {
|
||||
const mockPostMessage = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
global.vscode = { postMessage: mockPostMessage }
|
||||
})
|
||||
|
||||
it("renders OpenAI Supports Images input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const apiKeyInput = screen.getByText("Supports Images")
|
||||
expect(apiKeyInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders OpenAI Context Window Size input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const orgIdInput = screen.getByText("Context Window Size")
|
||||
expect(orgIdInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders OpenAI Max Output Tokens input", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions showModelOptions={true} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
const modelInput = screen.getByText("Max Output Tokens")
|
||||
expect(modelInput).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../../src/shared/AutoApprovalSettings"
|
||||
import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
|
||||
import { ExtensionMessage, ExtensionState, DEFAULT_PLATFORM } from "../../../src/shared/ExtensionMessage"
|
||||
import { ApiConfiguration, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../../src/shared/api"
|
||||
import { findLastIndex } from "../../../src/shared/array"
|
||||
import { McpServer } from "../../../src/shared/mcp"
|
||||
import { McpMarketplaceCatalog, McpServer } from "../../../src/shared/mcp"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "../../../src/shared/BrowserSettings"
|
||||
@@ -17,6 +17,7 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
openRouterModels: Record<string, ModelInfo>
|
||||
openAiModels: string[]
|
||||
mcpServers: McpServer[]
|
||||
mcpMarketplaceCatalog: McpMarketplaceCatalog
|
||||
filePaths: string[]
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
setCustomInstructions: (value?: string) => void
|
||||
@@ -37,6 +38,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
browserSettings: DEFAULT_BROWSER_SETTINGS,
|
||||
chatSettings: DEFAULT_CHAT_SETTINGS,
|
||||
isLoggedIn: false,
|
||||
platform: DEFAULT_PLATFORM,
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
@@ -48,7 +50,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
|
||||
const [openAiModels, setOpenAiModels] = useState<string[]>([])
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
|
||||
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
@@ -64,9 +66,13 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
config.openAiApiKey,
|
||||
config.ollamaModelId,
|
||||
config.lmStudioModelId,
|
||||
config.liteLlmApiKey,
|
||||
config.geminiApiKey,
|
||||
config.openAiNativeApiKey,
|
||||
config.deepSeekApiKey,
|
||||
config.requestyApiKey,
|
||||
config.togetherApiKey,
|
||||
config.qwenApiKey,
|
||||
config.mistralApiKey,
|
||||
config.vsCodeLmModelSelector,
|
||||
].some((key) => key !== undefined)
|
||||
@@ -116,6 +122,12 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
setMcpServers(message.mcpServers ?? [])
|
||||
break
|
||||
}
|
||||
case "mcpMarketplaceCatalog": {
|
||||
if (message.mcpMarketplaceCatalog) {
|
||||
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -133,6 +145,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
openRouterModels,
|
||||
openAiModels,
|
||||
mcpServers,
|
||||
mcpMarketplaceCatalog,
|
||||
filePaths,
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { renderHook } from "@testing-library/react"
|
||||
import { useShortcut, useMetaKeyDetection } from "../hooks"
|
||||
import { vi } from "vitest"
|
||||
|
||||
describe("useShortcut", () => {
|
||||
it("should call the callback when the shortcut is pressed", () => {
|
||||
const callback = vi.fn()
|
||||
renderHook(() => useShortcut("Meta+Shift+a", callback))
|
||||
|
||||
const event = new KeyboardEvent("keydown", { key: "a", metaKey: true, shiftKey: true })
|
||||
window.dispatchEvent(event)
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not call the callback when the shortcut is not pressed", () => {
|
||||
const callback = vi.fn()
|
||||
renderHook(() => useShortcut("Command+Shift+b", callback))
|
||||
|
||||
const event = new KeyboardEvent("keydown", { key: "a", metaKey: true, shiftKey: true })
|
||||
window.dispatchEvent(event)
|
||||
|
||||
expect(callback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not call the callback when typing in a text input when disableTextInputs is true", () => {
|
||||
const callback = vi.fn()
|
||||
renderHook(() => useShortcut("Meta+Shift+a", callback, { disableTextInputs: true }))
|
||||
|
||||
const input = document.createElement("input")
|
||||
document.body.appendChild(input)
|
||||
input.focus()
|
||||
|
||||
const event = new KeyboardEvent("keydown", { key: "a", metaKey: true, shiftKey: true })
|
||||
input.dispatchEvent(event)
|
||||
|
||||
expect(callback).not.toHaveBeenCalled()
|
||||
|
||||
document.body.removeChild(input)
|
||||
})
|
||||
})
|
||||
|
||||
describe("useMetaKeyDetection", () => {
|
||||
it("should detect Windows OS and metaKey from platform", () => {
|
||||
// mock the detect functions
|
||||
const { result } = renderHook(() => useMetaKeyDetection("win32"))
|
||||
expect(result.current[0]).toBe("windows")
|
||||
expect(result.current[1]).toBe("Win")
|
||||
})
|
||||
|
||||
it("should detect Mac OS and metaKey from platform", () => {
|
||||
// mock the detect functions
|
||||
const { result } = renderHook(() => useMetaKeyDetection("darwin"))
|
||||
expect(result.current[0]).toBe("mac")
|
||||
expect(result.current[1]).toBe("CMD")
|
||||
})
|
||||
|
||||
it("should detect Linux OS and metaKey from platform", () => {
|
||||
// mock the detect functions
|
||||
const { result } = renderHook(() => useMetaKeyDetection("linux"))
|
||||
expect(result.current[0]).toBe("linux")
|
||||
expect(result.current[1]).toBe("Alt")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { detectMetaKeyChar } from "../platformUtils"
|
||||
|
||||
describe("detectMetaKeyChar", () => {
|
||||
it("should return ⌘ Command for darwin platform", () => {
|
||||
const result = detectMetaKeyChar("darwin")
|
||||
expect(result).toBe("CMD")
|
||||
})
|
||||
|
||||
it("should return ⊞ Win for win32 platform", () => {
|
||||
const result = detectMetaKeyChar("win32")
|
||||
expect(result).toBe("Win")
|
||||
})
|
||||
|
||||
it("should return Alt for linux platform", () => {
|
||||
const result = detectMetaKeyChar("linux")
|
||||
expect(result).toBe("Alt")
|
||||
})
|
||||
|
||||
it("should return generic CMD for unknown platform", () => {
|
||||
const result = detectMetaKeyChar("somethingelse")
|
||||
expect(result).toBe("CMD")
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mentionRegex } from "../../../src/shared/context-mentions"
|
||||
import { Fzf } from "fzf"
|
||||
|
||||
export function insertMention(text: string, position: number, value: string): { newValue: string; mentionIndex: number } {
|
||||
const beforeCursor = text.slice(0, position)
|
||||
@@ -46,13 +47,17 @@ export enum ContextMenuOptionType {
|
||||
File = "file",
|
||||
Folder = "folder",
|
||||
Problems = "problems",
|
||||
Terminal = "terminal",
|
||||
URL = "url",
|
||||
Git = "git",
|
||||
NoResults = "noResults",
|
||||
}
|
||||
|
||||
export interface ContextMenuQueryItem {
|
||||
type: ContextMenuOptionType
|
||||
value?: string
|
||||
label?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function getContextMenuOptions(
|
||||
@@ -60,6 +65,13 @@ export function getContextMenuOptions(
|
||||
selectedType: ContextMenuOptionType | null = null,
|
||||
queryItems: ContextMenuQueryItem[],
|
||||
): ContextMenuQueryItem[] {
|
||||
const workingChanges: ContextMenuQueryItem = {
|
||||
type: ContextMenuOptionType.Git,
|
||||
value: "git-changes",
|
||||
label: "Working changes",
|
||||
description: "Current uncommitted changes",
|
||||
}
|
||||
|
||||
if (query === "") {
|
||||
if (selectedType === ContextMenuOptionType.File) {
|
||||
const files = queryItems
|
||||
@@ -81,30 +93,102 @@ export function getContextMenuOptions(
|
||||
return folders.length > 0 ? folders : [{ type: ContextMenuOptionType.NoResults }]
|
||||
}
|
||||
|
||||
if (selectedType === ContextMenuOptionType.Git) {
|
||||
const commits = queryItems.filter((item) => item.type === ContextMenuOptionType.Git)
|
||||
return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges]
|
||||
}
|
||||
|
||||
return [
|
||||
{ type: ContextMenuOptionType.URL },
|
||||
{ type: ContextMenuOptionType.Problems },
|
||||
{ type: ContextMenuOptionType.Terminal },
|
||||
{ type: ContextMenuOptionType.Git },
|
||||
{ type: ContextMenuOptionType.Folder },
|
||||
{ type: ContextMenuOptionType.File },
|
||||
]
|
||||
}
|
||||
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const suggestions: ContextMenuQueryItem[] = []
|
||||
|
||||
// Check for top-level option matches
|
||||
if ("git".startsWith(lowerQuery)) {
|
||||
suggestions.push({
|
||||
type: ContextMenuOptionType.Git,
|
||||
label: "Git Commits",
|
||||
description: "Search repository history",
|
||||
})
|
||||
} else if ("git-changes".startsWith(lowerQuery)) {
|
||||
suggestions.push(workingChanges)
|
||||
}
|
||||
if ("problems".startsWith(lowerQuery)) {
|
||||
suggestions.push({ type: ContextMenuOptionType.Problems })
|
||||
}
|
||||
if (query.startsWith("http")) {
|
||||
return [{ type: ContextMenuOptionType.URL, value: query }]
|
||||
} else {
|
||||
const matchingItems = queryItems.filter((item) => item.value?.toLowerCase().includes(lowerQuery))
|
||||
suggestions.push({ type: ContextMenuOptionType.URL, value: query })
|
||||
}
|
||||
|
||||
if (matchingItems.length > 0) {
|
||||
return matchingItems.map((item) => ({
|
||||
type: item.type,
|
||||
value: item.value,
|
||||
}))
|
||||
// Add exact SHA matches to suggestions
|
||||
if (/^[a-f0-9]{7,40}$/i.test(lowerQuery)) {
|
||||
const exactMatches = queryItems.filter(
|
||||
(item) => item.type === ContextMenuOptionType.Git && item.value?.toLowerCase() === lowerQuery,
|
||||
)
|
||||
if (exactMatches.length > 0) {
|
||||
suggestions.push(...exactMatches)
|
||||
} else {
|
||||
return [{ type: ContextMenuOptionType.NoResults }]
|
||||
// If no exact match but valid SHA format, add as option
|
||||
suggestions.push({
|
||||
type: ContextMenuOptionType.Git,
|
||||
value: lowerQuery,
|
||||
label: `Commit ${lowerQuery}`,
|
||||
description: "Git commit hash",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create searchable strings array for fzf
|
||||
const searchableItems = queryItems.map((item) => ({
|
||||
original: item,
|
||||
searchStr: [item.value, item.label, item.description].filter(Boolean).join(" "),
|
||||
}))
|
||||
|
||||
// Initialize fzf instance for fuzzy search
|
||||
const fzf = new Fzf(searchableItems, {
|
||||
selector: (item) => item.searchStr,
|
||||
})
|
||||
|
||||
// Get fuzzy matching items
|
||||
const matchingItems = query ? fzf.find(query).map((result) => result.item.original) : []
|
||||
|
||||
// Separate matches by type
|
||||
const fileMatches = matchingItems.filter(
|
||||
(item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.Folder,
|
||||
)
|
||||
const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
|
||||
const otherMatches = matchingItems.filter(
|
||||
(item) =>
|
||||
item.type !== ContextMenuOptionType.File &&
|
||||
item.type !== ContextMenuOptionType.Folder &&
|
||||
item.type !== ContextMenuOptionType.Git,
|
||||
)
|
||||
|
||||
// Combine suggestions with matching items in the desired order
|
||||
if (suggestions.length > 0 || matchingItems.length > 0) {
|
||||
const allItems = [...suggestions, ...fileMatches, ...gitMatches, ...otherMatches]
|
||||
|
||||
// Remove duplicates based on type and value
|
||||
const seen = new Set()
|
||||
const deduped = allItems.filter((item) => {
|
||||
const key = `${item.type}-${item.value}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
|
||||
return deduped
|
||||
}
|
||||
|
||||
return [{ type: ContextMenuOptionType.NoResults }]
|
||||
}
|
||||
|
||||
export function shouldShowContextMenu(text: string, position: number): boolean {
|
||||
@@ -121,8 +205,8 @@ export function shouldShowContextMenu(text: string, position: number): boolean {
|
||||
// Don't show the menu if it's a URL
|
||||
if (textAfterAt.toLowerCase().startsWith("http")) return false
|
||||
|
||||
// Don't show the menu if it's a problems
|
||||
if (textAfterAt.toLowerCase().startsWith("problems")) return false
|
||||
// Don't show the menu if it's a problems or terminal
|
||||
if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal")) return false
|
||||
|
||||
// NOTE: it's okay that menu shows when there's trailing punctuation since user could be inputting a path with marks
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user