Compare commits

..

63 Commits

Author SHA1 Message Date
Dennis Bartlett 4a2b97d856 Update workflow controls 2025-03-31 00:08:39 -07:00
Saoud Rizwan c721b34652 chore: remove global constants file 2025-03-30 21:55:55 -07:00
Saoud Rizwan 0eb2762ed2 refactor: move GlobalFileNames to disk module for better organization 2025-03-30 21:55:01 -07:00
Saoud Rizwan b34056ed87 chore: break out disk operations and prompts from Task (#2572)
* Add disk.ts and move out ensureTaskDirectoryExists

* refactor: move disk operations out of Task

* Move prompts out of Task

* Remove legacy tool_use conversion
2025-03-30 21:48:03 -07:00
yusheng chen 5f92b138e8 chore: move @types packages from dependencies to devDependencies (#2547) 2025-03-30 20:20:24 -07:00
yusheng chen 494ffac404 refactor: change unnecessary let to const (#2548)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-30 20:19:39 -07:00
pashpashpash 6e5afda275 Task feedback (#2546)
* task feedback telemetry

* only showing first time

* Update webview-ui/src/components/chat/ChatRow.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* package lock

* lint

* no need to pass messageTs

* using type from webviewmessage

* Fix merge conflicts

* Fix whitespace issue

* Update TaskFeedbackButtons to use VSCodeButton for feedback options

- Replaced custom FeedbackButton component with VSCodeButton for improved consistency with VSCode UI.
- Removed unnecessary animation state and feedback text, simplifying the component structure.
- Added IconWrapper for better styling of feedback icons.
- Adjusted layout and spacing in ButtonsContainer for a cleaner appearance.

* Move feedback icons placement

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-30 20:16:18 -07:00
yusheng chen 04e4f25865 feat: add string.test.ts (#2557)
* feat: add `string.test.ts`

refactor: type improvement of variable `match` in file `src/api/transform/o1-format.ts`

* feat: add more test with function `removeInvalidChars`
2025-03-30 18:55:53 -07:00
Saoud Rizwan 8cd50e7a31 Fix options prompt (#2566) 2025-03-30 18:53:28 -07:00
Saoud Rizwan dd4ac0167c Add instruction to not include option to toggle to Act mode (#2565) 2025-03-30 18:50:29 -07:00
Saoud Rizwan 55f7ce9b20 refactor: Pull WebviewProvider and Controller out of ClineProvider, rename Cline to Task (#2564)
* refactor: Replace ClineProvider with Controller for improved architecture

- Replaced instances of ClineProvider with Controller in extension.ts and related files to enhance code organization and maintainability.
- Introduced a new Controller class to manage interactions previously handled by ClineProvider, streamlining the extension's functionality.
- Updated command registrations and message handling to utilize the new Controller structure, ensuring consistent behavior across the extension.
- Removed the ClineProvider class and its associated methods, consolidating functionality within the Controller class.
- Added new state management and task handling capabilities within the Controller to support the updated architecture.

* clean up

* refactor: Update Task class to use Controller reference

- Replaced all instances of ClineProvider with Controller in the Task class to align with the recent architectural changes.
- Updated references for context management, task history, and message handling to utilize the new Controller structure.
- Ensured consistent behavior across the Task class by adapting to the Controller's methods and properties.

* refactor: Simplify WebviewProvider listeners structure

* Fixes

* Make controller a dependency of webview

* refactor: Improve message listener in WebviewProvider

- Updated the setWebviewMessageListener method to use an arrow function for the message handler, preserving the 'this' context of the controller.
- Added detailed comments explaining the importance of maintaining the correct 'this' context when passing methods as callbacks in JavaScript/TypeScript.

* Add doc

* Add to chat for visible webview

* Fixes
2025-03-30 18:48:34 -07:00
pashpashpash a80022795e adding task id to request headers (#2555)
* adding task id to request headers

* changeset
2025-03-30 05:57:26 -07:00
Adam Jones 669286bb8e fix: Update Google Gemini API key link (#2539)
* fix: Update Google Gemini API key link

Google updated where you can sign up for API keys. This is the new link that will take people closer to where they can generate an API key.

* Add changeset
2025-03-29 19:14:57 -07:00
Evan e1f60ce8af Disable Toggle (#2542)
* support toggle disabled for remote servers

* changeset

* remove unused type

* remove capabilities update calls from toggles; increase timeout
2025-03-29 19:14:02 -07:00
canvrno f948e2bc7b Fixed issue with checkpoint commit hashes (#2543) 2025-03-29 18:51:20 -07:00
Dennis Bartlett 329c269a8c Update Changeset Workflow (#2531)
* Update Changeset Workflow

* Remove RELEASE related content
2025-03-29 15:34:02 -07:00
Saoud Rizwan a9617a1acb Remove docs link (#2530) 2025-03-28 22:54:00 -07:00
Saoud Rizwan 0a691cffbc Remove max width on account view (#2528) 2025-03-28 17:10:08 -07:00
pashpashpash aec21bb299 Capturing telemetry events for write to file failures (#2527)
* added telemetry to track diff edit failures

* changeset
2025-03-28 16:54:43 -07:00
dependabot[bot] 931279c845 Bump tar-fs from 3.0.6 to 3.0.8 in the npm_and_yarn group (#2525)
Bumps the npm_and_yarn group with 1 update: [tar-fs](https://github.com/mafintosh/tar-fs).


Updates `tar-fs` from 3.0.6 to 3.0.8
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.6...v3.0.8)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-28 15:54:29 -07:00
Arri Rucker 841f3da57f feat: update gemini pro 2.0 exp to 2.5 exp in Vertex AI (#2526)
* feat: update gemini pro 2.0 exp to 2.5 exp in vertexai

* added changeset
2025-03-28 15:54:13 -07:00
Benny Yen 96538309db feat(extension): add access to history, mcp, and new task buttons in pop-out view (#2511)
* feat(extension): add access to history, mcp, and new task buttons in popped-out view

- Added new commands to the editor/title section in package.json to ensure buttons are available in the popped-out view.
- Updated command registrations in src/extension.ts to use ClineProvider.getVisibleInstance() for better instance management.

* chore: run changeset
2025-03-28 15:13:31 -07:00
Trevor Hudson 1efd84a6d6 Trevhud/eng 279 (#2505)
* Move restart/delete server and all toggle all

* changeset

* move buttons back to the bottom and show icons even when expanded

* Only display auto-approve all if auto-approve mcp is enabled

* Move auto-approve setting to bottom

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-28 15:04:13 -07:00
Evan 130922fe99 SSE Transport (#2520)
* refactor types and functions

* fix timeout validation type error

* changeset

* connect to SSE MCP servers

* remove old changeset

* changeset
2025-03-28 14:49:15 -07:00
Shixian Sheng 85d0e6b0b5 Update mcp-server-from-github.md (#2522) 2025-03-28 13:31:17 -07:00
pashpashpash dff2d59172 Updated feedback to model when diff edit fails (#2518)
* updated feedback to model when diff edit fails

* changeset

* bias

* typo

* language
2025-03-28 11:43:21 -07:00
Saoud Rizwan 9cb6cf52fc Fix typo 2025-03-28 01:36:45 -07:00
Saoud Rizwan 766e7da0f4 Fix plan mode prompt (#2506) 2025-03-28 01:26:07 -07:00
Saoud Rizwan a3dac16a36 Prepare for release 2025-03-28 00:49:45 -07:00
Saoud Rizwan 185cbe15f4 Remove shutdown call for conversationObservabilityService in ClineProvider 2025-03-28 00:45:33 -07:00
Saoud Rizwan d02ea77ce0 Remove ConversationObservabilityService 2025-03-27 22:34:34 -07:00
pashpashpash af17d6239d re-enabling conversation observability (#2448)
* re-enabling conversation observability

* renamed telemetry - observability

* removing global vs code check + console info instead of logs
2025-03-27 17:40:12 -07:00
akfoster 06739a82e6 Add Coverage Actions to package.json (#2499)
* Add Coverage Reporting

* automate tests via github workflow

* ensure documents are cleaned up at the end of the workflow

* add changeset

* backout github workflow changes

* update changeset to reflect split

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-03-27 17:25:29 -07:00
Saoud Rizwan c6c49ec8ce Rename plan_mode_response to plan_mode_respond (#2498)
* Rename plan_mode_response to plan_mode_respond

* Create good-carrots-tie.md
2025-03-27 16:36:04 -07:00
Evan 8c4b642859 McpHub.ts - Better Zod Types and Function Refactor (#2497)
* refactor types and functions

* fix timeout validation type error

* changeset
2025-03-27 16:11:44 -07:00
mikelmao 0204396b37 Added Sambanova deepseek-v3-0324 (#2495)
* Added Sambanova Deepseek-V3-0324

* Added changeset
2025-03-27 14:06:42 -07:00
Duncan Ogilvie e9b6659a4b Add cost calculation support for LiteLLM provider (#2403)
* Add cost calculation support for LiteLLM provider

* Calculate input/output cost up front once
2025-03-26 19:23:23 -07:00
Saoud Rizwan 7a1e757a2c Prepare for release (#2447) 2025-03-25 20:55:31 -07:00
Felipe Albuquerque ffd80d94df Patch/openai comp max tokens (#2411)
* take into account max_tokens for OpenAI Compatible provider

* change set

* considering default -1 value

* checking maxTokens is set and greater than 0

---------

Co-authored-by: Felipe Albuquerque <felipe.albuquerque@dell.com>
2025-03-25 19:07:26 -07:00
dependabot[bot] 54bb50f195 Bump vite from 6.2.1 to 6.2.3 in /webview-ui in the npm_and_yarn group (#2429)
Bumps the npm_and_yarn group in /webview-ui with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.1 to 6.2.3
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 18:33:31 -07:00
Evan 58a1eae73f SSE Schema and Types (#2441)
* Types and schemas for SSE connections

* changeset
2025-03-25 18:30:29 -07:00
holchan a295ac2456 feat: add gemini 2.5 pro to Google AI Studio avaliable Models (#2435)
* feat: add gemini 2.5 pro to Google AI Studio avaliable Models

* Create cuddly-countries-tap.md

---------

Co-authored-by: Daniel Steigman <35793213+NightTrek@users.noreply.github.com>
2025-03-25 15:59:14 -07:00
Roman Rozhdestvenskiy 1adebe7e13 feat: add isOpenAIR1FormatEnabled option for DeepSeek compatibility (#2408) 2025-03-25 15:30:28 -07:00
Evan df48b976f6 Filter Models ModelSelect (#2437)
* filter out free models

* changeset
2025-03-25 14:57:41 -07:00
watany 616d56468f chore: Allow CI testing locally (#1708)
* doc: Adding install for WebIDE Settings

* adding

* test:dev

* Revert "test:dev"

This reverts commit 91eb6a6510.

* update

* updated

* changeset

* update ci script

* update
2025-03-24 21:57:46 -07:00
Saoud Rizwan efd28b3eeb Revert "ContextManager Tests (#2415)" (#2422)
This reverts commit 19c4439877.
2025-03-24 21:36:43 -07:00
watany 997798fd0a feat(bedrock): adding two regions (#2420)
* feat(bedrock): adding two regions

* changeset
2025-03-24 21:35:24 -07:00
Peter Dave Hello 7247877049 Update and improve zh-tw Traditional Chinese docs (#2384) 2025-03-24 17:10:05 -07:00
Evan 3859d11beb Tasks Galore! (#2256)
* test script to create tasks

* changeset

* fix type error

* make is_dev checks consistent

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-24 17:07:47 -07:00
Evan 19c4439877 ContextManager Tests (#2415)
* add tests around truncating context

* remove comments
2025-03-24 17:00:43 -07:00
Toshii ec01e1f19d move remaining context management out of Cline.ts (#2367)
* move context management out

* changeset
2025-03-24 16:49:11 -07:00
Peter Dave Hello 4a0a40ead1 Add OpenAI "dynamic" model chatgpt-4o-latest (#2385)
This model represents the latest GPT-4o on chatgpt.com, dynamically
updated to always reflect the most current version, bridging the gap
between GPT-4o and GPT-4.5.

Reference:
- https://platform.openai.com/docs/models/chatgpt-4o-latest
- https://x.com/OpenAIDevs/status/1823510395619000525
- https://x.com/lmarena_ai/status/1823515229491515715
2025-03-24 16:20:45 -07:00
Nir 124569c391 feat: add QwQ-32B model to SambaNova provider (#2389)
Signed-off-by: Nir Ben-Or <nir@swirldslabs.com>
Co-authored-by: Nir Ben-Or <nir@swirldslabs.com>
2025-03-24 16:20:05 -07:00
Andrei Edell 5a08f977c3 #2272 fix 'see more' not showing up (#2390)
* #2272 fix 'see more' not showing up

* changeset & format

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-03-24 16:19:36 -07:00
Jille 4ec85af1fb fix: improve file handling for NextJS folder naming conventions and increase limits (#2398)
* fix: improve file handling for NextJS folder naming conventions and increase limits

* Update WorkspaceTracker.ts

* Update list-files.ts

* Update beige-monkeys-prove.md

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-24 16:16:49 -07:00
Peter Dave Hello 00f88050f7 Fix gpt-4.5-preview's supportsPromptCache value to true (#2399)
Reference:
- https://platform.openai.com/docs/models/gpt-4.5-preview
2025-03-24 16:11:56 -07:00
Evan c4f73e7752 Hoist The Sails - Danger Ahead! (#2405)
* Create Tailwind DangerButton

* changeset

* remove unused import

* Remove old DangerButton component

* rename DangerButtonTW to DangerButton
2025-03-24 16:03:39 -07:00
watany e84b8669a6 feat(bedrock): adding Amazon Nova (#2406)
* amazon nova

* changeset
2025-03-24 16:01:21 -07:00
Evan 5b6eae2905 remove delay package (#2414) 2025-03-24 15:57:29 -07:00
Evan c153077bde Handle Anthropic token input too large (#2407)
* correct calculation of quarter truncation

* changeset

* truncate and retry if we encounter context window exceeded error

* optional chaining for error message check

* changeset

* Handle Anthropic input breaks context window error

* changeset

* remove extra changeset

* remove logger

* use optional chaining
2025-03-24 15:56:16 -07:00
Saoud Rizwan 1796558733 Prepare for release 2025-03-24 14:51:59 -07:00
Saoud Rizwan 2f7f8bff20 Fix openrouter model id being set to object when switching plan/act (#2412) 2025-03-24 14:49:37 -07:00
Saoud Rizwan dc9fb4ddb8 Fix vs code lm plan/act toggle (#2409) 2025-03-24 14:03:31 -07:00
85 changed files with 5521 additions and 4101 deletions
-7
View File
@@ -1,7 +0,0 @@
---
"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
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
improving search and replace edit failure behaviors
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Update Google Gemini API key link
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
added telemetry to track replace_in_file tool failures
+5
View File
@@ -0,0 +1,5 @@
---
"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": minor
---
Support connecting to SSE servers
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Move the MCP Restart and Delete buttons and add an auto-approve all toggle
+5
View File
@@ -0,0 +1,5 @@
---
"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": minor
---
adding task id to request headers
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add toggle disabled for remote servers
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
DangerButton.tsx to Tailwind
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
added task feedback thumbs up thumbs down telemetry
+1 -1
View File
@@ -252,7 +252,7 @@ class Cline {
} catch (error) {
// 4. Error handling with retry
if (isOpenRouter && !this.didAutomaticallyRetryFailedApiRequest) {
await delay(1000)
await setTimeoutPromise(1000)
this.didAutomaticallyRetryFailedApiRequest = true
yield* this.attemptApiRequest(previousApiReqIndex)
return
@@ -22,7 +22,6 @@ Environment Variables:
#!/usr/bin/env python3
import os
import sys
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
VERSION = os.environ['VERSION']
@@ -32,72 +31,49 @@ NEW_CONTENT = os.environ.get("NEW_CONTENT", "")
def overwrite_changelog_section(changelog_text: str, new_content: str):
# Find the section for the specified version
version_pattern = f"## {VERSION}\n"
bracketed_version_pattern = f"## [{VERSION}]\n"
unformmatted_prev_version_pattern = f"## {PREV_VERSION}\n"
prev_version_pattern = f"## [{PREV_VERSION}]\n"
print(f"latest version: {VERSION}")
print(f"prev_version: {PREV_VERSION}")
# Try both unbracketed and bracketed version patterns
version_index = changelog_text.find(version_pattern)
if version_index == -1:
version_index = changelog_text.find(bracketed_version_pattern)
if version_index == -1:
# If version not found, add it at the top (after the first line)
first_newline = changelog_text.find('\n')
if first_newline == -1:
# If no newline found, just prepend
return f"## [{VERSION}]\n\n{changelog_text}"
return f"{changelog_text[:first_newline + 1]}## [{VERSION}]\n\n{changelog_text[first_newline + 1:]}"
else:
# Using bracketed version
version_pattern = bracketed_version_pattern
notes_start_index = version_index + len(version_pattern)
notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and prev_version_pattern in changelog_text else len(changelog_text)
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)
if new_content:
return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:]
else:
changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n")
# Ensure we have at least 2 lines before removing them
if len(changeset_lines) < 2:
print("Warning: Changeset content has fewer than 2 lines")
parsed_lines = "\n".join(changeset_lines)
else:
# Remove the first two lines from the regular changeset format, ex: \n### Patch Changes
parsed_lines = "\n".join(changeset_lines[2:])
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)
updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:]
# Ensure version number is bracketed
updated_changelog = updated_changelog.replace(f"## {VERSION}", f"## [{VERSION}]")
return updated_changelog
try:
print(f"Reading changelog from: {CHANGELOG_PATH}")
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
print(f"Changelog content length: {len(changelog_content)} characters")
print("First 200 characters of changelog:")
print(changelog_content[:200])
print("----------------------------------------------------------------------------------")
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
# print("----------------------------------------------------------------------------------")
# print(new_changelog)
# print("----------------------------------------------------------------------------------")
# Write back to CHANGELOG.md
with open(CHANGELOG_PATH, 'w') as f:
f.write(new_changelog)
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)
print(f"{CHANGELOG_PATH} updated successfully!")
+91
View File
@@ -0,0 +1,91 @@
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
@@ -1,117 +0,0 @@
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
});
}
+1
View File
@@ -1,4 +1,5 @@
out
coverage
dist
node_modules
tmp
+23 -2
View File
@@ -1,14 +1,35 @@
# 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
- Add OpenAI "dynamic" model chatgpt-4o-latest
- Add Amazon Nova models to AWS Bedrock
- Improve file handling for NextJS folder naming (fixes issues with parentheses in folder names)
- Add Gemini 2.5 Pro to Google AI Studio available models
- Handle "input too large" errors for Anthropic
- Fix "See more" not showing up for tasks after task un-fold
- Fix gpt-4.5-preview's supportsPromptCache value to true
## [3.8.2]
- Fix bug where switching to plan/act would result in VS Code LM/OpenRouter model being reset
## [3.8.0]
- Add 'Add to Cline' as an option when you right-click in a file or the terminal, making it easier to add context to your current task
- Add 'Fix with Cline' code action - when you see a lightbulb icon in your editor, you can now select 'Fix with Cline' to send the code and associated errors for Cline to fix. (Cursor users can also use the 'Quick Fix (CMD + .)' menu to see this option)
- Add 'Fix with Cline' code action - when you see a lightbulb icon in your editor, you can now select 'Fix with Cline' to send the code and associated errors for Cline to fix. (Cursor users can also use the 'Quick Fix (CMD + .)' menu to see this option)
- Add Account view to display billing and usage history for Cline account users. You can now keep track of credits used and transaction history right in the extension!
- Add 'Sort underling provider routing' setting to Cline/OpenRouter allowing you to sort provider used by throughput, price, latency, or the default (combination of price and uptime)
- Improve rich MCP display with dynamic image loading and support for GIFs
- Add 'Documentation' menu item to easily access Cline's docs
- Add OpenRouter's new usage_details feature for more reliable cost reporting
- Add OpenRouter's new usage_details feature for more reliable cost reporting
- Display total space Cline takes on disk next to 'Delete all Tasks' button in History view
- Fix 'Context Window Exceeded' error for OpenRouter/Cline Accounts (additional support coming soon)
- Fix bug where OpenRouter model ID would be set to invalid value
+30
View File
@@ -31,6 +31,36 @@ If you're planning to work on a bigger feature, please create a [feature request
- Run `npm run test` to run tests locally
- Before submitting PR, run `npm run format:fix` to format your code
3. **Linux-specific Setup**
VS Code extension tests on Linux require the following system libraries:
- `libatk1.0-0`
- `libatk-bridge2.0-0`
- `libxkbfile1`
- `libx11-xcb1`
- `libxcomposite1`
- `libxdamage1`
- `libxfixes3`
- `libxrandr2`
- `libgbm1`
- `libdrm2`
- `libgtk-3-0`
- `dbus`
- `xvfb`
These libraries provide necessary GUI components and system services for the test environment.
For example, on Debian-based distributions (e.g., Ubuntu), you can install these libraries using apt:
```bash
sudo apt update
sudo apt install -y \
libatk1.0-0 libatk-bridge2.0-0 libxkbfile1 libx11-xcb1 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 \
libdrm2 libgtk-3-0 dbus xvfb
```
- Run `npm run test:ci` to run tests locally
## Writing and Submitting Code
Anyone can contribute code to Cline, but we ask that you follow these guidelines to ensure your contributions can be smoothly integrated:
+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 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-search 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,6 +72,14 @@ 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",
+25 -23
View File
@@ -2,46 +2,48 @@
## 我們的承諾
為了促進一個開放和歡迎的環境,我們為貢獻者維護者承諾,使我們的項目和社區的參與對每個人來說都是一個無騷擾的體驗,不論年齡、體型、殘疾、種族、性別特徵、性別認同和表達、經驗水平、教育程度、社會經濟地位、國籍、個人外、種族、宗教或性向。
為了營造開放且友善的環境,我們為貢獻者維護者承諾讓參與本專案及社群的體驗,對每個人都不帶有騷擾,不論年齡、體型、身心障礙、族裔、性徵、性別認同與表現、經驗程度、教育程度、社地位、國籍、個人外、種族、宗教信仰、或性向。
## 我們的
## 我們的準
有助於創造積極環境的行為示例包括:
有助於創造正面環境的行為包括:
- 使用歡迎和包容的語言
- 尊重不同的觀點經驗
- 優雅地接受建設性批評
- 專注於對社最有利的事情
- 對其他社成員表示同情
- 使用友善和包容的語言
- 尊重不同的觀點經驗
- 優雅地接受建設性批評
- 著重於對社最有利的事情
- 對其他社成員展現同理心
參與者不可接受的行為示例包括:
參與者不可接受的行為包括:
- 使用性化語言或圖像以及不受歡迎的性注意或挑逗
- 騷擾、侮辱/貶低性評論和個人或政治攻擊
- 公開或私下騷擾
- 未經明確許可發布他人的私人信息,例如物理或電子地址
- 其他在專業環境中合理認為不當的行為
- 使用帶有性暗示的言語或影像,以及不受歡迎的性關注或騷擾
- 挑釁、羞辱/貶低他人的評論,以及人身或政治攻擊
- 公開或私下騷擾行為
- 未經他人明確許可,公開他人的私人資料,如實體或電子郵件地址
- 其他在專業環境中可被合理認為不當的行為
## 我們的責任
項目維護者有責任清可接受行為的標準,並預期對任何不可接受行為的實例採取適當公平的糾正行動
專案維護者有責任清可接受行為的標準,並對任何不可接受行為採取適當公平的糾正措施
項目維護者有權利和責任除、編輯或拒絕本行為準則不符的評論、提交、碼、維基編輯、題和其他貢獻,或暫時或永久禁止任何他們認為不當、威脅、冒犯或有害的貢獻者。
專案維護者有權利和責任除、編輯或拒絕不符合本行為準則的評論、提交、程式碼、維基編輯、題和其他貢獻,或暫時或永久封鎖任何他們認為不當、威脅、冒犯或有害行為的貢獻者。
## 範
## 範
行為準則適用於項目空間內以及當個人代表項目或其社區時的公共空間。代表項目或社區的示例包括使用官方項目電子郵件地址、過官方社媒體帳戶發布或作為在線或離線活動的指定代表。項目的代表可能由項目維護者進一步定義和澄清
行為準則適用於專案空間及公開場合,當個人代表本專案或其社群時都必須遵守。代表本專案或社群的情況包括使用官方專案電子郵件地址、過官方社媒體帳號發文,或在線上或實體活動中擔任指定代表。專案維護者進一步定義並釐清專案代表的其他情況
## 執行
濫用、騷擾或其他不可接受行為的實例可以通過聯繫項目團隊 hi@cline.bot 來報告。所有投訴將被審查和調查,並將根據情況作出必要和適當的回應。項目團隊有義務事件的報告者保密。具體執行政策的詳細信息可能會單獨發布
如發生辱罵、騷擾或其他不可接受行為,請透過 hi@cline.bot 聯絡專案團隊回報。所有申訴都將被審查和調查,並做出必要且合適的回應。專案團隊有義務事件回報者保密。具體執行政策的更多細節可能另行公佈
能善意遵循或執行行為準則的項目維護者可能會面臨由項目領導層其他成員決定的暫時或永久後果
遵守或未切實執行行為準則的專案維護者可能會面臨由專案領導團隊其他成員決定的暫時或永久的處置
## 歸屬
## 來源說明
行為準則改編自 [Contributor Covenant][homepage],版本 1.4,可在 https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 獲得。
行為準則改編自[貢獻者公約][homepage] 1.4,可在此查閱:
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
有關此行為準則的常見問題的答案,請參見 https://www.contributor-covenant.org/faq
關於本行為準則的常見問題解答,請參考:
https://www.contributor-covenant.org/faq
+57 -54
View File
@@ -1,82 +1,85 @@
# 貢獻 Cline
# 貢獻 Cline
我們很高興您有興趣為 Cline 做出貢獻。無論是修復錯誤、添加功能還是改進我們的文檔,每一貢獻都讓 Cline 更加智能!為了保持我們的社區充滿活力和歡迎,所有成員必須遵守我們的[行為準則](CODE_OF_CONDUCT.md)。
我們非常感謝您有意願貢獻至 Cline。無論是修正程式錯誤、新增功能或改善文件,每一貢獻都讓 Cline 更加出色!為了維持社群的活力與友善,所有成員必須遵守我們的[行為準則](CODE_OF_CONDUCT.md)。
## 報告錯誤或問題
## 回報程式錯誤或問題
錯誤報告有助於讓 Cline 對每個人都更好!在創建新問題之前,請[現有](https://github.com/cline/cline/issues)避免重複。當您準備報告錯誤時,請前往我們的[題頁面](https://github.com/cline/cline/issues/new/choose),您會找到一個模板來幫助您填寫相關信息
程式錯誤回報能幫助 Cline 變得更好!在建立新的議題之前,請[現有](https://github.com/cline/cline/issues)避免重複。當您準備好回報程式錯誤時,請前往我們的[題頁面](https://github.com/cline/cline/issues/new/choose),您會找到協助填寫相關資訊的範本
<blockquote class='warning-note'>
🔐 <b>重要:</b> 如果您發現安全漏洞,請使用<a href="https://github.com/cline/cline/security/advisories/new">Github 安全工具私下報告</a>。
🔐 <b>重要:</b> 您發現安全漏洞,請使用 <a href="https://github.com/cline/cline/security/advisories/new">GitHub 安全工具進行私密回報</a>。
</blockquote>
## 決定要做什麼
## 決定要處理的工作
尋找一個好的首次貢獻?查看標有["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue)或["help wanted"](https://github.com/cline/cline/labels/help%20wanted)的題。這些是專門為新貢獻者我們希望得到幫助的領域策劃的
想找適合第一次貢獻的工作嗎?請檢視標示為[good first issue](https://github.com/cline/cline/labels/good%20first%20issue)或[help wanted](https://github.com/cline/cline/labels/help%20wanted)的題。這些議題特別適合新手貢獻者我們也非常歡迎您的協助
我們也歡迎對我們[](https://github.com/cline/cline/tree/main/docs)的貢獻!無論是修正錯字、改現有指南還是創建新的教內容 - 我們希望建立一個由社區驅動的資源庫,助每個人充分用 Cline。您可以從深入研究 `/docs` 尋找需要改進的領域開始
我們也歡迎對[](https://github.com/cline/cline/tree/main/docs)的貢獻!無論是修正錯字、改現有指南或建立新的教內容,我們都期待能建立一個由社群共同維護的知識庫,助每個人充分用 Cline。您可以從 `/docs` 開始,尋找需要改善的地方
如果您計劃開發一個更大的功能,請先建一個[功能請求](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop),以便我們討論是否符合 Cline 的願景。
若您計畫處理較大的功能,請先建一個[功能請求](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop),以便我們討論該功能是否符合 Cline 的願景。
## 開發設置
## 開發環境設定
1. **VS Code 擴**
1. **VS Code 擴充套件**
- 開啟專案時,VS Code 會提示您安裝建議的擴充套件
- 這些擴充套件是開發所需,請接受所有安裝提示
- 若您已關閉提示,可從擴充套件面板手動安裝
- 打開項目時,VS Code 會提示您安裝推薦的擴展
- 這些擴展是開發所需的 - 請接受所有安裝提示
- 如果您忽略了提示,可以從擴展面板手動安裝它們
2. **本機開發**
- 執行 `npm run install:all` 安裝相依套件
- 執行 `npm run test` 在本機執行測試
- 提交 PR 前,執行 `npm run format:fix` 格式化您的程式碼
2. **本地開發**
- 運行 `npm run install:all` 安裝依賴項
- 運行 `npm run test` 本地運行測試
- 提交 PR 之前,運行 `npm run format:fix` 格式化您的代碼
## 撰寫與提交程式碼
## 編寫和提交代碼
任何人都可以貢獻程式碼至 Cline,但我們要求您遵守以下指引,以確保您的貢獻能順利整合:
任何人都可以為 Cline 貢獻代碼,但我們要求您遵循以下指南,以確保您的貢獻能夠順利集成:
1. **保持 Pull Request 聚焦**
- 每個 PR 限制在單一功能或錯誤修正
- 將較大的變更拆分成較小且相關的 PR
- 將變更拆分成邏輯性的提交,以便獨立審查
1. **保持 Pull Requests 集中**
- 將 PR 限制在單個功能或錯誤修復
- 將較大的更改拆分為較小的相關 PR
- 將更改分為邏輯提交,可以獨立審查
2. **代碼質量**
- 運行 `npm run lint` 檢查代碼風格
- 運行 `npm run format` 自動格式化代碼
- 所有 PR 必須通過包括 lint 和格式化在內的 CI 檢查
2. **程式碼品質**
- 執行 `npm run lint` 檢查程式碼風格
- 執行 `npm run format` 自動格式化程式碼
- 所有 PR 必須通過包含程式碼風格檢查與格式化的 CI 檢查
- 提交前解決所有 ESLint 警告或錯誤
- 遵循 TypeScript 最佳實踐並保持類型安全
- 遵循 TypeScript 最佳實務並維持型別安全
3. **測試**
- 為新功能新增測試
- 執行 `npm test` 確保所有測試通過
- 若您的變更影響現有測試,請更新測試
- 適當時包含單元測試與整合測試
- 為新功能添加測試
- 運行 `npm test` 確保所有測試通過
- 如果您的更改影響現有測試,請更新它們
- 在適當的地方包括單元測試和集成測試
4. **使用 Changesets 管理版本**
- 使用 `npm run changeset` 為任何面向使用者的變更建立 changeset
- 選擇適當的版本升級:
- `major` 重大變更 (1.0.0 → 2.0.0)
- `minor` 新功能 (1.0.0 → 1.1.0)
- `patch` 錯誤修正 (1.0.0 → 1.0.1)
- 撰寫清晰且描述性的 changeset 訊息,說明影響
- 僅文件變更不需建立 changeset
4. **提交指**
5. **提交指**
- 撰寫清晰且描述性的提交訊息
- 使用慣用提交格式(例如:「feat:」、「fix:」、「docs:」)
- 在提交中引用相關議題,使用 #issue-number
- 撰寫清晰、描述性的提交消息
- 使用常規提交格式(例如 "feat:"、"fix:"、"docs:"
- 在提交中引用相關問題,使用 #issue-number
6. **提交前檢查**
- 將您的分支 rebase 到最新的 main
- 確保您的分支可以成功建置
- 再次確認所有測試通過
- 檢查您的變更是否包含除錯程式碼或 console 紀錄
5. **提交前**
- 將您的分支重新基於最新的 main
- 確保您的分支成功構建
- 仔細檢查所有測試是否通過
- 檢查您的更改是否有任何調試代碼或控制台日誌
6. **Pull Request 描述**
- 清楚地描述您的更改內容
- 包括測試更改的步驟
- 列出任何重大更改
- 為 UI 更改添加截圖
7. **Pull Request 說明**
- 清楚描述您的變更內容
- 包含測試變更的步驟
- 列出任何重大變更
- 若有使用者介面變更,請附上截圖
## 貢獻協議
通過提交 pull request您同意您的貢獻將根據與項目相同的許可證[Apache 2.0](LICENSE))進行許可
提交 Pull Request 即表示您同意您的貢獻將依照專案相同的授權條款[Apache 2.0](LICENSE))進行授權
記住:貢獻 Cline 不僅僅是編寫代碼 - 這是關於成為一個塑造 AI 輔助開發未來的社區的一部分。讓我們一起創造一些驚人的東西!🚀
記住:貢獻 Cline 不只是撰寫程式碼,更是成為塑造 AI 輔助開發未來的社群一份子。讓我們一起打造令人驚艷的成果吧!🚀
+97 -71
View File
@@ -1,4 +1,8 @@
# Cline OpenRouter 上的 \#1
<div align="center"><sub>
<a href="https://github.com/cline/cline/blob/main/README.md" target="_blank">English</a> | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | 繁體中文 | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
# Cline OpenRouter 第一名的 AI 工具
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
@@ -8,7 +12,7 @@
<table>
<tbody>
<td align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong> VS Marketplace 下載</strong></a>
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong> VS Marketplace 下載</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
@@ -17,29 +21,29 @@
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
</td>
<td align="center">
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>功能請求</strong></a>
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>功能建議</strong></a>
</td>
<td align="center">
<a href="https://cline.bot/join-us" target="_blank"><strong>我們正在招聘!</strong></a>
<a href="https://docs.cline.bot/getting-started/getting-started-new-coders" target="_blank"><strong>新手上路</strong></a>
</td>
</tbody>
</table>
</div>
認識 Cline,一個可以使用你的 **CLI****編輯器** 的 AI 助
認識 Cline,一個可以使用您的**命令列介面** (CLI) 和**程式編輯器** (Editor) 的 AI 助
感謝 [Claude 3.7 Sonnet 的代理編碼能力](https://www.anthropic.com/claude/sonnet)Cline 可以一步步處理複雜的軟開發任務。通過允許他創建和編輯文件、探索大型項目、使用瀏覽器執行終端令(在你授予權限後),他可以提供超越代碼完成或技術支持的幫助。Cline 甚至可以使用 Model Context Protocol (MCP) 創建新工具並擴展自己的能。雖然自主 AI 腳本傳統上在沙環境中行,但此擴展提供了一個人機交互的 GUI 來批准每個文件更改和終端令,提供了一種安全且可訪問的方式來探索代理 AI 的潛力。
感謝 [Claude 3.7 Sonnet 的代理式程式設計能力](https://www.anthropic.com/claude/sonnet)Cline 能夠逐步處理複雜的軟開發任務。透過能讓他建立和編輯檔案、探索大型專案、使用瀏覽器,以及執行終端機指令(在您授權後)的工具,他能以超越程式碼自動完成或技術支援的方式協助您。Cline 甚至能使用模型上下文協定(Model Context ProtocolMCP)來建立新工具並擴展自己的能。雖然自主 AI 腳本傳統上在沙環境中行,但這個擴充套件提供了人機互動的圖形介面,讓您可以核准每個檔案變更和終端機指令,提供一個安全且容易使用的方式來探索代理 AI 的潛力。
1. 輸入的任務並添加圖像,將模型轉換功能應用程序或通過截圖修復錯誤。
2. Cline 先分析你的文件結構和源代碼 AST,運行正表達式搜,並閱讀相關文件以了解現有項目。通過仔細管理添加到上下文中的信息,Cline 即使在大型複雜項目中也能提供有價值的幫助,而不會使上下文窗口過載
3. 一旦 Cline 得所需信息,他可以:
- 建和編輯文件 + 監控 linter/編譯器錯誤,從而主動修復諸如缺少導入和語法錯誤等問題。
- 直接在的終端中執行令並監控其輸出,從而在編輯文件後對開發服務器問題做出反應
- 對於 Web 開發任務,Cline 可以在無頭瀏覽器中啟動網站,點擊、輸入、動並捕獲截圖和控制台日誌,從而修復運行時錯誤和視覺錯誤
4. 當任務完成時,Cline 將通過終端命令`open -a "Google Chrome" index.html` 向你展示結果,你可以通過點擊按鈕運行該命令
1. 輸入的任務,並可以加入圖片來將設計稿轉換功能應用程式,或使用截圖來修正錯誤。
2. Cline 先分析您的檔案結構和程式碼 AST、執行正表達式搜,並讀取相關檔案,以便在現有專案中快速掌握狀況。透過仔細管理加入上下文的資訊,Cline 可以在不超過上下文視窗的情況下,為大型複雜的專案提供有價值的協助
3. 一旦 Cline 得所需資訊後,他可以:
-和編輯檔案,並在過程中監控程式碼檢查工具/編譯器錯誤,讓他能主動修正缺少的匯入語句和語法錯誤等問題。
- 直接在的終端中執行令並監控其輸出,讓他能夠在編輯檔案後回應開發伺服器的問題
- 對於網頁開發任務,Cline 可以在無頭瀏覽器中啟動網站、點選、輸入、動並擷取螢幕截圖和主控台記錄,讓他能修正執行時錯誤和視覺問題
4. 當任務完成時,Cline 會以終端機指令(`open -a "Google Chrome" index.html`)向您呈現結果,您只需點選按鈕即可執行
> [!提示]
> 使用 `CMD/CTRL + Shift + P` 快捷鍵打開命令面板並輸入 "Cline: Open In New Tab" 將擴展作為標籤在編輯器中打開。這讓可以與文件資源管理器並排使用 Cline,更清楚地看到他如何改變你的工作空間
> [!TIP]
> 使用 `CMD/CTRL + Shift + P` 快速鍵開啟命令選擇區,輸入「Cline: Open In New Tab」即可在編輯器中以分頁方式開啟擴充套件。這讓可以同時檢視檔案總管,並更清楚地看到 Cline 如何變更您的工作
---
@@ -47,115 +51,137 @@
### 使用任何 API 和模型
Cline 支 OpenRouter、Anthropic、OpenAI、Google Gemini、AWS Bedrock、Azure 和 GCP Vertex 等 API 提供商。你還可以配置任何兼容 OpenAI 的 API,或過 LM Studio/Ollama 使用本模型。如果你使用 OpenRouter擴展會獲取他們最新模型列表,讓在新模型可用時立即使用。
Cline 支 OpenRouter、Anthropic、OpenAI、Google Gemini、AWS Bedrock、Azure 和 GCP Vertex 等 API 提供者。您也可以設定任何與 OpenAI 相容的 API,或過 LM Studio/Ollama 使用本模型。若您使用 OpenRouter此擴充套件會擷取他們最新模型列表,讓您能在新模型推出時立即使用。
擴展還會跟蹤整個任務循環和單個請求的總令牌和 API 使用成本,讓你在每一步都了解支出情況
<!-- 透明像素以在浮動圖像後創建換行 -->
此擴充套件也會追蹤整個任務迴圈和個別請求的 token 總數和 API 使用成本,讓您隨時掌握費用支出
<!-- 透明像素用於浮動圖片後的換行 -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
### 在終端中運行命
### 在終端機中執行指
感謝 VSCode v1.93 中的新 [終端 shell 集成更新](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api)Cline 可以直接在的終端中執行令並接收輸出。這使他能執行廣泛的任務,從安裝包和運行構建腳本到部署應用程、管理數據庫和執行測試,同時適應的開發環境和工具鏈以正確完成工作。
感謝 [VSCode v1.93 的終端機整合更新](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api)Cline 可以直接在的終端中執行令並接收輸出。這他能執行各種任務,從安裝套件和執行建置腳本到部署應用程、管理資料庫和執行測試,同時適應的開發環境和工具鏈以正確完成工作。
對於長時間行的進程如開發服務器,使用“在運行時繼續”按鈕讓 Cline 在命令後台運行時繼續任務。當 Cline 工作時,他會在過程中收到任何新的終端輸出通知,讓他可能出現的問題做出反應,例如編輯文件時的編譯錯誤。
<!-- 透明像素以在浮動圖像後創建換行 -->
對於開發伺服器等長時間行的程序,使用「繼續執行中的程序」按鈕讓 Cline 在指令於背景執行時繼續任務。當 Cline 工作時,他會收到任何新的終端輸出通知,讓他能回應可能出現的問題,例如編輯檔案時的編譯錯誤。
<!-- 透明像素用於浮動圖片後的換行 -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
### 建和編輯文件
### 建和編輯檔案
Cline 可以直接在的編輯器中建和編輯文件,向你展示更改的差異視圖。你可以直接在差異視編輯器中編輯或恢復 Cline 的更,或在聊天中提供饋,直到你對結果滿意。Cline 會監控 linter/編譯器錯誤(缺少導入、語法錯誤等),以便他在過程中自行修復出現的問題。
Cline 可以直接在的編輯器中建和編輯檔案,並顯示變更的差異檢視。您可以直接在差異視編輯器中編輯或還原 Cline 的更,或在聊天中提供意見回饋,直到您滿意結果為止。Cline 會監控程式碼檢查工具/編譯器錯誤(缺少的匯入語句、語法錯誤等),讓他能自行修正過程中出現的問題。
Cline 做的所有更改都會記錄在你的文件時間軸中,提供了一種簡單的方法來跟蹤和恢復修改(如果需要)
<!-- 透明像素以在浮動圖像後創建換行 -->
所有 Cline 做的變更都會記錄在您檔案的時間軸中,提供簡單的方式來追蹤和還原修改
<!-- 透明像素用於浮動圖片後的換行 -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
### 使用瀏覽器
借助 Claude 3.5 Sonnet 的新 [計算機使用](https://www.anthropic.com/news/3-5-models-and-computer-use) 功能,Cline 可以啟動瀏覽器,點擊元素輸入文本和滾動,在每一步捕獲截圖和控制台日誌。這允許進行交互式調試、端端測試,甚至一般網頁使用!這使他能夠自主修復視覺錯誤和運行時問題,而無需你親自操作和複製粘貼錯誤日誌
透過 Claude 3.5 Sonnet 的新[電腦使用](https://www.anthropic.com/news/3-5-models-and-computer-use)功能,Cline 可以啟動瀏覽器、點選元素輸入文字和捲動,在每個步驟擷取螢幕截圖和主控台記錄。這讓互動式除錯、端端測試,甚至一般網頁使用成為可能!這他能獨立修正視覺問題和執行時錯誤,而不需要您手動複製錯誤記錄
試讓 Cline 測試應用程序”,看看他如何`npm run dev` 命令,在瀏覽器中啟動你本地運行的開發服務器,並執行一系列測試確認一切正常。[在這裡查看演示。](https://x.com/sdrzn/status/1850880547825823989)
<!-- 透明像素以在浮動圖像後創建換行 -->
著請 Cline 測試應用程式」,觀察他如何`npm run dev`在瀏覽器中啟動您的本機開發伺服器,並執行一系列測試確認一切正常運作。[點此觀看示範](https://x.com/sdrzn/status/1850880547825823989)
<!-- 透明像素用於浮動圖片後的換行 -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
### “添加一個工具……”
### 「新增一個工具來...」
感謝 [Model Context Protocol](https://github.com/modelcontextprotocol)Cline 可以過自定義工具擴展他的能。雖然可以使用 [製作的服](https://github.com/modelcontextprotocol/servers),但 Cline 可以創建和安裝適合你特定工作流程的工具。只需讓 Cline “添加一個工具,他處理所有事情,從建新的 MCP 服器到將其安裝到擴中。這些自定義工具將成為 Cline 工具的一部分,準備在未來的任務中使用
感謝[模型上下文協定](https://github.com/modelcontextprotocol)Cline 可以過自工具擴展他的能。雖然可以使用[製作的服器](https://github.com/modelcontextprotocol/servers),但 Cline 可以改為建立專門為您的工作流程量身打造的工具。只要請 Cline 「新增工具,他就會處理所有事情,從建新的 MCP 服器到將其安裝到擴充套件中。這些自訂工具就會成為 Cline 工具的一部分,隨時可用於未來的任務。
- “添加一個取 Jira 工單的工具”:檢索工單 AC 並讓 Cline 開始工作
- “添加一個管理 AWS EC2 的工具:檢查服器指標並上下擴展實例
- “添加一個取最新 PagerDuty 事件的工具”:獲取詳細信息並讓 Cline 修復錯誤
<!-- 透明像素以在浮動圖像後創建換行 -->
- 「新增一個取 Jira 工單的工具」:取得工單驗收條件並讓 Cline 開始工作
- 「新增一個管理 AWS EC2 的工具:檢查服器指標並調整執行個體規模
- 「新增一個取最新 PagerDuty 事件的工具」:取得詳細資訊並請 Cline 修復錯誤
<!-- 透明像素用於浮動圖片後的換行 -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
### 添加上下文
### 新增上下文
**`@url`** 粘貼一個 URL 以供擴展獲取並轉換為 markdown,當想給 Cline 提供最新文檔時非常有用
**`@problems`:** 添加工作區錯誤和警告(“問題”面板)以供 Cline 修復
**`@file`:** 添加文件內容,這樣你就不必浪費 API 請求批准讀取文件(+ 輸入以搜索文件)
**`@folder`:** 一次添加文件夾的文件,以進一步加快你的工作流程
<!-- 透明像素以在浮動圖像後創建換行 -->
**`@url`**貼上網址讓擴充套件擷取並轉換為 Markdown,當想給 Cline 最新文件時很有用
**`@problems`:**新增工作區的錯誤和警告(「問題」面板)給 Cline 修正
**`@file`:**新增檔案內容,讓您不必浪費 API 請求來核准讀取檔案(+ 輸入以搜尋檔案)
**`@folder`:**一次新增整個資料夾的檔案,讓您的工作流程更快速
<!-- 透明像素用於浮動圖片後的換行 -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
### 檢查點:比較和恢復
### 檢查點:比較和還原
當 Cline 完成任務時,擴展會在每一步拍攝你的工作區快照。可以使用比較按鈕查看快照和當前工作區之間的差異,並使用“恢復”按鈕回到該點。
當 Cline 處理任務時,擴充套件會在每個步驟擷取您工作區快照。可以使用比較按鈕檢視快照與目前工作區的差異,並使用「還原」按鈕回到該時間點。
例如,使用本地 Web 服務器時,可以使用“僅恢復工作區快速測試應用程的不同版本,然後在找到要繼續構建的版本時使用“恢復任務和工作區。這讓你可以安全地探索不同方法而不會失進度。
<!-- 透明像素以在浮動圖像後創建換行 -->
例如,使用本機網頁伺服器時,可以使用「僅還原工作區」來快速測試應用程的不同版本,然後在找到要繼續開發的版本時使用「還原任務和工作區。這讓您能安全地探索不同方法而不會失進度。
<!-- 透明像素用於浮動圖片後的換行 -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## 貢獻
要為項目做出貢獻,請我們的 [貢獻指南](CONTRIBUTING.md) 開始,了解基礎知識。你還可以加入我們的 [Discord](https://discord.gg/cline) `#contributors` 頻道與其他貢獻者聊天。如果你正在尋找全職工作,請查看我們在 [招聘頁面](https://cline.bot/join-us) 上的開放職位
要為專案貢獻,請先閱讀我們的[貢獻指南](CONTRIBUTING.md)了解基礎知識。您也可以加入我們的 [Discord](https://discord.gg/cline)`#contributors` 頻道與其他貢獻者交流。如果在尋找全職工作,請檢視我們[職涯頁面](https://cline.bot/join-us)上的職缺
<details>
<summary>本開發說明</summary>
<summary>本開發說明</summary>
1. 克隆倉庫 _(需要 [git-lfs](https://git-lfs.com/))_
```bash
git clone https://github.com/cline/cline.git
```
2. 在 VSCode 中打開項目:
```bash
code cline
```
3. 安裝擴展和 webview-gui 的必要依賴:
```bash
npm run install:all
```
4. 按 `F5`(或 `運行`->`開始調試`)啟動以打開一個加載了擴展的新 VSCode 窗口。(如果你在構建項目時遇到問題,可能需要安裝 [esbuild problem matchers 擴展](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers)
1. 複製程式碼庫(需要 [git-lfs](https://git-lfs.com/)
```bash
git clone https://github.com/cline/cline.git
```
2. 在 VSCode 中開啟專案:
```bash
code cline
```
3. 安裝擴充套件和網頁介面所需的相依套件:
```bash
npm run install:all
```
4. 按下 `F5`(或選擇「執行」->「開始除錯」)來啟動並開啟一個已載入擴充套件的新 VSCode 視窗。(如果建置專案時遇到問題,您可能需要安裝 [esbuild problem matchers 擴充套件](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers)
</details>
## 許可證
<details>
<summary>建立 Pull Request</summary>
1. 在建立 PR 前,產生一個 changeset 項目:
```bash
npm run changeset
```
這會提示您填寫:
- 變更類型(major、minor、patch
- `major` → 重大變更(1.0.0 → 2.0.0
- `minor` → 新功能(1.0.0 → 1.1.0
- `patch` → 錯誤修正(1.0.0 → 1.0.1
- 您的變更說明
2. 提交您的變更和產生的 `.changeset` 檔案
3. 推送您的分支並在 GitHub 上建立 PR。我們的 CI 會:
- 執行測試和檢查
- Changesetbot 會建立一個顯示版本影響的評論
- 當合併到 main 時,changesetbot 會建立一個 Version Packages PR
- 當 Version Packages PR 合併時,就會發布新版本
</details>
## 授權條款
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+67 -56
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.8.0",
"version": "3.8.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.8.0",
"version": "3.8.4",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
@@ -23,17 +23,12 @@
"@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",
"chokidar": "^4.0.1",
"clone-deep": "^4.0.1",
"default-shell": "^2.2.0",
"delay": "^6.0.0",
"diff": "^5.2.0",
"execa": "^9.5.2",
"fast-deep-equal": "^3.1.3",
@@ -65,11 +60,15 @@
"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",
@@ -8582,7 +8581,9 @@
"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=="
"integrity": "sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
@@ -8601,6 +8602,7 @@
"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": "*"
@@ -8643,6 +8645,7 @@
"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": {
@@ -8673,6 +8676,7 @@
"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": {
@@ -9341,50 +9345,75 @@
"license": "MIT"
},
"node_modules/bare-events": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz",
"integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==",
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
"license": "Apache-2.0",
"optional": true
},
"node_modules/bare-fs": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
"integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz",
"integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-events": "^2.0.0",
"bare-path": "^2.0.0",
"bare-stream": "^2.0.0"
"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
}
}
},
"node_modules/bare-os": {
"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==",
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
"license": "Apache-2.0",
"optional": true
"optional": true,
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
"integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-os": "^2.1.0"
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz",
"integrity": "sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==",
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"b4a": "^1.6.6",
"streamx": "^2.20.0"
"streamx": "^2.21.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/base64-js": {
@@ -10360,17 +10389,6 @@
"node": ">= 14"
}
},
"node_modules/delay": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/delay/-/delay-6.0.0.tgz",
"integrity": "sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -15183,12 +15201,6 @@
],
"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",
@@ -16125,13 +16137,12 @@
}
},
"node_modules/streamx": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz",
"integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==",
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
"license": "MIT",
"dependencies": {
"fast-fifo": "^1.3.2",
"queue-tick": "^1.0.1",
"text-decoder": "^1.1.0"
},
"optionalDependencies": {
@@ -16394,17 +16405,17 @@
}
},
"node_modules/tar-fs": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
"integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^2.1.1",
"bare-path": "^2.1.0"
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
}
},
"node_modules/tar-stream": {
+42 -13
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.0",
"version": "3.8.4",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -97,9 +97,10 @@
"category": "Cline"
},
{
"command": "cline.openDocumentation",
"title": "Documentation",
"icon": "$(book)"
"command": "cline.dev.createTestTasks",
"title": "Create Test Tasks",
"category": "Cline",
"when": "cline.isDevMode"
},
{
"command": "cline.addToChat",
@@ -140,19 +141,46 @@
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.openDocumentation",
"command": "cline.accountButtonClicked",
"group": "navigation@5",
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.accountButtonClicked",
"command": "cline.settingsButtonClicked",
"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@7",
"when": "view == claude-dev.SidebarProvider"
"group": "navigation@6",
"when": "activeWebviewPanelId == claude-dev.TabPanelProvider"
}
],
"editor/context": [
@@ -274,6 +302,8 @@
"format": "prettier . --check",
"format:fix": "prettier . --write",
"test": "vscode-test",
"test:ci": "node scripts/test-ci.js",
"test:coverage": "vscode-test --coverage",
"install:all": "npm install && cd webview-ui && npm install",
"dev:webview": "cd webview-ui && npm run dev",
"build:webview": "cd webview-ui && npm run build",
@@ -287,11 +317,15 @@
"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",
@@ -322,17 +356,12 @@
"@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",
"chokidar": "^4.0.1",
"clone-deep": "^4.0.1",
"default-shell": "^2.2.0",
"delay": "^6.0.0",
"diff": "^5.2.0",
"execa": "^9.5.2",
"fast-deep-equal": "^3.1.3",
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env node
const { execSync } = require("child_process")
const process = require("process")
try {
if (process.platform === "linux") {
console.log("Detected Linux environment.")
execSync("which xvfb-run", { stdio: "ignore" })
console.log("xvfb-run is installed. Running tests with xvfb-run...")
execSync("xvfb-run -a npm run test", { stdio: "inherit" })
} else {
console.log("Non-Linux environment detected. Running tests normally.")
execSync("npm run test", { stdio: "inherit" })
}
} catch (error) {
if (process.platform === "linux") {
console.error(
`Error: xvfb-run is not installed.\n` +
`Please install it using the following command:\n` +
` Debian/Ubuntu: sudo apt install xvfb\n` +
` RHEL/CentOS: sudo yum install xvfb\n` +
` Arch Linux: sudo pacman -S xvfb`,
)
} else {
console.error("Error running tests:", error.message)
}
process.exit(1)
}
+1 -1
View File
@@ -23,7 +23,7 @@ export class AnthropicHandler implements ApiHandler {
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
const modelId = model.id
let budget_tokens = this.options.thinkingBudgetTokens || 0
const budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
switch (modelId) {
+203 -4
View File
@@ -7,7 +7,12 @@ import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels
import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } from "@aws-sdk/client-bedrock-runtime"
import {
BedrockRuntimeClient,
ConversationRole,
ConverseStreamCommand,
InvokeModelWithResponseStreamCommand,
} from "@aws-sdk/client-bedrock-runtime"
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
@@ -20,16 +25,22 @@ 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
let modelId = await this.getModelId()
const modelId = await this.getModelId()
const model = this.getModel()
// Check if this is an Amazon Nova model
if (modelId.includes("amazon.nova")) {
yield* this.createNovaMessage(systemPrompt, messages, modelId, model)
return
}
// Check if this is a Deepseek model
if (modelId.includes("deepseek")) {
yield* this.createDeepseekMessage(systemPrompt, messages, modelId, model)
return
}
let budget_tokens = this.options.thinkingBudgetTokens || 0
const 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
@@ -239,7 +250,7 @@ export class AwsBedrockHandler implements ApiHandler {
*/
async getModelId(): Promise<string> {
if (this.options.awsUseCrossRegionInference) {
let regionPrefix = this.getRegion().slice(0, 3)
const regionPrefix = this.getRegion().slice(0, 3)
switch (regionPrefix) {
case "us-":
return `us.${this.getModel().id}`
@@ -462,4 +473,192 @@ export class AwsBedrockHandler implements ApiHandler {
// Approximate 4 characters per token
return Math.ceil(text.length / 4)
}
/**
* Creates a message using Amazon Nova models through AWS Bedrock
* Implements support for Nova Micro, Nova Lite, and Nova Pro models
*/
private async *createNovaMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
modelId: string,
model: { id: BedrockModelId; info: ModelInfo },
): ApiStream {
// Get Bedrock client with proper credentials
const client = await this.getBedrockClient()
// Format messages for Nova model
const formattedMessages = this.formatNovaMessages(messages)
// Prepare request for Nova model
const command = new ConverseStreamCommand({
modelId: modelId,
messages: formattedMessages,
system: systemPrompt ? [{ text: systemPrompt }] : undefined,
inferenceConfig: {
maxTokens: model.info.maxTokens || 5000,
temperature: 0,
// topP: 0.9, // Alternative: use topP instead of temperature
},
})
// Execute the streaming request and handle response
try {
const response = await client.send(command)
if (response.stream) {
let hasReportedInputTokens = false
for await (const chunk of response.stream) {
// Handle metadata events with token usage information
if (chunk.metadata?.usage) {
// Report complete token usage from the model itself
const inputTokens = chunk.metadata.usage.inputTokens || 0
const outputTokens = chunk.metadata.usage.outputTokens || 0
yield {
type: "usage",
inputTokens,
outputTokens,
totalCost: calculateApiCostOpenAI(model.info, inputTokens, outputTokens, 0, 0),
}
hasReportedInputTokens = true
}
// Handle content delta (text generation)
if (chunk.contentBlockDelta?.delta?.text) {
yield {
type: "text",
text: chunk.contentBlockDelta.delta.text,
}
}
// Handle reasoning content if present
if (chunk.contentBlockDelta?.delta?.reasoningContent?.text) {
yield {
type: "reasoning",
reasoning: chunk.contentBlockDelta.delta.reasoningContent.text,
}
}
// Handle errors
if (chunk.internalServerException) {
yield {
type: "text",
text: `[ERROR] Internal server error: ${chunk.internalServerException.message}`,
}
} else if (chunk.modelStreamErrorException) {
yield {
type: "text",
text: `[ERROR] Model stream error: ${chunk.modelStreamErrorException.message}`,
}
} else if (chunk.validationException) {
yield {
type: "text",
text: `[ERROR] Validation error: ${chunk.validationException.message}`,
}
} else if (chunk.throttlingException) {
yield {
type: "text",
text: `[ERROR] Throttling error: ${chunk.throttlingException.message}`,
}
} else if (chunk.serviceUnavailableException) {
yield {
type: "text",
text: `[ERROR] Service unavailable: ${chunk.serviceUnavailableException.message}`,
}
}
}
}
} catch (error) {
console.error("Error processing Nova model response:", error)
yield {
type: "text",
text: `[ERROR] Failed to process Nova response: ${error instanceof Error ? error.message : String(error)}`,
}
}
}
/**
* Formats messages for Amazon Nova models according to the SDK specification
*/
private formatNovaMessages(messages: Anthropic.Messages.MessageParam[]): { role: ConversationRole; content: any[] }[] {
return messages.map((message) => {
// Determine role (user or assistant)
const role = message.role === "user" ? ConversationRole.USER : ConversationRole.ASSISTANT
// Process content based on type
let content: any[] = []
if (typeof message.content === "string") {
// Simple text content
content = [{ text: message.content }]
} else if (Array.isArray(message.content)) {
// Convert Anthropic content format to Nova content format
content = message.content
.map((item) => {
// Text content
if (item.type === "text") {
return { text: item.text }
}
// Image content
if (item.type === "image") {
// Handle different image source formats
let imageData: Uint8Array
let format = "jpeg" // default format
// Extract format from media_type if available
if (item.source.media_type) {
// Extract format from media_type (e.g., "image/jpeg" -> "jpeg")
const formatMatch = item.source.media_type.match(/image\/(\w+)/)
if (formatMatch && formatMatch[1]) {
format = formatMatch[1]
// Ensure format is one of the allowed values
if (!["png", "jpeg", "gif", "webp"].includes(format)) {
format = "jpeg" // Default to jpeg if not supported
}
}
}
// Get image data
try {
if (typeof item.source.data === "string") {
// Handle base64 encoded data
const base64Data = item.source.data.replace(/^data:image\/\w+;base64,/, "")
imageData = new Uint8Array(Buffer.from(base64Data, "base64"))
} else if (item.source.data && typeof item.source.data === "object") {
// Try to convert to Uint8Array
imageData = new Uint8Array(Buffer.from(item.source.data as any))
} else {
console.error("Unsupported image data format")
return null // Skip this item if format is not supported
}
} catch (error) {
console.error("Could not convert image data to Uint8Array:", error)
return null // Skip this item if conversion fails
}
return {
image: {
format,
source: {
bytes: imageData,
},
},
}
}
// Return null for unsupported content types
return null
})
.filter(Boolean) // Remove any null items
}
// Return formatted message
return {
role,
content,
}
})
}
}
+5
View File
@@ -17,6 +17,11 @@ 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
},
})
}
+40
View File
@@ -17,6 +17,40 @@ export class LiteLlmHandler implements ApiHandler {
})
}
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
try {
const response = await fetch(`${this.client.baseURL}/spend/calculate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.options.liteLlmApiKey}`,
},
body: JSON.stringify({
completion_response: {
model: modelId,
usage: {
prompt_tokens,
completion_tokens,
},
},
}),
})
if (response.ok) {
const data: { cost: number } = await response.json()
return data.cost
} else {
console.error("Error calculating spend:", response.statusText)
return undefined
}
} catch (error) {
console.error("Error calculating spend:", error)
return undefined
}
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
@@ -39,6 +73,9 @@ export class LiteLlmHandler implements ApiHandler {
stream_options: { include_usage: true },
})
const inputCost = (await this.calculateCost(1e6, 0)) || 0
const outputCost = (await this.calculateCost(0, 1e6)) || 0
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
@@ -49,10 +86,13 @@ export class LiteLlmHandler implements ApiHandler {
}
if (chunk.usage) {
const totalCost =
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
totalCost,
}
}
}
+10 -1
View File
@@ -34,6 +34,7 @@ export class OpenAiHandler implements ApiHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
const isO3Mini = modelId.includes("o3-mini")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -42,8 +43,15 @@ export class OpenAiHandler implements ApiHandler {
]
let temperature: number | undefined = this.options.openAiModelInfo?.temperature ?? openAiModelInfoSaneDefaults.temperature
let reasoningEffort: ChatCompletionReasoningEffort | undefined = undefined
let maxTokens: number | undefined
if (isDeepseekReasoner) {
if (this.options.openAiModelInfo?.maxTokens && this.options.openAiModelInfo.maxTokens > 0) {
maxTokens = Number(this.options.openAiModelInfo.maxTokens)
} else {
maxTokens = undefined
}
if (isDeepseekReasoner || isR1FormatRequired) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
@@ -57,6 +65,7 @@ export class OpenAiHandler implements ApiHandler {
model: modelId,
messages: openAiMessages,
temperature,
max_tokens: maxTokens,
reasoning_effort: reasoningEffort,
stream: true,
stream_options: { include_usage: true },
+2 -2
View File
@@ -1,6 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import delay from "delay"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
@@ -96,7 +96,7 @@ export class OpenRouterHandler implements ApiHandler {
async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
if (this.lastGenerationId) {
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
await setTimeoutPromise(500) // FIXME: necessary delay to ensure generation endpoint is ready
try {
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
const generation = (await generationIterator.next()).value
+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 ?? ""
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
const 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
let match: RegExpExecArray | null
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
let toolResultImages: string[] = []
const toolResultImages: string[] = []
toolMessages.forEach((toolMessage) => {
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the Ollama SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
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
let toolResultImages: Anthropic.Messages.ImageBlockParam[] = []
const 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
let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
const tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
id: toolMessage.id,
type: "function",
function: {
+11
View File
@@ -0,0 +1,11 @@
# 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 -1
View File
@@ -20,7 +20,7 @@ export const toolUseNames = [
"use_mcp_tool",
"access_mcp_resource",
"ask_followup_question",
"plan_mode_response",
"plan_mode_respond",
"attempt_completion",
] as const
@@ -1,7 +1,7 @@
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
export function parseAssistantMessage(assistantMessage: string) {
let contentBlocks: AssistantMessageContent[] = []
const contentBlocks: AssistantMessageContent[] = []
let currentTextContent: TextContent | undefined = undefined
let currentTextContentStartIndex = 0
let currentToolUse: ToolUse | undefined = undefined
+75 -8
View File
@@ -1,10 +1,77 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ClineApiReqInfo, ClineMessage } from "../../shared/ExtensionMessage"
import { ApiHandler } from "../../api"
import { OpenAiHandler } from "../../api/providers/openai"
export class ContextManager {
getNextTruncationRange(
messages: Anthropic.Messages.MessageParam[],
currentDeletedRange: [number, number] | undefined = undefined,
keep: "half" | "quarter" = "half",
getNewContextMessagesAndMetadata(
apiConversationHistory: Anthropic.Messages.MessageParam[],
clineMessages: ClineMessage[],
api: ApiHandler,
conversationHistoryDeletedRange: [number, number] | undefined,
previousApiReqIndex: number,
) {
let updatedConversationHistoryDeletedRange = false
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
const previousRequest = clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
let contextWindow = api.getModel().info.contextWindow || 128_000
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
contextWindow = 64_000
}
let maxAllowedSize: number
switch (contextWindow) {
case 64_000: // deepseek models
maxAllowedSize = contextWindow - 27_000
break
case 128_000: // most models
maxAllowedSize = contextWindow - 30_000
break
case 200_000: // claude models
maxAllowedSize = contextWindow - 40_000
break
default:
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
}
// This is the most reliable way to know when we're close to hitting the context window.
if (totalTokens >= maxAllowedSize) {
// Since the user may switch between models with different context windows, truncating half may not be enough (ie if switching from claude 200k to deepseek 64k, half truncation will only remove 100k tokens, but we need to remove much more)
// So if totalTokens/2 is greater than maxAllowedSize, we truncate 3/4 instead of 1/2
// FIXME: truncating the conversation in a way that is optimal for prompt caching AND takes into account multi-context window complexity is something we need to improve
const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
// NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range
conversationHistoryDeletedRange = this.getNextTruncationRange(
apiConversationHistory,
conversationHistoryDeletedRange,
keep,
)
updatedConversationHistoryDeletedRange = true
}
}
}
// conversationHistoryDeletedRange is updated only when we're close to hitting the context window, so we don't continuously break the prompt cache
const truncatedConversationHistory = this.getTruncatedMessages(apiConversationHistory, conversationHistoryDeletedRange)
return {
conversationHistoryDeletedRange: conversationHistoryDeletedRange,
updatedConversationHistoryDeletedRange: updatedConversationHistoryDeletedRange,
truncatedConversationHistory: truncatedConversationHistory,
}
}
public getNextTruncationRange(
apiMessages: Anthropic.Messages.MessageParam[],
currentDeletedRange: [number, number] | undefined,
keep: "half" | "quarter",
): [number, number] {
// Since we always keep the first message, currentDeletedRange[0] will always be 1 (for now until we have a smarter truncation algorithm)
const rangeStartIndex = 1
@@ -16,20 +83,20 @@ export class ContextManager {
// We first calculate half of the messages then divide by 2 to get the number of pairs.
// After flooring, we multiply by 2 to get the number of messages.
// Note that this will also always be an even number.
messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number
messagesToRemove = Math.floor((apiMessages.length - startOfRest) / 4) * 2 // Keep even number
} else {
// Remove 3/4 of remaining user-assistant pairs
// We calculate 3/4ths of the messages then divide by 2 to get the number of pairs.
// After flooring, we multiply by 2 to get the number of messages.
// Note that this will also always be an even number.
messagesToRemove = Math.floor(((messages.length - startOfRest) * 3) / 4 / 2) * 2
messagesToRemove = Math.floor(((apiMessages.length - startOfRest) * 3) / 4 / 2) * 2
}
let rangeEndIndex = startOfRest + messagesToRemove - 1
// Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure.
// NOTE: anthropic format messages are always user-assistant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
if (messages[rangeEndIndex].role !== "user") {
if (apiMessages[rangeEndIndex].role !== "user") {
rangeEndIndex -= 1
}
@@ -37,7 +104,7 @@ export class ContextManager {
return [rangeStartIndex, rangeEndIndex]
}
getTruncatedMessages(
public getTruncatedMessages(
messages: Anthropic.Messages.MessageParam[],
deletedRange: [number, number] | undefined,
): Anthropic.Messages.MessageParam[] {
@@ -1,3 +1,10 @@
export function checkIsOpenRouterContextWindowError(error: any): boolean {
return error.code === 400 && error.message?.includes("context length")
}
export function checkIsAnthropicContextWindowError(response: any): boolean {
return (
response?.error?.error?.type === "invalid_request_error" &&
response?.error?.error?.message?.includes("prompt is too long")
)
}
File diff suppressed because it is too large Load Diff
+82
View File
@@ -117,6 +117,88 @@ 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
+8 -8
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.
- 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.
Usage:
<ask_followup_question>
<question>Your question here</question>
@@ -239,18 +239,18 @@ Your final result description here
<command>Command to demonstrate result (optional)</command>
</attempt_completion>
## plan_mode_response
## 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.
- 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.)
- 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_response>
<plan_mode_respond>
<response>Your response here</response>
<options>
Array of options here (optional), e.g. ["Option 1", "Option 2", "Option 3"]
</options>
</plan_mode_response>
</plan_mode_respond>
# Tool Use Examples
@@ -881,11 +881,11 @@ ACT MODE V.S. PLAN MODE
In each user message, the environment_details will specify the current mode. There are two modes:
- ACT MODE: In this mode, you have access to all tools EXCEPT the plan_mode_response tool.
- ACT MODE: In this mode, you have access to all tools EXCEPT the plan_mode_respond tool.
- In ACT MODE, you use tools to accomplish the user's task. Once you've completed the user's task, you use the attempt_completion tool to present the result of the task to the user.
- PLAN MODE: In this special mode, you have access to the plan_mode_response tool.
- PLAN MODE: In this special mode, you have access to the plan_mode_respond tool.
- In PLAN MODE, the goal is to gather information and get context to create a detailed plan for accomplishing the task, which the user will review and approve before they switch you to ACT MODE to implement the solution.
- In PLAN MODE, when you need to converse with the user or present a plan, you should use the plan_mode_response tool to deliver your response directly, rather than using <thinking> tags to analyze when to respond. Do not talk about using plan_mode_response - just use it directly to share your thoughts and provide helpful answers.
- In PLAN MODE, when you need to converse with the user or present a plan, you should use the plan_mode_respond tool to deliver your response directly, rather than using <thinking> tags to analyze when to respond. Do not talk about using plan_mode_respond - just use it directly to share your thoughts and provide helpful answers.
## What is PLAN MODE?
+73
View File
@@ -0,0 +1,73 @@
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
@@ -0,0 +1,67 @@
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
@@ -0,0 +1,421 @@
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
File diff suppressed because it is too large Load Diff
+322
View File
@@ -0,0 +1,322 @@
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,
)
}
}
+288
View File
@@ -0,0 +1,288 @@
import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as path from "path"
import { Controller } from "../../core/controller"
import { HistoryItem } from "../../shared/HistoryItem"
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[] {
return [
vscode.commands.registerCommand("cline.dev.createTestTasks", async () => {
const count = await vscode.window.showInputBox({
title: "Test Tasks",
prompt: "How many test tasks to create?",
value: "10",
})
if (!count) {
return
}
const tasksCount = parseInt(count)
const globalStoragePath = context.globalStorageUri.fsPath
const tasksDir = path.join(globalStoragePath, "tasks")
vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: `Creating ${tasksCount} test tasks...`,
cancellable: false,
},
async (progress) => {
for (let i = 0; i < tasksCount; i++) {
// Generate a timestamp to ensure unique IDs
const timestamp = Date.now() + i
const taskId = `${timestamp}`
const taskDir = path.join(tasksDir, taskId)
await fs.mkdir(taskDir, { recursive: true })
// Generate a task prompt
const taskName = getRandomTaskName(i)
// Create realistic message sequence
const messages = createRealisticMessageSequence(timestamp, taskName, i)
// Create API conversation history file
await fs.writeFile(
path.join(taskDir, "api_conversation_history.json"),
JSON.stringify(
[
{
role: "user",
content: [{ type: "text", text: `<task>\n${taskName}\n</task>` }],
},
{
role: "assistant",
content: [
{
type: "text",
text: `I'll help you ${taskName.toLowerCase()}. Let me break this down into steps.`,
},
],
},
],
null,
2,
),
)
// Create UI messages file with realistic message sequence
await fs.writeFile(path.join(taskDir, "ui_messages.json"), JSON.stringify(messages, null, 2))
// Create history item to be shown in the HistoryView
const historyItem: HistoryItem = {
id: taskId,
ts: timestamp,
task: taskName,
tokensIn: Math.floor(100 + Math.random() * 900), // Random token count from 100-1000
tokensOut: Math.floor(200 + Math.random() * 1800), // Random token count from 200-2000
cacheWrites: i % 3 === 0 ? Math.floor(50 + Math.random() * 150) : undefined, // Only add cache writes to every 3rd task
cacheReads: i % 3 === 0 ? Math.floor(20 + Math.random() * 80) : undefined, // Only add cache reads to every 3rd task
totalCost: Number((0.0001 + Math.random() * 0.01).toFixed(5)), // Random cost from $0.0001 to $0.0101
size: 1024 * 1024, // 1MB
}
// Update task history in global state
await controller.updateTaskHistory(historyItem)
progress.report({ increment: 100 / tasksCount })
}
// Update the UI to show the new tasks
await controller.postStateToWebview()
vscode.window.showInformationMessage(`Created ${tasksCount} test tasks`)
},
)
}),
]
}
/**
* Creates a realistic sequence of messages that would occur in a typical task
*/
function createRealisticMessageSequence(baseTimestamp: number, taskPrompt: string, taskIndex: number): ClineMessage[] {
// Use an incrementing timestamp to ensure messages appear in sequence
let timestamp = baseTimestamp
const getNextTimestamp = () => {
timestamp += 1000 // Add 1 second between messages
return timestamp
}
// Variables to make different test tasks look unique
const fileName = getRandomFileName(taskIndex)
const commitHash = `commit${taskIndex}${Math.floor(Math.random() * 1000000).toString(16)}`
// Create a realistic message sequence
const messages: ClineMessage[] = [
// Initial task message - uses "say" with "text" which is the format used in Cline.ts
{
ts: baseTimestamp,
type: "say",
say: "text",
text: taskPrompt,
},
// API request started
{
ts: getNextTimestamp(),
type: "say",
say: "api_req_started",
text: JSON.stringify({
request: `<task>\n${taskPrompt}\n</task>`,
tokensIn: Math.floor(100 + Math.random() * 200),
tokensOut: Math.floor(300 + Math.random() * 500),
}),
},
// Reasoning message
{
ts: getNextTimestamp(),
type: "say",
say: "reasoning",
text: `I'll approach this task by breaking it down into manageable steps. First, I'll analyze the requirements, then create a plan, and finally implement the solution systematically.`,
},
// Text response
{
ts: getNextTimestamp(),
type: "say",
say: "text",
text: `I'll help you with this task. Let me start by creating the necessary files and implementing the core functionality.`,
},
]
// Add task-specific messages based on index modulo to create variety
const messageType = taskIndex % 5
if (messageType === 0 || messageType === 2) {
// Tool use - file operations
messages.push({
ts: getNextTimestamp(),
type: "say",
say: "tool",
text: JSON.stringify({
tool: "newFileCreated",
path: fileName,
content: `// Sample code for ${taskPrompt}`,
}),
})
}
if (messageType === 1 || messageType === 3) {
// Command execution
messages.push(
{
ts: getNextTimestamp(),
type: "ask",
ask: "command",
text: `ls -la`,
},
{
ts: getNextTimestamp(),
type: "say",
say: "command_output",
text: `total 24\ndrwxr-xr-x 3 user staff 96 Mar 10 12:34 .\ndrwxr-xr-x 8 user staff 256 Mar 10 12:30 ..\n-rw-r--r-- 1 user staff 158 Mar 10 12:34 ${fileName}`,
},
)
}
if (messageType === 2 || messageType === 4) {
// Browser actions
messages.push(
{
ts: getNextTimestamp(),
type: "ask",
ask: "browser_action_launch",
text: `https://example.com`,
},
{
ts: getNextTimestamp(),
type: "say",
say: "browser_action_result",
text: JSON.stringify({
logs: "Page loaded successfully",
screenshot:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
}),
},
{
ts: getNextTimestamp(),
type: "say",
say: "browser_action",
text: JSON.stringify({
action: "close",
}),
},
)
}
// Add checkpoint
messages.push({
ts: getNextTimestamp(),
type: "say",
say: "checkpoint_created",
lastCheckpointHash: commitHash,
})
// Add completion result (all tasks end with this)
messages.push({
ts: getNextTimestamp(),
type: "say",
say: "completion_result",
text: `I've completed the task to ${taskPrompt.toLowerCase()}. The implementation includes all the required functionality and meets the specifications. ${"x".repeat(1024 * 1024)}`, // 1MB file
lastCheckpointHash: commitHash,
})
return messages
}
/**
* Returns a random task name for test data
*/
function getRandomTaskName(index: number): string {
const tasks = [
"Create a simple todo application",
"Build a weather forecast widget",
"Implement a markdown parser",
"Design a responsive landing page",
"Develop a currency converter",
"Create a file upload component",
"Build a data visualization dashboard",
"Implement a search functionality",
"Create a user authentication system",
"Design a dark mode toggle",
"Build a countdown timer",
"Create a drag and drop interface",
"Implement form validation",
"Design a multi-step wizard",
"Create a notification system",
]
return tasks[index % tasks.length] + ` (Test ${index + 1})`
}
/**
* Returns a random file name for test data
*/
function getRandomFileName(index: number): string {
const files = [
"index.html",
"styles.css",
"script.js",
"app.jsx",
"main.ts",
"utils.py",
"config.json",
"server.js",
"data.csv",
"README.md",
]
return files[index % files.length]
}
+12 -11
View File
@@ -1,27 +1,28 @@
import * as vscode from "vscode"
import { ClineProvider } from "../core/webview/ClineProvider"
import { Controller } from "../core/controller"
import { ClineAPI } from "./cline"
import { getGlobalState } from "../core/storage/state"
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvider: ClineProvider): ClineAPI {
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarController: Controller): ClineAPI {
const api: ClineAPI = {
setCustomInstructions: async (value: string) => {
await sidebarProvider.updateCustomInstructions(value)
await sidebarController.updateCustomInstructions(value)
outputChannel.appendLine("Custom instructions set")
},
getCustomInstructions: async () => {
return (await sidebarProvider.getGlobalState("customInstructions")) as string | undefined
return (await getGlobalState(sidebarController.context, "customInstructions")) as string | undefined
},
startNewTask: async (task?: string, images?: string[]) => {
outputChannel.appendLine("Starting new task")
await sidebarProvider.clearTask()
await sidebarProvider.postStateToWebview()
await sidebarProvider.postMessageToWebview({
await sidebarController.clearTask()
await sidebarController.postStateToWebview()
await sidebarController.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sidebarProvider.postMessageToWebview({
await sidebarController.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: task,
@@ -36,7 +37,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
outputChannel.appendLine(
`Sending message: ${message ? `"${message}"` : "undefined"} with ${images?.length || 0} image(s)`,
)
await sidebarProvider.postMessageToWebview({
await sidebarController.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: message,
@@ -46,7 +47,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
pressPrimaryButton: async () => {
outputChannel.appendLine("Pressing primary button")
await sidebarProvider.postMessageToWebview({
await sidebarController.postMessageToWebview({
type: "invoke",
invoke: "primaryButtonClick",
})
@@ -54,7 +55,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
pressSecondaryButton: async () => {
outputChannel.appendLine("Pressing secondary button")
await sidebarProvider.postMessageToWebview({
await sidebarController.postMessageToWebview({
type: "invoke",
invoke: "secondaryButtonClick",
})
+73 -31
View File
@@ -1,14 +1,14 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import delay from "delay"
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,10 +30,12 @@ export function activate(context: vscode.ExtensionContext) {
Logger.initialize(outputChannel)
Logger.log("Cline extension activated")
const sidebarProvider = new ClineProvider(context, outputChannel)
const sidebarWebview = new WebviewProvider(context, outputChannel)
vscode.commands.executeCommand("setContext", "cline.isDevMode", IS_DEV && IS_DEV === "true")
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, sidebarProvider, {
vscode.window.registerWebviewViewProvider(WebviewProvider.sideBarId, sidebarWebview, {
webviewOptions: { retainContextWhenHidden: true },
}),
)
@@ -41,9 +43,15 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.plusButtonClicked", async () => {
Logger.log("Plus button Clicked")
await sidebarProvider.clearTask()
await sidebarProvider.postStateToWebview()
await sidebarProvider.postMessageToWebview({
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({
type: "action",
action: "chatButtonClicked",
})
@@ -52,7 +60,13 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.mcpButtonClicked", () => {
sidebarProvider.postMessageToWebview({
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleWebview.controller.postMessageToWebview({
type: "action",
action: "mcpButtonClicked",
})
@@ -63,7 +77,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 tabProvider = new ClineProvider(context, outputChannel)
const tabWebview = new WebviewProvider(context, outputChannel)
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
@@ -74,7 +88,7 @@ export function activate(context: vscode.ExtensionContext) {
}
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Cline", targetCol, {
const panel = vscode.window.createWebviewPanel(WebviewProvider.tabPanelId, "Cline", targetCol, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [context.extensionUri],
@@ -85,10 +99,10 @@ 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"),
}
tabProvider.resolveWebviewView(panel)
tabWebview.resolveWebviewView(panel)
// Lock the editor group so clicking on files doesn't open them over the panel
await delay(100)
await setTimeoutPromise(100)
await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
}
@@ -98,7 +112,13 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.settingsButtonClicked", () => {
//vscode.window.showInformationMessage(message)
sidebarProvider.postMessageToWebview({
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleWebview.controller.postMessageToWebview({
type: "action",
action: "settingsButtonClicked",
})
@@ -107,7 +127,13 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.historyButtonClicked", () => {
sidebarProvider.postMessageToWebview({
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleWebview.controller.postMessageToWebview({
type: "action",
action: "historyButtonClicked",
})
@@ -116,19 +142,19 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.accountButtonClicked", () => {
sidebarProvider.postMessageToWebview({
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleWebview.controller.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.
@@ -153,15 +179,15 @@ export function activate(context: vscode.ExtensionContext) {
const path = uri.path
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
return
}
switch (path) {
case "/openrouter": {
const code = query.get("code")
if (code) {
await visibleProvider.handleOpenRouterCallback(code)
await visibleWebview?.controller.handleOpenRouterCallback(code)
}
break
}
@@ -177,13 +203,13 @@ export function activate(context: vscode.ExtensionContext) {
})
// Validate state parameter
if (!(await visibleProvider.validateAuthState(state))) {
if (!(await visibleWebview?.controller.validateAuthState(state))) {
vscode.window.showErrorMessage("Invalid auth state")
return
}
if (token && apiKey) {
await visibleProvider.handleAuthCallback(token, apiKey)
await visibleWebview?.controller.handleAuthCallback(token, apiKey)
}
break
}
@@ -193,6 +219,20 @@ export function activate(context: vscode.ExtensionContext) {
}
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
// Register size testing commands in development mode
if (IS_DEV && IS_DEV === "true") {
// Use dynamic import to avoid loading the module in production
import("./dev/commands/tasks")
.then((module) => {
const devTaskCommands = module.registerTaskCommands(context, sidebarWebview.controller)
context.subscriptions.push(...devTaskCommands)
Logger.log("Cline dev task commands registered")
})
.catch((error) => {
Logger.log("Failed to register dev task commands: " + error)
})
}
context.subscriptions.push(
vscode.commands.registerCommand("cline.addToChat", async (range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) => {
const editor = vscode.window.activeTextEditor
@@ -213,8 +253,8 @@ export function activate(context: vscode.ExtensionContext) {
const filePath = editor.document.uri.fsPath
const languageId = editor.document.languageId
// Send to sidebar provider
await sidebarProvider.addSelectedCodeToChat(
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.addSelectedCodeToChat(
selectedText,
filePath,
languageId,
@@ -263,7 +303,8 @@ export function activate(context: vscode.ExtensionContext) {
*/
// Send to sidebar provider
await sidebarProvider.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
} catch (error) {
// Ensure clipboard is restored even if an error occurs
await vscode.env.clipboard.writeText(tempCopyBuffer)
@@ -334,11 +375,12 @@ export function activate(context: vscode.ExtensionContext) {
const languageId = editor.document.languageId
// Send to sidebar provider with diagnostics
await sidebarProvider.fixWithCline(selectedText, filePath, languageId, diagnostics)
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.fixWithCline(selectedText, filePath, languageId, diagnostics)
}),
)
return createClineAPI(outputChannel, sidebarProvider)
return createClineAPI(outputChannel, sidebarWebview.controller)
}
// This method is called when your extension is deactivated
-8
View File
@@ -1,8 +0,0 @@
// 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 { ClineProvider } from "../../core/webview/ClineProvider"
import { Controller as ClineProvider } from "../../core/controller"
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 || ""
console.warn(`Checkpoint commit created.`)
const commitHash = (result.commit || "").replace(/^HEAD\s+/, "")
console.warn(`Checkpoint commit created: `, commitHash)
const durationMs = Math.round(performance.now() - startTime)
telemetryService.captureCheckpointUsage(this.taskId, "commit_created", durationMs)
@@ -158,7 +158,6 @@ export class DiffViewProvider {
await updatedDocument.save()
}
// await delay(100)
// get text after save in case there is any auto-formatting done by the editor
const postSaveContent = updatedDocument.getText()
+1 -1
View File
@@ -71,7 +71,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
// remove ansi
data = stripAnsi(data)
// Split data by newlines
let lines = data ? data.split("\n") : []
const 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 { ClineProvider } from "../../core/webview/ClineProvider"
import { Controller } from "../../core/controller"
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 providerRef: WeakRef<ClineProvider>
private controllerRef: WeakRef<Controller>
private disposables: vscode.Disposable[] = []
private filePaths: Set<string> = new Set()
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
this.registerListeners()
}
@@ -85,7 +85,7 @@ class WorkspaceTracker {
if (!cwd) {
return
}
this.providerRef.deref()?.postMessageToWebview({
this.controllerRef.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 { ClineProvider } from "../../core/webview/ClineProvider"
import { Controller } from "../../core/controller"
import type { BalanceResponse, PaymentTransaction, UsageTransaction } from "../../shared/ClineAccount"
export class ClineAccountService {
private readonly baseUrl = "https://api.cline.bot/v1"
private providerRef: WeakRef<ClineProvider>
private controllerRef: WeakRef<Controller>
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
}
/**
* Get the user's Cline Account key from the apiConfiguration
*/
private async getClineApiKey(): Promise<string | undefined> {
const provider = this.providerRef.deref()
const provider = this.controllerRef.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.providerRef.deref()?.postMessageToWebview({
await this.controllerRef.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.providerRef.deref()?.postMessageToWebview({
await this.controllerRef.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.providerRef.deref()?.postMessageToWebview({
await this.controllerRef.deref()?.postMessageToWebview({
type: "userCreditsPayments",
userCreditsPayments: data,
})
+6 -6
View File
@@ -5,7 +5,7 @@ import { Browser, Page, ScreenshotOptions, TimeoutError, launch } from "puppetee
// @ts-ignore
import PCR from "puppeteer-chromium-resolver"
import pWaitFor from "p-wait-for"
import delay from "delay"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import { fileExistsAtPath } from "../../utils/fs"
import { BrowserActionResult } from "../../shared/ExtensionMessage"
import { BrowserSettings } from "../../shared/BrowserSettings"
@@ -212,7 +212,7 @@ export class BrowserSession {
interval: 100,
}).catch(() => {})
let options: ScreenshotOptions = {
const options: ScreenshotOptions = {
encoding: "base64",
// clip: {
@@ -295,7 +295,7 @@ export class BrowserSession {
}
lastHTMLSize = currentHTMLSize
await delay(checkDurationMsecs)
await setTimeoutPromise(checkDurationMsecs)
}
}
@@ -314,7 +314,7 @@ export class BrowserSession {
this.currentMousePosition = coordinate
// Small delay to check if click triggered any network activity
await delay(100)
await setTimeoutPromise(100)
if (hasNetworkActivity) {
// If we detected network activity, wait for navigation/loading
@@ -346,7 +346,7 @@ export class BrowserSession {
behavior: "auto",
})
})
await delay(300)
await setTimeoutPromise(300)
})
}
@@ -358,7 +358,7 @@ export class BrowserSession {
behavior: "auto",
})
})
await delay(300)
await setTimeoutPromise(300)
})
}
}
+9 -3
View File
@@ -4,6 +4,7 @@ import * as path from "path"
import { arePathsEqual } from "../../utils/path"
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
// First resolve the path normally - path.resolve doesn't care about glob special characters
const absolutePath = path.resolve(dirPath)
// Do not allow listing files in root or home directory, which cline tends to want to do when the user's prompt is vague.
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
@@ -48,6 +49,7 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
}
// * globs all files in one dir, ** globs files in nested directories
// For non-recursive listing, we still use a simple pattern
const filePaths = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)
return [filePaths, filePaths.length >= limit]
@@ -66,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) {
let results: Set<string> = new Set()
let queue: string[] = ["*"]
const results: Set<string> = new Set()
const queue: string[] = ["*"]
const globbingProcess = async () => {
while (queue.length > 0 && results.size < limit) {
@@ -80,7 +82,11 @@ async function globbyLevelByLevel(limit: number, options?: Options) {
}
results.add(file)
if (file.endsWith("/")) {
queue.push(`${file}*`)
// Escape parentheses in the path to prevent glob pattern interpretation
// This is crucial for NextJS folder naming conventions which use parentheses like (auth), (dashboard)
// Without escaping, glob treats parentheses as special pattern grouping characters
const escapedFile = file.replace(/\(/g, "\\(").replace(/\)/g, "\\)")
queue.push(`${escapedFile}*`)
}
}
}
+169 -167
View File
@@ -1,5 +1,5 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import {
CallToolResultSchema,
ListResourcesResultSchema,
@@ -8,13 +8,13 @@ import {
ReadResourceResultSchema,
} from "@modelcontextprotocol/sdk/types.js"
import chokidar, { FSWatcher } from "chokidar"
import delay from "delay"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import deepEqual from "fast-deep-equal"
import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { Controller } from "../../core/controller"
import {
DEFAULT_MCP_TIMEOUT_SECONDS,
McpMode,
@@ -29,38 +29,62 @@ import {
import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual } from "../../utils/path"
import { secondsToMs } from "../../utils/time"
import { GlobalFileNames } from "../../global-constants"
import { GlobalFileNames } from "../../core/storage/disk"
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
transport: StdioClientTransport
transport: StdioClientTransport | SSEClientTransport
}
export type McpTransportType = "stdio" | "sse"
export type McpServerConfig = z.infer<typeof ServerConfigSchema>
const AutoApproveSchema = z.array(z.string()).default([])
const StdioConfigSchema = z.object({
command: z.string(),
args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(),
const BaseConfigSchema = z.object({
autoApprove: AutoApproveSchema.optional(),
disabled: z.boolean().optional(),
timeout: z.number().min(MIN_MCP_TIMEOUT_SECONDS).optional().default(DEFAULT_MCP_TIMEOUT_SECONDS),
})
const SseConfigSchema = BaseConfigSchema.extend({
url: z.string().url(),
}).transform((config) => ({
...config,
transportType: "sse" as const,
}))
const StdioConfigSchema = BaseConfigSchema.extend({
command: z.string(),
args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(),
}).transform((config) => ({
...config,
transportType: "stdio" as const,
}))
const ServerConfigSchema = z.union([StdioConfigSchema, SseConfigSchema])
const McpSettingsSchema = z.object({
mcpServers: z.record(StdioConfigSchema),
mcpServers: z.record(ServerConfigSchema),
})
export class McpHub {
private providerRef: WeakRef<ClineProvider>
private controllerRef: WeakRef<Controller>
private disposables: vscode.Disposable[] = []
private settingsWatcher?: vscode.FileSystemWatcher
private fileWatchers: Map<string, FSWatcher> = new Map()
connections: McpConnection[] = []
isConnecting: boolean = false
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
this.watchMcpSettingsFile()
this.initializeMcpServers()
}
@@ -75,7 +99,7 @@ export class McpHub {
}
async getMcpServersPath(): Promise<string> {
const provider = this.providerRef.deref()
const provider = this.controllerRef.deref()
if (!provider) {
throw new Error("Provider not available")
}
@@ -84,7 +108,7 @@ export class McpHub {
}
async getMcpSettingsFilePath(): Promise<string> {
const provider = this.providerRef.deref()
const provider = this.controllerRef.deref()
if (!provider) {
throw new Error("Provider not available")
}
@@ -103,32 +127,51 @@ export class McpHub {
return mcpSettingsFilePath
}
private async readAndValidateMcpSettingsFile(): Promise<z.infer<typeof McpSettingsSchema> | undefined> {
try {
const settingsPath = await this.getMcpSettingsFilePath()
const content = await fs.readFile(settingsPath, "utf-8")
let config: any
// Parse JSON file content
try {
config = JSON.parse(content)
} catch (error) {
vscode.window.showErrorMessage(
"Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
)
return undefined
}
// Validate against schema
const result = McpSettingsSchema.safeParse(config)
if (!result.success) {
vscode.window.showErrorMessage("Invalid MCP settings schema.")
return undefined
}
return result.data
} catch (error) {
console.error("Failed to read MCP settings:", error)
return undefined
}
}
private async watchMcpSettingsFile(): Promise<void> {
const settingsPath = await this.getMcpSettingsFilePath()
this.disposables.push(
vscode.workspace.onDidSaveTextDocument(async (document) => {
if (arePathsEqual(document.uri.fsPath, settingsPath)) {
const content = await fs.readFile(settingsPath, "utf-8")
const errorMessage =
"Invalid MCP settings format. Please ensure your settings follow the correct JSON format."
let config: any
try {
config = JSON.parse(content)
} catch (error) {
vscode.window.showErrorMessage(errorMessage)
return
}
const result = McpSettingsSchema.safeParse(config)
if (!result.success) {
vscode.window.showErrorMessage(errorMessage)
return
}
try {
vscode.window.showInformationMessage("Updating MCP servers...")
await this.updateServerConnections(result.data.mcpServers || {})
vscode.window.showInformationMessage("MCP servers updated")
} catch (error) {
console.error("Failed to process MCP settings change:", error)
const settings = await this.readAndValidateMcpSettingsFile()
if (settings) {
try {
vscode.window.showInformationMessage("Updating MCP servers...")
await this.updateServerConnections(settings.mcpServers)
vscode.window.showInformationMessage("MCP servers updated")
} catch (error) {
console.error("Failed to process MCP settings change:", error)
}
}
}
}),
@@ -136,17 +179,16 @@ export class McpHub {
}
private async initializeMcpServers(): Promise<void> {
try {
const settingsPath = await this.getMcpSettingsFilePath()
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
await this.updateServerConnections(config.mcpServers || {})
} catch (error) {
console.error("Failed to initialize MCP servers:", error)
const settings = await this.readAndValidateMcpSettingsFile()
if (settings) {
await this.updateServerConnections(settings.mcpServers)
}
}
private async connectToServer(name: string, config: StdioServerParameters): Promise<void> {
private async connectToServer(
name: string,
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
): Promise<void> {
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
this.connections = this.connections.filter((conn) => conn.server.name !== name)
@@ -155,23 +197,29 @@ export class McpHub {
const client = new Client(
{
name: "Cline",
version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
version: this.controllerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
},
{
capabilities: {},
},
)
const transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: {
...config.env,
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
},
stderr: "pipe", // necessary for stderr to be available
})
let transport: StdioClientTransport | SSEClientTransport
if (config.transportType === "sse") {
transport = new SSEClientTransport(new URL(config.url), {})
} else {
transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: {
...config.env,
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
},
stderr: "pipe", // necessary for stderr to be available
})
}
transport.onerror = async (error) => {
console.error(`Transport error for "${name}":`, error)
@@ -191,59 +239,42 @@ 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: parsedConfig.disabled,
disabled: config.disabled,
},
client,
transport,
}
this.connections.push(connection)
// 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()
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()
}
}
}
})
} else {
console.error(`No stderr stream for ${name}`)
})
} else {
console.error(`No stderr stream for ${name}`)
}
transport.start = async () => {} // No-op now, .connect() won't fail
}
transport.start = async () => {} // No-op now, .connect() won't fail
// Connect
await client.connect(transport)
@@ -272,9 +303,15 @@ export class McpHub {
private async fetchToolsList(serverName: string): Promise<McpTool[]> {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "tools/list" }, ListToolsResultSchema)
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,
})
// Get autoApprove settings
const settingsPath = await this.getMcpSettingsFilePath()
@@ -300,7 +337,7 @@ export class McpHub {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "resources/list" }, ListResourcesResultSchema)
?.client.request({ method: "resources/list" }, ListResourcesResultSchema, { timeout: DEFAULT_REQUEST_TIMEOUT_MS })
return response?.resources || []
} catch (error) {
// console.error(`Failed to fetch resources for ${serverName}:`, error)
@@ -312,7 +349,10 @@ export class McpHub {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema)
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema, {
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
})
return response?.resourceTemplates || []
} catch (error) {
// console.error(`Failed to fetch resource templates for ${serverName}:`, error)
@@ -333,7 +373,7 @@ export class McpHub {
}
}
async updateServerConnections(newServers: Record<string, any>): Promise<void> {
async updateServerConnections(newServers: Record<string, McpServerConfig>): Promise<void> {
this.isConnecting = true
this.removeAllFileWatchers()
const currentNames = new Set(this.connections.map((conn) => conn.server.name))
@@ -354,7 +394,9 @@ export class McpHub {
if (!currentConnection) {
// New server
try {
this.setupFileWatcher(name, config)
if (config.transportType === "stdio") {
this.setupFileWatcher(name, config)
}
await this.connectToServer(name, config)
} catch (error) {
console.error(`Failed to connect to new MCP server ${name}:`, error)
@@ -362,7 +404,9 @@ export class McpHub {
} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
// Existing server with changed config
try {
this.setupFileWatcher(name, config)
if (config.transportType === "stdio") {
this.setupFileWatcher(name, config)
}
await this.deleteConnection(name)
await this.connectToServer(name, config)
console.log(`Reconnected MCP server with updated config: ${name}`)
@@ -402,7 +446,7 @@ export class McpHub {
async restartConnection(serverName: string): Promise<void> {
this.isConnecting = true
const provider = this.providerRef.deref()
const provider = this.controllerRef.deref()
if (!provider) {
return
}
@@ -415,7 +459,7 @@ export class McpHub {
connection.server.status = "connecting"
connection.server.error = ""
await this.notifyWebviewOfServerChanges()
await delay(500) // artificial delay to show user that server is restarting
await setTimeoutPromise(500) // artificial delay to show user that server is restarting
try {
await this.deleteConnection(serverName)
// Try to connect again using existing config
@@ -437,7 +481,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.providerRef.deref()?.postMessageToWebview({
await this.controllerRef.deref()?.postMessageToWebview({
type: "mcpServers",
mcpServers: [...this.connections]
.sort((a, b) => {
@@ -458,64 +502,21 @@ export class McpHub {
// Public methods for server management
public async toggleServerDisabled(serverName: string, disabled: boolean): Promise<void> {
let settingsPath: string
try {
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 = {}
const config = await this.readAndValidateMcpSettingsFile()
if (!config) {
throw new Error("Failed to read or validate MCP settings")
}
if (config.mcpServers[serverName]) {
// Create a new server config object to ensure clean structure
const serverConfig = {
...config.mcpServers[serverName],
disabled,
}
config.mcpServers[serverName].disabled = disabled
// 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 settingsPath = await this.getMcpSettingsFilePath()
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (connection) {
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)
}
connection.server.disabled = disabled
}
await this.notifyWebviewOfServerChanges()
@@ -568,7 +569,7 @@ export class McpHub {
try {
const config = JSON.parse(connection.server.config)
const parsedConfig = StdioConfigSchema.parse(config)
const parsedConfig = ServerConfigSchema.parse(config)
timeout = secondsToMs(parsedConfig.timeout)
} catch (error) {
console.error(`Failed to parse timeout configuration for server ${serverName}: ${error}`)
@@ -589,7 +590,7 @@ export class McpHub {
)
}
async toggleToolAutoApprove(serverName: string, toolName: string, shouldAllow: boolean): Promise<void> {
async toggleToolAutoApprove(serverName: string, toolNames: string[], shouldAllow: boolean): Promise<void> {
try {
const settingsPath = await this.getMcpSettingsFilePath()
const content = await fs.readFile(settingsPath, "utf-8")
@@ -601,14 +602,16 @@ export class McpHub {
}
const autoApprove = config.mcpServers[serverName].autoApprove
const toolIndex = autoApprove.indexOf(toolName)
for (const toolName of toolNames) {
const toolIndex = autoApprove.indexOf(toolName)
if (shouldAllow && toolIndex === -1) {
// Add tool to autoApprove list
autoApprove.push(toolName)
} else if (!shouldAllow && toolIndex !== -1) {
// Remove tool from autoApprove list
autoApprove.splice(toolIndex, 1)
if (shouldAllow && toolIndex === -1) {
// Add tool to autoApprove list
autoApprove.push(toolName)
} else if (!shouldAllow && toolIndex !== -1) {
// Remove tool from autoApprove list
autoApprove.splice(toolIndex, 1)
}
}
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
@@ -616,7 +619,6 @@ 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) {
@@ -656,7 +658,7 @@ export class McpHub {
public async updateServerTimeout(serverName: string, timeout: number): Promise<void> {
try {
// Validate timeout against schema
const setConfigResult = StdioConfigSchema.shape.timeout.safeParse(timeout)
const setConfigResult = BaseConfigSchema.shape.timeout.safeParse(timeout)
if (!setConfigResult.success) {
throw new Error(`Invalid timeout value: ${timeout}. Must be at minimum ${MIN_MCP_TIMEOUT_SECONDS} seconds.`)
}
@@ -1,312 +0,0 @@
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
}
const { IS_DEV } = process.env
/**
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.conversationTelemetry": {
"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 ConversationTelemetryService {
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 isOptedInToConversationTelemetry(): boolean {
// First check global telemetry level - telemetry should only be enabled when level is "all"
const telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
const isGlobalTelemetryEnabled = telemetryLevel === "all"
// User has to manually opt in to conversation telemetry in Advanced Settings
const isConversationTelemetryEnabled =
vscode.workspace.getConfiguration("cline").get<boolean>("conversationTelemetry") ?? false
// Currently only enabled in dev environment
const isDevEnvironment = !!IS_DEV
return isDevEnvironment && isGlobalTelemetryEnabled && isConversationTelemetryEnabled
}
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.log("[ConversationTelemetry] 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.log("[ConversationTelemetry] OpenTelemetry tracer initialized successfully")
} catch (error) {
console.error("[ConversationTelemetry] 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.isOptedInToConversationTelemetry()) {
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.log(`[ConversationTelemetry] Captured ${message.role} message for task ${taskId}`, { span })
} catch (error) {
console.error("[ConversationTelemetry] 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.isOptedInToConversationTelemetry()) {
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.log(`[ConversationTelemetry] Cleanup data sent for task ${taskId}`)
} catch (error) {
console.error("[ConversationTelemetry] Error sending cleanup data:", error)
}
}
/**
* Shutdown the tracer provider
*/
public async shutdown(): Promise<void> {
if (this.tracerProvider) {
await this.tracerProvider.shutdown()
}
}
}
@@ -2,6 +2,8 @@ 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
@@ -18,6 +20,8 @@ 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
@@ -32,6 +36,8 @@ 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: {
@@ -234,6 +240,22 @@ 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
@@ -377,6 +399,21 @@ 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
+1 -1
View File
@@ -130,7 +130,7 @@ export interface ClineMessage {
export type ClineAsk =
| "followup"
| "plan_mode_response"
| "plan_mode_respond"
| "command"
| "command_output"
| "completion_result"
+6 -1
View File
@@ -65,6 +65,7 @@ export interface WebviewMessage {
| "fetchUserCreditsData"
| "optionsResponse"
| "requestTotalTasksSize"
| "taskFeedback"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
@@ -81,7 +82,7 @@ export interface WebviewMessage {
timeout?: number
// For toggleToolAutoApprove
serverName?: string
toolName?: string
toolNames?: string[]
autoApprove?: boolean
// For auth
@@ -92,8 +93,12 @@ 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"
+66 -4
View File
@@ -24,6 +24,7 @@ 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
@@ -93,6 +94,7 @@ export interface ModelInfo {
export interface OpenAiCompatibleModelInfo extends ModelInfo {
temperature?: number
isR1FormatRequired?: boolean
}
// Anthropic
@@ -159,6 +161,33 @@ export const anthropicModels = {
export type BedrockModelId = keyof typeof bedrockModels
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-7-sonnet-20250219-v1:0"
export const bedrockModels = {
"amazon.nova-pro-v1:0": {
maxTokens: 5000,
contextWindow: 300_000,
supportsImages: true,
supportsComputerUse: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 3.2,
},
"amazon.nova-lite-v1:0": {
maxTokens: 5000,
contextWindow: 300_000,
supportsImages: true,
supportsComputerUse: false,
supportsPromptCache: false,
inputPrice: 0.06,
outputPrice: 0.24,
},
"amazon.nova-micro-v1:0": {
maxTokens: 5000,
contextWindow: 128_000,
supportsImages: false,
supportsComputerUse: false,
supportsPromptCache: false,
inputPrice: 0.035,
outputPrice: 0.14,
},
"anthropic.claude-3-7-sonnet-20250219-v1:0": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -339,9 +368,9 @@ export const vertexModels = {
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-pro-exp-02-05": {
maxTokens: 8192,
contextWindow: 2_097_152,
"gemini-2.5-pro-exp-03-25": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
@@ -410,6 +439,7 @@ export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
isR1FormatRequired: false,
inputPrice: 0,
outputPrice: 0,
temperature: 0,
@@ -420,6 +450,14 @@ export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
export type GeminiModelId = keyof typeof geminiModels
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001"
export const geminiModels = {
"gemini-2.5-pro-exp-03-25": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-001": {
maxTokens: 8192,
contextWindow: 1_048_576,
@@ -578,11 +616,19 @@ export const openAiNativeModels = {
outputPrice: 0.6,
cacheReadsPrice: 0.075,
},
"gpt-4.5-preview": {
"chatgpt-4o-latest": {
maxTokens: 16_384,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5,
outputPrice: 15,
},
"gpt-4.5-preview": {
maxTokens: 16_384,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 75,
outputPrice: 150,
},
@@ -1364,4 +1410,20 @@ export const sambanovaModels = {
inputPrice: 0,
outputPrice: 0,
},
"QwQ-32B": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.5,
outputPrice: 1.0,
},
"DeepSeek-V3-0324": {
maxTokens: 4096,
contextWindow: 8192,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.0,
outputPrice: 1.5,
},
} as const satisfies Record<string, ModelInfo>
+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") {
let startedRequest = JSON.parse(messages[i].text || "{}")
const 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") {
let finishedRequest = JSON.parse(messages[j].text || "{}")
let combinedRequest = {
const finishedRequest = JSON.parse(messages[j].text || "{}")
const combinedRequest = {
...startedRequest,
...finishedRequest,
}
+57
View File
@@ -0,0 +1,57 @@
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")
})
})
+610 -103
View File
@@ -42,6 +42,7 @@
"@types/react-dom": "^18.3.5",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-v8": "^3.0.9",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
@@ -50,7 +51,7 @@
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.2.1",
"vite": "^6.2.3",
"vitest": "^3.0.5"
}
},
@@ -61,6 +62,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@antfu/install-pkg": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz",
@@ -95,13 +110,6 @@
"lru-cache": "^10.4.3"
}
},
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@@ -117,6 +125,16 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
@@ -127,6 +145,22 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
@@ -139,6 +173,30 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@bcoe/v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@braintree/sanitize-url": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz",
@@ -1651,6 +1709,119 @@
"mlly": "^1.7.4"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@jest/expect-utils": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
@@ -1680,8 +1851,6 @@
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@@ -1697,8 +1866,6 @@
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6.0.0"
}
@@ -1709,8 +1876,6 @@
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6.0.0"
}
@@ -1740,8 +1905,6 @@
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -1875,6 +2038,17 @@
"node": ">= 8"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -3609,15 +3783,48 @@
"vite": "^4 || ^5 || ^6"
}
},
"node_modules/@vitest/expect": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz",
"integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==",
"node_modules/@vitest/coverage-v8": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.9.tgz",
"integrity": "sha512-15OACZcBtQ34keIEn19JYTVuMFTlFrClclwWjHo/IRPg/8ELpkgNTl0o7WLP9WO9XGH6+tip9CPYtEOrIDJvBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.0.7",
"@vitest/utils": "3.0.7",
"@ampproject/remapping": "^2.3.0",
"@bcoe/v8-coverage": "^1.0.2",
"debug": "^4.4.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-lib-source-maps": "^5.0.6",
"istanbul-reports": "^3.1.7",
"magic-string": "^0.30.17",
"magicast": "^0.3.5",
"std-env": "^3.8.0",
"test-exclude": "^7.0.1",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "3.0.9",
"vitest": "3.0.9"
},
"peerDependenciesMeta": {
"@vitest/browser": {
"optional": true
}
}
},
"node_modules/@vitest/expect": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz",
"integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.0.9",
"@vitest/utils": "3.0.9",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
@@ -3626,13 +3833,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz",
"integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz",
"integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.0.7",
"@vitest/spy": "3.0.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@@ -3652,30 +3859,10 @@
}
}
},
"node_modules/@vitest/mocker/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/@vitest/mocker/node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz",
"integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz",
"integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3686,13 +3873,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz",
"integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz",
"integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.0.7",
"@vitest/utils": "3.0.9",
"pathe": "^2.0.3"
},
"funding": {
@@ -3700,13 +3887,13 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz",
"integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz",
"integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.0.7",
"@vitest/pretty-format": "3.0.9",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
@@ -3714,20 +3901,10 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot/node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/@vitest/spy": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz",
"integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz",
"integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3738,13 +3915,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz",
"integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz",
"integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.0.7",
"@vitest/pretty-format": "3.0.9",
"loupe": "^3.1.3",
"tinyrainbow": "^2.0.0"
},
@@ -4871,6 +5048,13 @@
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -5163,6 +5347,16 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -5408,6 +5602,23 @@
"dev": true,
"license": "ISC"
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
@@ -5462,6 +5673,27 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -5475,6 +5707,32 @@
"node": ">=10.13.0"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
@@ -5592,6 +5850,13 @@
"node": ">=18"
}
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/http-parser-js": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz",
@@ -5852,6 +6117,76 @@
"dev": true,
"license": "ISC"
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=8"
}
},
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"make-dir": "^4.0.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-lib-source-maps": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.23",
"debug": "^4.1.1",
"istanbul-lib-coverage": "^3.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-reports": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
"integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
@@ -6353,6 +6688,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -6364,6 +6706,44 @@
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/magicast": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.4",
"@babel/types": "^7.25.4",
"source-map-js": "^1.2.0"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/marked": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz",
@@ -6646,6 +7026,16 @@
"node": "*"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mlly": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
@@ -6765,6 +7155,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/package-manager-detector": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.9.tgz",
@@ -6852,6 +7249,23 @@
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -7524,6 +7938,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -7671,6 +8098,22 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -7683,6 +8126,20 @@
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -7865,6 +8322,47 @@
"optional": true,
"peer": true
},
"node_modules/test-exclude": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
"integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
"dev": true,
"license": "ISC",
"dependencies": {
"@istanbuljs/schema": "^0.1.2",
"glob": "^10.4.1",
"minimatch": "^9.0.4"
},
"engines": {
"node": ">=18"
}
},
"node_modules/test-exclude/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/test-exclude/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/throttle-debounce": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz",
@@ -8378,9 +8876,9 @@
}
},
"node_modules/vite": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
"integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8450,9 +8948,9 @@
}
},
"node_modules/vite-node": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz",
"integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz",
"integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8512,19 +9010,19 @@
}
},
"node_modules/vitest": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz",
"integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz",
"integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "3.0.7",
"@vitest/mocker": "3.0.7",
"@vitest/pretty-format": "^3.0.7",
"@vitest/runner": "3.0.7",
"@vitest/snapshot": "3.0.7",
"@vitest/spy": "3.0.7",
"@vitest/utils": "3.0.7",
"@vitest/expect": "3.0.9",
"@vitest/mocker": "3.0.9",
"@vitest/pretty-format": "^3.0.9",
"@vitest/runner": "3.0.9",
"@vitest/snapshot": "3.0.9",
"@vitest/spy": "3.0.9",
"@vitest/utils": "3.0.9",
"chai": "^5.2.0",
"debug": "^4.4.0",
"expect-type": "^1.1.0",
@@ -8536,7 +9034,7 @@
"tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.0.7",
"vite-node": "3.0.9",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -8552,8 +9050,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.0.7",
"@vitest/ui": "3.0.7",
"@vitest/browser": "3.0.9",
"@vitest/ui": "3.0.9",
"happy-dom": "*",
"jsdom": "*"
},
@@ -8581,16 +9079,6 @@
}
}
},
"node_modules/vitest/node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
@@ -8793,6 +9281,25 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+4 -2
View File
@@ -9,7 +9,8 @@
"preview": "vite preview",
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest dev"
"test:watch": "vitest dev",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@floating-ui/react": "^0.27.4",
@@ -46,6 +47,7 @@
"@types/react-dom": "^18.3.5",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-v8": "^3.0.9",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
@@ -54,7 +56,7 @@
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.2.1",
"vite": "^6.2.3",
"vitest": "^3.0.5"
}
}
@@ -147,7 +147,7 @@ export const ClineAccountView = () => {
</div>
</div>
) : (
<div className="flex flex-col items-center pr-3 max-w-[400px]">
<div className="flex flex-col items-center pr-3">
<ClineLogoWhite className="size-16 mb-4" />
<p style={{}}>
+25 -2
View File
@@ -30,6 +30,7 @@ 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;
@@ -1000,6 +1001,17 @@ 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={{
@@ -1142,6 +1154,17 @@ 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={{
@@ -1210,7 +1233,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
</div>
</>
)
case "plan_mode_response": {
case "plan_mode_respond": {
let response: string | undefined
let options: string[] | undefined
let selected: string | undefined
@@ -1229,7 +1252,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
<OptionsButtons
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "plan_mode_response"}
isActive={isLast && lastModifiedMessage?.ask === "plan_mode_respond"}
/>
</div>
)
+3 -3
View File
@@ -119,9 +119,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
break
case "plan_mode_response":
case "plan_mode_respond":
setTextAreaDisabled(isPartial)
setClineAsk("plan_mode_response")
setClineAsk("plan_mode_respond")
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
@@ -285,7 +285,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
} else if (clineAsk) {
switch (clineAsk) {
case "followup":
case "plan_mode_response":
case "plan_mode_respond":
case "tool":
case "browser_action_launch":
case "command": // user can provide feedback to a tool or command use
@@ -0,0 +1,126 @@
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
+19 -10
View File
@@ -52,6 +52,13 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
}
}, [checkpointTrackerErrorMessage])
// Reset isTextExpanded when task is collapsed
useEffect(() => {
if (!isTaskExpanded) {
setIsTextExpanded(false)
}
}, [isTaskExpanded])
/*
When dealing with event listeners in React components that depend on state variables, we face a challenge. We want our listener to always use the most up-to-date version of a callback function that relies on current state, but we don't want to constantly add and remove event listeners as that function updates. This scenario often arises with resize listeners or other window events. Simply adding the listener in a useEffect with an empty dependency array risks using stale state, while including the callback in the dependencies can lead to unnecessary re-registrations of the listener. There are react hook libraries that provide a elegant solution to this problem by utilizing the useRef hook to maintain a reference to the latest callback function without triggering re-renders or effect re-runs. This approach ensures that our event listener always has access to the most current state while minimizing performance overhead and potential memory leaks from multiple listener registrations.
Sources
@@ -95,17 +102,19 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
useEffect(() => {
if (isTaskExpanded && textRef.current && textContainerRef.current) {
let textContainerHeight = textContainerRef.current.clientHeight
if (!textContainerHeight) {
textContainerHeight = textContainerRef.current.getBoundingClientRect().height
}
const isOverflowing = textRef.current.scrollHeight > textContainerHeight
// Use requestAnimationFrame to ensure DOM is fully updated
requestAnimationFrame(() => {
// Check if refs are still valid
if (textRef.current && textContainerRef.current) {
let textContainerHeight = textContainerRef.current.clientHeight
if (!textContainerHeight) {
textContainerHeight = textContainerRef.current.getBoundingClientRect().height
}
const isOverflowing = textRef.current.scrollHeight > textContainerHeight
// necessary to show see more button again if user resizes window to expand and then back to collapse
if (!isOverflowing) {
setIsTextExpanded(false)
}
setShowSeeMore(isOverflowing)
setShowSeeMore(isOverflowing)
}
})
}
}, [task.text, windowWidth, isTaskExpanded])
@@ -85,7 +85,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
}, [presentableTasks])
const taskHistorySearchResults = useMemo(() => {
let results = searchQuery ? highlight(fuse.search(searchQuery)) : presentableTasks
const results = searchQuery ? highlight(fuse.search(searchQuery)) : presentableTasks
results.sort((a, b) => {
switch (sortOption) {
+5 -3
View File
@@ -11,13 +11,15 @@ type McpToolRowProps = {
const McpToolRow = ({ tool, serverName }: McpToolRowProps) => {
const { autoApprovalSettings } = useExtensionState()
const handleAutoApproveChange = () => {
if (!serverName) return
// Accept the event object
const handleAutoApproveChange = (event: any) => {
// Only proceed if the event was triggered by a direct user interaction
if (!serverName || !event.isTrusted) return
vscode.postMessage({
type: "toggleToolAutoApprove",
serverName,
toolName: tool.name,
toolNames: [tool.name],
autoApprove: !tool.autoApprove,
})
}
+49 -2
View File
@@ -6,6 +6,7 @@ import {
VSCodePanelView,
VSCodeDropdown,
VSCodeOption,
VSCodeCheckbox,
} from "@vscode/webview-ui-toolkit/react"
import { useEffect, useState } from "react"
import styled from "styled-components"
@@ -204,7 +205,7 @@ export const TabButton = ({
// Server Row Component
const ServerRow = ({ server }: { server: McpServer }) => {
const { mcpMarketplaceCatalog } = useExtensionState()
const { mcpMarketplaceCatalog, autoApprovalSettings } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
@@ -271,6 +272,17 @@ const ServerRow = ({ server }: { server: McpServer }) => {
})
}
const handleAutoApproveChange = () => {
if (!server.name) return
vscode.postMessage({
type: "toggleToolAutoApprove",
serverName: server.name,
toolNames: server.tools?.map((tool) => tool.name) || [],
autoApprove: !server.tools?.every((tool) => tool.autoApprove),
})
}
return (
<div style={{ marginBottom: "10px" }}>
<div
@@ -299,7 +311,33 @@ const ServerRow = ({ server }: { server: McpServer }) => {
}}>
{getMcpServerDisplayName(server.name, mcpMarketplaceCatalog)}
</span>
<div style={{ display: "flex", alignItems: "center", marginRight: "8px" }} onClick={(e) => e.stopPropagation()}>
{/* Collapsed view controls */}
{!server.error && (
<div style={{ display: "flex", alignItems: "center", gap: "4px", marginLeft: "8px" }}>
<VSCodeButton
appearance="icon"
title="Restart Server"
onClick={(e) => {
e.stopPropagation()
handleRestart()
}}
disabled={server.status === "connecting"}>
<span className="codicon codicon-sync"></span>
</VSCodeButton>
<VSCodeButton
appearance="icon"
title="Delete Server"
onClick={(e) => {
e.stopPropagation()
handleDelete()
}}
disabled={isDeleting}>
<span className="codicon codicon-trash"></span>
</VSCodeButton>
</div>
)}
{/* Toggle Switch */}
<div style={{ display: "flex", alignItems: "center", marginLeft: "8px" }} onClick={(e) => e.stopPropagation()}>
<div
role="switch"
aria-checked={!server.disabled}
@@ -422,6 +460,15 @@ const ServerRow = ({ server }: { server: McpServer }) => {
{server.tools.map((tool) => (
<McpToolRow key={tool.name} tool={tool} serverName={server.name} />
))}
{server.name && autoApprovalSettings.enabled && autoApprovalSettings.actions.useMcp && (
<VSCodeCheckbox
style={{ marginBottom: -10 }}
checked={server.tools.every((tool) => tool.autoApprove)}
onChange={handleAutoApproveChange}
data-tool="all-tools">
Auto-approve all tools
</VSCodeCheckbox>
)}
</div>
) : (
<div
@@ -568,7 +568,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="ap-south-1">ap-south-1</VSCodeOption>
<VSCodeOption value="ap-northeast-1">ap-northeast-1</VSCodeOption>
<VSCodeOption value="ap-northeast-2">ap-northeast-2</VSCodeOption>
{/* <VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption> */}
<VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption>
<VSCodeOption value="ap-southeast-1">ap-southeast-1</VSCodeOption>
<VSCodeOption value="ap-southeast-2">ap-southeast-2</VSCodeOption>
<VSCodeOption value="ca-central-1">ca-central-1</VSCodeOption>
@@ -577,7 +577,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="eu-west-1">eu-west-1</VSCodeOption>
<VSCodeOption value="eu-west-2">eu-west-2</VSCodeOption>
<VSCodeOption value="eu-west-3">eu-west-3</VSCodeOption>
{/* <VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption> */}
<VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption>
{/* <VSCodeOption value="me-south-1">me-south-1</VSCodeOption> */}
<VSCodeOption value="sa-east-1">sa-east-1</VSCodeOption>
<VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption>
@@ -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://ai.google.dev/"
href="https://aistudio.google.com/apikey"
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
let modelInfo = apiConfiguration?.openAiModelInfo
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
@@ -846,6 +846,22 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}}>
Supports Computer Use
</VSCodeCheckbox>
<VSCodeCheckbox
checked={!!apiConfiguration?.openAiModelInfo?.isR1FormatRequired}
onChange={(e: any) => {
const isChecked = e.target.checked === true
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo = { ...modelInfo, isR1FormatRequired: isChecked }
setApiConfiguration({
...apiConfiguration,
openAiModelInfo: modelInfo,
})
}}>
Enable R1 messages format
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
@@ -855,7 +871,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}
style={{ flex: 1 }}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
@@ -874,7 +890,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}
style={{ flex: 1 }}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
@@ -895,7 +911,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}
style={{ flex: 1 }}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.inputPrice = input.target.value
@@ -914,7 +930,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}
style={{ flex: 1 }}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.outputPrice = input.target.value
@@ -934,7 +950,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
: openAiModelInfoSaneDefaults.temperature?.toString()
}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
@@ -66,7 +66,7 @@ const OpenAiModelPicker: React.FC = () => {
}, [searchableItems])
const modelSearchResults = useMemo(() => {
let results: { id: string; html: string }[] = searchTerm
const 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
@@ -60,8 +60,12 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
}, [])
const modelIds = useMemo(() => {
return Object.keys(openRouterModels).sort((a, b) => a.localeCompare(b))
}, [openRouterModels])
const unfilteredModelIds = Object.keys(openRouterModels).sort((a, b) => a.localeCompare(b))
return apiConfiguration?.apiProvider === "cline"
? unfilteredModelIds.filter((id) => !id.includes(":free"))
: unfilteredModelIds
}, [openRouterModels, apiConfiguration?.apiProvider])
const searchableItems = useMemo(() => {
return modelIds.map((id) => ({
@@ -83,7 +87,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
}, [searchableItems])
const modelSearchResults = useMemo(() => {
let results: { id: string; html: string }[] = searchTerm
const 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
+4
View File
@@ -10,6 +10,10 @@ export default defineConfig({
environment: "jsdom",
globals: true,
setupFiles: ["./src/setupTests.ts"],
coverage: {
provider: "v8",
reportOnFailure: true,
},
},
build: {
outDir: "build",