Compare commits

..
66 changed files with 3703 additions and 3677 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"claude-dev": minor
---
Add Bedrock prompt caching support (optional).
This feature protected under checkbox because it is not yet rolled out to everyone, and if you will try to send cache headers, and its not enabled for you, you will get error.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
improving search and replace edit failure behaviors
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Update Google Gemini API key link
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
added telemetry to track replace_in_file tool failures
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
feat(extension): add access to history, mcp, and new task buttons in popout view
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix bug where Cline would use plan_mode_response bug without response parameter
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Support connecting to SSE servers
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
adding code coverage actions to npm scripts
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
updated gemini-2.0-pro-exp-02-05 to gemini-2.5-pro-exp-03-25 for Vertex AI
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Refactor types and functions in McpHub
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add cost calculation support for LiteLLM provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
adding task id to request headers
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Added sambanova Deepseek-V3-0324
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add toggle disabled for remote servers
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
added task feedback thumbs up thumbs down telemetry
@@ -22,6 +22,7 @@ Environment Variables:
#!/usr/bin/env python3
import os
import sys
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
VERSION = os.environ['VERSION']
@@ -31,49 +32,72 @@ 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"
unformmatted_prev_version_pattern = f"## {PREV_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}")
notes_start_index = changelog_text.find(version_pattern) + 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 or unformmatted_prev_version_pattern in changelog_text) else len(changelog_text)
# 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")
filtered_lines = []
for line in changeset_lines:
# If the previous line is a changeset format
if len(filtered_lines) > 1 and filtered_lines[-1].startswith("### "):
# Remove the last two lines from the filted_lines
filtered_lines.pop()
filtered_lines.pop()
else:
filtered_lines.append(line.strip())
# Prepend a new line to the first line of filtered_lines
if filtered_lines:
filtered_lines[0] = "\n" + filtered_lines[0]
# Print filted_lines wiht a "\n" at the end of each line
for line in filtered_lines:
print(line.strip())
parsed_lines = "\n".join(line for line in filtered_lines)
# 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
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
try:
print(f"Reading changelog from: {CHANGELOG_PATH}")
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
# print("----------------------------------------------------------------------------------")
# print(new_changelog)
# print("----------------------------------------------------------------------------------")
# Write back to CHANGELOG.md
with open(CHANGELOG_PATH, 'w') as f:
f.write(new_changelog)
print(f"Changelog content length: {len(changelog_content)} characters")
print("First 200 characters of changelog:")
print(changelog_content[:200])
print("----------------------------------------------------------------------------------")
print(f"{CHANGELOG_PATH} updated successfully!")
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)
-91
View File
@@ -1,91 +0,0 @@
name: Changeset Converter
run-name: Changeset Conversion
on:
pull_request:
branches:
- main
types: [closed]
env:
REPO_PATH: ${{ github.repository }}
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
NODE_VERSION: 20.18.1
jobs:
# Job 1: Create version bump PR when changesets are merged to main
changeset-pr-version-bump:
if: >
github.event.pull_request.merged == true &&
github.actor != 'github-actions'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Git Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ env.GIT_REF }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install Dependencies
run: npm install changeset
# 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: Create Changeset Pull Request
if: steps.check-changesets.outputs.new_changesets != '0'
uses: changesets/action@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 }}
# 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
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 to Pull Request
run: |
git config user.name "github-actions"
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 "--------------------------------------------------------------------------------"
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
git push origin $CURRENT_BRANCH
+117
View File
@@ -0,0 +1,117 @@
name: Check Changeset
run-name: Check for Changeset in PR
permissions:
contents: read
pull-requests: write
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
check-changeset:
# Skip draft PRs and dependabot PRs
if: github.event.pull_request.draft == false && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Check for changeset
id: check-changeset
run: |
# Debug info
echo "Current directory: $(pwd)"
echo "PR Base Ref: ${{ github.event.pull_request.base.ref }}"
echo "PR Head Ref: ${{ github.event.pull_request.head.ref }}"
echo "PR Head SHA: ${{ github.event.pull_request.head.sha }}"
echo "Git status:"
git status
# Get list of changed files
git fetch origin ${{ github.event.pull_request.base.ref }}
CHANGED_FILES=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} HEAD)
echo "Changed files:"
echo "$CHANGED_FILES"
# Check if any of the changed files are in docs/ or .github/
echo "Checking if changes are docs-only..."
DOCS_ONLY=true
while IFS= read -r file; do
if [[ ! "$file" =~ ^(docs/|.github/) ]]; then
echo "Found non-docs change: $file"
DOCS_ONLY=false
break
fi
done <<< "$CHANGED_FILES"
# If changes are docs-only, skip changeset check
if [ "$DOCS_ONLY" = true ]; then
echo "All changes are in docs/ or .github/, skipping changeset check"
exit 0
else
echo "Changes include non-docs files, checking for changeset..."
fi
# Check if any changeset files are in the changed files
echo "Checking for changeset files in changed files..."
CHANGESET_IN_PR=false
while IFS= read -r file; do
if [[ "$file" =~ ^\.changeset/.*\.md$ && "$file" != ".changeset/README.md" && "$file" != ".changeset/config.json" ]]; then
echo "Found changeset file in PR: $file"
CHANGESET_IN_PR=true
break
fi
done <<< "$CHANGED_FILES"
if [ "$CHANGESET_IN_PR" = false ]; then
echo "No changeset files found in changed files. Changed files in .changeset/:"
echo "$CHANGED_FILES" | grep "^\.changeset/" || true
echo "::error::No changeset file found in PR changes. Please run 'npm run changeset' to create one."
exit 1
fi
- name: Comment on PR
if: failure()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
const message = `This PR requires a changeset since it includes user-facing changes. Please:
1. Run \`npm run changeset\` locally
2. Choose the appropriate version bump:
- \`major\` for breaking changes (1.0.0 → 2.0.0)
- \`minor\` for new features (1.0.0 → 1.1.0)
- \`patch\` for bug fixes (1.0.0 → 1.0.1)
3. Write a clear description of your changes
4. Commit the generated changeset file
Note: Documentation-only changes do not require a changeset.`;
// Get existing comments
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
// Check if we already commented
const botComment = comments.data.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('This PR requires a changeset')
);
if (!botComment) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
}
-6
View File
@@ -1,11 +1,5 @@
# Changelog
## [3.8.4]
- Add Sambanova Deepseek-V3-0324
- Add cost calculation support for LiteLLM provider
- Fix bug where Cline would use plan_mode_response bug without response parameter
## [3.8.3]
- Add support for SambaNova QwQ-32B model
+1 -1
View File
@@ -28,7 +28,7 @@ There are multiple places online to find MCP servers:
2. **Example Interaction with Cline:**
```
User: "Cline, I want to add the MCP server for Brave browser control. Here's the GitHub link: https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search Can you add it?"
User: "Cline, I want to add the MCP server for Brave browser control. Here's the GitHub link: https://github.com/modelcontextprotocol/servers/tree/main/src/brave Can you add it?"
Cline: "OK. Cloning the repository to the MCP directory. It needs to be built because it has a 'package.json' file. Should I run 'npm run build'?"
-8
View File
@@ -72,14 +72,6 @@ const extensionConfig = {
copyWasmFiles,
/* add to the end of plugins array */
esbuildProblemMatcherPlugin,
{
name: "alias-plugin",
setup(build) {
build.onResolve({ filter: /^pkce-challenge$/ }, (args) => {
return { path: require.resolve("pkce-challenge/dist/index.browser.js") }
})
},
},
],
entryPoints: ["src/extension.ts"],
format: "cjs",
+44 -67
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.8.4",
"version": "3.8.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.8.4",
"version": "3.8.2",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
@@ -23,6 +23,10 @@
"@opentelemetry/sdk-node": "^0.39.1",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@types/clone-deep": "^4.0.4",
"@types/get-folder-size": "^3.0.4",
"@types/pdf-parse": "^1.1.4",
"@types/turndown": "^5.0.5",
"@vscode/codicons": "^0.0.36",
"axios": "^1.8.2",
"cheerio": "^1.0.0",
@@ -60,15 +64,11 @@
"devDependencies": {
"@changesets/cli": "^2.27.12",
"@types/chai": "^5.0.1",
"@types/clone-deep": "^4.0.4",
"@types/diff": "^5.2.1",
"@types/get-folder-size": "^3.0.4",
"@types/mocha": "^10.0.7",
"@types/node": "20.x",
"@types/pdf-parse": "^1.1.4",
"@types/should": "^11.2.0",
"@types/sinon": "^17.0.4",
"@types/turndown": "^5.0.5",
"@types/vscode": "^1.84.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
@@ -8581,9 +8581,7 @@
"node_modules/@types/clone-deep": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/clone-deep/-/clone-deep-4.0.4.tgz",
"integrity": "sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA==",
"dev": true,
"license": "MIT"
"integrity": "sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA=="
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
@@ -8602,7 +8600,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/get-folder-size/-/get-folder-size-3.0.4.tgz",
"integrity": "sha512-tSf/k7Undx6jKRwpChR9tl+0ZPf0BVwkjBRtJ5qSnz6iWm2ZRYMAS2MktC2u7YaTAFHmxpL/LBxI85M7ioJCSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -8645,7 +8642,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.4.tgz",
"integrity": "sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/should": {
@@ -8676,7 +8672,6 @@
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz",
"integrity": "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/uuid": {
@@ -9345,75 +9340,50 @@
"license": "MIT"
},
"node_modules/bare-events": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz",
"integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==",
"license": "Apache-2.0",
"optional": true
},
"node_modules/bare-fs": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz",
"integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==",
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
"integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
"bare-events": "^2.0.0",
"bare-path": "^2.0.0",
"bare-stream": "^2.0.0"
}
},
"node_modules/bare-os": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
"integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"bare": ">=1.14.0"
}
"optional": true
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
"integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-os": "^3.0.1"
"bare-os": "^2.1.0"
}
},
"node_modules/bare-stream": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz",
"integrity": "sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"streamx": "^2.21.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
"b4a": "^1.6.6",
"streamx": "^2.20.0"
}
},
"node_modules/base64-js": {
@@ -15201,6 +15171,12 @@
],
"license": "MIT"
},
"node_modules/queue-tick": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
"license": "MIT"
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -16137,12 +16113,13 @@
}
},
"node_modules/streamx": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz",
"integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==",
"license": "MIT",
"dependencies": {
"fast-fifo": "^1.3.2",
"queue-tick": "^1.0.1",
"text-decoder": "^1.1.0"
},
"optionalDependencies": {
@@ -16405,17 +16382,17 @@
}
},
"node_modules/tar-fs": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
"integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
"bare-fs": "^2.1.1",
"bare-path": "^2.1.0"
}
},
"node_modules/tar-stream": {
+19 -36
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.8.4",
"version": "3.8.3",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -102,6 +102,11 @@
"category": "Cline",
"when": "cline.isDevMode"
},
{
"command": "cline.openDocumentation",
"title": "Documentation",
"icon": "$(book)"
},
{
"command": "cline.addToChat",
"title": "Add to Cline",
@@ -141,46 +146,19 @@
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.accountButtonClicked",
"command": "cline.openDocumentation",
"group": "navigation@5",
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.settingsButtonClicked",
"command": "cline.accountButtonClicked",
"group": "navigation@6",
"when": "view == claude-dev.SidebarProvider"
}
],
"editor/title": [
{
"command": "cline.plusButtonClicked",
"group": "navigation@1",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.mcpButtonClicked",
"group": "navigation@2",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.historyButtonClicked",
"group": "navigation@3",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.popoutButtonClicked",
"group": "navigation@4",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.accountButtonClicked",
"group": "navigation@5",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
},
{
"command": "cline.settingsButtonClicked",
"group": "navigation@6",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
"group": "navigation@7",
"when": "view == claude-dev.SidebarProvider"
}
],
"editor/context": [
@@ -283,6 +261,11 @@
"type": "boolean",
"default": true,
"description": "Controls whether the MCP Marketplace is enabled."
},
"cline.conversationObservability": {
"type": "boolean",
"default": false,
"markdownDescription": "Share message data, code, and more extensive observability. This data may be used to improve prompts used in Cline, train models, and understand failure states more accurately. [Learn more](https://docs.cline.bot/more-info/conversation-observability)"
}
}
}
@@ -317,15 +300,11 @@
"devDependencies": {
"@changesets/cli": "^2.27.12",
"@types/chai": "^5.0.1",
"@types/clone-deep": "^4.0.4",
"@types/diff": "^5.2.1",
"@types/get-folder-size": "^3.0.4",
"@types/mocha": "^10.0.7",
"@types/node": "20.x",
"@types/pdf-parse": "^1.1.4",
"@types/should": "^11.2.0",
"@types/sinon": "^17.0.4",
"@types/turndown": "^5.0.5",
"@types/vscode": "^1.84.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
@@ -356,6 +335,10 @@
"@opentelemetry/sdk-node": "^0.39.1",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@types/clone-deep": "^4.0.4",
"@types/get-folder-size": "^3.0.4",
"@types/pdf-parse": "^1.1.4",
"@types/turndown": "^5.0.5",
"@vscode/codicons": "^0.0.36",
"axios": "^1.8.2",
"cheerio": "^1.0.0",
+1 -1
View File
@@ -23,7 +23,7 @@ export class AnthropicHandler implements ApiHandler {
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
const modelId = model.id
const budget_tokens = this.options.thinkingBudgetTokens || 0
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
switch (modelId) {
+3 -3
View File
@@ -25,7 +25,7 @@ export class AwsBedrockHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// cross region inference requires prefixing the model id with the region
const modelId = await this.getModelId()
let modelId = await this.getModelId()
const model = this.getModel()
// Check if this is an Amazon Nova model
@@ -40,7 +40,7 @@ export class AwsBedrockHandler implements ApiHandler {
return
}
const budget_tokens = this.options.thinkingBudgetTokens || 0
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
// Get model info and message indices for caching
@@ -250,7 +250,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
async getModelId(): Promise<string> {
if (this.options.awsUseCrossRegionInference) {
const regionPrefix = this.getRegion().slice(0, 3)
let regionPrefix = this.getRegion().slice(0, 3)
switch (regionPrefix) {
case "us-":
return `us.${this.getModel().id}`
-5
View File
@@ -17,11 +17,6 @@ export class ClineHandler implements ApiHandler {
this.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
},
})
}
+1 -1
View File
@@ -26,7 +26,7 @@ export class RequestyHandler implements ApiHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.requestyModelId ?? ""
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
+1 -1
View File
@@ -307,7 +307,7 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
// Parse nested XML elements
const paramRegex = /<(\w+)>([\s\S]*?)<\/\1>/gs
let match: RegExpExecArray | null
let match
while ((match = paramRegex.exec(innerContent)) !== null) {
const [, paramName, paramValue] = match
+1 -1
View File
@@ -28,7 +28,7 @@ export function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.Me
)
// Process tool result messages FIRST since they must follow the tool use messages
const toolResultImages: string[] = []
let toolResultImages: string[] = []
toolMessages.forEach((toolMessage) => {
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the Ollama SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
let content: string
+2 -2
View File
@@ -38,7 +38,7 @@ export function convertToOpenAiMessages(
)
// Process tool result messages FIRST since they must follow the tool use messages
const toolResultImages: Anthropic.Messages.ImageBlockParam[] = []
let toolResultImages: Anthropic.Messages.ImageBlockParam[] = []
toolMessages.forEach((toolMessage) => {
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the OpenAI SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
let content: string
@@ -127,7 +127,7 @@ export function convertToOpenAiMessages(
}
// Process tool use messages
const tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
id: toolMessage.id,
type: "function",
function: {
File diff suppressed because it is too large Load Diff
-11
View File
@@ -1,11 +0,0 @@
# Core Architecture
Extension entry point (extension.ts) -> webview -> controller -> task
```tree
core/
├── webview/ # Manages webview lifecycle
├── controller/ # Handles webview messages and task management
├── task/ # Executes API requests and tool operations
└── ... # Additional components to help with context, parsing user/assistant messages, etc.
```
@@ -1,7 +1,7 @@
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
export function parseAssistantMessage(assistantMessage: string) {
const contentBlocks: AssistantMessageContent[] = []
let contentBlocks: AssistantMessageContent[] = []
let currentTextContent: TextContent | undefined = undefined
let currentTextContentStartIndex = 0
let currentToolUse: ToolUse | undefined = undefined
File diff suppressed because it is too large Load Diff
-82
View File
@@ -117,88 +117,6 @@ Otherwise, if you have not completed the task and do not need additional informa
const prettyPatchLines = lines.slice(4)
return prettyPatchLines.join("\n")
},
taskResumption: (
mode: "plan" | "act",
agoText: string,
cwd: string,
wasRecent: boolean | 0 | undefined,
responseText?: string,
) => {
return `[TASK RESUMPTION] ${
mode === "plan"
? `This task was interrupted ${agoText}. The conversation may have been incomplete. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful. However you are in PLAN MODE, so rather than continuing the task, you must respond to the user's message.`
: `This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.`
}${
wasRecent
? "\n\nIMPORTANT: If the last tool use was a replace_in_file or write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents."
: ""
}${
responseText
? `\n\n${mode === "plan" ? "New message to respond to with plan_mode_respond tool (be sure to provide your response in the <response> parameter)" : "New instructions for task continuation"}:\n<user_message>\n${responseText}\n</user_message>`
: mode === "plan"
? "(The user did not provide a new message. Consider asking them how they'd like you to proceed, or to switch to Act mode to continue with the task.)"
: ""
}`
},
planModeInstructions: () => {
return `In this mode you should focus on information gathering, asking questions, and architecting a solution. Once you have a plan, use the plan_mode_respond tool to engage in a conversational back and forth with the user. Do not use the plan_mode_respond tool until you've gathered all the information you need e.g. with read_file or ask_followup_question.
(Remember: If it seems the user wants you to use tools only available in Act Mode, you should ask the user to "toggle to Act mode" (use those words) - they will have to manually do this themselves with the Plan/Act toggle button below. You do not have the ability to switch to Act Mode yourself, and must wait for the user to do it themselves once they are satisfied with the plan. You also cannot present an option to toggle to Act mode, as this will be something you need to direct the user to do manually themselves.)`
},
fileEditWithUserChanges: (
relPath: string,
userEdits: string,
autoFormattingEdits: string | undefined,
finalContent: string | undefined,
newProblemsMessage: string | undefined,
) =>
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
(autoFormattingEdits
? `The user's editor also applied the following auto-formatting to your content:\n\n${autoFormattingEdits}\n\n(Note: Pay close attention to changes such as single quotes being converted to double quotes, semicolons being removed or added, long lines being broken into multiple lines, adjusting indentation style, adding/removing trailing commas, etc. This will help you ensure future SEARCH/REPLACE operations to this file are accurate.)\n\n`
: "") +
`The updated content, which includes both your original modifications and the additional edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file that was saved:\n\n` +
`<final_file_content path="${relPath.toPosix()}">\n${finalContent}\n</final_file_content>\n\n` +
`Please note:\n` +
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
`2. Proceed with the task using this updated file content as the new baseline.\n` +
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
`4. IMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference. This content reflects the current state of the file, including both user edits and any auto-formatting (e.g., if you used single quotes but the formatter converted them to double quotes). Always base your SEARCH/REPLACE operations on this final version to ensure accuracy.\n` +
`${newProblemsMessage}`,
fileEditWithoutUserChanges: (
relPath: string,
autoFormattingEdits: string | undefined,
finalContent: string | undefined,
newProblemsMessage: string | undefined,
) =>
`The content was successfully saved to ${relPath.toPosix()}.\n\n` +
(autoFormattingEdits
? `Along with your edits, the user's editor applied the following auto-formatting to your content:\n\n${autoFormattingEdits}\n\n(Note: Pay close attention to changes such as single quotes being converted to double quotes, semicolons being removed or added, long lines being broken into multiple lines, adjusting indentation style, adding/removing trailing commas, etc. This will help you ensure future SEARCH/REPLACE operations to this file are accurate.)\n\n`
: "") +
`Here is the full, updated content of the file that was saved:\n\n` +
`<final_file_content path="${relPath.toPosix()}">\n${finalContent}\n</final_file_content>\n\n` +
`IMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference. This content reflects the current state of the file, including any auto-formatting (e.g., if you used single quotes but the formatter converted them to double quotes). Always base your SEARCH/REPLACE operations on this final version to ensure accuracy.\n\n` +
`${newProblemsMessage}`,
diffError: (relPath: string, originalContent: string | undefined) =>
`This is likely because the SEARCH block content doesn't match exactly with what's in the file, or if you used multiple SEARCH/REPLACE blocks they may not have been in the order they appear in the file.\n\n` +
`The file was reverted to its original state:\n\n` +
`<file_content path="${relPath.toPosix()}">\n${originalContent}\n</file_content>\n\n` +
`Now that you have the latest state of the file, try the operation again with fewer/more precise SEARCH blocks.\n(If you run into this error 3 times in a row, you may use the write_to_file tool as a fallback. Keep in mind, the write_to_file fallback is far from ideal, as this means you'll be re-writing the entire contents of the file just to make a few edits, which takes time and money. So let's bias towards using replace_in_file as effectively as possible)`,
toolAlreadyUsed: (toolName: string) =>
`Tool [${toolName}] was not executed because a tool has already been used in this message. Only one tool may be used per message. You must assess the first tool's result before proceeding to use the next tool.`,
clineIgnoreInstructions: (content: string) =>
`# .clineignore\n\n(The following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${content}\n.clineignore`,
clineRulesDirectoryInstructions: (cwd: string, content: string) =>
`# .clinerules/\n\nThe following is provided by a root-level .clinerules/ directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
clineRulesFileInstructions: (cwd: string, content: string) =>
`# .clinerules\n\nThe following is provided by a root-level .clinerules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
}
// to avoid circular dependency
+2 -2
View File
@@ -216,7 +216,7 @@ Usage:
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- options: (optional) An array of 2-5 options for the user to choose from. Each option should be a string describing a possible answer. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually. IMPORTANT: NEVER include an option to toggle to Act mode, as this would be something you need to direct the user to do manually themselves if needed.
- options: (optional) An array of 2-5 options for the user to choose from. Each option should be a string describing a possible answer. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually.
Usage:
<ask_followup_question>
<question>Your question here</question>
@@ -242,7 +242,7 @@ Your final result description here
## plan_mode_respond
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution.
Parameters:
- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response. (You MUST use the response parameter, do not simply place the response text directly within <plan_mode_respond> tags.)
- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response.
- options: (optional) An array of 2-5 options for the user to choose from. Each option should be a string describing a possible choice or path forward in the planning process. This can help guide the discussion and make it easier for the user to provide input on key decisions. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually. Do NOT present an option to toggle to Act mode, as this will be something you need to direct the user to do manually themselves.
Usage:
<plan_mode_respond>
-73
View File
@@ -1,73 +0,0 @@
import * as path from "path"
import * as vscode from "vscode"
import fs from "fs/promises"
import { Anthropic } from "@anthropic-ai/sdk"
import { fileExistsAtPath } from "../../utils/fs"
import { ClineMessage } from "../../shared/ExtensionMessage"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
openRouterModels: "openrouter_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
}
export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext, taskId: string): Promise<string> {
const globalStoragePath = context.globalStorageUri.fsPath
const taskDir = path.join(globalStoragePath, "tasks", taskId)
await fs.mkdir(taskDir, { recursive: true })
return taskDir
}
export async function getSavedApiConversationHistory(
context: vscode.ExtensionContext,
taskId: string,
): Promise<Anthropic.MessageParam[]> {
const filePath = path.join(await ensureTaskDirectoryExists(context, taskId), GlobalFileNames.apiConversationHistory)
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
}
return []
}
export async function saveApiConversationHistory(
context: vscode.ExtensionContext,
taskId: string,
apiConversationHistory: Anthropic.MessageParam[],
) {
try {
const filePath = path.join(await ensureTaskDirectoryExists(context, taskId), GlobalFileNames.apiConversationHistory)
await fs.writeFile(filePath, JSON.stringify(apiConversationHistory))
} catch (error) {
// in the off chance this fails, we don't want to stop the task
console.error("Failed to save API conversation history:", error)
}
}
export async function getSavedClineMessages(context: vscode.ExtensionContext, taskId: string): Promise<ClineMessage[]> {
const filePath = path.join(await ensureTaskDirectoryExists(context, taskId), GlobalFileNames.uiMessages)
if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
} else {
// check old location
const oldPath = path.join(await ensureTaskDirectoryExists(context, taskId), "claude_messages.json")
if (await fileExistsAtPath(oldPath)) {
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
await fs.unlink(oldPath) // remove old file
return data
}
}
return []
}
export async function saveClineMessages(context: vscode.ExtensionContext, taskId: string, uiMessages: ClineMessage[]) {
try {
const taskDir = await ensureTaskDirectoryExists(context, taskId)
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
await fs.writeFile(filePath, JSON.stringify(uiMessages))
} catch (error) {
console.error("Failed to save ui messages:", error)
}
}
-67
View File
@@ -1,67 +0,0 @@
export type SecretKey =
| "apiKey"
| "clineApiKey"
| "openRouterApiKey"
| "awsAccessKey"
| "awsSecretKey"
| "awsSessionToken"
| "openAiApiKey"
| "geminiApiKey"
| "openAiNativeApiKey"
| "deepSeekApiKey"
| "requestyApiKey"
| "togetherApiKey"
| "qwenApiKey"
| "mistralApiKey"
| "liteLlmApiKey"
| "authNonce"
| "asksageApiKey"
| "xaiApiKey"
| "sambanovaApiKey"
export type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "awsRegion"
| "awsUseCrossRegionInference"
| "awsBedrockUsePromptCache"
| "awsBedrockEndpoint"
| "awsProfile"
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
| "lastShownAnnouncementId"
| "customInstructions"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiModelInfo"
| "ollamaModelId"
| "ollamaBaseUrl"
| "ollamaApiOptionsCtxNum"
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "azureApiVersion"
| "openRouterModelId"
| "openRouterModelInfo"
| "openRouterProviderSorting"
| "autoApprovalSettings"
| "browserSettings"
| "chatSettings"
| "vsCodeLmModelSelector"
| "userInfo"
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeThinkingBudgetTokens"
| "previousModeVsCodeLmModelSelector"
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
| "qwenApiLine"
| "requestyModelId"
| "togetherModelId"
| "mcpMarketplaceCatalog"
| "telemetrySetting"
| "asksageApiUrl"
| "thinkingBudgetTokens"
| "planActSeparateModelsSetting"
-421
View File
@@ -1,421 +0,0 @@
import * as vscode from "vscode"
import { DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings"
import { DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
import { GlobalStateKey, SecretKey } from "./state-keys"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { HistoryItem } from "../../shared/HistoryItem"
import { AutoApprovalSettings } from "../../shared/AutoApprovalSettings"
import { BrowserSettings } from "../../shared/BrowserSettings"
import { ChatSettings } from "../../shared/ChatSettings"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { UserInfo } from "../../shared/UserInfo"
/*
Storage
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
https://www.eliostruyf.com/devhack-code-extension-storage-options/
*/
// global
export async function updateGlobalState(context: vscode.ExtensionContext, key: GlobalStateKey, value: any) {
await context.globalState.update(key, value)
}
export async function getGlobalState(context: vscode.ExtensionContext, key: GlobalStateKey) {
return await context.globalState.get(key)
}
// secrets
export async function storeSecret(context: vscode.ExtensionContext, key: SecretKey, value?: string) {
if (value) {
await context.secrets.store(key, value)
} else {
await context.secrets.delete(key)
}
}
export async function getSecret(context: vscode.ExtensionContext, key: SecretKey) {
return await context.secrets.get(key)
}
// workspace
export async function updateWorkspaceState(context: vscode.ExtensionContext, key: string, value: any) {
await context.workspaceState.update(key, value)
}
export async function getWorkspaceState(context: vscode.ExtensionContext, key: string) {
return await context.workspaceState.get(key)
}
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
storedApiProvider,
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings,
browserSettings,
chatSettings,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
userInfo,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
qwenApiLine,
liteLlmApiKey,
telemetrySetting,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
sambanovaApiKey,
planActSeparateModelsSettingRaw,
] = await Promise.all([
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
getSecret(context, "openRouterApiKey") as Promise<string | undefined>,
getSecret(context, "clineApiKey") as Promise<string | undefined>,
getSecret(context, "awsAccessKey") as Promise<string | undefined>,
getSecret(context, "awsSecretKey") as Promise<string | undefined>,
getSecret(context, "awsSessionToken") as Promise<string | undefined>,
getGlobalState(context, "awsRegion") as Promise<string | undefined>,
getGlobalState(context, "awsUseCrossRegionInference") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockUsePromptCache") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockEndpoint") as Promise<string | undefined>,
getGlobalState(context, "awsProfile") as Promise<string | undefined>,
getGlobalState(context, "awsUseProfile") as Promise<boolean | undefined>,
getGlobalState(context, "vertexProjectId") as Promise<string | undefined>,
getGlobalState(context, "vertexRegion") as Promise<string | undefined>,
getGlobalState(context, "openAiBaseUrl") as Promise<string | undefined>,
getSecret(context, "openAiApiKey") as Promise<string | undefined>,
getGlobalState(context, "openAiModelId") as Promise<string | undefined>,
getGlobalState(context, "openAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "ollamaModelId") as Promise<string | undefined>,
getGlobalState(context, "ollamaBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "ollamaApiOptionsCtxNum") as Promise<string | undefined>,
getGlobalState(context, "lmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "lmStudioBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "anthropicBaseUrl") as Promise<string | undefined>,
getSecret(context, "geminiApiKey") as Promise<string | undefined>,
getSecret(context, "openAiNativeApiKey") as Promise<string | undefined>,
getSecret(context, "deepSeekApiKey") as Promise<string | undefined>,
getSecret(context, "requestyApiKey") as Promise<string | undefined>,
getGlobalState(context, "requestyModelId") as Promise<string | undefined>,
getSecret(context, "togetherApiKey") as Promise<string | undefined>,
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
getSecret(context, "qwenApiKey") as Promise<string | undefined>,
getSecret(context, "mistralApiKey") as Promise<string | undefined>,
getGlobalState(context, "azureApiVersion") as Promise<string | undefined>,
getGlobalState(context, "openRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "openRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "openRouterProviderSorting") as Promise<string | undefined>,
getGlobalState(context, "lastShownAnnouncementId") as Promise<string | undefined>,
getGlobalState(context, "customInstructions") as Promise<string | undefined>,
getGlobalState(context, "taskHistory") as Promise<HistoryItem[] | undefined>,
getGlobalState(context, "autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
getGlobalState(context, "browserSettings") as Promise<BrowserSettings | undefined>,
getGlobalState(context, "chatSettings") as Promise<ChatSettings | undefined>,
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "liteLlmBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
getGlobalState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "qwenApiLine") as Promise<string | undefined>,
getSecret(context, "liteLlmApiKey") as Promise<string | undefined>,
getGlobalState(context, "telemetrySetting") as Promise<TelemetrySetting | undefined>,
getSecret(context, "asksageApiKey") as Promise<string | undefined>,
getGlobalState(context, "asksageApiUrl") as Promise<string | undefined>,
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
])
let apiProvider: ApiProvider
if (storedApiProvider) {
apiProvider = storedApiProvider
} else {
// Either new user or legacy user that doesn't have the apiProvider stored in state
// (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
if (apiKey) {
apiProvider = "anthropic"
} else {
// New users should default to openrouter, since they've opted to use an API key instead of signing in
apiProvider = "openrouter"
}
}
const o3MiniReasoningEffort = vscode.workspace.getConfiguration("cline.modelSettings.o3Mini").get("reasoningEffort", "medium")
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
// On win11 state sometimes initializes as empty string instead of undefined
let planActSeparateModelsSetting: boolean | undefined = undefined
if (planActSeparateModelsSettingRaw === true || planActSeparateModelsSettingRaw === false) {
planActSeparateModelsSetting = planActSeparateModelsSettingRaw
} else {
// default to true for existing users
if (storedApiProvider) {
planActSeparateModelsSetting = true
} else {
// default to false for new users
planActSeparateModelsSetting = false
}
// this is a special case where it's a new state, but we want it to default to different values for existing and new users.
// persist so next time state is retrieved it's set to the correct value.
await updateGlobalState(context, "planActSeparateModelsSetting", planActSeparateModelsSetting)
}
return {
apiConfiguration: {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
qwenApiLine,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
o3MiniReasoningEffort,
thinkingBudgetTokens,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
sambanovaApiKey,
},
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS,
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
userInfo,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
mcpMarketplaceEnabled,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
}
}
export async function updateApiConfiguration(context: vscode.ExtensionContext, apiConfiguration: ApiConfiguration) {
const {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
qwenApiLine,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
clineApiKey,
sambanovaApiKey,
} = apiConfiguration
await updateGlobalState(context, "apiProvider", apiProvider)
await updateGlobalState(context, "apiModelId", apiModelId)
await storeSecret(context, "apiKey", apiKey)
await storeSecret(context, "openRouterApiKey", openRouterApiKey)
await storeSecret(context, "awsAccessKey", awsAccessKey)
await storeSecret(context, "awsSecretKey", awsSecretKey)
await storeSecret(context, "awsSessionToken", awsSessionToken)
await updateGlobalState(context, "awsRegion", awsRegion)
await updateGlobalState(context, "awsUseCrossRegionInference", awsUseCrossRegionInference)
await updateGlobalState(context, "awsBedrockUsePromptCache", awsBedrockUsePromptCache)
await updateGlobalState(context, "awsBedrockEndpoint", awsBedrockEndpoint)
await updateGlobalState(context, "awsProfile", awsProfile)
await updateGlobalState(context, "awsUseProfile", awsUseProfile)
await updateGlobalState(context, "vertexProjectId", vertexProjectId)
await updateGlobalState(context, "vertexRegion", vertexRegion)
await updateGlobalState(context, "openAiBaseUrl", openAiBaseUrl)
await storeSecret(context, "openAiApiKey", openAiApiKey)
await updateGlobalState(context, "openAiModelId", openAiModelId)
await updateGlobalState(context, "openAiModelInfo", openAiModelInfo)
await updateGlobalState(context, "ollamaModelId", ollamaModelId)
await updateGlobalState(context, "ollamaBaseUrl", ollamaBaseUrl)
await updateGlobalState(context, "ollamaApiOptionsCtxNum", ollamaApiOptionsCtxNum)
await updateGlobalState(context, "lmStudioModelId", lmStudioModelId)
await updateGlobalState(context, "lmStudioBaseUrl", lmStudioBaseUrl)
await updateGlobalState(context, "anthropicBaseUrl", anthropicBaseUrl)
await storeSecret(context, "geminiApiKey", geminiApiKey)
await storeSecret(context, "openAiNativeApiKey", openAiNativeApiKey)
await storeSecret(context, "deepSeekApiKey", deepSeekApiKey)
await storeSecret(context, "requestyApiKey", requestyApiKey)
await storeSecret(context, "togetherApiKey", togetherApiKey)
await storeSecret(context, "qwenApiKey", qwenApiKey)
await storeSecret(context, "mistralApiKey", mistralApiKey)
await storeSecret(context, "liteLlmApiKey", liteLlmApiKey)
await storeSecret(context, "xaiApiKey", xaiApiKey)
await updateGlobalState(context, "azureApiVersion", azureApiVersion)
await updateGlobalState(context, "openRouterModelId", openRouterModelId)
await updateGlobalState(context, "openRouterModelInfo", openRouterModelInfo)
await updateGlobalState(context, "openRouterProviderSorting", openRouterProviderSorting)
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
await updateGlobalState(context, "requestyModelId", requestyModelId)
await updateGlobalState(context, "togetherModelId", togetherModelId)
await storeSecret(context, "asksageApiKey", asksageApiKey)
await updateGlobalState(context, "asksageApiUrl", asksageApiUrl)
await updateGlobalState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
await storeSecret(context, "clineApiKey", clineApiKey)
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
}
export async function resetExtensionState(context: vscode.ExtensionContext) {
for (const key of context.globalState.keys()) {
await context.globalState.update(key, undefined)
}
const secretKeys: SecretKey[] = [
"apiKey",
"openRouterApiKey",
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
"openAiApiKey",
"geminiApiKey",
"openAiNativeApiKey",
"deepSeekApiKey",
"requestyApiKey",
"togetherApiKey",
"qwenApiKey",
"mistralApiKey",
"clineApiKey",
"liteLlmApiKey",
"asksageApiKey",
"xaiApiKey",
"sambanovaApiKey",
]
for (const key of secretKeys) {
await storeSecret(context, key, undefined)
}
}
File diff suppressed because it is too large Load Diff
-322
View File
@@ -1,322 +0,0 @@
import axios from "axios"
import * as vscode from "vscode"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { getTheme } from "../../integrations/theme/getTheme"
import { Controller } from "../controller"
import { findLast } from "../../shared/array"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
*/
export class WebviewProvider implements vscode.WebviewViewProvider {
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
private static activeInstances: Set<WebviewProvider> = new Set()
public view?: vscode.WebviewView | vscode.WebviewPanel
private disposables: vscode.Disposable[] = []
controller: Controller
constructor(
readonly context: vscode.ExtensionContext,
private readonly outputChannel: vscode.OutputChannel,
) {
WebviewProvider.activeInstances.add(this)
this.controller = new Controller(context, outputChannel, this)
}
async dispose() {
if (this.view && "dispose" in this.view) {
this.view.dispose()
}
while (this.disposables.length) {
const x = this.disposables.pop()
if (x) {
x.dispose()
}
}
await this.controller.dispose()
WebviewProvider.activeInstances.delete(this)
}
public static getVisibleInstance(): WebviewProvider | undefined {
return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
}
async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
this.view = webviewView
webviewView.webview.options = {
// Allow scripts in the webview
enableScripts: true,
localResourceRoots: [this.context.extensionUri],
}
webviewView.webview.html =
this.context.extensionMode === vscode.ExtensionMode.Development
? await this.getHMRHtmlContent(webviewView.webview)
: 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 received
this.setWebviewMessageListener(webviewView.webview)
// Logs show up in bottom panel > Debug Console
//console.log("registering listener")
// Listen for when the panel becomes visible
// https://github.com/microsoft/vscode-discussions/discussions/840
if ("onDidChangeViewState" in webviewView) {
// WebviewView and WebviewPanel have all the same properties except for this visibility listener
// panel
webviewView.onDidChangeViewState(
() => {
if (this.view?.visible) {
this.controller.postMessageToWebview({
type: "action",
action: "didBecomeVisible",
})
}
},
null,
this.disposables,
)
} else if ("onDidChangeVisibility" in webviewView) {
// sidebar
webviewView.onDidChangeVisibility(
() => {
if (this.view?.visible) {
this.controller.postMessageToWebview({
type: "action",
action: "didBecomeVisible",
})
}
},
null,
this.disposables,
)
}
// Listen for when the view is disposed
// This happens when the user closes the view or when the view is closed programmatically
webviewView.onDidDispose(
async () => {
await this.dispose()
},
null,
this.disposables,
)
// // if the extension is starting a new session, clear previous task state
// this.clearTask()
{
// Listen for configuration changes
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
// Sends latest theme name to webview
await this.controller.postMessageToWebview({
type: "theme",
text: JSON.stringify(await getTheme()),
})
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
await this.controller.postStateToWebview()
}
},
null,
this.disposables,
)
// if the extension is starting a new session, clear previous task state
this.controller.clearTask()
this.outputChannel.appendLine("Webview view resolved")
}
}
/**
* Defines and returns the HTML that should be rendered within the webview panel.
*
* @remarks This is also the place where references to the React webview build files
* are created and inserted into the webview HTML.
*
* @param webview A reference to the extension webview
* @param extensionUri The URI of the directory containing the extension
* @returns A template string literal containing the HTML that should be
* rendered within the webview panel
*/
private getHtmlContent(webview: vscode.Webview): string {
// Get the local path to main script run in the webview,
// then convert it to a uri we can use in the webview.
// The CSS file from the React build output
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
// The JS file from the React build output
const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.js"])
// The codicon font from the React build output
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
// we installed this package in the extension so that we can access it how its intended from the extension (the font file is likely bundled in vscode), and we just import the css fileinto our react app we don't have access to it
// don't forget to add font-src ${webview.cspSource};
const codiconsUri = getUri(webview, this.context.extensionUri, [
"node_modules",
"@vscode",
"codicons",
"dist",
"codicon.css",
])
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
// const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
// const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "vscode.css"))
// // Same for stylesheet
// const stylesheetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.css"))
// 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}';">
- '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.
*/
const nonce = getNonce()
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src https://*.posthog.com https://*.firebaseauth.com https://*.firebaseio.com https://*.googleapis.com https://*.firebase.com; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}' 'unsafe-eval';">
<title>Cline</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>
`
}
/**
* Connects to the local Vite dev server to allow HMR, with fallback to the bundled assets
*
* @param webview A reference to the extension webview
* @returns A template string literal containing the HTML that should be
* rendered within the webview panel
*/
private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
const localPort = 25463
const localServerUrl = `localhost:${localPort}`
// Check if local dev server is running.
try {
await axios.get(`http://${localServerUrl}`)
} catch (error) {
vscode.window.showErrorMessage(
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
)
return this.getHtmlContent(webview)
}
const nonce = getNonce()
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
const codiconsUri = getUri(webview, this.context.extensionUri, [
"node_modules",
"@vscode",
"codicons",
"dist",
"codicon.css",
])
const scriptEntrypoint = "src/main.tsx"
const scriptUri = `http://${localServerUrl}/${scriptEntrypoint}`
const reactRefresh = /*html*/ `
<script nonce="${nonce}" type="module">
import RefreshRuntime from "http://${localServerUrl}/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
`
const csp = [
"default-src 'none'",
`font-src ${webview.cspSource}`,
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${webview.cspSource} https: data:`,
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
]
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<title>Cline</title>
</head>
<body>
<div id="root"></div>
${reactRefresh}
<script type="module" src="${scriptUri}"></script>
</body>
</html>
`
}
/**
* Sets up an event listener to listen for messages passed from the webview context and
* executes code based on the message that is received.
*
* IMPORTANT: When passing methods as callbacks in JavaScript/TypeScript, the method's
* 'this' context can be lost. This happens because the method is passed as a
* standalone function reference, detached from its original object.
*
* The Problem:
* Doing: webview.onDidReceiveMessage(this.controller.handleWebviewMessage)
* Would cause 'this' inside handleWebviewMessage to be undefined or wrong,
* leading to "TypeError: this.setUserInfo is not a function"
*
* The Solution:
* We wrap the method call in an arrow function, which:
* 1. Preserves the lexical scope's 'this' binding
* 2. Ensures handleWebviewMessage is called as a method on the controller instance
* 3. Maintains access to all controller methods and properties
*
* Alternative solutions could use .bind() or making handleWebviewMessage an arrow
* function property, but this approach is clean and explicit.
*
* @param webview The webview instance to attach the message listener to
*/
private setWebviewMessageListener(webview: vscode.Webview) {
webview.onDidReceiveMessage(
(message) => {
this.controller.handleWebviewMessage(message)
},
null,
this.disposables,
)
}
}
+4 -4
View File
@@ -1,7 +1,7 @@
import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as path from "path"
import { Controller } from "../../core/controller"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { HistoryItem } from "../../shared/HistoryItem"
import { ClineMessage } from "../../shared/ExtensionMessage"
@@ -9,7 +9,7 @@ import { ClineMessage } from "../../shared/ExtensionMessage"
* Registers development-only commands for task manipulation.
* These are only activated in development mode.
*/
export function registerTaskCommands(context: vscode.ExtensionContext, controller: Controller): vscode.Disposable[] {
export function registerTaskCommands(context: vscode.ExtensionContext, provider: ClineProvider): vscode.Disposable[] {
return [
vscode.commands.registerCommand("cline.dev.createTestTasks", async () => {
const count = await vscode.window.showInputBox({
@@ -88,13 +88,13 @@ export function registerTaskCommands(context: vscode.ExtensionContext, controlle
}
// Update task history in global state
await controller.updateTaskHistory(historyItem)
await provider.updateTaskHistory(historyItem)
progress.report({ increment: 100 / tasksCount })
}
// Update the UI to show the new tasks
await controller.postStateToWebview()
await provider.postStateToWebview()
vscode.window.showInformationMessage(`Created ${tasksCount} test tasks`)
},
+11 -12
View File
@@ -1,28 +1,27 @@
import * as vscode from "vscode"
import { Controller } from "../core/controller"
import { ClineProvider } from "../core/webview/ClineProvider"
import { ClineAPI } from "./cline"
import { getGlobalState } from "../core/storage/state"
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarController: Controller): ClineAPI {
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvider: ClineProvider): ClineAPI {
const api: ClineAPI = {
setCustomInstructions: async (value: string) => {
await sidebarController.updateCustomInstructions(value)
await sidebarProvider.updateCustomInstructions(value)
outputChannel.appendLine("Custom instructions set")
},
getCustomInstructions: async () => {
return (await getGlobalState(sidebarController.context, "customInstructions")) as string | undefined
return (await sidebarProvider.getGlobalState("customInstructions")) as string | undefined
},
startNewTask: async (task?: string, images?: string[]) => {
outputChannel.appendLine("Starting new task")
await sidebarController.clearTask()
await sidebarController.postStateToWebview()
await sidebarController.postMessageToWebview({
await sidebarProvider.clearTask()
await sidebarProvider.postStateToWebview()
await sidebarProvider.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sidebarController.postMessageToWebview({
await sidebarProvider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: task,
@@ -37,7 +36,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarContr
outputChannel.appendLine(
`Sending message: ${message ? `"${message}"` : "undefined"} with ${images?.length || 0} image(s)`,
)
await sidebarController.postMessageToWebview({
await sidebarProvider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: message,
@@ -47,7 +46,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarContr
pressPrimaryButton: async () => {
outputChannel.appendLine("Pressing primary button")
await sidebarController.postMessageToWebview({
await sidebarProvider.postMessageToWebview({
type: "invoke",
invoke: "primaryButtonClick",
})
@@ -55,7 +54,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarContr
pressSecondaryButton: async () => {
outputChannel.appendLine("Pressing secondary button")
await sidebarController.postMessageToWebview({
await sidebarProvider.postMessageToWebview({
type: "invoke",
invoke: "secondaryButtonClick",
})
+30 -56
View File
@@ -2,13 +2,13 @@
// Import the module and reference it with the alias vscode in your code below
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import * as vscode from "vscode"
import { ClineProvider } from "./core/webview/ClineProvider"
import { Logger } from "./services/logging/Logger"
import { createClineAPI } from "./exports"
import "./utils/path" // necessary to have access to String.prototype.toPosix
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
import assert from "node:assert"
import { telemetryService } from "./services/telemetry/TelemetryService"
import { WebviewProvider } from "./core/webview"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -30,12 +30,12 @@ export function activate(context: vscode.ExtensionContext) {
Logger.initialize(outputChannel)
Logger.log("Cline extension activated")
const sidebarWebview = new WebviewProvider(context, outputChannel)
const sidebarProvider = new ClineProvider(context, outputChannel)
vscode.commands.executeCommand("setContext", "cline.isDevMode", IS_DEV && IS_DEV === "true")
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(WebviewProvider.sideBarId, sidebarWebview, {
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, sidebarProvider, {
webviewOptions: { retainContextWhenHidden: true },
}),
)
@@ -43,15 +43,9 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.plusButtonClicked", async () => {
Logger.log("Plus button Clicked")
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
await visibleWebview.controller.clearTask()
await visibleWebview.controller.postStateToWebview()
await visibleWebview.controller.postMessageToWebview({
await sidebarProvider.clearTask()
await sidebarProvider.postStateToWebview()
await sidebarProvider.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
@@ -60,13 +54,7 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.mcpButtonClicked", () => {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleWebview.controller.postMessageToWebview({
sidebarProvider.postMessageToWebview({
type: "action",
action: "mcpButtonClicked",
})
@@ -77,7 +65,7 @@ export function activate(context: vscode.ExtensionContext) {
Logger.log("Opening Cline in new tab")
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
const tabWebview = new WebviewProvider(context, outputChannel)
const tabProvider = new ClineProvider(context, outputChannel)
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
@@ -88,7 +76,7 @@ export function activate(context: vscode.ExtensionContext) {
}
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
const panel = vscode.window.createWebviewPanel(WebviewProvider.tabPanelId, "Cline", targetCol, {
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Cline", targetCol, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [context.extensionUri],
@@ -99,7 +87,7 @@ export function activate(context: vscode.ExtensionContext) {
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_light.png"),
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_dark.png"),
}
tabWebview.resolveWebviewView(panel)
tabProvider.resolveWebviewView(panel)
// Lock the editor group so clicking on files doesn't open them over the panel
await setTimeoutPromise(100)
@@ -112,13 +100,7 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.settingsButtonClicked", () => {
//vscode.window.showInformationMessage(message)
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleWebview.controller.postMessageToWebview({
sidebarProvider.postMessageToWebview({
type: "action",
action: "settingsButtonClicked",
})
@@ -127,13 +109,7 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.historyButtonClicked", () => {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleWebview.controller.postMessageToWebview({
sidebarProvider.postMessageToWebview({
type: "action",
action: "historyButtonClicked",
})
@@ -142,19 +118,19 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.accountButtonClicked", () => {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleWebview.controller.postMessageToWebview({
sidebarProvider.postMessageToWebview({
type: "action",
action: "accountButtonClicked",
})
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("cline.openDocumentation", () => {
vscode.env.openExternal(vscode.Uri.parse("https://docs.cline.bot/"))
}),
)
/*
We use the text document content provider API to show the left side for diff view by creating a virtual document for the original content. This makes it readonly so users know to edit the right side if they want to keep their changes.
@@ -179,15 +155,15 @@ export function activate(context: vscode.ExtensionContext) {
const path = uri.path
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
return
}
switch (path) {
case "/openrouter": {
const code = query.get("code")
if (code) {
await visibleWebview?.controller.handleOpenRouterCallback(code)
await visibleProvider.handleOpenRouterCallback(code)
}
break
}
@@ -203,13 +179,13 @@ export function activate(context: vscode.ExtensionContext) {
})
// Validate state parameter
if (!(await visibleWebview?.controller.validateAuthState(state))) {
if (!(await visibleProvider.validateAuthState(state))) {
vscode.window.showErrorMessage("Invalid auth state")
return
}
if (token && apiKey) {
await visibleWebview?.controller.handleAuthCallback(token, apiKey)
await visibleProvider.handleAuthCallback(token, apiKey)
}
break
}
@@ -224,7 +200,7 @@ export function activate(context: vscode.ExtensionContext) {
// Use dynamic import to avoid loading the module in production
import("./dev/commands/tasks")
.then((module) => {
const devTaskCommands = module.registerTaskCommands(context, sidebarWebview.controller)
const devTaskCommands = module.registerTaskCommands(context, sidebarProvider)
context.subscriptions.push(...devTaskCommands)
Logger.log("Cline dev task commands registered")
})
@@ -253,8 +229,8 @@ export function activate(context: vscode.ExtensionContext) {
const filePath = editor.document.uri.fsPath
const languageId = editor.document.languageId
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.addSelectedCodeToChat(
// Send to sidebar provider
await sidebarProvider.addSelectedCodeToChat(
selectedText,
filePath,
languageId,
@@ -303,8 +279,7 @@ export function activate(context: vscode.ExtensionContext) {
*/
// Send to sidebar provider
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
await sidebarProvider.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
} catch (error) {
// Ensure clipboard is restored even if an error occurs
await vscode.env.clipboard.writeText(tempCopyBuffer)
@@ -375,12 +350,11 @@ export function activate(context: vscode.ExtensionContext) {
const languageId = editor.document.languageId
// Send to sidebar provider with diagnostics
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.fixWithCline(selectedText, filePath, languageId, diagnostics)
await sidebarProvider.fixWithCline(selectedText, filePath, languageId, diagnostics)
}),
)
return createClineAPI(outputChannel, sidebarWebview.controller)
return createClineAPI(outputChannel, sidebarProvider)
}
// This method is called when your extension is deactivated
+8
View File
@@ -0,0 +1,8 @@
// NOTE: These are here temporarily until we find a better home for them
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
openRouterModels: "openrouter_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
}
@@ -3,7 +3,7 @@ import os from "os"
import * as path from "path"
import simpleGit, { SimpleGit } from "simple-git"
import * as vscode from "vscode"
import { Controller as ClineProvider } from "../../core/controller"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { fileExistsAtPath } from "../../utils/fs"
import { globby } from "globby"
@@ -166,8 +166,8 @@ class CheckpointTracker {
"--allow-empty": null,
"--no-verify": null,
})
const commitHash = (result.commit || "").replace(/^HEAD\s+/, "")
console.warn(`Checkpoint commit created: `, commitHash)
const commitHash = result.commit || ""
console.warn(`Checkpoint commit created.`)
const durationMs = Math.round(performance.now() - startTime)
telemetryService.captureCheckpointUsage(this.taskId, "commit_created", durationMs)
+1 -1
View File
@@ -71,7 +71,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
// remove ansi
data = stripAnsi(data)
// Split data by newlines
const lines = data ? data.split("\n") : []
let lines = data ? data.split("\n") : []
// Remove non-human readable characters from the first line
if (lines.length > 0) {
lines[0] = lines[0].replace(/[^\x20-\x7E]/g, "")
@@ -1,18 +1,18 @@
import * as vscode from "vscode"
import * as path from "path"
import { listFiles } from "../../services/glob/list-files"
import { Controller } from "../../core/controller"
import { ClineProvider } from "../../core/webview/ClineProvider"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
class WorkspaceTracker {
private controllerRef: WeakRef<Controller>
private providerRef: WeakRef<ClineProvider>
private disposables: vscode.Disposable[] = []
private filePaths: Set<string> = new Set()
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
this.registerListeners()
}
@@ -85,7 +85,7 @@ class WorkspaceTracker {
if (!cwd) {
return
}
this.controllerRef.deref()?.postMessageToWebview({
this.providerRef.deref()?.postMessageToWebview({
type: "workspaceUpdated",
filePaths: Array.from(this.filePaths).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
+8 -8
View File
@@ -1,20 +1,20 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
import { Controller } from "../../core/controller"
import { ClineProvider } from "../../core/webview/ClineProvider"
import type { BalanceResponse, PaymentTransaction, UsageTransaction } from "../../shared/ClineAccount"
export class ClineAccountService {
private readonly baseUrl = "https://api.cline.bot/v1"
private controllerRef: WeakRef<Controller>
private providerRef: WeakRef<ClineProvider>
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
}
/**
* Get the user's Cline Account key from the apiConfiguration
*/
private async getClineApiKey(): Promise<string | undefined> {
const provider = this.controllerRef.deref()
const provider = this.providerRef.deref()
if (!provider) {
return undefined
}
@@ -64,7 +64,7 @@ export class ClineAccountService {
const data = await this.authenticatedRequest<BalanceResponse>("/user/credits/balance")
// Post to webview
await this.controllerRef.deref()?.postMessageToWebview({
await this.providerRef.deref()?.postMessageToWebview({
type: "userCreditsBalance",
userCreditsBalance: data,
})
@@ -84,7 +84,7 @@ export class ClineAccountService {
const data = await this.authenticatedRequest<UsageTransaction[]>("/user/credits/usage")
// Post to webview
await this.controllerRef.deref()?.postMessageToWebview({
await this.providerRef.deref()?.postMessageToWebview({
type: "userCreditsUsage",
userCreditsUsage: data,
})
@@ -104,7 +104,7 @@ export class ClineAccountService {
const data = await this.authenticatedRequest<PaymentTransaction[]>("/user/credits/payments")
// Post to webview
await this.controllerRef.deref()?.postMessageToWebview({
await this.providerRef.deref()?.postMessageToWebview({
type: "userCreditsPayments",
userCreditsPayments: data,
})
+1 -1
View File
@@ -212,7 +212,7 @@ export class BrowserSession {
interval: 100,
}).catch(() => {})
const options: ScreenshotOptions = {
let options: ScreenshotOptions = {
encoding: "base64",
// clip: {
+2 -2
View File
@@ -68,8 +68,8 @@ Breadth-first traversal of directory structure level by level up to a limit:
- Timeout mechanism prevents infinite loops
*/
async function globbyLevelByLevel(limit: number, options?: Options) {
const results: Set<string> = new Set()
const queue: string[] = ["*"]
let results: Set<string> = new Set()
let queue: string[] = ["*"]
const globbingProcess = async () => {
while (queue.length > 0 && results.size < limit) {
+111 -66
View File
@@ -1,5 +1,5 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
import {
CallToolResultSchema,
ListResourcesResultSchema,
@@ -14,7 +14,7 @@ import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { Controller } from "../../core/controller"
import { ClineProvider } from "../../core/webview/ClineProvider"
import {
DEFAULT_MCP_TIMEOUT_SECONDS,
McpMode,
@@ -29,12 +29,9 @@ import {
import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual } from "../../utils/path"
import { secondsToMs } from "../../utils/time"
import { GlobalFileNames } from "../../core/storage/disk"
import { GlobalFileNames } from "../../global-constants"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
// Default timeout for internal MCP data requests in milliseconds; is not the same as the user facing timeout stored as DEFAULT_MCP_TIMEOUT_SECONDS
const DEFAULT_REQUEST_TIMEOUT_MS = 5000
export type McpConnection = {
server: McpServer
client: Client
@@ -76,15 +73,15 @@ const McpSettingsSchema = z.object({
})
export class McpHub {
private controllerRef: WeakRef<Controller>
private providerRef: WeakRef<ClineProvider>
private disposables: vscode.Disposable[] = []
private settingsWatcher?: vscode.FileSystemWatcher
private fileWatchers: Map<string, FSWatcher> = new Map()
connections: McpConnection[] = []
isConnecting: boolean = false
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
this.watchMcpSettingsFile()
this.initializeMcpServers()
}
@@ -99,7 +96,7 @@ export class McpHub {
}
async getMcpServersPath(): Promise<string> {
const provider = this.controllerRef.deref()
const provider = this.providerRef.deref()
if (!provider) {
throw new Error("Provider not available")
}
@@ -108,7 +105,7 @@ export class McpHub {
}
async getMcpSettingsFilePath(): Promise<string> {
const provider = this.controllerRef.deref()
const provider = this.providerRef.deref()
if (!provider) {
throw new Error("Provider not available")
}
@@ -197,7 +194,7 @@ export class McpHub {
const client = new Client(
{
name: "Cline",
version: this.controllerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
},
{
capabilities: {},
@@ -207,7 +204,7 @@ export class McpHub {
let transport: StdioClientTransport | SSEClientTransport
if (config.transportType === "sse") {
transport = new SSEClientTransport(new URL(config.url), {})
return
} else {
transport = new StdioClientTransport({
command: config.command,
@@ -239,42 +236,59 @@ export class McpHub {
await this.notifyWebviewOfServerChanges()
}
// If the config is invalid, show an error
if (!StdioConfigSchema.safeParse(config).success) {
console.error(`Invalid config for "${name}": missing or invalid parameters`)
const connection: McpConnection = {
server: {
name,
config: JSON.stringify(config),
status: "disconnected",
error: "Invalid config: missing or invalid parameters",
},
client,
transport,
}
this.connections.push(connection)
return
}
// valid schema
const parsedConfig = StdioConfigSchema.parse(config)
const connection: McpConnection = {
server: {
name,
config: JSON.stringify(config),
status: "connecting",
disabled: config.disabled,
disabled: parsedConfig.disabled,
},
client,
transport,
}
this.connections.push(connection)
if (config.transportType === "stdio") {
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
await transport.start()
const stderrStream = (transport as StdioClientTransport).stderr
if (stderrStream) {
stderrStream.on("data", async (data: Buffer) => {
const errorOutput = data.toString()
console.error(`Server "${name}" stderr:`, errorOutput)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs "<name> server running on stdio" to stderr.
this.appendErrorMessage(connection, errorOutput)
// Only need to update webview right away if it's already disconnected
if (connection.server.status === "disconnected") {
await this.notifyWebviewOfServerChanges()
}
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
await transport.start()
const stderrStream = transport.stderr
if (stderrStream) {
stderrStream.on("data", async (data: Buffer) => {
const errorOutput = data.toString()
console.error(`Server "${name}" stderr:`, errorOutput)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs "<name> server running on stdio" to stderr.
this.appendErrorMessage(connection, errorOutput)
// Only need to update webview right away if it's already disconnected
if (connection.server.status === "disconnected") {
await this.notifyWebviewOfServerChanges()
}
})
} else {
console.error(`No stderr stream for ${name}`)
}
transport.start = async () => {} // No-op now, .connect() won't fail
}
})
} else {
console.error(`No stderr stream for ${name}`)
}
transport.start = async () => {} // No-op now, .connect() won't fail
// Connect
await client.connect(transport)
@@ -303,15 +317,9 @@ export class McpHub {
private async fetchToolsList(serverName: string): Promise<McpTool[]> {
try {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(`No connection found for server: ${serverName}`)
}
const response = await connection.client.request({ method: "tools/list" }, ListToolsResultSchema, {
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
})
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "tools/list" }, ListToolsResultSchema)
// Get autoApprove settings
const settingsPath = await this.getMcpSettingsFilePath()
@@ -337,7 +345,7 @@ export class McpHub {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "resources/list" }, ListResourcesResultSchema, { timeout: DEFAULT_REQUEST_TIMEOUT_MS })
?.client.request({ method: "resources/list" }, ListResourcesResultSchema)
return response?.resources || []
} catch (error) {
// console.error(`Failed to fetch resources for ${serverName}:`, error)
@@ -349,10 +357,7 @@ export class McpHub {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema, {
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
})
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema)
return response?.resourceTemplates || []
} catch (error) {
// console.error(`Failed to fetch resource templates for ${serverName}:`, error)
@@ -394,9 +399,7 @@ export class McpHub {
if (!currentConnection) {
// New server
try {
if (config.transportType === "stdio") {
this.setupFileWatcher(name, config)
}
this.setupFileWatcher(name, config)
await this.connectToServer(name, config)
} catch (error) {
console.error(`Failed to connect to new MCP server ${name}:`, error)
@@ -404,9 +407,7 @@ export class McpHub {
} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
// Existing server with changed config
try {
if (config.transportType === "stdio") {
this.setupFileWatcher(name, config)
}
this.setupFileWatcher(name, config)
await this.deleteConnection(name)
await this.connectToServer(name, config)
console.log(`Reconnected MCP server with updated config: ${name}`)
@@ -446,7 +447,7 @@ export class McpHub {
async restartConnection(serverName: string): Promise<void> {
this.isConnecting = true
const provider = this.controllerRef.deref()
const provider = this.providerRef.deref()
if (!provider) {
return
}
@@ -481,7 +482,7 @@ export class McpHub {
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
const serverOrder = Object.keys(config.mcpServers || {})
await this.controllerRef.deref()?.postMessageToWebview({
await this.providerRef.deref()?.postMessageToWebview({
type: "mcpServers",
mcpServers: [...this.connections]
.sort((a, b) => {
@@ -502,21 +503,64 @@ export class McpHub {
// Public methods for server management
public async toggleServerDisabled(serverName: string, disabled: boolean): Promise<void> {
let settingsPath: string
try {
const config = await this.readAndValidateMcpSettingsFile()
if (!config) {
throw new Error("Failed to read or validate MCP settings")
settingsPath = await this.getMcpSettingsFilePath()
// Ensure the settings file exists and is accessible
try {
await fs.access(settingsPath)
} catch (error) {
console.error("Settings file not accessible:", error)
throw new Error("Settings file not accessible")
}
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
// Validate the config structure
if (!config || typeof config !== "object") {
throw new Error("Invalid config structure")
}
if (!config.mcpServers || typeof config.mcpServers !== "object") {
config.mcpServers = {}
}
if (config.mcpServers[serverName]) {
config.mcpServers[serverName].disabled = disabled
// Create a new server config object to ensure clean structure
const serverConfig = {
...config.mcpServers[serverName],
disabled,
}
const settingsPath = await this.getMcpSettingsFilePath()
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
// Ensure required fields exist
if (!serverConfig.autoApprove) {
serverConfig.autoApprove = []
}
config.mcpServers[serverName] = serverConfig
// Write the entire config back
const updatedConfig = {
mcpServers: config.mcpServers,
}
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (connection) {
connection.server.disabled = disabled
try {
connection.server.disabled = disabled
// Only refresh capabilities if connected
if (connection.server.status === "connected") {
connection.server.tools = await this.fetchToolsList(serverName)
connection.server.resources = await this.fetchResourcesList(serverName)
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(serverName)
}
} catch (error) {
console.error(`Failed to refresh capabilities for ${serverName}:`, error)
}
}
await this.notifyWebviewOfServerChanges()
@@ -569,7 +613,7 @@ export class McpHub {
try {
const config = JSON.parse(connection.server.config)
const parsedConfig = ServerConfigSchema.parse(config)
const parsedConfig = StdioConfigSchema.parse(config)
timeout = secondsToMs(parsedConfig.timeout)
} catch (error) {
console.error(`Failed to parse timeout configuration for server ${serverName}: ${error}`)
@@ -619,6 +663,7 @@ export class McpHub {
// Update the tools list to reflect the change
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (connection) {
connection.server.tools = await this.fetchToolsList(serverName)
await this.notifyWebviewOfServerChanges()
}
} catch (error) {
@@ -0,0 +1,303 @@
import { context, SpanKind, trace } from "@opentelemetry/api"
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
import { Resource } from "@opentelemetry/resources"
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"
import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { ClineProvider } from "../../core/webview/ClineProvider"
export type TelemetryChatMessage = {
role: "user" | "assistant" | "system"
ts: number
content: Anthropic.Messages.MessageParam["content"]
}
interface ConversationMetadata {
apiProvider?: string
model?: string
tokensIn: number
tokensOut: number
}
/**
Cline Telemetry (currently only available in DEV builds)
Advanced Setting to opt-in to LLM observability, allowing you to share message data, code, and more extensive telemetry to help improve prompts used in Cline, train our models, and understand failure states more accurately.
"cline.conversationObservability": {
"type": "boolean",
"default": false,
"markdownDescription": "Share message data, code, and more extensive telemetry. This data may be used to improve prompts used in Cline, train models, and understand failure states more accurately. [Learn more](https://docs.cline.bot/more-info/llm-observability)"
}
*/
export class ConversationObservabilityService {
private providerRef: WeakRef<ClineProvider>
private distinctId: string = vscode.env.machineId
private apiEndpoint: string = "https://api.cline.bot/v1/traces"
private tracerProvider: NodeTracerProvider | undefined
private tracer: any
private messageIndices: Map<string, number> = new Map()
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
}
private async getClineApiKey(): Promise<string | undefined> {
const provider = this.providerRef.deref()
if (!provider) {
return undefined
}
const { apiConfiguration } = await provider.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
}
public isOptedInToConversationObservability(): boolean {
// User has to manually opt in to conversation telemetry in Advanced Settings
const isConversationObservabilityEnabled =
vscode.workspace.getConfiguration("cline").get<boolean>("conversationObservability") ?? false
return isConversationObservabilityEnabled
}
private async initializeTracer() {
try {
// Create a resource that identifies our service
const resource = new Resource({
[ATTR_SERVICE_NAME]: "cline-extension",
[ATTR_SERVICE_VERSION]: "1.0.0",
})
const clineApiKey = await this.getClineApiKey()
console.info("[ConversationObservability] Initializing OpenTelemetry tracer...")
// Configure the OTLP exporter
const headers: Record<string, string> = {
"Content-Type": "application/json",
}
// Add API key to headers if available
if (clineApiKey) {
headers["Authorization"] = `Bearer ${clineApiKey}`
}
const exporter = new OTLPTraceExporter({
url: this.apiEndpoint,
headers,
})
// Create the span processor
const spanProcessor = new SimpleSpanProcessor(exporter as any)
// Create the trace provider with the span processor in the config
this.tracerProvider = new NodeTracerProvider({
resource,
spanProcessors: [spanProcessor as any],
})
// Register the provider
this.tracerProvider.register()
// Get a tracer
this.tracer = trace.getTracer("cline-conversation-tracer")
console.info("[ConversationObservability] OpenTelemetry tracer initialized successfully")
} catch (error) {
console.error("[ConversationObservability] Failed to initialize OpenTelemetry tracer:", error)
}
}
/**
* Captures a message in the conversation as an OpenTelemetry span
* ONLY HAPPENS IF USER IS OPTED INTO CONVERSATION TELEMETRY IN ADVANCED SETTINGS
*/
public async captureMessage(taskId: string, message: TelemetryChatMessage, metadata: ConversationMetadata) {
// Do NOT capture message if user has not explicitly opted in
if (!this.isOptedInToConversationObservability()) {
return
}
if (!this.tracer) {
await this.initializeTracer()
}
try {
// Convert taskId to a valid trace ID (must be 32 hex chars)
const traceId = this.generateTraceIdFromTimestamp(taskId)
// Convert message timestamp to a valid span ID (must be 16 hex chars)
if (!message.ts && message.ts !== 0) {
throw new Error("Message timestamp is required")
}
const timestamp = message.ts
const spanId = this.generateSpanIdFromTimestamp(timestamp)
// Create a span context with our IDs
const spanContext = trace.setSpanContext(context.active(), {
traceId,
spanId,
isRemote: false,
traceFlags: 1, // Sampled
})
// Start a new span with the context
const span = this.tracer.startSpan(
`message.${message.role}`,
{
kind: SpanKind.CLIENT,
startTime: this.millisecondsToHrTime(timestamp), // Convert to nanoseconds
},
spanContext,
)
// Get the message index for this task
const messageIndex = this.getNextMessageIndex(taskId)
// Add attributes to the span
span.setAttribute("task.id", taskId)
span.setAttribute("user.id", this.distinctId)
span.setAttribute("message.role", message.role)
span.setAttribute("message.timestamp", timestamp)
span.setAttribute("message.index", messageIndex)
const c = message.content
// Add Braintrust-compatible attributes
span.setAttribute("gen_ai.request.model", metadata.model)
if (message.role === "user") {
span.setAttribute("gen_ai.prompt", this.extractContent(message))
} else if (message.role === "assistant") {
span.setAttribute("gen_ai.completion", this.extractContent(message))
span.setAttribute("gen_ai.usage.prompt_tokens", metadata.tokensIn)
span.setAttribute("gen_ai.usage.completion_tokens", metadata.tokensOut)
} else if (message.role === "system") {
span.setAttribute("gen_ai.system_prompt", this.extractContent(message))
}
// Add custom metadata in Braintrust format
span.setAttribute("braintrust.metadata.api_provider", metadata.apiProvider)
span.setAttribute("braintrust.metadata.ts", message.ts)
// End the span immediately since messages are discrete events
span.end(this.millisecondsToHrTime(timestamp)) // Convert to nanoseconds
console.info(`[ConversationObservability] Captured ${message.role} message for task ${taskId}`, { span })
} catch (error) {
console.error("[ConversationObservability] Error capturing message:", error)
}
}
/**
* Convert a decimal timestamp to a valid trace ID (32 hex chars)
*/
private generateTraceIdFromTimestamp(timestamp: string): string {
// Pad with zeros and convert to hex
const hex = BigInt(timestamp).toString(16).padStart(32, "0")
return hex.substring(0, 32) // Ensure it's exactly 32 chars
}
/**
* Converts milliseconds to high-resolution time format expected by OpenTelemetry
* Returns [seconds, nanoseconds]
*/
private millisecondsToHrTime(milliseconds: number): [number, number] {
return [
Math.floor(milliseconds / 1000), // seconds
(milliseconds % 1000) * 1000000, // nanoseconds (remainder in ms * 10^6)
]
}
/**
* Convert a decimal timestamp to a valid span ID (16 hex chars)
*/
private generateSpanIdFromTimestamp(timestamp: number): string {
// Pad with zeros and convert to hex
const hex = BigInt(timestamp).toString(16).padStart(16, "0")
return hex.substring(0, 16) // Ensure it's exactly 16 chars
}
/**
* Helper to extract content from different message formats
*/
private extractContent(message: TelemetryChatMessage): string {
if (typeof message.content === "string") {
return message.content
}
return message.content
.map((block) => (block.type === "text" ? block.text : null))
.filter(Boolean)
.join("\n")
}
/**
* Track message indices per task
*/
private getNextMessageIndex(taskId: string): number {
const currentIndex = this.messageIndices.get(taskId) || 0
this.messageIndices.set(taskId, currentIndex + 1)
return currentIndex
}
/**
* Sends conversation data to cleanup endpoint to remove deleted messages from telemetry
* ONLY HAPPENS IF USER IS OPTED INTO CONVERSATION TELEMETRY IN ADVANCED SETTINGS
*/
public async cleanupTask(taskId: string, conversationData: any): Promise<void> {
// Do NOT send data if user has not explicitly opted in
if (!this.isOptedInToConversationObservability()) {
return
}
const clineApiKey = await this.getClineApiKey()
if (!clineApiKey) {
return
}
try {
// Configure the headers with API key
const headers: Record<string, string> = {
"Content-Type": "application/json",
}
// Add API key to headers
headers["Authorization"] = `Bearer ${clineApiKey}`
// Send the data to the cleanup endpoint
const cleanupEndpoint = `${this.apiEndpoint.replace("/traces", "/traces/cleanup")}`
// Use fetch API to send the data
const response = await fetch(cleanupEndpoint, {
method: "POST",
headers,
body: JSON.stringify({
taskId: taskId,
conversationData,
userId: this.distinctId,
}),
})
if (!response.ok) {
throw new Error(`Failed to send cleanup data: ${response.status} ${response.statusText}`)
}
console.info(`[ConversationObservability] Cleanup data sent for task ${taskId}`)
} catch (error) {
console.error("[ConversationObservability] Error sending cleanup data:", error)
}
}
/**
* Shutdown the tracer provider
*/
public async shutdown(): Promise<void> {
if (this.tracerProvider) {
await this.tracerProvider.shutdown()
}
}
}
@@ -2,8 +2,6 @@ import { PostHog } from "posthog-node"
import * as vscode from "vscode"
import { version as extensionVersion } from "../../../package.json"
import type { TaskFeedbackType } from "../../shared/WebviewMessage"
/**
* PostHogClient handles telemetry event tracking for the Cline extension
* Uses PostHog analytics to track user interactions and system events
@@ -20,8 +18,6 @@ class PostHogClient {
RESTARTED: "task.restarted",
// Tracks when a task is finished, with acceptance or rejection status
COMPLETED: "task.completed",
// Tracks user feedback on completed tasks
FEEDBACK: "task.feedback",
// Tracks when a message is sent in a conversation
CONVERSATION_TURN: "task.conversation_turn",
// Tracks token consumption for cost and usage analysis
@@ -36,8 +32,6 @@ class PostHogClient {
HISTORICAL_LOADED: "task.historical_loaded",
// Tracks when the retry button is clicked for failed operations
RETRY_CLICKED: "task.retry_clicked",
// Tracks when a diff edit (replace_in_file) operation fails
DIFF_EDIT_FAILED: "task.diff_edit_failed",
},
// UI interaction events for tracking user engagement
UI: {
@@ -240,22 +234,6 @@ class PostHogClient {
})
}
/**
* Records user feedback on completed tasks
* @param taskId Unique identifier for the task
* @param feedbackType The type of feedback ("thumbs_up" or "thumbs_down")
*/
public captureTaskFeedback(taskId: string, feedbackType: TaskFeedbackType) {
console.info("TelemetryService: Capturing task feedback", { taskId, feedbackType })
this.capture({
event: PostHogClient.EVENTS.TASK.FEEDBACK,
properties: {
taskId,
feedbackType,
},
})
}
// Tool events
/**
* Records when a tool is used during task execution
@@ -399,21 +377,6 @@ class PostHogClient {
})
}
/**
* Records when a diff edit (replace_in_file) operation fails
* @param taskId Unique identifier for the task
* @param errorType Type of error that occurred (e.g., "search_not_found", "invalid_format")
*/
public captureDiffEditFailure(taskId: string, errorType?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
},
})
}
/**
* Records when a different model is selected for use
* @param model Name of the selected model
-5
View File
@@ -65,7 +65,6 @@ export interface WebviewMessage {
| "fetchUserCreditsData"
| "optionsResponse"
| "requestTotalTasksSize"
| "taskFeedback"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
@@ -93,12 +92,8 @@ export interface WebviewMessage {
planActSeparateModelsSetting?: boolean
telemetrySetting?: TelemetrySetting
customInstructionsSetting?: string
// For task feedback
feedbackType?: TaskFeedbackType
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
export type ClineCheckpointRestore = "task" | "workspace" | "taskAndWorkspace"
export type TaskFeedbackType = "thumbs_up" | "thumbs_down"
+3 -4
View File
@@ -24,7 +24,6 @@ export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
clineApiKey?: string
taskId?: string // Used to identify the task in API requests
liteLlmBaseUrl?: string
liteLlmModelId?: string
liteLlmApiKey?: string
@@ -368,9 +367,9 @@ export const vertexModels = {
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.5-pro-exp-03-25": {
maxTokens: 65536,
contextWindow: 1_048_576,
"gemini-2.0-pro-exp-02-05": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
+3 -3
View File
@@ -23,13 +23,13 @@ export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] {
for (let i = 0; i < messages.length; i++) {
if (messages[i].type === "say" && messages[i].say === "api_req_started") {
const startedRequest = JSON.parse(messages[i].text || "{}")
let startedRequest = JSON.parse(messages[i].text || "{}")
let j = i + 1
while (j < messages.length) {
if (messages[j].type === "say" && messages[j].say === "api_req_finished") {
const finishedRequest = JSON.parse(messages[j].text || "{}")
const combinedRequest = {
let finishedRequest = JSON.parse(messages[j].text || "{}")
let combinedRequest = {
...startedRequest,
...finishedRequest,
}
-57
View File
@@ -1,57 +0,0 @@
import { describe, it } from "mocha"
import "should"
import { fixModelHtmlEscaping, removeInvalidChars } from "./string"
describe("fixModelHtmlEscaping", () => {
it("should convert &gt; to >", () => {
fixModelHtmlEscaping("foo &gt; bar").should.equal("foo > bar")
})
it("should convert &lt; to <", () => {
fixModelHtmlEscaping("foo &lt; bar").should.equal("foo < bar")
})
it('should convert &quot; to "', () => {
fixModelHtmlEscaping("foo &quot;bar&quot;").should.equal('foo "bar"')
})
it("should convert &amp; to &", () => {
fixModelHtmlEscaping("foo &amp; bar").should.equal("foo & bar")
})
it("should convert &apos; to '", () => {
fixModelHtmlEscaping("foo &apos;bar&apos;").should.equal("foo 'bar'")
})
it("should handle multiple entities in the same string", () => {
fixModelHtmlEscaping("&lt;div&gt;Hello &quot;World&quot; &amp; &apos;Universe&apos;&lt;/div&gt;").should.equal(
"<div>Hello \"World\" & 'Universe'</div>",
)
})
it("should return unchanged string when no HTML entities are present", () => {
fixModelHtmlEscaping("normal string").should.equal("normal string")
})
})
describe("removeInvalidChars", () => {
it("should remove replacement characters", () => {
removeInvalidChars("hello\uFFFDworld").should.equal("helloworld")
})
it("should remove characters", () => {
removeInvalidChars("helloworld").should.equal("helloworld")
})
it("should remove multiple replacement characters", () => {
removeInvalidChars("h\uFFFDe\uFFFDl\uFFFDl\uFFFDo").should.equal("hello")
})
it("should remove multiple characters", () => {
removeInvalidChars("hello").should.equal("hello")
})
it("should return unchanged string when no replacement characters are present", () => {
removeInvalidChars("normal string").should.equal("normal string")
})
})
@@ -147,7 +147,7 @@ export const ClineAccountView = () => {
</div>
</div>
) : (
<div className="flex flex-col items-center pr-3">
<div className="flex flex-col items-center pr-3 max-w-[400px]">
<ClineLogoWhite className="size-16 mb-4" />
<p style={{}}>
@@ -30,7 +30,6 @@ import CreditLimitError from "./CreditLimitError"
import { OptionsButtons } from "./OptionsButtons"
import { highlightMentions } from "./TaskHeader"
import SuccessButton from "../common/SuccessButton"
import TaskFeedbackButtons from "./TaskFeedbackButtons"
const ChatRowContainer = styled.div`
padding: 10px 6px 10px 15px;
@@ -1001,17 +1000,6 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
}}>
{icon}
{title}
<TaskFeedbackButtons
messageTs={message.ts}
isFromHistory={
!isLast ||
lastModifiedMessage?.ask === "resume_completed_task" ||
lastModifiedMessage?.ask === "resume_task"
}
style={{
marginLeft: "auto",
}}
/>
</div>
<div
style={{
@@ -1154,17 +1142,6 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
}}>
{icon}
{title}
<TaskFeedbackButtons
messageTs={message.ts}
isFromHistory={
!isLast ||
lastModifiedMessage?.ask === "resume_completed_task" ||
lastModifiedMessage?.ask === "resume_task"
}
style={{
marginLeft: "auto",
}}
/>
</div>
<div
style={{
@@ -1,126 +0,0 @@
import React, { useState, useEffect } from "react"
import styled from "styled-components"
import { vscode } from "../../utils/vscode"
import { TaskFeedbackType } from "../../../../src/shared/WebviewMessage"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
interface TaskFeedbackButtonsProps {
messageTs: number
isFromHistory?: boolean
style?: React.CSSProperties
}
const IconWrapper = styled.span`
color: var(--vscode-descriptionForeground);
`
const ButtonWrapper = styled.div`
transform: scale(0.85);
`
const TaskFeedbackButtons: React.FC<TaskFeedbackButtonsProps> = ({ messageTs, isFromHistory = false, style }) => {
const [feedback, setFeedback] = useState<TaskFeedbackType | null>(null)
const [shouldShow, setShouldShow] = useState<boolean>(true)
// Check localStorage on mount to see if feedback was already given for this message
useEffect(() => {
try {
const feedbackHistory = localStorage.getItem("taskFeedbackHistory") || "{}"
const history = JSON.parse(feedbackHistory)
// Check if this specific message timestamp has received feedback
if (history[messageTs]) {
setShouldShow(false)
}
} catch (e) {
console.error("Error checking feedback history:", e)
}
}, [messageTs])
// Don't show buttons if this is from history or feedback was already given
if (isFromHistory || !shouldShow) {
return null
}
const handleFeedback = (type: TaskFeedbackType) => {
if (feedback !== null) return // Already provided feedback
setFeedback(type)
// Send feedback to extension
vscode.postMessage({
type: "taskFeedback",
feedbackType: type,
})
// Store in localStorage that feedback was provided for this message
try {
const feedbackHistory = localStorage.getItem("taskFeedbackHistory") || "{}"
const history = JSON.parse(feedbackHistory)
history[messageTs] = true
localStorage.setItem("taskFeedbackHistory", JSON.stringify(history))
} catch (e) {
console.error("Error updating feedback history:", e)
}
}
return (
<Container style={style}>
<ButtonsContainer>
<ButtonWrapper>
<VSCodeButton
appearance="icon"
onClick={() => handleFeedback("thumbs_up")}
disabled={feedback !== null}
title="This was helpful"
aria-label="This was helpful">
<IconWrapper>
<span
className={`codicon ${feedback === "thumbs_up" ? "codicon-thumbsup-filled" : "codicon-thumbsup"}`}
/>
</IconWrapper>
</VSCodeButton>
</ButtonWrapper>
<ButtonWrapper>
<VSCodeButton
appearance="icon"
onClick={() => handleFeedback("thumbs_down")}
disabled={feedback !== null && feedback !== "thumbs_down"}
title="This wasn't helpful"
aria-label="This wasn't helpful">
<IconWrapper>
<span
className={`codicon ${feedback === "thumbs_down" ? "codicon-thumbsdown-filled" : "codicon-thumbsdown"}`}
/>
</IconWrapper>
</VSCodeButton>
</ButtonWrapper>
{/* <VSCodeButtonLink
href="https://github.com/cline/cline/issues/new?template=bug_report.yml"
appearance="icon"
title="Report a bug"
aria-label="Report a bug">
<span className="codicon codicon-bug" />
</VSCodeButtonLink> */}
</ButtonsContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
`
const ButtonsContainer = styled.div`
display: flex;
gap: 0px;
opacity: 0.5;
&:hover {
opacity: 1;
}
`
export default TaskFeedbackButtons
@@ -85,7 +85,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
}, [presentableTasks])
const taskHistorySearchResults = useMemo(() => {
const results = searchQuery ? highlight(fuse.search(searchQuery)) : presentableTasks
let results = searchQuery ? highlight(fuse.search(searchQuery)) : presentableTasks
results.sort((a, b) => {
switch (sortOption) {
@@ -733,7 +733,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.geminiApiKey && (
<VSCodeLink
href="https://aistudio.google.com/apikey"
href="https://ai.google.dev/"
style={{
display: "inline",
fontSize: "inherit",
@@ -820,7 +820,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
checked={!!apiConfiguration?.openAiModelInfo?.supportsImages}
onChange={(e: any) => {
const isChecked = e.target.checked === true
const modelInfo = apiConfiguration?.openAiModelInfo
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
@@ -871,7 +871,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.openAiModelInfo
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
@@ -890,7 +890,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.openAiModelInfo
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
@@ -911,7 +911,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.openAiModelInfo
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.inputPrice = input.target.value
@@ -930,7 +930,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.openAiModelInfo
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.outputPrice = input.target.value
@@ -950,7 +950,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
: openAiModelInfoSaneDefaults.temperature?.toString()
}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.openAiModelInfo
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
@@ -66,7 +66,7 @@ const OpenAiModelPicker: React.FC = () => {
}, [searchableItems])
const modelSearchResults = useMemo(() => {
const results: { id: string; html: string }[] = searchTerm
let results: { id: string; html: string }[] = searchTerm
? highlight(fuse.search(searchTerm), "model-item-highlight")
: searchableItems
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched
@@ -87,7 +87,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
}, [searchableItems])
const modelSearchResults = useMemo(() => {
const results: { id: string; html: string }[] = searchTerm
let results: { id: string; html: string }[] = searchTerm
? highlight(fuse.search(searchTerm), "model-item-highlight")
: searchableItems
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched