Compare commits

..

43 Commits

Author SHA1 Message Date
Dennis Bartlett 4d9b345527 Fix Changeset 2025-05-01 16:25:47 -07:00
Tomás Barreiro 26242f6378 Fix tests: compile esmodules to cjs and bundle them (#3030)
* Run pretest in CI to build all tests

* Alias paths when running tests

* Bundle ES modules with esbuild

* alias packages
2025-04-30 17:51:32 -07:00
Tomás Barreiro 13228ed46f Fail the test workflow on test failures (#3197)
* Run pretest in CI to build all tests

* Alias paths when running tests

* Bundle ES modules with esbuild

* alias packages

* Preserve the test scripts exit code and display the output

* Remove outdated test
2025-04-30 17:51:03 -07:00
Tomás Barreiro d162a4b420 Alias paths on integration tests (#3196)
* Run pretest in CI to build all tests

* Alias paths when running tests
2025-04-30 17:49:40 -07:00
dependabot[bot] 1704684af8 Bump vite from 6.2.6 to 6.3.4 in /webview-ui in the npm_and_yarn group (#3214)
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.6 to 6.3.4
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  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-04-30 17:25:43 -07:00
pashpashpash c63d9a13a5 Fix text to say "drop" instead of "drag" (#3221)
* drag -> drop

* text fix

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-04-30 17:18:48 -07:00
Frostbourne 65243adb24 [ENG-514] Introduce UI library (#3222)
* update tailwind

* install heroui

* Introduce HeroUIProvider and reorganize providers

* changeset

* use tailwind config

* use custom theme

* changeset

* Delete .changeset/large-boxes-behave.md
2025-04-30 17:18:18 -07:00
Tomás Barreiro e35f7b4e21 Run pretest in CI to build all tests (#2930) 2025-04-30 17:17:29 -07:00
Frostbourne 82449dabd6 Revert "actually stop tasks (#3061)" (#3220)
This reverts commit 29458d7675.
2025-04-30 15:53:36 -07:00
nomaven 74ec823017 Enhances visual feedback during drag-and-drop with dashed outline and transition effects. (#3184)
* Add drag-and-drop functionality to ChatTextArea component

- Introduced state management for drag feedback with `isDraggingOver`.
- Implemented drag event handlers: `handleDragEnter`, `handleDragLeave`, and updated `onDragOver`.
- Enhanced visual feedback during drag-and-drop with dashed outline and transition effects.
- Reset drag state on drop event.

* New Ast salvage

* shift to drag

* shift to drag

* shift to drag

* shift to drag

* shift to drag

* shift to drag

* quote

* quote

* 500ms -> 100ms

* updated language and 250ms delay sweetspot

* better transitions

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-04-30 14:50:43 -07:00
Toshii 91e222fe37 Add checkpoints between all messages (#3213)
* checkpoints after messages

* changeset
2025-04-30 13:45:12 -07:00
Toshii 14230e7221 newrule (#3180)
* base

* words

* changeset
2025-04-29 15:40:51 -07:00
David Nanyan 4b697d8695 PROTOBUS: addRemoteServer message (#3147)
* PROTOBUS addRemoteServer

* Remove redundant types

* Properly handle any error when adding remote mcp server

* Refactor after code review

* remove redundant import
2025-04-29 11:33:43 -07:00
nomaven deeda6e273 Lowering the Gemini Caching TTL time to 15 minutes (#3116)
* Lowering the Gemini Cahcing TTL time to 15 minutes

* Lowering the Gemini Cahcing TTL time to 15 minutes
2025-04-29 23:55:35 +05:30
Trevor Hudson 7e7844529f add to launch json (#3173) 2025-04-29 11:09:41 -07:00
Toshii 4196c14c9c add openrouter / cline provider caching metrics to ui (#3176)
* cache reads

* changeset
2025-04-28 23:26:28 -07:00
Evan 5294e78dde Remove showMcpView message (#3171)
* refactor to not pass message for showMcpView

* changeset
2025-04-28 21:27:11 -07:00
pashpashpash d97424fcab Expanding task header by default (#3170)
* task expanded

* changeset

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-04-28 21:26:12 -07:00
Dennis Bartlett 2b3c0bb633 Add warn setting to codespell workflow (#3175) 2025-04-28 18:46:47 -07:00
Tomás Barreiro b8e2fd669d fix unit test set-up and CI workflow (#3154)
* Fix unit test set-up

* run tests on CI

* update failing tests

* Update node

Update the coverage job node version
2025-04-28 16:30:40 -07:00
Evan df7f9fcba4 It depends... (#3126)
* add protos to more dependsOn, also make it so that the scripts are always displayed and the window does not automatically close

* changeset

* add back build script
2025-04-27 21:47:49 -07:00
nomaven db0b022b6e ENG-318 feat: Show openrouter balance next to provider (#3003)
* feat: add OpenRouter base URL and balance display component

* feat: add OpenRouter base URL and balance display component
2025-04-27 16:27:28 -07:00
Wesley Smith 459adf0450 Markdown copy (#3060)
* add markdown copy

* add changeset

* fmt

---------

Co-authored-by: Wesley Smith <wes@neofactory.ai>
2025-04-27 15:24:08 -07:00
WingsDrafterwork df37f29746 Support for custom timeout (#3029)
* Support for custom timeout

* Make custom timeout visible only for ollama

* Remove parameters from other providers, only kept for ollama

* Update webview-ui/src/components/settings/ApiOptions.tsx

Co-authored-by: nomaven <arafat.da.khan@gmail.com>

---------

Co-authored-by: nomaven <arafat.da.khan@gmail.com>
2025-04-27 15:17:04 -07:00
Sarah Fortune 60c210b017 Add java options to protobufs (#3141)
Set the java class path.
2025-04-27 09:25:37 -10:00
Tomás Barreiro d4bd755e60 Set the default output cost (#3079) 2025-04-26 23:03:04 -07:00
Toshii aed152b530 narrative narrative narrative (#3121)
* notice

* changeset
2025-04-25 19:42:24 -07:00
canvrno 5a8e9d8fa8 [PROTOBUS] file openImage (#3106)
* openFile protobus

* openImage protobus

* Delete .changeset/nine-numbers-boil.md
2025-04-25 19:21:07 -07:00
Sarah Fortune 7610cecde6 [PROTOBUS] Move accountLoginClicked to protobus (#3115)
* Move accountLoginClick to protobus

* Remove ununsed import

* Add account service to handleRequest in grpc handler.

* Add documentation

* github pr un-stick empty commit

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-04-25 12:45:28 -10:00
github-actions[bot] bb26b3b64d v3.13.3 Release Notes
v3.13.3 Release Notes
2025-04-25 13:43:23 -07:00
Dennis Bartlett aabdeba0f3 Feat/change reset state colors (#3112)
* Update Reset Button color to Red

* Add Changeset
2025-04-24 23:38:32 -07:00
Dennis Bartlett fd68a81a26 Alias smol to compact (#3111) 2025-04-24 22:02:36 -07:00
Toshii 0e07b92be2 Caching (#3110)
* add

* changeset

* Fix spelling error

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-24 21:38:07 -07:00
Andrei Eternal 4addffe94c [PROTOBUS] browser getDetectedChromePath (#3001)
* protobus impl for getDetectedChromePath

* [PROTOBUS] browserSettings (#3007)

* protobus for browserSettings

* Update index.ts

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-04-24 21:36:55 -07:00
Evan 0836e4d45a add npm protos to tests.json for dev build (#3107) 2025-04-24 17:07:31 -07:00
0x23d11 4a57e5a075 fix(settings): rename AWS Bedrock to Amazon Bedrock (#3094) 2025-04-24 16:17:05 -07:00
Wesley Smith 29458d7675 actually stop tasks (#3061)
Co-authored-by: Wesley Smith <wes@neofactory.ai>
2025-04-24 13:00:32 -10:00
pashpashpash 90b0d6a73b evals formatting (#3105)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-04-24 15:26:12 -07:00
pashpashpash 547051bfa8 enabling download count on marketplace (#3104)
* enabling download count on marketplace

* changeset

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-04-24 15:25:40 -07:00
Trevor Hudson 93595af09f Trevhud/eng 592 add title tags to buttons in the bottom left corner (#3080)
* add title tags

* add tooltips, change Cline Rules name, introduce contrast to auto approve + dismiss when click outside

* change to prompts and add hook for click outside to close

* use useClickAway, delete unused component, rename back to cline rules

---------

Co-authored-by: celestial-vault <58194240+celestial-vault@users.noreply.github.com>
2025-04-24 15:24:49 -07:00
Toshii 3828c0d1bc smol (#3086)
* base

* button callback

* prompt

* smol

* full truncate

* base 2

* changeset

* dup new task resp

* Update src/core/prompts/commands.ts

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

* comments

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-04-24 15:00:44 -07:00
Evan a2263de7cb Protobus: updateMcpTimeout message (#3085)
* updateMcpTimeout protobus conversion

* changeset
2025-04-24 14:57:00 -07:00
Andrei Eternal e53fa8307d run protos, uh, more -- to prevent WAT maybe (#3091)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-04-24 11:01:21 -10:00
118 changed files with 8247 additions and 1231 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
updated drag and drop text to say "drop" instead of "drag"
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add markdown copy to chat
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add support for custom API request timeout. Previously, timeouts were hardcoded to 30 seconds for providers like Ollama or 15 seconds for OpenRouter and Cline. Now users can set a custom timeout value in milliseconds through the settings interface.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Minor UX improvement to drag and drop ux
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add dependsOn to more blocks in the tasks.json
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add truncation notice when truncating manually
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
add prompt caching to gemini models in cline / openrouter providers
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add checkpoints after more messages
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Introduce UI library for future UI development
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add newrule slash command
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
protobus migration for openImage
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add cache ui for open router and cline provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
showing expanded task by default
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Refactor to not pass a message for showing the MCP View from the servers modal
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate the addRemoteServer to protobus
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Lowering Gemini cache TTL time
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding UI to show openrouter balance next to provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix cost calculation
+2
View File
@@ -23,3 +23,5 @@ jobs:
uses: codespell-project/codespell-problem-matcher@v1
- name: Codespell
uses: codespell-project/actions-codespell@v2
with:
only_warn: 1
+19 -8
View File
@@ -23,7 +23,7 @@ jobs:
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20.15.1
node-version: 22
# Setup Python for coverage script
- name: Setup Python
@@ -70,19 +70,18 @@ jobs:
run: npm run format
# Build the extension before running tests
- name: Build Extension
run: npm run compile
- name: Build Tests and Extension
run: npm run pretest
# Disabling due to compatability with test framework and ESM modules
# - name: Unit Tests
# run: npm run test:unit
- name: Unit Tests
run: npm run test:unit
# Run extension tests with coverage
- name: Extension Tests with Coverage
id: extension_coverage
continue-on-error: true
run: |
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1 || true
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
# Run webview tests with coverage
@@ -107,6 +106,18 @@ jobs:
webview-ui/webview_coverage.txt
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
# Set the check as failed if any of the tests failed
- name: Check for test failures
run: |
# Check if any of the test steps failed
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
echo "Tests failed."
cat extension_coverage.txt
cat webview-ui/webview_coverage.txt
exit 1
fi
coverage:
needs: test
runs-on: ubuntu-latest
@@ -132,7 +143,7 @@ jobs:
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20.15.1
node-version: 22
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extension": ["ts"],
"spec": "src/**/__tests__/*.ts",
"require": ["ts-node/register", "source-map-support/register"],
"require": ["ts-node/register", "source-map-support/register", "./src/test/requires.ts"],
"recursive": true
}
+4
View File
@@ -6,6 +6,10 @@ export default defineConfig({
mocha: {
ui: "bdd",
timeout: 20000, // Maximum time (in ms) that a test can run before failing
/** Set up alias path resolution during tests
* @See {@link file://./test-setup.js}
*/
require: ["./test-setup.js"],
},
workspaceFolder: "test-workspace",
version: "stable",
+1 -1
View File
@@ -9,7 +9,7 @@
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}",
"env": {
+41 -19
View File
@@ -4,10 +4,25 @@
"version": "2.0.0",
"tasks": [
{
"label": "watch",
"dependsOn": ["npm: build:webview", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild"],
"label": "npm: protos",
"type": "npm",
"script": "protos",
"problemMatcher": [],
"isBackground": false,
"presentation": {
"reveal": "never"
"reveal": "always"
},
"options": {
"env": {
"IS_DEV": "true"
}
}
},
{
"label": "watch",
"dependsOn": ["npm: protos", "npm: build:webview", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild"],
"presentation": {
"reveal": "always"
},
"group": {
"kind": "build",
@@ -16,9 +31,15 @@
},
{
"label": "watch:test",
"dependsOn": ["npm: build:webview:test", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild:test"],
"dependsOn": [
"npm: protos",
"npm: build:webview:test",
"npm: dev:webview",
"npm: watch:tsc",
"npm: watch:esbuild:test"
],
"presentation": {
"reveal": "never"
"reveal": "always"
},
"group": "build"
},
@@ -29,10 +50,10 @@
"problemMatcher": [],
"isBackground": true,
"label": "npm: build:webview",
"dependsOn": ["npm: protos"],
"presentation": {
"group": "watch",
"reveal": "never",
"close": true
"reveal": "always"
},
"options": {
"env": {
@@ -47,10 +68,10 @@
"problemMatcher": [],
"isBackground": true,
"label": "npm: build:webview:test",
"dependsOn": ["npm: protos"],
"presentation": {
"group": "watch",
"reveal": "never",
"close": true
"reveal": "always"
},
"options": {
"env": {
@@ -82,10 +103,10 @@
],
"isBackground": true,
"label": "npm: dev:webview",
"dependsOn": ["npm: protos"],
"presentation": {
"group": "watch",
"reveal": "never",
"close": true
"reveal": "always"
},
"options": {
"env": {
@@ -100,10 +121,10 @@
"problemMatcher": "$esbuild-watch",
"isBackground": true,
"label": "npm: watch:esbuild",
"dependsOn": ["npm: protos"],
"presentation": {
"group": "watch",
"reveal": "never",
"close": true
"reveal": "always"
},
"options": {
"env": {
@@ -118,10 +139,10 @@
"problemMatcher": "$esbuild-watch",
"isBackground": true,
"label": "npm: watch:esbuild:test",
"dependsOn": ["npm: protos"],
"presentation": {
"group": "watch",
"reveal": "never",
"close": true
"reveal": "always"
},
"options": {
"env": {
@@ -137,10 +158,10 @@
"problemMatcher": "$tsc-watch",
"isBackground": true,
"label": "npm: watch:tsc",
"dependsOn": ["npm: protos"],
"presentation": {
"group": "watch",
"reveal": "never",
"close": true
"reveal": "always"
}
},
{
@@ -148,15 +169,16 @@
"script": "watch-tests",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"dependsOn": ["npm: protos"],
"presentation": {
"reveal": "never",
"reveal": "always",
"group": "watchers"
},
"group": "build"
},
{
"label": "tasks: watch-tests",
"dependsOn": ["npm: watch", "npm: watch-tests"],
"dependsOn": ["npm: protos", "npm: watch", "npm: watch-tests"],
"problemMatcher": []
},
{
+7 -1
View File
@@ -1,5 +1,12 @@
# Changelog
## [3.13.3]
- Add download counts to MCP marketplace items
- Add `/compact` command
- Add prompt caching to gemini models in cline / openrouter providers
- Add tooltips to bottom row menu
## [3.13.2]
- Add Gemini 2.5 Flash model to Vertex and Gemini Providers (Thanks monotykamary!)
@@ -19,7 +26,6 @@
- Fix Mermaid syntax error in documentation (Thanks tuki0918!)
- Remove supportsComputerUse restriction and support browser use through any model that supports images (Thanks arafatkatze!)
## [3.13.1]
- Fix bug where task cancellation during thinking stream would result in error state
+1
View File
@@ -19,6 +19,7 @@ const aliasResolverPlugin = {
"@services": path.resolve(__dirname, "src/services"),
"@shared": path.resolve(__dirname, "src/shared"),
"@utils": path.resolve(__dirname, "src/utils"),
"@packages": path.resolve(__dirname, "src/packages"),
}
// For each alias entry, create a resolver
+6 -2
View File
@@ -167,8 +167,12 @@ export class ExercismAdapter implements BenchmarkAdapter {
output += stdout + "\n"
} catch (error: any) {
success = false
if (error.stdout) output += error.stdout + "\n"
if (error.stderr) output += error.stderr + "\n"
if (error.stdout) {
output += error.stdout + "\n"
}
if (error.stderr) {
output += error.stderr + "\n"
}
}
}
-1
View File
@@ -3,7 +3,6 @@ import * as path from "path"
import * as fs from "fs"
import fetch from "node-fetch"
import * as os from "os"
import * as child_process from "child_process"
import { installRequiredExtensions, configureExtensionSettings } from "./extensions"
// Store temporary directories for cleanup
+2 -2
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.13.2",
"version": "3.13.3",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -292,7 +292,7 @@
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
"compile-tests": "tsc -p ./tsconfig.test.json --outDir out",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit",
+15
View File
@@ -0,0 +1,15 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for account-related operations
service AccountService {
// Handles the user clicking the login link in the UI.
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc accountLoginClicked(EmptyRequest) returns (String);
}
+27
View File
@@ -1,6 +1,8 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
@@ -8,6 +10,8 @@ service BrowserService {
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
rpc discoverBrowser(EmptyRequest) returns (BrowserConnection);
rpc getDetectedChromePath(EmptyRequest) returns (ChromePath);
rpc updateBrowserSettings(UpdateBrowserSettingsRequest) returns (Boolean);
}
message BrowserConnectionInfo {
@@ -21,3 +25,26 @@ message BrowserConnection {
string message = 2;
optional string endpoint = 3;
}
message ChromePath {
string path = 1;
bool is_bundled = 2;
}
message Viewport {
int32 width = 1;
int32 height = 2;
}
message BrowserSettings {
Viewport viewport = 1;
optional string remote_browser_host = 2;
optional bool remote_browser_enabled = 3;
}
message UpdateBrowserSettingsRequest {
Metadata metadata = 1;
Viewport viewport = 2;
optional string remote_browser_host = 3;
optional bool remote_browser_enabled = 4;
}
+2 -1
View File
@@ -77,10 +77,11 @@ async function generateMethodRegistrations() {
console.log(chalk.cyan("Generating method registration files..."))
const serviceDirs = [
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
path.join(ROOT_DIR, "src", "core", "controller", "account"),
path.join(ROOT_DIR, "src", "core", "controller", "browser"),
path.join(ROOT_DIR, "src", "core", "controller", "checkpoints"),
path.join(ROOT_DIR, "src", "core", "controller", "file"),
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
path.join(ROOT_DIR, "src", "core", "controller", "task"),
// Add more service directories here as needed
]
+2
View File
@@ -1,6 +1,8 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
+11
View File
@@ -1,6 +1,8 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
message Metadata {
}
@@ -38,3 +40,12 @@ message BytesRequest {
message Bytes {
bytes value = 1;
}
message BooleanRequest {
Metadata metadata = 1;
bool value = 2;
}
message Boolean {
bool value = 1;
}
+5
View File
@@ -1,6 +1,8 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
@@ -8,4 +10,7 @@ import "common.proto";
service FileService {
// Opens a file in the editor
rpc openFile(StringRequest) returns (Empty);
// Opens an image in the system viewer
rpc openImage(StringRequest) returns (Empty);
}
+22 -6
View File
@@ -1,11 +1,33 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service McpService {
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
}
message ToggleMcpServerRequest {
Metadata metadata = 1;
string server_name = 2;
bool disabled = 3;
}
message UpdateMcpTimeoutRequest {
Metadata metadata = 1;
string server_name = 2;
int32 timeout = 3;
}
message AddRemoteMcpServerRequest {
Metadata metadata = 1;
string server_name = 2;
string server_url = 3;
}
message McpTool {
@@ -49,12 +71,6 @@ message McpServer {
optional int32 timeout = 9;
}
message ToggleMcpServerRequest {
Metadata metadata = 1;
string server_name = 2;
bool disabled = 3;
}
message McpServers {
repeated McpServer mcp_servers = 1;
}
+2
View File
@@ -1,6 +1,8 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
+61
View File
@@ -0,0 +1,61 @@
const { execSync } = require("child_process")
const esbuild = require("esbuild")
const watch = process.argv.includes("--watch")
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[watch] build started")
})
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
console.error(` ${location.file}:${location.line}:${location.column}:`)
})
console.log("[watch] build finished")
})
},
}
const srcConfig = {
bundle: true,
minify: false,
sourcemap: true,
sourcesContent: true,
logLevel: "silent",
entryPoints: ["src/packages/**/*.ts"],
outdir: "out/packages",
format: "cjs",
platform: "node",
define: {
"process.env.IS_DEV": "true",
"process.env.IS_TEST": "true",
},
external: ["vscode"],
plugins: [esbuildProblemMatcherPlugin],
}
async function main() {
const srcCtx = await esbuild.context(srcConfig)
if (watch) {
await srcCtx.watch()
} else {
await srcCtx.rebuild()
await srcCtx.dispose()
}
}
execSync("tsc -p ./tsconfig.test.json --outDir out", { encoding: "utf-8" })
main().catch((e) => {
console.error(e)
process.exit(1)
})
+5
View File
@@ -74,6 +74,8 @@ export class ClineHandler implements ApiHandler {
if (!didOutputUsage && chunk.usage) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
@@ -105,6 +107,9 @@ export class ClineHandler implements ApiHandler {
const generation = response.data
return {
type: "usage",
// at this time there's no support for gatting cached_tokens from generation endpoint
cacheWriteTokens: 0,
cacheReadTokens: 0,
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
+2 -2
View File
@@ -7,8 +7,8 @@ import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, M
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
// Define a default TTL for the cache (e.g., 1 hour in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 3600
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 900
export class GeminiHandler implements ApiHandler {
private options: ApiHandlerOptions
+4 -2
View File
@@ -21,8 +21,9 @@ export class OllamaHandler implements ApiHandler {
try {
// Create a promise that rejects after timeout
const timeoutMs = this.options.requestTimeoutMs || 30000
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("Ollama request timed out after 120 seconds")), 120000)
setTimeout(() => reject(new Error(`Ollama request timed out after ${timeoutMs / 1000} seconds`)), timeoutMs)
})
// Create the actual API request promise
@@ -63,7 +64,8 @@ export class OllamaHandler implements ApiHandler {
} catch (error: any) {
// Check if it's a timeout error
if (error.message && error.message.includes("timed out")) {
throw new Error("Ollama request timed out after 120 seconds")
const timeoutMs = this.options.requestTimeoutMs || 30000
throw new Error(`Ollama request timed out after ${timeoutMs / 1000} seconds`)
}
// Enhance error reporting
+5 -2
View File
@@ -76,6 +76,8 @@ export class OpenRouterHandler implements ApiHandler {
if (!didOutputUsage && chunk.usage) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
@@ -103,8 +105,9 @@ export class OpenRouterHandler implements ApiHandler {
// console.log("OpenRouter generation details:", generation)
return {
type: "usage",
// cacheWriteTokens: 0,
// cacheReadTokens: 0,
// at this time there's no support for gatting cached_tokens from generation endpoint
cacheWriteTokens: 0,
cacheReadTokens: 0,
// openrouter generation endpoint fails often
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
+2
View File
@@ -24,6 +24,8 @@ export const toolUseNames = [
"load_mcp_documentation",
"attempt_completion",
"new_task",
"condense",
"new_rule",
] as const
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
@@ -55,7 +55,10 @@ export function parseAssistantMessage(assistantMessage: string) {
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
const contentParamName: ToolParamName = "content"
if (currentToolUse.name === "write_to_file" && accumulator.endsWith(`</${contentParamName}>`)) {
if (
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
accumulator.endsWith(`</${contentParamName}>`)
) {
const toolContent = accumulator.slice(currentToolUseStartIndex)
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
@@ -193,14 +193,20 @@ export class ContextManager {
public getNextTruncationRange(
apiMessages: Anthropic.Messages.MessageParam[],
currentDeletedRange: [number, number] | undefined,
keep: "half" | "quarter",
keep: "none" | "lastTwo" | "half" | "quarter",
): [number, number] {
// We always keep the first user-assistant pairing, and truncate an even number of messages from there
const rangeStartIndex = 2 // index 0 and 1 are kept
const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 2 // inclusive starting index
let messagesToRemove: number
if (keep === "half") {
if (keep === "none") {
// Removes all messages beyond the first core user/assistant message pair
messagesToRemove = Math.max(apiMessages.length - startOfRest, 0)
} else if (keep === "lastTwo") {
// Keep the last user-assistant pair in addition to the first core user/assistant message pair
messagesToRemove = Math.max(apiMessages.length - startOfRest - 2, 0)
} else if (keep === "half") {
// Remove half of remaining user-assistant pairs
// 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.
@@ -382,6 +388,17 @@ export class ContextManager {
return [contextHistoryUpdated, uniqueFileReadIndices]
}
/**
* Public function for triggering potentially setting the truncation message
* If the truncation message already exists, does nothing, otherwise adds the message
*/
async triggerApplyStandardContextTruncationNoticeChange(timestamp: number, taskDirectory: string) {
const updated = this.applyStandardContextTruncationNoticeChange(timestamp)
if (updated) {
await this.saveContextHistory(taskDirectory)
}
}
/**
* if there is any truncation and there is no other alteration already set, alter the assistant message to indicate this occurred
*/
@@ -34,24 +34,24 @@ describe("ContextManager", () => {
const messages = createMessages(11)
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
expect(result).to.deep.equal([1, 4])
expect(result).to.deep.equal([2, 5])
})
it("first truncation with quarter keep", () => {
const messages = createMessages(11)
const result = contextManager.getNextTruncationRange(messages, undefined, "quarter")
expect(result).to.deep.equal([1, 6])
expect(result).to.deep.equal([2, 7])
})
it("sequential truncation with half keep", () => {
const messages = createMessages(21)
const firstRange = contextManager.getNextTruncationRange(messages, undefined, "half")
expect(firstRange).to.deep.equal([1, 10])
expect(firstRange).to.deep.equal([2, 9])
// Pass the previous range for sequential truncation
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "half")
expect(secondRange).to.deep.equal([1, 14])
expect(secondRange).to.deep.equal([2, 13])
})
it("sequential truncation with quarter keep", () => {
@@ -60,7 +60,7 @@ describe("ContextManager", () => {
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "quarter")
expect(secondRange[0]).to.equal(1)
expect(secondRange[0]).to.equal(2)
expect(secondRange[1]).to.be.greaterThan(firstRange[1])
})
@@ -68,20 +68,20 @@ describe("ContextManager", () => {
const messages = createMessages(14)
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
// Check if the message at the end of range is a user message
// Check if the message at the end of range is an assistant message
const lastRemovedMessage = messages[result[1]]
expect(lastRemovedMessage.role).to.equal("user")
expect(lastRemovedMessage.role).to.equal("assistant")
// Check if the next message after the range is an assistant message
// Check if the next message after the range is a user message
const nextMessage = messages[result[1] + 1]
expect(nextMessage.role).to.equal("assistant")
expect(nextMessage.role).to.equal("user")
})
it("handles small message arrays", () => {
const messages = createMessages(3)
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
expect(result).to.deep.equal([1, 0])
expect(result).to.deep.equal([2, 1])
})
it("preserves the message structure when truncating", () => {
@@ -120,9 +120,10 @@ describe("ContextManager", () => {
const range: [number, number] = [1, 3]
const result = contextManager.getTruncatedMessages(messages, range)
expect(result).to.have.lengthOf(2)
expect(result).to.have.lengthOf(3)
expect(result[0]).to.deep.equal(messages[0])
expect(result[1]).to.deep.equal(messages[4])
expect(result[1]).to.deep.equal(messages[1])
expect(result[2]).to.deep.equal(messages[4])
})
it("works with a range that starts at the first message after task", () => {
@@ -131,20 +132,21 @@ describe("ContextManager", () => {
const range: [number, number] = [1, 2]
const result = contextManager.getTruncatedMessages(messages, range)
expect(result).to.have.lengthOf(2)
expect(result).to.have.lengthOf(3)
expect(result[0]).to.deep.equal(messages[0])
expect(result[1]).to.deep.equal(messages[3])
expect(result[1]).to.deep.equal(messages[1])
expect(result[2]).to.deep.equal(messages[3])
})
it("correctly handles removing a range while preserving alternation pattern", () => {
const messages = createMessages(5)
const range: [number, number] = [1, 2]
const range: [number, number] = [2, 3]
const result = contextManager.getTruncatedMessages(messages, range)
expect(result).to.have.lengthOf(3)
expect(result[0]).to.deep.equal(messages[0])
expect(result[1]).to.deep.equal(messages[3])
expect(result[1]).to.deep.equal(messages[1])
expect(result[2]).to.deep.equal(messages[4])
expect(result[0].role).to.equal("user")
@@ -76,26 +76,6 @@ describe("ModelContextTracker", () => {
}
})
it("should throw an error when controller is dereferenced", async () => {
// Create a new tracker with a controller that will be garbage collected
const weakTracker = new ModelContextTracker(mockContext, taskId)
// Force the WeakRef to return null by overriding the deref method
const weakRef = { deref: sandbox.stub().returns(null) }
sandbox.stub(WeakRef.prototype, "deref").callsFake(() => weakRef.deref())
try {
// Try to call the method - this should throw
await weakTracker.recordModelUsage("any-provider", "any-model", "any-mode")
// If we get here, the test should fail
expect.fail("Expected an error to be thrown")
} catch (error) {
// Verify the error message
expect(error.message).to.equal("Unable to access extension context")
}
})
it("should append model usage to existing entries", async () => {
// Add an existing model usage entry
const existingTimestamp = 1617200000000
@@ -7,6 +7,45 @@ import { ClineRulesToggles } from "@shared/cline-rules"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
import * as vscode from "vscode"
/**
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
* Doesn't do anything if .clinerules dir already exists or doesn't exist
* Returns whether there are any uncaught errors
*/
export async function ensureLocalClinerulesDirExists(cwd: string): Promise<boolean> {
const clinerulePath = path.resolve(cwd, GlobalFileNames.clineRules)
const defaultRuleFilename = "default-rules.md"
try {
const exists = await fileExistsAtPath(clinerulePath)
if (exists && !(await isDirectory(clinerulePath))) {
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
const content = await fs.readFile(clinerulePath, "utf8")
const tempPath = clinerulePath + ".bak"
await fs.rename(clinerulePath, tempPath) // create backup
try {
await fs.mkdir(clinerulePath, { recursive: true })
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
await fs.unlink(tempPath).catch(() => {}) // delete backup
return false // conversion successful with no errors
} catch (conversionError) {
// attempt to restore backup on conversion failure
try {
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
await fs.rename(tempPath, clinerulePath) // restore backup
} catch (restoreError) {}
return true // in either case here we consider this an error
}
}
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
return false
} catch (error) {
return true
}
}
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
@@ -0,0 +1,30 @@
import * as vscode from "vscode"
import crypto from "crypto"
import { Controller } from "../index"
import { storeSecret } from "../../storage/state"
/**
* Handles the user clicking the login link in the UI.
* Generates a secure nonce for state validation, stores it in secrets,
* and opens the authentication URL in the external browser.
*
* @param controller The controller instance.
* @returns The login URL as a string.
*/
export async function accountLoginClicked(controller: Controller): Promise<String> {
// Generate nonce for state validation
const nonce = crypto.randomBytes(32).toString("hex")
await storeSecret(controller.context, "authNonce", nonce)
// Open browser for authentication with state param
console.log("Login button clicked in account page")
console.log("Opening auth page with state param")
const uriScheme = vscode.env.uriScheme
const authUrl = vscode.Uri.parse(
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
)
vscode.env.openExternal(authUrl)
return authUrl.toString()
}
+14
View File
@@ -0,0 +1,14 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
import { registerAllMethods } from "./methods"
const accountService = createServiceRegistry("account")
// Export the method handler type and registration function
export type AccountMethodHandler = ServiceMethodHandler
export const registerMethod = accountService.registerMethod
// Export the request handler
export const handleAccountServiceRequest = accountService.handleRequest
// Register all account methods
registerAllMethods()
+12
View File
@@ -0,0 +1,12 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { accountLoginClicked } from "./accountLoginClicked"
// Register all account service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("accountLoginClicked", accountLoginClicked)
}
@@ -0,0 +1,30 @@
import { ChromePath } from "../../../shared/proto/browser"
import { EmptyRequest } from "../../../shared/proto/common"
import { Controller } from "../index"
import { getAllExtensionState } from "../../storage/state"
import { BrowserSession } from "../../../services/browser/BrowserSession"
/**
* Get the detected Chrome executable path
* @param controller The controller instance
* @param request The empty request message
* @returns The detected Chrome path and whether it's bundled
*/
export async function getDetectedChromePath(controller: Controller, request: EmptyRequest): Promise<ChromePath> {
try {
const { browserSettings } = await getAllExtensionState(controller.context)
const browserSession = new BrowserSession(controller.context, browserSettings)
const result = await browserSession.getDetectedChromePath()
return {
path: result.path,
isBundled: result.isBundled,
}
} catch (error) {
console.error("Error getting detected Chrome path:", error)
return {
path: "",
isBundled: false,
}
}
}
+4
View File
@@ -5,12 +5,16 @@
import { registerMethod } from "./index"
import { discoverBrowser } from "./discoverBrowser"
import { getBrowserConnectionInfo } from "./getBrowserConnectionInfo"
import { getDetectedChromePath } from "./getDetectedChromePath"
import { testBrowserConnection } from "./testBrowserConnection"
import { updateBrowserSettings } from "./updateBrowserSettings"
// Register all browser service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("discoverBrowser", discoverBrowser)
registerMethod("getBrowserConnectionInfo", getBrowserConnectionInfo)
registerMethod("getDetectedChromePath", getDetectedChromePath)
registerMethod("testBrowserConnection", testBrowserConnection)
registerMethod("updateBrowserSettings", updateBrowserSettings)
}
@@ -0,0 +1,46 @@
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
import { Boolean } from "../../../shared/proto/common"
import { Controller } from "../index"
import { updateGlobalState } from "../../storage/state"
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
/**
* Update browser settings
* @param controller The controller instance
* @param request The browser settings request message
* @returns Success response
*/
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
try {
// Convert from protobuf format to shared format
const browserSettings: SharedBrowserSettings = {
viewport: {
width: request.viewport?.width || 900,
height: request.viewport?.height || 600,
},
remoteBrowserEnabled: request.remoteBrowserEnabled || false,
remoteBrowserHost: request.remoteBrowserHost || undefined,
}
// Update global state with new settings
await updateGlobalState(controller.context, "browserSettings", browserSettings)
// Update task browser settings if task exists
if (controller.task) {
controller.task.browserSettings = browserSettings
controller.task.browserSession.browserSettings = browserSettings
}
// Post updated state to webview
await controller.postStateToWebview()
return {
value: true,
}
} catch (error) {
console.error("Error updating browser settings:", error)
return {
value: false,
}
}
}
+2
View File
@@ -4,9 +4,11 @@
// Import all method implementations
import { registerMethod } from "./index"
import { openFile } from "./openFile"
import { openImage } from "./openImage"
// Register all file service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("openFile", openFile)
registerMethod("openImage", openImage)
}
+17
View File
@@ -0,0 +1,17 @@
import { Controller } from ".."
import { Empty, StringRequest } from "@shared/proto/common"
import { openImage as openImageIntegration } from "@integrations/misc/open-file"
import { FileMethodHandler } from "./index"
/**
* Opens an image in the system viewer
* @param controller The controller instance
* @param request The request message containing the image path or data URI in the 'value' field
* @returns Empty response
*/
export const openImage: FileMethodHandler = async (controller: Controller, request: StringRequest): Promise<Empty> => {
if (request.value) {
await openImageIntegration(request.value)
}
return Empty.create()
}
+6
View File
@@ -1,4 +1,5 @@
import { Controller } from "./index"
import { handleAccountServiceRequest } from "./account"
import { handleBrowserServiceRequest } from "./browser/index"
import { handleFileServiceRequest } from "./file"
import { handleTaskServiceRequest } from "./task"
@@ -31,6 +32,11 @@ export class GrpcHandler {
}> {
try {
switch (service) {
case "cline.AccountService":
return {
message: await handleAccountServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.BrowserService":
return {
message: await handleBrowserServiceRequest(this.controller, method, message),
+24 -89
View File
@@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import type { AxiosRequestConfig } from "axios"
import crypto from "crypto"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pWaitFor from "p-wait-for"
@@ -12,7 +12,6 @@ import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import { fetchOpenGraphData, isImageUrl } from "@integrations/misc/link-preview"
import { openImage } from "@integrations/misc/open-file"
import { handleFileServiceRequest } from "./file"
import { selectImages } from "@integrations/misc/process-images"
import { getTheme } from "@integrations/theme/getTheme"
@@ -187,28 +186,6 @@ export class Controller {
*/
async handleWebviewMessage(message: WebviewMessage) {
switch (message.type) {
case "addRemoteServer": {
try {
await this.mcpHub?.addRemoteServer(message.serverName!, message.serverUrl!)
await this.postMessageToWebview({
type: "addRemoteServerResult",
addRemoteServerResult: {
success: true,
serverName: message.serverName!,
},
})
} catch (error) {
await this.postMessageToWebview({
type: "addRemoteServerResult",
addRemoteServerResult: {
success: false,
serverName: message.serverName!,
error: error.message,
},
})
}
break
}
case "authStateChanged":
await this.setUserInfo(message.user || undefined)
await this.postStateToWebview()
@@ -274,6 +251,20 @@ export class Controller {
})
break
}
case "newTask":
// Code that should run in response to the hello message command
//vscode.window.showInformationMessage(message.text!)
// Send a message to our webview.
// You can send any JSON serializable data.
// Could also do this in extension .ts
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
await this.initTask(message.text, message.images)
break
case "condense":
this.task?.handleWebviewAskResponse("yesButtonClicked")
break
case "apiConfiguration":
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
@@ -297,22 +288,6 @@ export class Controller {
}
}
break
case "browserSettings":
if (message.browserSettings) {
// remoteBrowserEnabled now means "enable remote browser connection"
// commenting out since this is being done in BrowserSettingsSection updateRemoteBrowserEnabled
// if (!message.browserSettings.remoteBrowserEnabled) {
// // If disabling remote browser connection, clear the remoteBrowserHost
// message.browserSettings.remoteBrowserHost = undefined
// }
await updateGlobalState(this.context, "browserSettings", message.browserSettings)
if (this.task) {
this.task.browserSettings = message.browserSettings
this.task.browserSession.browserSettings = message.browserSettings
}
await this.postStateToWebview()
}
break
case "togglePlanActMode":
if (message.chatSettings) {
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
@@ -395,9 +370,6 @@ export class Controller {
await refreshClineRulesToggles(this.context, cwd)
await this.postStateToWebview()
break
case "openImage":
openImage(message.text!)
break
case "openInBrowser":
if (message.url) {
vscode.env.openExternal(vscode.Uri.parse(message.url))
@@ -451,23 +423,6 @@ export class Controller {
case "getLatestState":
await this.postStateToWebview()
break
case "accountLoginClicked": {
// Generate nonce for state validation
const nonce = crypto.randomBytes(32).toString("hex")
await storeSecret(this.context, "authNonce", nonce)
// Open browser for authentication with state param
console.log("Login button clicked in account page")
console.log("Opening auth page with state param")
const uriScheme = vscode.env.uriScheme
const authUrl = vscode.Uri.parse(
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
)
vscode.env.openExternal(authUrl)
break
}
case "accountLogoutClicked": {
await this.handleSignOut()
break
@@ -480,10 +435,6 @@ export class Controller {
await this.fetchUserCreditsData()
break
}
case "showMcpView": {
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked", tab: message.tab || undefined })
break
}
case "openMcpSettings": {
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
if (mcpSettingsFilePath) {
@@ -646,16 +597,6 @@ export class Controller {
}
break
}
case "updateMcpTimeout": {
try {
if (message.serverName && message.timeout) {
await this.mcpHub?.updateServerTimeout(message.serverName, message.timeout)
}
} catch (error) {
console.error(`Failed to update timeout for server ${message.serverName}:`, error)
}
break
}
case "openExtensionSettings": {
const settingsFilter = message.text || ""
await vscode.commands.executeCommand(
@@ -728,21 +669,6 @@ export class Controller {
this.postMessageToWebview({ type: "relinquishControl" })
break
}
case "getDetectedChromePath": {
try {
const { browserSettings } = await getAllExtensionState(this.context)
const browserSession = new BrowserSession(this.context, browserSettings)
const { path, isBundled } = await browserSession.getDetectedChromePath()
await this.postMessageToWebview({
type: "detectedChromePath",
text: path,
isBundled,
})
} catch (error) {
console.error("Error getting detected Chrome path:", error)
}
break
}
case "getRelativePaths": {
if (message.uris && message.uris.length > 0) {
const resolvedPaths = await Promise.all(
@@ -849,6 +775,15 @@ export class Controller {
}
break
}
case "copyToClipboard": {
try {
await vscode.env.clipboard.writeText(message.text || "")
} catch (error) {
console.error("Error copying to clipboard:", error)
}
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -0,0 +1,32 @@
import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
import type { AddRemoteMcpServerRequest, McpServers } from "../../../shared/proto/mcp"
import type { Controller } from "../index"
/**
* Adds a new remote MCP server via gRPC
* @param controller The controller instance
* @param request The request containing server name and URL
* @returns An array of McpServer objects
*/
export async function addRemoteMcpServer(controller: Controller, request: AddRemoteMcpServerRequest): Promise<McpServers> {
try {
// Validate required fields
if (!request.serverName) {
throw new Error("Server name is required")
}
if (!request.serverUrl) {
throw new Error("Server URL is required")
}
// Call the McpHub method to add the remote server
const servers = await controller.mcpHub?.addRemoteServer(request.serverName, request.serverUrl)
const protoServers = convertMcpServersToProtoMcpServers(servers)
return { mcpServers: protoServers }
} catch (error) {
console.error(`Failed to add remote MCP server ${request.serverName}:`, error)
throw error
}
}
+4
View File
@@ -3,10 +3,14 @@
// Import all method implementations
import { registerMethod } from "./index"
import { addRemoteMcpServer } from "./addRemoteMcpServer"
import { toggleMcpServer } from "./toggleMcpServer"
import { updateMcpTimeout } from "./updateMcpTimeout"
// Register all mcp service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
registerMethod("toggleMcpServer", toggleMcpServer)
registerMethod("updateMcpTimeout", updateMcpTimeout)
}
@@ -0,0 +1,27 @@
import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
import { Controller } from ".."
import { UpdateMcpTimeoutRequest, McpServers } from "../../../shared/proto/mcp"
/**
* Updates the timeout configuration for an MCP server.
* @param controller - The Controller instance
* @param request - Contains server name and timeout value
* @returns Array of updated McpServer objects
*/
export async function updateMcpTimeout(controller: Controller, request: UpdateMcpTimeoutRequest): Promise<McpServers> {
try {
if (request.serverName && typeof request.serverName === "string" && typeof request.timeout === "number") {
const mcpServers = await controller.mcpHub?.updateServerTimeoutRPC(request.serverName, request.timeout)
console.log("mcpServers", mcpServers)
const convertedMcpServers = convertMcpServersToProtoMcpServers(mcpServers)
console.log("convertedMcpServers", convertedMcpServers)
return { mcpServers: convertedMcpServers }
} else {
console.error("Server name and timeout are required")
throw new Error("Server name and timeout are required")
}
} catch (error) {
console.error(`Failed to update timeout for server ${request.serverName}:`, error)
throw error
}
}
+120
View File
@@ -25,3 +25,123 @@ Usage:
Below is the the user's input when they indicated that they wanted to create a new task.
</explicit_instructions>\n
`
export const condenseToolResponse = () =>
`<explicit_instructions type="condense">
The user has explicitly asked you to create a detailed summary of the conversation so far, which will be used to compact the current context window while retaining key information. The user may have provided instructions or additional information for you to consider when summarizing the conversation.
Irrespective of whether additional information or instructions are given, you are only allowed to respond to this message by calling the condense tool.
The condense tool is defined below:
Description:
Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks.
The user will be presented with a preview of your generated summary and can choose to use it to compact their context window or keep chatting in the current conversation.
Users may refer to this tool as 'smol' or 'compact' as well. You should consider these to be equivalent to 'condense' when used in a similar context.
Parameters:
- Context: (required) The context to continue the conversation with. If applicable based on the current task, this should include:
1. Previous Conversation: High level details about what was discussed throughout the entire conversation with the user. This should be written to allow someone to be able to follow the general overarching conversation flow.
2. Current Work: Describe in detail what was being worked on prior to this request to compact the context window. Pay special attention to the more recent messages / conversation.
3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work.
4. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes.
5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts.
6. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks.
Usage:
<condense>
<context>Your detailed summary</context>
</condense>
Example:
<condense>
<context>
1. Previous Conversation:
[Detailed description]
2. Current Work:
[Detailed description]
3. Key Technical Concepts:
- [Concept 1]
- [Concept 2]
- [...]
4. Relevant Files and Code:
- [File Name 1]
- [Summary of why this file is important]
- [Summary of the changes made to this file, if any]
- [Important Code Snippet]
- [File Name 2]
- [Important Code Snippet]
- [...]
5. Problem Solving:
[Detailed description]
6. Pending Tasks and Next Steps:
- [Task 1 details & next steps]
- [Task 2 details & next steps]
- [...]
</context>
</condense>
</explicit_instructions>\n
`
export const newRuleToolResponse = () =>
`<explicit_instructions type="new_rule">
The user has explicitly asked you to help them create a new Cline rule file inside the .clinerules top-level directory based on the conversation up to this point in time. The user may have provided instructions or additional information for you to consider when creating the new Cline rule.
When creating a new Cline rule file, you should NOT overwrite or alter an existing Cline rule file. To create the Cline rule file you MUST use the new_rule tool. The new_rule tool can be used in either of the PLAN or ACT modes.
The new_rule tool is defined below:
Description:
Your task is to create a new Cline rule file which includes guidelines on how to approach developing code in tandem with the user, which can be either project specific or cover more global rules. This includes but is not limited to: desired conversational style, favorite project dependencies, coding styles, naming conventions, architectural choices, ui/ux preferences, etc.
The Cline rule file must be formatted as markdown and be a '.md' file. The name of the file you generate must be as succinct as possible and be encompassing the main overarching concept of the rules you added to the file (e.g., 'memory-bank.md' or 'project-overview.md').
Parameters:
- Path: (required) The path of the file to write to (relative to the current working directory). This will be the Cline rule file you create, and it must be placed inside the .clinerules top-level directory (create this if it doesn't exist). The filename created CANNOT be "default-clineignore.md". For filenames, use hyphens ("-") instead of underscores ("_") to separate words.
- Content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. The content for the Cline rule file MUST be created according to the following instructions:
1. Format the Cline rule file to have distinct guideline sections, each with their own markdown heading, starting with "## Brief overview". Under each of these headings, include bullet points fully fleshing out the details, with examples and/or trigger cases ONLY when applicable.
2. These guidelines can be specific to the task(s) or project worked on thus far, or cover more high-level concepts. Guidelines can include coding conventions, general design patterns, preferred tech stack including favorite libraries and language, communication style with Cline (verbose vs concise), prompting strategies, naming conventions, testing strategies, comment verbosity, time spent on architecting prior to development, and other preferences.
3. When creating guidelines, you should not invent preferences or make assumptions based on what you think a typical user might want. These should be specific to the conversation you had with the user. Your guidelines / rules should not be overly verbose.
4. Your guidelines should NOT be a recollection of the conversation up to this point in time, meaning you should NOT be including arbitrary details of the conversation.
Usage:
<new_rule>
<path>.clinerules/{file name}.md</path>
<content>Cline rule file content here</content>
</new_rule>
Example:
<new_rule>
<path>.clinerules/project-preferences.md</path>
<content>
## Brief overview
[Brief description of the rules, including if this set of guidelines is project-specific or global]
## Communication style
- [Description, rule, preference, instruction]
- [...]
## Development workflow
- [Description, rule, preference, instruction]
- [...]
## Coding best practices
- [Description, rule, preference, instruction]
- [...]
## Project context
- [Description, rule, preference, instruction]
- [...]
## Other guidelines
- [Description, rule, preference, instruction]
- [...]
</content>
</new_rule>
Below is the user's input when they indicated that they wanted to create a new Cline rule file.
</explicit_instructions>\n
`
+3
View File
@@ -10,6 +10,9 @@ export const formatResponse = {
contextTruncationNotice: () =>
`[NOTE] Some previous conversation history with the user has been removed to maintain optimal context window length. The initial user task and the most recent exchanges have been retained for continuity, while intermediate conversation history has been removed. Please keep this in mind as you continue assisting the user.`,
condense: () =>
`The user has accepted the condensed conversation summary you generated. This summary covers important details of the historical conversation with the user which has been truncated.\n<explicit_instructions type="condense_response">It's crucial that you respond by ONLY asking the user what you should work on next. You should NOT take any initiative or make any assumptions about continuing with work. For example you should NOT suggest file changes or attempt to read any files.\nWhen asking the user what you should work on next, you can reference information in the summary which was just generated. However, you should NOT reference information outside of what's contained in the summary for this response. Keep this response CONCISE.</explicit_instructions>`,
toolDenied: () => `The user denied this operation.`,
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
+8 -5
View File
@@ -1,14 +1,17 @@
import { newTaskToolResponse } from "../prompts/commands"
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse } from "../prompts/commands"
/**
* Processes text for slash commands and transforms them with appropriate instructions
* This is called after parseMentions() to process any slash commands in the user's message
*/
export function parseSlashCommands(text: string): string {
const SUPPORTED_COMMANDS = ["newtask"]
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule"]
const commandReplacements: Record<string, string> = {
newtask: newTaskToolResponse(),
smol: condenseToolResponse(),
compact: condenseToolResponse(),
newrule: newRuleToolResponse(),
}
// this currently allows matching prepended whitespace prior to /slash-command
@@ -45,11 +48,11 @@ export function parseSlashCommands(text: string): string {
const textWithoutSlashCommand = text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
const processedText = commandReplacements[commandName] + textWithoutSlashCommand
return processedText
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
}
}
}
// if no supported commands are found, return the original text
return text
return { processedText: text, needsClinerulesFileCheck: false }
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { fileExistsAtPath } from "@utils/fs"
import { ClineMessage } from "@shared/ExtensionMessage"
import { TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
import os from "os"
import { execa } from "execa"
import { execa } from "@packages/execa"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
+1
View File
@@ -75,5 +75,6 @@ export type GlobalStateKey =
| "reasoningEffort"
| "planActSeparateModelsSetting"
| "favoritedModelIds"
| "requestTimeoutMs"
export type LocalStateKey = "localClineRulesToggles"
+4
View File
@@ -125,6 +125,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
planActSeparateModelsSettingRaw,
favoritedModelIds,
globalClineRulesToggles,
requestTimeoutMs,
] = await Promise.all([
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -198,6 +199,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "requestTimeoutMs") as Promise<number | undefined>,
])
let apiProvider: ApiProvider
@@ -297,6 +299,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
xaiApiKey,
sambanovaApiKey,
favoritedModelIds,
requestTimeoutMs,
},
lastShownAnnouncementId,
customInstructions,
@@ -434,6 +437,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await storeSecret(context, "clineApiKey", clineApiKey)
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
await updateGlobalState(context, "requestTimeoutMs", apiConfiguration.requestTimeoutMs)
}
export async function resetExtensionState(context: vscode.ExtensionContext) {
+196 -52
View File
@@ -86,6 +86,7 @@ import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
ensureLocalClinerulesDirExists,
} from "@core/context/instructions/user-instructions/cline-rules"
import { getGlobalState } from "@core/storage/state"
import { parseSlashCommands } from "@core/slash-commands"
@@ -926,6 +927,7 @@ export class Task {
let responseImages: string[] | undefined
if (response === "messageResponse") {
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
responseText = text
responseImages = images
}
@@ -1325,6 +1327,7 @@ export class Task {
if (userFeedback) {
await this.say("user_feedback", userFeedback.text, userFeedback.images)
await this.saveCheckpoint()
return [
true,
formatResponse.toolResult(
@@ -1361,6 +1364,7 @@ export class Task {
this.autoApprovalSettings.actions.readFiles,
this.autoApprovalSettings.actions.readFilesExternally ?? false,
]
case "new_rule":
case "write_to_file":
case "replace_in_file":
return [
@@ -1505,6 +1509,10 @@ export class Task {
"quarter", // Force aggressive truncation
)
await this.saveClineMessagesAndUpdateHistory()
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
Date.now(),
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
)
this.didAutomaticallyRetryFailedApiRequest = true
} else if (isOpenRouter && !this.didAutomaticallyRetryFailedApiRequest) {
@@ -1515,6 +1523,10 @@ export class Task {
"quarter", // Force aggressive truncation
)
await this.saveClineMessagesAndUpdateHistory()
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
Date.now(),
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
)
}
console.log("first chunk failed, waiting 1 second before retrying")
@@ -1676,6 +1688,10 @@ export class Task {
return `[${block.name}]`
case "new_task":
return `[${block.name} for creating a new task]`
case "condense":
return `[${block.name}]`
case "new_rule":
return `[${block.name} for '${block.params.path}']`
}
}
@@ -1749,6 +1765,7 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
this.didRejectTool = true // Prevent further tool uses in this message
return false
@@ -1757,6 +1774,7 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
return true
}
@@ -1815,6 +1833,7 @@ export class Task {
}
switch (block.name) {
case "new_rule":
case "write_to_file":
case "replace_in_file": {
const relPath: string | undefined = block.params.path
@@ -1830,7 +1849,7 @@ export class Task {
if (!accessAllowed) {
await this.say("clineignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
await this.saveCheckpoint()
break
}
@@ -1886,6 +1905,7 @@ export class Task {
)
await this.diffViewProvider.revertChanges()
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
} else if (content) {
@@ -1943,21 +1963,28 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError(block.name, "path"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
if (block.name === "replace_in_file" && !diff) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("replace_in_file", "diff"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
if (block.name === "write_to_file" && !content) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "content"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
if (block.name === "new_rule" && !content) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("new_rule", "content"))
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
@@ -2016,6 +2043,7 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
this.didRejectTool = true
didApprove = false
@@ -2025,12 +2053,14 @@ export class Task {
if (text || images?.length) {
pushAdditionalToolFeedback(text, images)
await this.say("user_feedback", text, images)
await this.saveCheckpoint()
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
if (!didApprove) {
await this.diffViewProvider.revertChanges()
await this.saveCheckpoint()
break
}
}
@@ -2091,7 +2121,7 @@ export class Task {
await handleError("writing file", error)
await this.diffViewProvider.revertChanges()
await this.diffViewProvider.reset()
await this.saveCheckpoint()
break
}
}
@@ -2120,7 +2150,7 @@ export class Task {
if (!relPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
await this.saveCheckpoint()
break
}
@@ -2128,7 +2158,7 @@ export class Task {
if (!accessAllowed) {
await this.say("clineignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
await this.saveCheckpoint()
break
}
@@ -2151,6 +2181,7 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "tool")
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
await this.saveCheckpoint()
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
break
}
@@ -2163,12 +2194,12 @@ export class Task {
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
pushToolResult(content)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("reading file", error)
await this.saveCheckpoint()
break
}
}
@@ -2199,7 +2230,7 @@ export class Task {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2232,17 +2263,18 @@ export class Task {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
await this.saveCheckpoint()
break
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(result)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("listing files", error)
await this.saveCheckpoint()
break
}
}
@@ -2271,7 +2303,7 @@ export class Task {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("list_code_definition_names", "path"))
await this.saveCheckpoint()
break
}
@@ -2301,17 +2333,18 @@ export class Task {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
await this.saveCheckpoint()
break
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(result)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("parsing source code definitions", error)
await this.saveCheckpoint()
break
}
}
@@ -2344,13 +2377,13 @@ export class Task {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "path"))
await this.saveCheckpoint()
break
}
if (!regex) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "regex"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2382,17 +2415,18 @@ export class Task {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
await this.saveCheckpoint()
break
}
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
}
pushToolResult(results)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("searching files", error)
await this.saveCheckpoint()
break
}
}
@@ -2408,6 +2442,7 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "action"))
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
}
break
}
@@ -2451,7 +2486,7 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "url"))
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2467,6 +2502,7 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "browser_action_launch")
const didApprove = await askApproval("browser_action_launch", url)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2492,7 +2528,7 @@ export class Task {
await this.sayAndCreateMissingParamError("browser_action", "coordinate"),
)
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
break // can't be within an inner switch
}
}
@@ -2501,7 +2537,7 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "text"))
await this.browserSession.closeBrowser()
await this.saveCheckpoint()
break
}
}
@@ -2550,7 +2586,7 @@ export class Task {
browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
),
)
await this.saveCheckpoint()
break
case "close":
pushToolResult(
@@ -2558,7 +2594,7 @@ export class Task {
`The browser has been closed. You may now proceed to using other tools.`,
),
)
await this.saveCheckpoint()
break
}
@@ -2567,7 +2603,7 @@ export class Task {
} catch (error) {
await this.browserSession.closeBrowser() // if any error occurs, the browser session is terminated
await handleError("executing browser action", error)
await this.saveCheckpoint()
break
}
}
@@ -2595,7 +2631,7 @@ export class Task {
if (!command) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("execute_command", "command"))
await this.saveCheckpoint()
break
}
if (!requiresApprovalRaw) {
@@ -2603,7 +2639,7 @@ export class Task {
pushToolResult(
await this.sayAndCreateMissingParamError("execute_command", "requires_approval"),
)
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2619,7 +2655,7 @@ export class Task {
pushToolResult(
formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)),
)
await this.saveCheckpoint()
break
}
@@ -2651,6 +2687,7 @@ export class Task {
`${this.shouldAutoApproveTool(block.name) && requiresApprovalPerLLM ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2686,7 +2723,7 @@ export class Task {
}
} catch (error) {
await handleError("executing command", error)
await this.saveCheckpoint()
break
}
}
@@ -2716,13 +2753,13 @@ export class Task {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
await this.saveCheckpoint()
break
}
if (!tool_name) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
await this.saveCheckpoint()
break
}
// arguments are optional, but if they are provided they must be valid JSON
@@ -2746,7 +2783,7 @@ export class Task {
formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
),
)
await this.saveCheckpoint()
break
}
}
@@ -2773,6 +2810,7 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2824,7 +2862,7 @@ export class Task {
}
} catch (error) {
await handleError("executing MCP tool", error)
await this.saveCheckpoint()
break
}
}
@@ -2852,13 +2890,13 @@ export class Task {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"))
await this.saveCheckpoint()
break
}
if (!uri) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2879,6 +2917,7 @@ export class Task {
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
await this.saveCheckpoint()
break
}
}
@@ -2898,12 +2937,12 @@ export class Task {
.join("\n\n") || "(Empty response)"
await this.say("mcp_server_response", resourceResultPretty)
pushToolResult(formatResponse.toolResult(resourceResultPretty))
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("accessing MCP resource", error)
await this.saveCheckpoint()
break
}
}
@@ -2922,7 +2961,7 @@ export class Task {
if (!question) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("ask_followup_question", "question"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2959,12 +2998,12 @@ export class Task {
}
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("asking question", error)
await this.saveCheckpoint()
break
}
}
@@ -2978,6 +3017,7 @@ export class Task {
if (!context) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "context"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -3006,10 +3046,74 @@ export class Task {
formatResponse.toolResult(`The user has created a new task with the provided context.`),
)
}
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("creating new task", error)
await this.saveCheckpoint()
break
}
}
case "condense": {
const context: string | undefined = block.params.context
try {
if (block.partial) {
await this.ask("condense", removeClosingTag("context", context), block.partial).catch(() => {})
break
} else {
if (!context) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("condense", "context"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
showSystemNotification({
subtitle: "Cline wants to condense the conversation...",
message: `Cline is suggesting to condense your conversation with: ${context}`,
})
}
const { text, images } = await this.ask("condense", context, false)
// If the user provided a response, treat it as feedback
if (text || images?.length) {
await this.say("user_feedback", text ?? "", images)
pushToolResult(
formatResponse.toolResult(
`The user provided feedback on the condensed conversation summary:\n<feedback>\n${text}\n</feedback>`,
images,
),
)
} else {
// If no response, the user accepted the condensed version
pushToolResult(formatResponse.toolResult(formatResponse.condense()))
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
const summaryAlreadyAppended = lastMessage && lastMessage.role === "assistant"
const keepStrategy = summaryAlreadyAppended ? "lastTwo" : "none"
// clear the context history at this point in time
this.conversationHistoryDeletedRange = this.contextManager.getNextTruncationRange(
this.apiConversationHistory,
this.conversationHistoryDeletedRange,
keepStrategy,
)
await this.saveClineMessagesAndUpdateHistory()
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
Date.now(),
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
)
}
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("condensing context window", error)
await this.saveCheckpoint()
break
}
}
@@ -3070,6 +3174,7 @@ export class Task {
if (text || images?.length) {
telemetryService.captureOptionsIgnored(this.taskId, options.length, "plan")
await this.say("user_feedback", text ?? "", images)
await this.saveCheckpoint()
}
}
@@ -3216,12 +3321,14 @@ export class Task {
// complete command message
const didApprove = await askApproval("command", command)
if (!didApprove) {
await this.saveCheckpoint()
break
}
const [userRejected, execCommandResult] = await this.executeCommandTool(command!)
if (userRejected) {
this.didRejectTool = true
pushToolResult(execCommandResult)
await this.saveCheckpoint()
break
}
// user didn't reject, but the command may have output
@@ -3240,6 +3347,7 @@ export class Task {
break
}
await this.say("user_feedback", text ?? "", images)
await this.saveCheckpoint()
const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
if (commandResult) {
@@ -3268,7 +3376,7 @@ export class Task {
}
} catch (error) {
await handleError("attempting completion", error)
await this.saveCheckpoint()
break
}
}
@@ -3370,9 +3478,6 @@ export class Task {
// Save checkpoint if this is the first API request
const isFirstRequest = this.clineMessages.filter((m) => m.say === "api_req_started").length === 0
if (isFirstRequest) {
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
}
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
@@ -3383,6 +3488,10 @@ export class Task {
}),
)
if (isFirstRequest) {
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
}
// use this opportunity to initialize the checkpoint tracker (can be expensive to initialize in the constructor)
// FIXME: right now we're letting users init checkpoints for old tasks, but this could be a problem if opening a task in the wrong workspace
// isNewTask &&
@@ -3413,7 +3522,16 @@ export class Task {
}
}
const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
const [parsedUserContent, environmentDetails, clinerulesError] = await this.loadContext(userContent, includeFileDetails)
// error handling if the user uses the /newrule command & their .clinerules is a file, for file read operations didnt work properly
if (clinerulesError === true) {
await this.say(
"error",
"Issue with processing the /newrule command. Double check that, if '.clinerules' already exists, it's a directory and not a file. Otherwise there was an issue referencing this file/directory.",
)
}
userContent = parsedUserContent
// add environment details as its own text block, separate from tool results
userContent.push({ type: "text", text: environmentDetails })
@@ -3698,11 +3816,14 @@ export class Task {
}
}
async loadContext(userContent: UserContent, includeFileDetails: boolean = false) {
return await Promise.all([
async loadContext(userContent: UserContent, includeFileDetails: boolean = false): Promise<[UserContent, string, boolean]> {
// Track if we need to check clinerulesFile
let needsClinerulesFileCheck = false
const processUserContent = async () => {
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
// (Note: this caused the @/ import alias bug where file contents were being parsed as well, since v2 converted tool results to text blocks)
Promise.all(
return await Promise.all(
userContent.map(async (block) => {
if (block.type === "text") {
// We need to ensure any user generated content is wrapped in one of these tags so that we know to parse mentions
@@ -3713,22 +3834,45 @@ export class Task {
block.text.includes("<task>") ||
block.text.includes("<user_message>")
) {
let parsedText = await parseMentions(block.text, cwd, this.urlContentFetcher, this.fileContextTracker)
const parsedText = await parseMentions(
block.text,
cwd,
this.urlContentFetcher,
this.fileContextTracker,
)
// when parsing slash commands, we still want to allow the user to provide their desired context
parsedText = parseSlashCommands(parsedText)
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
if (needsCheck) {
needsClinerulesFileCheck = true
}
return {
...block,
text: parsedText,
text: processedText,
}
}
}
return block
}),
),
)
}
// Run initial promises in parallel
const [processedUserContent, environmentDetails] = await Promise.all([
processUserContent(),
this.getEnvironmentDetails(includeFileDetails),
])
// After processing content, check clinerulesData if needed
let clinerulesError = false
if (needsClinerulesFileCheck) {
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
}
// Return all results
return [processedUserContent, environmentDetails, clinerulesError]
}
async getEnvironmentDetails(includeFileDetails: boolean = false) {
+1
View File
@@ -0,0 +1 @@
export { execa } from "execa"
+74 -25
View File
@@ -329,10 +329,9 @@ export class McpHub {
autoApprove: autoApproveConfig.includes(tool.name),
}))
// console.log(`[MCP] Fetched tools for ${serverName}:`, tools)
return tools
} catch (error) {
// console.error(`Failed to fetch tools for ${serverName}:`, error)
console.error(`Failed to fetch tools for ${serverName}:`, error)
return []
}
}
@@ -377,6 +376,53 @@ export class McpHub {
}
}
async updateServerConnectionsRPC(newServers: Record<string, McpServerConfig>): Promise<void> {
this.isConnecting = true
this.removeAllFileWatchers()
const currentNames = new Set(this.connections.map((conn) => conn.server.name))
const newNames = new Set(Object.keys(newServers))
// Delete removed servers
for (const name of currentNames) {
if (!newNames.has(name)) {
await this.deleteConnection(name)
console.log(`Deleted MCP server: ${name}`)
}
}
// Update or add servers
for (const [name, config] of Object.entries(newServers)) {
const currentConnection = this.connections.find((conn) => conn.server.name === name)
if (!currentConnection) {
// New server
try {
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)
}
} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
// Existing server with changed config
try {
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}`)
} catch (error) {
console.error(`Failed to reconnect MCP server ${name}:`, error)
}
}
// If server exists with same config, do nothing
}
this.isConnecting = false
}
async updateServerConnections(newServers: Record<string, McpServerConfig>): Promise<void> {
this.isConnecting = true
this.removeAllFileWatchers()
@@ -475,6 +521,21 @@ export class McpHub {
this.isConnecting = false
}
/**
* Gets sorted MCP servers based on the order defined in settings
* @param serverOrder Array of server names in the order they appear in settings
* @returns Array of McpServer objects sorted according to settings order
*/
private getSortedMcpServers(serverOrder: string[]): McpServer[] {
return [...this.connections]
.sort((a, b) => {
const indexA = serverOrder.indexOf(a.server.name)
const indexB = serverOrder.indexOf(b.server.name)
return indexA - indexB
})
.map((connection) => connection.server)
}
private async notifyWebviewOfServerChanges(): Promise<void> {
// servers should always be sorted in the order they are defined in the settings file
const settingsPath = await this.getMcpSettingsFilePath()
@@ -483,13 +544,7 @@ export class McpHub {
const serverOrder = Object.keys(config.mcpServers || {})
await this.postMessageToWebview({
type: "mcpServers",
mcpServers: [...this.connections]
.sort((a, b) => {
const indexA = serverOrder.indexOf(a.server.name)
const indexB = serverOrder.indexOf(b.server.name)
return indexA - indexB
})
.map((connection) => connection.server),
mcpServers: this.getSortedMcpServers(serverOrder),
})
}
@@ -520,16 +575,7 @@ export class McpHub {
}
const serverOrder = Object.keys(config.mcpServers || {})
const mcpServers = [...this.connections]
.sort((a, b) => {
const indexA = serverOrder.indexOf(a.server.name)
const indexB = serverOrder.indexOf(b.server.name)
return indexA - indexB
})
.map((connection) => connection.server)
return mcpServers
return this.getSortedMcpServers(serverOrder)
}
console.error(`Server "${serverName}" not found in MCP configuration`)
throw new Error(`Server "${serverName}" not found in MCP configuration`)
@@ -645,7 +691,7 @@ export class McpHub {
}
}
public async addRemoteServer(serverName: string, serverUrl: string) {
public async addRemoteServer(serverName: string, serverUrl: string): Promise<McpServer[]> {
try {
const settings = await this.readAndValidateMcpSettingsFile()
if (!settings) {
@@ -682,12 +728,12 @@ export class McpHub {
JSON.stringify({ mcpServers: { ...settings.mcpServers, [serverName]: serverConfig } }, null, 2),
)
await this.updateServerConnections(settings.mcpServers)
await this.updateServerConnectionsRPC(settings.mcpServers)
vscode.window.showInformationMessage(`Added ${serverName} MCP server`)
const serverOrder = Object.keys(settings.mcpServers || {})
return this.getSortedMcpServers(serverOrder)
} catch (error) {
console.error("Failed to add remote MCP server:", error)
throw error
}
}
@@ -719,7 +765,7 @@ export class McpHub {
}
}
public async updateServerTimeout(serverName: string, timeout: number): Promise<void> {
public async updateServerTimeoutRPC(serverName: string, timeout: number): Promise<McpServer[]> {
try {
// Validate timeout against schema
const setConfigResult = BaseConfigSchema.shape.timeout.safeParse(timeout)
@@ -742,7 +788,10 @@ export class McpHub {
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
await this.updateServerConnections(config.mcpServers)
await this.updateServerConnectionsRPC(config.mcpServers)
const serverOrder = Object.keys(config.mcpServers || {})
return this.getSortedMcpServers(serverOrder)
} catch (error) {
console.error("Failed to update server timeout:", error)
if (error instanceof Error) {
+1 -8
View File
@@ -37,14 +37,12 @@ export interface ExtensionMessage {
| "openGraphData"
| "isImageUrlResult"
| "didUpdateSettings"
| "addRemoteServerResult"
| "userCreditsBalance"
| "userCreditsUsage"
| "userCreditsPayments"
| "totalTasksSize"
| "addToInput"
| "browserConnectionResult"
| "detectedChromePath"
| "scrollToSettings"
| "browserRelaunchResult"
| "relativePathsResponse" // Handles single and multiple path responses
@@ -58,7 +56,6 @@ export interface ExtensionMessage {
| "settingsButtonClicked"
| "historyButtonClicked"
| "didBecomeVisible"
| "accountLoginClicked"
| "accountLogoutClicked"
| "accountButtonClicked"
| "focusChatInput"
@@ -105,11 +102,6 @@ export interface ExtensionMessage {
type: "file" | "folder"
label?: string
}>
addRemoteServerResult?: {
success: boolean
serverName: string
error?: string
}
tab?: McpViewTab
grpc_response?: {
message?: any // JSON serialized protobuf message
@@ -183,6 +175,7 @@ export type ClineAsk =
| "browser_action_launch"
| "use_mcp_server"
| "new_task"
| "condense"
export type ClineSay =
| "task"
+3 -9
View File
@@ -9,9 +9,10 @@ import { McpViewTab } from "./mcp"
export interface WebviewMessage {
type:
| "addRemoteServer"
| "apiConfiguration"
| "webviewDidLaunch"
| "newTask"
| "condense"
| "askResponse"
| "didShowAnnouncement"
| "selectImages"
@@ -22,7 +23,6 @@ export interface WebviewMessage {
| "resetState"
| "requestOllamaModels"
| "requestLmStudioModels"
| "openImage"
| "openInBrowser"
| "createRuleFile"
| "openMention"
@@ -35,16 +35,13 @@ export interface WebviewMessage {
| "restartMcpServer"
| "deleteMcpServer"
| "autoApprovalSettings"
| "browserSettings"
| "browserRelaunchResult"
| "togglePlanActMode"
| "taskCompletionViewChanges"
| "openExtensionSettings"
| "requestVsCodeLmModels"
| "toggleToolAutoApprove"
| "toggleMcpServer"
| "getLatestState"
| "accountLoginClicked"
| "accountLogoutClicked"
| "showAccountViewClicked"
| "authStateChanged"
@@ -53,11 +50,9 @@ export interface WebviewMessage {
| "downloadMcp"
| "silentlyRefreshMcpMarketplace"
| "searchCommits"
| "showMcpView"
| "fetchLatestMcpServersFromHub"
| "telemetrySetting"
| "openSettings"
| "updateMcpTimeout"
| "fetchOpenGraphData"
| "checkIsImageUrl"
| "invoke"
@@ -68,8 +63,6 @@ export interface WebviewMessage {
| "requestTotalTasksSize"
| "relaunchChromeDebugMode"
| "taskFeedback"
| "getDetectedChromePath"
| "detectedChromePath"
| "scrollToSettings"
| "getRelativePaths" // Handles single and multiple URI resolution
| "searchFiles"
@@ -77,6 +70,7 @@ export interface WebviewMessage {
| "grpc_request"
| "toggleClineRule"
| "deleteClineRule"
| "copyToClipboard"
// | "relaunchChromeDebugMode"
text?: string
+1
View File
@@ -80,6 +80,7 @@ export interface ApiHandlerOptions {
thinkingBudgetTokens?: number
reasoningEffort?: string
sambanovaApiKey?: string
requestTimeoutMs?: number
}
export type ApiConfiguration = ApiHandlerOptions & {
+32
View File
@@ -0,0 +1,32 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v3.19.1
// source: account.proto
/* eslint-disable */
import { EmptyRequest, String } from "./common"
export const protobufPackage = "cline"
/** Service for account-related operations */
export type AccountServiceDefinition = typeof AccountServiceDefinition
export const AccountServiceDefinition = {
name: "AccountService",
fullName: "cline.AccountService",
methods: {
/**
* Handles the user clicking the login link in the UI.
* Generates a secure nonce for state validation, stores it in secrets,
* and opens the authentication URL in the external browser.
*/
accountLoginClicked: {
name: "accountLoginClicked",
requestType: EmptyRequest,
requestStream: false,
responseType: String,
responseStream: false,
options: {},
},
},
} as const
+399 -1
View File
@@ -6,7 +6,7 @@
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { EmptyRequest, StringRequest } from "./common"
import { Boolean, EmptyRequest, Metadata, StringRequest } from "./common"
export const protobufPackage = "cline"
@@ -22,6 +22,29 @@ export interface BrowserConnection {
endpoint?: string | undefined
}
export interface ChromePath {
path: string
isBundled: boolean
}
export interface Viewport {
width: number
height: number
}
export interface BrowserSettings {
viewport?: Viewport | undefined
remoteBrowserHost?: string | undefined
remoteBrowserEnabled?: boolean | undefined
}
export interface UpdateBrowserSettingsRequest {
metadata?: Metadata | undefined
viewport?: Viewport | undefined
remoteBrowserHost?: string | undefined
remoteBrowserEnabled?: boolean | undefined
}
function createBaseBrowserConnectionInfo(): BrowserConnectionInfo {
return { isConnected: false, isRemote: false, host: undefined }
}
@@ -206,6 +229,365 @@ export const BrowserConnection: MessageFns<BrowserConnection> = {
},
}
function createBaseChromePath(): ChromePath {
return { path: "", isBundled: false }
}
export const ChromePath: MessageFns<ChromePath> = {
encode(message: ChromePath, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.path !== "") {
writer.uint32(10).string(message.path)
}
if (message.isBundled !== false) {
writer.uint32(16).bool(message.isBundled)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): ChromePath {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseChromePath()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.path = reader.string()
continue
}
case 2: {
if (tag !== 16) {
break
}
message.isBundled = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): ChromePath {
return {
path: isSet(object.path) ? globalThis.String(object.path) : "",
isBundled: isSet(object.isBundled) ? globalThis.Boolean(object.isBundled) : false,
}
},
toJSON(message: ChromePath): unknown {
const obj: any = {}
if (message.path !== "") {
obj.path = message.path
}
if (message.isBundled !== false) {
obj.isBundled = message.isBundled
}
return obj
},
create<I extends Exact<DeepPartial<ChromePath>, I>>(base?: I): ChromePath {
return ChromePath.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<ChromePath>, I>>(object: I): ChromePath {
const message = createBaseChromePath()
message.path = object.path ?? ""
message.isBundled = object.isBundled ?? false
return message
},
}
function createBaseViewport(): Viewport {
return { width: 0, height: 0 }
}
export const Viewport: MessageFns<Viewport> = {
encode(message: Viewport, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.width !== 0) {
writer.uint32(8).int32(message.width)
}
if (message.height !== 0) {
writer.uint32(16).int32(message.height)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Viewport {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseViewport()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.width = reader.int32()
continue
}
case 2: {
if (tag !== 16) {
break
}
message.height = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): Viewport {
return {
width: isSet(object.width) ? globalThis.Number(object.width) : 0,
height: isSet(object.height) ? globalThis.Number(object.height) : 0,
}
},
toJSON(message: Viewport): unknown {
const obj: any = {}
if (message.width !== 0) {
obj.width = Math.round(message.width)
}
if (message.height !== 0) {
obj.height = Math.round(message.height)
}
return obj
},
create<I extends Exact<DeepPartial<Viewport>, I>>(base?: I): Viewport {
return Viewport.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Viewport>, I>>(object: I): Viewport {
const message = createBaseViewport()
message.width = object.width ?? 0
message.height = object.height ?? 0
return message
},
}
function createBaseBrowserSettings(): BrowserSettings {
return { viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined }
}
export const BrowserSettings: MessageFns<BrowserSettings> = {
encode(message: BrowserSettings, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.viewport !== undefined) {
Viewport.encode(message.viewport, writer.uint32(10).fork()).join()
}
if (message.remoteBrowserHost !== undefined) {
writer.uint32(18).string(message.remoteBrowserHost)
}
if (message.remoteBrowserEnabled !== undefined) {
writer.uint32(24).bool(message.remoteBrowserEnabled)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): BrowserSettings {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBrowserSettings()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.viewport = Viewport.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.remoteBrowserHost = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.remoteBrowserEnabled = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): BrowserSettings {
return {
viewport: isSet(object.viewport) ? Viewport.fromJSON(object.viewport) : undefined,
remoteBrowserHost: isSet(object.remoteBrowserHost) ? globalThis.String(object.remoteBrowserHost) : undefined,
remoteBrowserEnabled: isSet(object.remoteBrowserEnabled)
? globalThis.Boolean(object.remoteBrowserEnabled)
: undefined,
}
},
toJSON(message: BrowserSettings): unknown {
const obj: any = {}
if (message.viewport !== undefined) {
obj.viewport = Viewport.toJSON(message.viewport)
}
if (message.remoteBrowserHost !== undefined) {
obj.remoteBrowserHost = message.remoteBrowserHost
}
if (message.remoteBrowserEnabled !== undefined) {
obj.remoteBrowserEnabled = message.remoteBrowserEnabled
}
return obj
},
create<I extends Exact<DeepPartial<BrowserSettings>, I>>(base?: I): BrowserSettings {
return BrowserSettings.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<BrowserSettings>, I>>(object: I): BrowserSettings {
const message = createBaseBrowserSettings()
message.viewport =
object.viewport !== undefined && object.viewport !== null ? Viewport.fromPartial(object.viewport) : undefined
message.remoteBrowserHost = object.remoteBrowserHost ?? undefined
message.remoteBrowserEnabled = object.remoteBrowserEnabled ?? undefined
return message
},
}
function createBaseUpdateBrowserSettingsRequest(): UpdateBrowserSettingsRequest {
return { metadata: undefined, viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined }
}
export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsRequest> = {
encode(message: UpdateBrowserSettingsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.viewport !== undefined) {
Viewport.encode(message.viewport, writer.uint32(18).fork()).join()
}
if (message.remoteBrowserHost !== undefined) {
writer.uint32(26).string(message.remoteBrowserHost)
}
if (message.remoteBrowserEnabled !== undefined) {
writer.uint32(32).bool(message.remoteBrowserEnabled)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): UpdateBrowserSettingsRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseUpdateBrowserSettingsRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.viewport = Viewport.decode(reader, reader.uint32())
continue
}
case 3: {
if (tag !== 26) {
break
}
message.remoteBrowserHost = reader.string()
continue
}
case 4: {
if (tag !== 32) {
break
}
message.remoteBrowserEnabled = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): UpdateBrowserSettingsRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
viewport: isSet(object.viewport) ? Viewport.fromJSON(object.viewport) : undefined,
remoteBrowserHost: isSet(object.remoteBrowserHost) ? globalThis.String(object.remoteBrowserHost) : undefined,
remoteBrowserEnabled: isSet(object.remoteBrowserEnabled)
? globalThis.Boolean(object.remoteBrowserEnabled)
: undefined,
}
},
toJSON(message: UpdateBrowserSettingsRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.viewport !== undefined) {
obj.viewport = Viewport.toJSON(message.viewport)
}
if (message.remoteBrowserHost !== undefined) {
obj.remoteBrowserHost = message.remoteBrowserHost
}
if (message.remoteBrowserEnabled !== undefined) {
obj.remoteBrowserEnabled = message.remoteBrowserEnabled
}
return obj
},
create<I extends Exact<DeepPartial<UpdateBrowserSettingsRequest>, I>>(base?: I): UpdateBrowserSettingsRequest {
return UpdateBrowserSettingsRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<UpdateBrowserSettingsRequest>, I>>(object: I): UpdateBrowserSettingsRequest {
const message = createBaseUpdateBrowserSettingsRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.viewport =
object.viewport !== undefined && object.viewport !== null ? Viewport.fromPartial(object.viewport) : undefined
message.remoteBrowserHost = object.remoteBrowserHost ?? undefined
message.remoteBrowserEnabled = object.remoteBrowserEnabled ?? undefined
return message
},
}
export type BrowserServiceDefinition = typeof BrowserServiceDefinition
export const BrowserServiceDefinition = {
name: "BrowserService",
@@ -235,6 +617,22 @@ export const BrowserServiceDefinition = {
responseStream: false,
options: {},
},
getDetectedChromePath: {
name: "getDetectedChromePath",
requestType: EmptyRequest,
requestStream: false,
responseType: ChromePath,
responseStream: false,
options: {},
},
updateBrowserSettings: {
name: "updateBrowserSettings",
requestType: UpdateBrowserSettingsRequest,
requestStream: false,
responseType: Boolean,
responseStream: false,
options: {},
},
},
} as const
+144
View File
@@ -44,6 +44,15 @@ export interface Bytes {
value: Buffer
}
export interface BooleanRequest {
metadata?: Metadata | undefined
value: boolean
}
export interface Boolean {
value: boolean
}
function createBaseMetadata(): Metadata {
return {}
}
@@ -594,6 +603,141 @@ export const Bytes: MessageFns<Bytes> = {
},
}
function createBaseBooleanRequest(): BooleanRequest {
return { metadata: undefined, value: false }
}
export const BooleanRequest: MessageFns<BooleanRequest> = {
encode(message: BooleanRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.value !== false) {
writer.uint32(16).bool(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): BooleanRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBooleanRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 16) {
break
}
message.value = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): BooleanRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
value: isSet(object.value) ? globalThis.Boolean(object.value) : false,
}
},
toJSON(message: BooleanRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.value !== false) {
obj.value = message.value
}
return obj
},
create<I extends Exact<DeepPartial<BooleanRequest>, I>>(base?: I): BooleanRequest {
return BooleanRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<BooleanRequest>, I>>(object: I): BooleanRequest {
const message = createBaseBooleanRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.value = object.value ?? false
return message
},
}
function createBaseBoolean(): Boolean {
return { value: false }
}
export const Boolean: MessageFns<Boolean> = {
encode(message: Boolean, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.value !== false) {
writer.uint32(8).bool(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Boolean {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBoolean()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.value = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): Boolean {
return { value: isSet(object.value) ? globalThis.Boolean(object.value) : false }
},
toJSON(message: Boolean): unknown {
const obj: any = {}
if (message.value !== false) {
obj.value = message.value
}
return obj
},
create<I extends Exact<DeepPartial<Boolean>, I>>(base?: I): Boolean {
return Boolean.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Boolean>, I>>(object: I): Boolean {
const message = createBaseBoolean()
message.value = object.value ?? false
return message
},
}
function bytesFromBase64(b64: string): Uint8Array {
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"))
}
+9
View File
@@ -24,5 +24,14 @@ export const FileServiceDefinition = {
responseStream: false,
options: {},
},
/** Opens an image in the system viewer */
openImage: {
name: "openImage",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
},
} as const
+313 -99
View File
@@ -53,6 +53,24 @@ export function mcpServerStatusToJSON(object: McpServerStatus): string {
}
}
export interface ToggleMcpServerRequest {
metadata?: Metadata | undefined
serverName: string
disabled: boolean
}
export interface UpdateMcpTimeoutRequest {
metadata?: Metadata | undefined
serverName: string
timeout: number
}
export interface AddRemoteMcpServerRequest {
metadata?: Metadata | undefined
serverName: string
serverUrl: string
}
export interface McpTool {
name: string
description?: string | undefined
@@ -86,16 +104,289 @@ export interface McpServer {
timeout?: number | undefined
}
export interface ToggleMcpServerRequest {
metadata?: Metadata | undefined
serverName: string
disabled: boolean
}
export interface McpServers {
mcpServers: McpServer[]
}
function createBaseToggleMcpServerRequest(): ToggleMcpServerRequest {
return { metadata: undefined, serverName: "", disabled: false }
}
export const ToggleMcpServerRequest: MessageFns<ToggleMcpServerRequest> = {
encode(message: ToggleMcpServerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.serverName !== "") {
writer.uint32(18).string(message.serverName)
}
if (message.disabled !== false) {
writer.uint32(24).bool(message.disabled)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): ToggleMcpServerRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseToggleMcpServerRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.serverName = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.disabled = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): ToggleMcpServerRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
disabled: isSet(object.disabled) ? globalThis.Boolean(object.disabled) : false,
}
},
toJSON(message: ToggleMcpServerRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.serverName !== "") {
obj.serverName = message.serverName
}
if (message.disabled !== false) {
obj.disabled = message.disabled
}
return obj
},
create<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(base?: I): ToggleMcpServerRequest {
return ToggleMcpServerRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(object: I): ToggleMcpServerRequest {
const message = createBaseToggleMcpServerRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.serverName = object.serverName ?? ""
message.disabled = object.disabled ?? false
return message
},
}
function createBaseUpdateMcpTimeoutRequest(): UpdateMcpTimeoutRequest {
return { metadata: undefined, serverName: "", timeout: 0 }
}
export const UpdateMcpTimeoutRequest: MessageFns<UpdateMcpTimeoutRequest> = {
encode(message: UpdateMcpTimeoutRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.serverName !== "") {
writer.uint32(18).string(message.serverName)
}
if (message.timeout !== 0) {
writer.uint32(24).int32(message.timeout)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): UpdateMcpTimeoutRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseUpdateMcpTimeoutRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.serverName = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.timeout = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): UpdateMcpTimeoutRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
timeout: isSet(object.timeout) ? globalThis.Number(object.timeout) : 0,
}
},
toJSON(message: UpdateMcpTimeoutRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.serverName !== "") {
obj.serverName = message.serverName
}
if (message.timeout !== 0) {
obj.timeout = Math.round(message.timeout)
}
return obj
},
create<I extends Exact<DeepPartial<UpdateMcpTimeoutRequest>, I>>(base?: I): UpdateMcpTimeoutRequest {
return UpdateMcpTimeoutRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<UpdateMcpTimeoutRequest>, I>>(object: I): UpdateMcpTimeoutRequest {
const message = createBaseUpdateMcpTimeoutRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.serverName = object.serverName ?? ""
message.timeout = object.timeout ?? 0
return message
},
}
function createBaseAddRemoteMcpServerRequest(): AddRemoteMcpServerRequest {
return { metadata: undefined, serverName: "", serverUrl: "" }
}
export const AddRemoteMcpServerRequest: MessageFns<AddRemoteMcpServerRequest> = {
encode(message: AddRemoteMcpServerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.serverName !== "") {
writer.uint32(18).string(message.serverName)
}
if (message.serverUrl !== "") {
writer.uint32(26).string(message.serverUrl)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): AddRemoteMcpServerRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseAddRemoteMcpServerRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.serverName = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.serverUrl = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): AddRemoteMcpServerRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
serverUrl: isSet(object.serverUrl) ? globalThis.String(object.serverUrl) : "",
}
},
toJSON(message: AddRemoteMcpServerRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.serverName !== "") {
obj.serverName = message.serverName
}
if (message.serverUrl !== "") {
obj.serverUrl = message.serverUrl
}
return obj
},
create<I extends Exact<DeepPartial<AddRemoteMcpServerRequest>, I>>(base?: I): AddRemoteMcpServerRequest {
return AddRemoteMcpServerRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<AddRemoteMcpServerRequest>, I>>(object: I): AddRemoteMcpServerRequest {
const message = createBaseAddRemoteMcpServerRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.serverName = object.serverName ?? ""
message.serverUrl = object.serverUrl ?? ""
return message
},
}
function createBaseMcpTool(): McpTool {
return { name: "", description: undefined, inputSchema: undefined, autoApprove: undefined }
}
@@ -622,99 +913,6 @@ export const McpServer: MessageFns<McpServer> = {
},
}
function createBaseToggleMcpServerRequest(): ToggleMcpServerRequest {
return { metadata: undefined, serverName: "", disabled: false }
}
export const ToggleMcpServerRequest: MessageFns<ToggleMcpServerRequest> = {
encode(message: ToggleMcpServerRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.serverName !== "") {
writer.uint32(18).string(message.serverName)
}
if (message.disabled !== false) {
writer.uint32(24).bool(message.disabled)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): ToggleMcpServerRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseToggleMcpServerRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.serverName = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.disabled = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): ToggleMcpServerRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
serverName: isSet(object.serverName) ? globalThis.String(object.serverName) : "",
disabled: isSet(object.disabled) ? globalThis.Boolean(object.disabled) : false,
}
},
toJSON(message: ToggleMcpServerRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.serverName !== "") {
obj.serverName = message.serverName
}
if (message.disabled !== false) {
obj.disabled = message.disabled
}
return obj
},
create<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(base?: I): ToggleMcpServerRequest {
return ToggleMcpServerRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<ToggleMcpServerRequest>, I>>(object: I): ToggleMcpServerRequest {
const message = createBaseToggleMcpServerRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.serverName = object.serverName ?? ""
message.disabled = object.disabled ?? false
return message
},
}
function createBaseMcpServers(): McpServers {
return { mcpServers: [] }
}
@@ -790,6 +988,22 @@ export const McpServiceDefinition = {
responseStream: false,
options: {},
},
updateMcpTimeout: {
name: "updateMcpTimeout",
requestType: UpdateMcpTimeoutRequest,
requestStream: false,
responseType: McpServers,
responseStream: false,
options: {},
},
addRemoteMcpServer: {
name: "addRemoteMcpServer",
requestType: AddRemoteMcpServerRequest,
requestStream: false,
responseType: McpServers,
responseStream: false,
options: {},
},
},
} as const
+17
View File
@@ -0,0 +1,17 @@
const Module = require("module")
const originalRequire = Module.prototype.require
/**
* VSCode is not available during unit tests
* @see {@link file://./vscode-mock.ts}
*/
Module.prototype.require = function (path: string) {
if (path === "vscode") {
return require("./vscode-mock")
}
return originalRequire.call(this, path)
}
// Required to have access to String.prototype.toPosix
import "../utils/path"
+1
View File
@@ -0,0 +1 @@
export {}
+1 -1
View File
@@ -27,7 +27,7 @@ function calculateApiCostInternal(
}
// Determine effective output price
let effectiveOutputPrice = 0
let effectiveOutputPrice = modelInfo.outputPrice || 0
// Check if thinking budget was used and has a specific price
if (usedThinkingBudget && modelInfo.thinkingConfig?.outputPrice !== undefined) {
effectiveOutputPrice = modelInfo.thinkingConfig.outputPrice
+22
View File
@@ -0,0 +1,22 @@
const tsConfigPaths = require("tsconfig-paths")
const fs = require("fs")
const tsConfig = JSON.parse(fs.readFileSync("./tsconfig.json", "utf-8"))
/**
* The aliases point towards the `src` directory.
* However, `tsc` doesn't compile paths by itself
* (https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-does-not-affect-emit)
* So we need to use tsconfig-paths to resolve the aliases when running tests,
* but pointing to `out` instead.
*/
const outPaths = {}
Object.keys(tsConfig.compilerOptions.paths).forEach((key) => {
const value = tsConfig.compilerOptions.paths[key]
outPaths[key] = value.map((path) => path.replace("src", "out"))
})
tsConfigPaths.register({
baseUrl: ".",
paths: outPaths,
})
+2 -1
View File
@@ -27,7 +27,8 @@
"@integrations/*": ["src/integrations/*"],
"@services/*": ["src/services/*"],
"@shared/*": ["src/shared/*"],
"@utils/*": ["src/utils/*"]
"@utils/*": ["src/utils/*"],
"@packages/*": ["src/packages/*"]
}
},
"include": ["src/**/*", "scripts/**/*"],
+5246 -218
View File
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -15,11 +15,13 @@
},
"dependencies": {
"@floating-ui/react": "^0.27.4",
"@heroui/react": "^2.8.0-beta.2",
"@vscode/webview-ui-toolkit": "^1.4.0",
"debounce": "^2.1.1",
"dompurify": "^3.2.4",
"fast-deep-equal": "^3.1.3",
"firebase": "^11.3.0",
"framer-motion": "^12.7.4",
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"mermaid": "^11.4.1",
@@ -33,12 +35,16 @@
"react-use": "^17.6.0",
"react-virtuoso": "^4.12.3",
"rehype-highlight": "^7.0.1",
"rehype-parse": "^9.0.1",
"rehype-remark": "^10.0.1",
"remark-stringify": "^11.0.0",
"styled-components": "^6.1.15",
"unified": "^11.0.5",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tailwindcss/vite": "^4.0.12",
"@tailwindcss/vite": "^4.1.4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -56,10 +62,10 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"jsdom": "^26.0.0",
"tailwindcss": "^4.0.12",
"tailwindcss": "^4.1.4",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.2.6",
"vite": "^6.3.4",
"vitest": "^3.0.5"
}
}
+60 -55
View File
@@ -6,64 +6,71 @@ import HistoryView from "./components/history/HistoryView"
import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import AccountView from "./components/account/AccountView"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { useExtensionState } from "./context/ExtensionStateContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/configuration/McpConfigurationView"
import { McpViewTab } from "@shared/mcp"
import { Providers } from "./Providers"
const AppContent = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, vscMachineId } = useExtensionState()
const { didHydrateState, showWelcome, shouldShowAnnouncement, showMcp, mcpTab } = useExtensionState()
const [showSettings, setShowSettings] = useState(false)
const hideSettings = useCallback(() => setShowSettings(false), [])
const [showHistory, setShowHistory] = useState(false)
const [showMcp, setShowMcp] = useState(false)
const [showAccount, setShowAccount] = useState(false)
const [showAnnouncement, setShowAnnouncement] = useState(false)
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
const handleMessage = useCallback((e: MessageEvent) => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "action":
switch (message.action!) {
case "settingsButtonClicked":
setShowSettings(true)
setShowHistory(false)
setShowMcp(false)
setShowAccount(false)
break
case "historyButtonClicked":
setShowSettings(false)
setShowHistory(true)
setShowMcp(false)
setShowAccount(false)
break
case "mcpButtonClicked":
setShowSettings(false)
setShowHistory(false)
if (message.tab) {
setMcpTab(message.tab)
}
setShowMcp(true)
setShowAccount(false)
break
case "accountButtonClicked":
setShowSettings(false)
setShowHistory(false)
setShowMcp(false)
setShowAccount(true)
break
case "chatButtonClicked":
setShowSettings(false)
setShowHistory(false)
setShowMcp(false)
setShowAccount(false)
break
}
break
}
}, [])
const { setShowMcp, setMcpTab } = useExtensionState()
const closeMcpView = useCallback(() => {
setShowMcp(false)
setMcpTab(undefined)
}, [setShowMcp, setMcpTab])
const handleMessage = useCallback(
(e: MessageEvent) => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "action":
switch (message.action!) {
case "settingsButtonClicked":
setShowSettings(true)
setShowHistory(false)
closeMcpView()
setShowAccount(false)
break
case "historyButtonClicked":
setShowSettings(false)
setShowHistory(true)
closeMcpView()
setShowAccount(false)
break
case "mcpButtonClicked":
setShowSettings(false)
setShowHistory(false)
if (message.tab) {
setMcpTab(message.tab)
}
setShowMcp(true)
setShowAccount(false)
break
case "accountButtonClicked":
setShowSettings(false)
setShowHistory(false)
closeMcpView()
setShowAccount(true)
break
case "chatButtonClicked":
setShowSettings(false)
setShowHistory(false)
closeMcpView()
setShowAccount(false)
break
}
break
}
},
[setShowMcp, setMcpTab, closeMcpView],
)
useEvent("message", handleMessage)
@@ -95,13 +102,13 @@ const AppContent = () => {
<>
{showSettings && <SettingsView onDone={hideSettings} />}
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
{showMcp && <McpView initialTab={mcpTab} onDone={() => setShowMcp(false)} />}
{showMcp && <McpView initialTab={mcpTab} onDone={closeMcpView} />}
{showAccount && <AccountView onDone={() => setShowAccount(false)} />}
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
<ChatView
showHistoryView={() => {
setShowSettings(false)
setShowMcp(false)
closeMcpView()
setShowAccount(false)
setShowHistory(true)
}}
@@ -119,11 +126,9 @@ const AppContent = () => {
const App = () => {
return (
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<AppContent />
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
<Providers>
<AppContent />
</Providers>
)
}
+15
View File
@@ -0,0 +1,15 @@
import type { ReactNode } from "react"
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { HeroUIProvider } from "@heroui/react"
export function Providers({ children }: { children: ReactNode }) {
return (
<ExtensionStateContextProvider>
<FirebaseAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
)
}
@@ -1,9 +1,12 @@
import { memo } from "react"
import { vscode } from "@/utils/vscode"
import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
const AccountOptions = () => {
const handleAccountClick = () => {
vscode.postMessage({ type: "accountLoginClicked" })
AccountServiceClient.accountLoginClicked(EmptyRequest.create()).catch((err) =>
console.error("Failed to get login URL:", err),
)
}
// Call handleAccountClick immediately when component mounts
@@ -8,6 +8,8 @@ import CountUp from "react-countup"
import CreditsHistoryTable from "./CreditsHistoryTable"
import { UsageTransaction, PaymentTransaction } from "@shared/ClineAccount"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
type AccountViewProps = {
onDone: () => void
@@ -68,7 +70,9 @@ export const ClineAccountView = () => {
}, [user])
const handleLogin = () => {
vscode.postMessage({ type: "accountLoginClicked" })
AccountServiceClient.accountLoginClicked(EmptyRequest.create()).catch((err) =>
console.error("Failed to get login URL:", err),
)
}
const handleLogout = () => {
@@ -1,10 +1,12 @@
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useMemo, useState } from "react"
import { useCallback, useMemo, useRef, useState } from "react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { vscode } from "@/utils/vscode"
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { useClickAway } from "react-use"
interface AutoApproveMenuProps {
style?: React.CSSProperties
@@ -80,6 +82,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
const { autoApprovalSettings } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [isHoveringCollapsibleSection, setIsHoveringCollapsibleSection] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
// Careful not to use partials to mutate since spread operator only does shallow copy
const enabledActions = ACTION_METADATA.filter((action) => autoApprovalSettings.actions[action.id])
@@ -227,8 +230,16 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
[extensionState.autoApprovalSettings],
)
// Handle clicks outside the menu to close it
useClickAway(menuRef, () => {
if (isExpanded) {
setIsExpanded(false)
}
})
return (
<div
ref={menuRef}
style={{
padding: "0 15px",
userSelect: "none",
@@ -236,6 +247,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
? `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`
: "none",
overflowY: "auto",
backgroundColor: isExpanded ? CODE_BLOCK_BG_COLOR : "transparent",
...style,
}}>
<div
@@ -6,7 +6,7 @@ import styled from "styled-components"
import { BROWSER_VIEWPORT_PRESETS } from "@shared/BrowserSettings"
import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "@shared/ExtensionMessage"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { FileServiceClient } from "@/services/grpc-client"
import { BrowserSettingsMenu } from "@/components/browser/BrowserSettingsMenu"
import { CheckpointControls } from "@/components/common/CheckpointControls"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
@@ -397,10 +397,9 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
alt="Browser screenshot"
style={imgScreenshotStyle}
onClick={() =>
vscode.postMessage({
type: "openImage",
text: displayState.screenshot,
})
FileServiceClient.openImage({ value: displayState.screenshot }).catch((err) =>
console.error("Failed to open image:", err),
)
}
/>
) : (
@@ -1271,6 +1271,23 @@ export const ChatRowContent = ({
<NewTaskPreview context={message.text || ""} />
</>
)
case "condense":
return (
<>
<div style={headerStyle}>
<span
className="codicon codicon-new-file"
style={{
color: normalColor,
marginBottom: "-1.5px",
}}></span>
<span style={{ color: normalColor, fontWeight: "bold" }}>
Cline wants to condense your conversation:
</span>
</div>
<NewTaskPreview context={message.text || ""} />
</>
)
case "plan_mode_respond": {
let response: string | undefined
let options: string[] | undefined
+278 -76
View File
@@ -237,6 +237,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
) => {
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [isDraggingOver, setIsDraggingOver] = useState(false)
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
const [showSlashCommandsMenu, setShowSlashCommandsMenu] = useState(false)
@@ -266,6 +267,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [menuPosition, setMenuPosition] = useState(0)
const [shownTooltipMode, setShownTooltipMode] = useState<ChatSettings["mode"] | null>(null)
const [pendingInsertions, setPendingInsertions] = useState<string[]>([])
const [showShiftDragTip, setShowShiftDragTip] = useState(false)
const shiftHoldTimerRef = useRef<NodeJS.Timeout | null>(null)
const [showUnsupportedFileError, setShowUnsupportedFileError] = useState(false)
const unsupportedFileTimerRef = useRef<NodeJS.Timeout | null>(null)
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
const [searchLoading, setSearchLoading] = useState(false)
@@ -1016,6 +1021,84 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [showModelSelector])
// Effect for Shift key hold detection
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Shift" && !event.repeat) {
// Start timer only if Shift is pressed and not already held down
if (shiftHoldTimerRef.current === null) {
shiftHoldTimerRef.current = setTimeout(() => {
setShowShiftDragTip(true)
}, 250) // 250ms delay
}
}
}
const handleKeyUp = (event: KeyboardEvent) => {
if (event.key === "Shift") {
// Clear timer and hide tip when Shift is released
if (shiftHoldTimerRef.current !== null) {
clearTimeout(shiftHoldTimerRef.current)
shiftHoldTimerRef.current = null
}
setShowShiftDragTip(false)
}
}
// Add listeners
window.addEventListener("keydown", handleKeyDown)
window.addEventListener("keyup", handleKeyUp)
// Cleanup listeners on component unmount
return () => {
window.removeEventListener("keydown", handleKeyDown)
window.removeEventListener("keyup", handleKeyUp)
// Clear any running timer on unmount
if (shiftHoldTimerRef.current !== null) {
clearTimeout(shiftHoldTimerRef.current)
}
}
}, []) // Empty dependency array ensures this runs only once on mount/unmount
// Function to show error message for unsupported files for drag and drop
const showUnsupportedFileErrorMessage = () => {
// Show error message for unsupported files
setShowUnsupportedFileError(true)
// Clear any existing timer
if (unsupportedFileTimerRef.current) {
clearTimeout(unsupportedFileTimerRef.current)
}
// Set timer to hide error after 3 seconds
unsupportedFileTimerRef.current = setTimeout(() => {
setShowUnsupportedFileError(false)
unsupportedFileTimerRef.current = null
}, 3000)
}
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault()
setIsDraggingOver(true)
// Check if files are being dragged
if (e.dataTransfer.types.includes("Files")) {
// Check if any of the files are not images
const items = Array.from(e.dataTransfer.items)
const hasNonImageFile = items.some((item) => {
if (item.kind === "file") {
const type = item.type.split("/")[0]
return type !== "image"
}
return false
})
if (hasNonImageFile) {
showUnsupportedFileErrorMessage()
}
}
}
/**
* Handles the drag over event to allow dropping.
* Prevents the default behavior to enable drop.
@@ -1024,8 +1107,37 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
*/
const onDragOver = (e: React.DragEvent) => {
e.preventDefault()
// Ensure state remains true if dragging continues over the element
if (!isDraggingOver) {
setIsDraggingOver(true)
}
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
// Check if the related target is still within the drop zone; prevents flickering
const dropZone = e.currentTarget as HTMLElement
if (!dropZone.contains(e.relatedTarget as Node)) {
setIsDraggingOver(false)
// Don't clear the error message here, let it time out naturally
}
}
// Effect to detect when drag operation ends outside the component
useEffect(() => {
const handleGlobalDragEnd = () => {
// This will be triggered when the drag operation ends anywhere
setIsDraggingOver(false)
// Don't clear error message, let it time out naturally
}
document.addEventListener("dragend", handleGlobalDragEnd)
return () => {
document.removeEventListener("dragend", handleGlobalDragEnd)
}
}, [])
/**
* Handles the drop event for files and text.
* Processes dropped images and text, updating the state accordingly.
@@ -1034,6 +1146,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
*/
const onDrop = async (e: React.DragEvent) => {
e.preventDefault()
setIsDraggingOver(false) // Reset state on drop
// Clear any error message when something is actually dropped
setShowUnsupportedFileError(false)
if (unsupportedFileTimerRef.current) {
clearTimeout(unsupportedFileTimerRef.current)
unsupportedFileTimerRef.current = null
}
// --- 1. VSCode Explorer Drop Handling ---
let uris: string[] = []
@@ -1153,9 +1273,37 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
opacity: textAreaDisabled ? 0.5 : 1,
position: "relative",
display: "flex",
// Drag-over styles moved to DynamicTextArea
transition: "background-color 0.1s ease-in-out, border 0.1s ease-in-out",
}}
onDrop={onDrop}
onDragOver={onDragOver}>
onDragOver={onDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}>
{showUnsupportedFileError && (
<div
style={{
position: "absolute",
inset: "10px 15px",
backgroundColor: "rgba(var(--vscode-errorForeground-rgb), 0.1)",
border: "2px solid var(--vscode-errorForeground)",
borderRadius: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 10,
pointerEvents: "none",
}}>
<span
style={{
color: "var(--vscode-errorForeground)",
fontWeight: "bold",
fontSize: "12px",
}}>
Only image files are supported
</span>
</div>
)}
{showSlashCommandsMenu && (
<div ref={slashCommandsMenuContainerRef}>
<SlashCommandMenu
@@ -1250,7 +1398,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
onHeightChange?.(height)
}}
placeholder={placeholderText}
placeholder={showUnsupportedFileError ? "" : placeholderText}
maxRows={10}
autoFocus={true}
style={{
@@ -1282,9 +1430,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
cursor: textAreaDisabled ? "not-allowed" : undefined,
flex: 1,
zIndex: 1,
outline: isTextAreaFocused
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
: "none",
outline:
isDraggingOver && !showUnsupportedFileError // Only show drag outline if not showing error
? "2px dashed var(--vscode-focusBorder)"
: isTextAreaFocused
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
: "none",
outlineOffset: isDraggingOver && !showUnsupportedFileError ? "1px" : "0px", // Add offset for drag-over outline
}}
onScroll={() => updateHighlights()}
/>
@@ -1346,79 +1498,129 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</div>
<ControlsContainer>
<ButtonGroup>
<VSCodeButton
data-testid="context-button"
appearance="icon"
aria-label="Add Context"
disabled={textAreaDisabled}
onClick={handleContextButtonClick}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span className="flex items-center" style={{ fontSize: "13px", marginBottom: 1 }}>
@
</span>
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Context</span>} */}
</ButtonContainer>
</VSCodeButton>
{/* Always render both components, but control visibility with CSS */}
<div
style={{
position: "relative",
flex: 1,
minWidth: 0,
height: "28px", // Fixed height to prevent container shrinking
}}>
{/* ButtonGroup - always in DOM but visibility controlled */}
<ButtonGroup
style={{
opacity: showShiftDragTip ? 0 : 1,
pointerEvents: showShiftDragTip ? "none" : "auto",
position: "absolute",
top: 0,
left: 0,
right: 0,
transition: "opacity 0.3s ease-in-out",
transitionDelay: showShiftDragTip ? "0s" : "0.2s",
width: "100%",
height: "100%",
zIndex: showShiftDragTip ? 0 : 1,
}}>
<Tooltip tipText="Add Context" style={{ left: 0 }}>
<VSCodeButton
data-testid="context-button"
appearance="icon"
aria-label="Add Context"
disabled={textAreaDisabled}
onClick={handleContextButtonClick}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span className="flex items-center" style={{ fontSize: "13px", marginBottom: 1 }}>
@
</span>
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<VSCodeButton
data-testid="images-button"
appearance="icon"
aria-label="Add Images"
disabled={shouldDisableImages}
onClick={() => {
if (!shouldDisableImages) {
onSelectImages()
}
}}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span
className="codicon codicon-device-camera flex items-center"
style={{ fontSize: "14px", marginBottom: -3 }}
/>
{/* {showButtonText && <span style={{ fontSize: "10px" }}>Images</span>} */}
</ButtonContainer>
</VSCodeButton>
<ServersToggleModal />
<ClineRulesToggleModal />
<Tooltip tipText="Add Images">
<VSCodeButton
data-testid="images-button"
appearance="icon"
aria-label="Add Images"
disabled={shouldDisableImages}
onClick={() => {
if (!shouldDisableImages) {
onSelectImages()
}
}}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
<span
className="codicon codicon-device-camera flex items-center"
style={{ fontSize: "14px", marginBottom: -3 }}
/>
</ButtonContainer>
</VSCodeButton>
</Tooltip>
<ServersToggleModal />
<ClineRulesToggleModal />
<ModelContainer ref={modelSelectorRef}>
<ModelButtonWrapper ref={buttonRef}>
<ModelDisplayButton
role="button"
isActive={showModelSelector}
disabled={false}
title="Select Model / API Provider"
onClick={handleModelButtonClick}
tabIndex={0}>
<ModelButtonContent>{modelDisplayName}</ModelButtonContent>
</ModelDisplayButton>
</ModelButtonWrapper>
{showModelSelector && (
<ModelSelectorTooltip
arrowPosition={arrowPosition}
menuPosition={menuPosition}
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
}}>
<ApiOptions
showModelOptions={true}
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
saveImmediately={true} // Ensure popup saves immediately
/>
</ModelSelectorTooltip>
)}
</ModelContainer>
</ButtonGroup>
<ModelContainer ref={modelSelectorRef}>
<ModelButtonWrapper ref={buttonRef}>
<ModelDisplayButton
role="button"
isActive={showModelSelector}
disabled={false}
onClick={handleModelButtonClick}
// onKeyDown={(e) => {
// if (e.key === "Enter" || e.key === " ") {
// e.preventDefault()
// handleModelButtonClick()
// }
// }}
tabIndex={0}>
<ModelButtonContent>{modelDisplayName}</ModelButtonContent>
</ModelDisplayButton>
</ModelButtonWrapper>
{showModelSelector && (
<ModelSelectorTooltip
arrowPosition={arrowPosition}
menuPosition={menuPosition}
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
}}>
<ApiOptions
showModelOptions={true}
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
saveImmediately={true} // Ensure popup saves immediately
/>
</ModelSelectorTooltip>
)}
</ModelContainer>
</ButtonGroup>
{/* Shift Tip - always in DOM but visibility controlled */}
<div
style={{
display: "flex",
justifyContent: "flex-start", // Left align horizontally
alignItems: "center", // Center vertically
height: "100%", // Fill the container height
padding: "4px 0", // Add padding to match button group height
boxSizing: "border-box", // Include padding in height calculation
opacity: showShiftDragTip ? 1 : 0,
pointerEvents: showShiftDragTip ? "auto" : "none",
position: "absolute",
top: 0,
left: 0,
right: 0,
transition: "opacity 0.3s ease-in-out",
transitionDelay: showShiftDragTip ? "0.2s" : "0s",
width: "100%",
zIndex: showShiftDragTip ? 1 : 0,
}}>
<span
style={{
fontSize: "10px",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "nowrap",
}}>
Hold Shift to Drop Files
</span>
</div>
</div>
{/* Tooltip for Plan/Act toggle remains outside the conditional rendering */}
<Tooltip
style={{ zIndex: 1000 }}
visible={shownTooltipMode !== null}
@@ -28,6 +28,10 @@ import ChatRow from "@/components/chat/ChatRow"
import ChatTextArea from "@/components/chat/ChatTextArea"
import TaskHeader from "@/components/chat/TaskHeader"
import TelemetryBanner from "@/components/common/TelemetryBanner"
import { unified } from "unified"
import remarkStringify from "remark-stringify"
import rehypeRemark from "rehype-remark"
import rehypeParse from "rehype-parse"
interface ChatViewProps {
isHidden: boolean
@@ -36,6 +40,26 @@ interface ChatViewProps {
showHistoryView: () => void
}
async function convertHtmlToMarkdown(html: string) {
// Process the HTML to Markdown
const result = await unified()
.use(rehypeParse as any, { fragment: true }) // Parse HTML fragments
.use(rehypeRemark as any) // Convert HTML to Markdown AST
.use(remarkStringify as any, {
// Convert Markdown AST to text
bullet: "-", // Use - for unordered lists
emphasis: "*", // Use * for emphasis
strong: "_", // Use _ for strong
listItemIndent: "one", // Use one space for list indentation
rule: "-", // Use - for horizontal rules
ruleSpaces: false, // No spaces in horizontal rules
fences: true,
})
.process(html)
return String(result)
}
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
@@ -79,6 +103,33 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
const [isAtBottom, setIsAtBottom] = useState(false)
useEffect(() => {
const handleCopy = async (e: ClipboardEvent) => {
if (window.getSelection) {
const selection = window.getSelection()
if (selection && selection.rangeCount > 0) {
// Get the selected HTML content
const range = selection.getRangeAt(0)
const clonedSelection = range.cloneContents()
const div = document.createElement("div")
div.appendChild(clonedSelection)
const selectedHtml = div.innerHTML
// Convert HTML to Markdown
const markdown = await convertHtmlToMarkdown(selectedHtml)
vscode.postMessage({ type: "copyToClipboard", text: markdown })
e.preventDefault()
}
}
}
document.addEventListener("copy", handleCopy)
return () => {
document.removeEventListener("copy", handleCopy)
}
}, [])
// UI layout depends on the last 2 messages
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
const lastMessage = useMemo(() => messages.at(-1), [messages])
@@ -203,6 +254,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setPrimaryButtonText("Start New Task with Context")
setSecondaryButtonText(undefined)
break
case "condense":
setTextAreaDisabled(isPartial)
setClineAsk("condense")
setEnableButtons(!isPartial)
setPrimaryButtonText("Condense Conversation")
setSecondaryButtonText(undefined)
break
}
break
case "say":
@@ -312,6 +370,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
images,
})
break
case "condense":
vscode.postMessage({
type: "askResponse",
askResponse: "messageResponse",
text,
images,
})
break
// there is no other case that a textfield should be enabled
}
}
@@ -377,6 +443,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
images: [],
})
break
case "condense":
vscode.postMessage({
type: "condense",
text: lastMessage?.text,
})
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
@@ -1,13 +1,16 @@
import React, { useRef, useState, useEffect } from "react"
import { useClickAway, useWindowSize } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useNavigator } from "@/hooks/useNavigator"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import ServersToggleList from "@/components/mcp/configuration/tabs/installed/ServersToggleList"
import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import Tooltip from "@/components/common/Tooltip"
const ServersToggleModal: React.FC = () => {
const { mcpServers } = useExtensionState()
const { navigateToMcp } = useNavigator()
const [isVisible, setIsVisible] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
const modalRef = useRef<HTMLDivElement>(null)
@@ -41,18 +44,20 @@ const ServersToggleModal: React.FC = () => {
return (
<div ref={modalRef}>
<div ref={buttonRef} className="inline-flex min-w-0 max-w-full">
<VSCodeButton
appearance="icon"
aria-label="MCP Servers"
onClick={() => setIsVisible(!isVisible)}
style={{ padding: "0px 0px", height: "20px" }}>
<div className="flex items-center gap-1 text-xs whitespace-nowrap min-w-0 w-full">
<span
className="codicon codicon-server flex items-center"
style={{ fontSize: "12.5px", marginBottom: 1 }}
/>
</div>
</VSCodeButton>
<Tooltip tipText="Manage MCP Servers">
<VSCodeButton
appearance="icon"
aria-label="MCP Servers"
onClick={() => setIsVisible(!isVisible)}
style={{ padding: "0px 0px", height: "20px" }}>
<div className="flex items-center gap-1 text-xs whitespace-nowrap min-w-0 w-full">
<span
className="codicon codicon-server flex items-center"
style={{ fontSize: "12.5px", marginBottom: 1 }}
/>
</div>
</VSCodeButton>
</Tooltip>
</div>
{isVisible && (
@@ -78,11 +83,8 @@ const ServersToggleModal: React.FC = () => {
<VSCodeButton
appearance="icon"
onClick={() => {
vscode.postMessage({
type: "showMcpView",
tab: "installed",
})
setIsVisible(false)
navigateToMcp("installed")
}}>
<span className="codicon codicon-gear text-[10px]"></span>
</VSCodeButton>
+32 -1
View File
@@ -35,7 +35,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
onClose,
}) => {
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage } = useExtensionState()
const [isTaskExpanded, setIsTaskExpanded] = useState(false)
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
const [isTextExpanded, setIsTextExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
@@ -138,6 +138,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
const shouldShowPromptCacheInfo =
doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter" && apiConfiguration?.apiProvider !== "cline"
const shouldShowPromptCacheInfoClineOR =
doesModelSupportPromptCache &&
(apiConfiguration?.apiProvider === "openrouter" || apiConfiguration?.apiProvider === "cline")
const ContextWindowComponent = (
<>
{isTaskExpanded && contextWindow && (
@@ -406,6 +410,33 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
)}
</div>
{shouldShowPromptCacheInfoClineOR && cacheReads !== undefined && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span style={{ fontWeight: "bold" }}>Cache:</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-right"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
</div>
)}
{shouldShowPromptCacheInfo &&
(cacheReads !== undefined ||
cacheWrites !== undefined ||
@@ -5,6 +5,7 @@ import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import RulesToggleList from "./RulesToggleList"
import Tooltip from "@/components/common/Tooltip"
const ClineRulesToggleModal: React.FC = () => {
const { globalClineRulesToggles = {}, localClineRulesToggles = {} } = useExtensionState()
@@ -61,15 +62,20 @@ const ClineRulesToggleModal: React.FC = () => {
return (
<div ref={modalRef}>
<div ref={buttonRef} className="inline-flex min-w-0 max-w-full">
<VSCodeButton
appearance="icon"
aria-label="Cline Rules"
onClick={() => setIsVisible(!isVisible)}
style={{ padding: "0px 0px", height: "20px" }}>
<div className="flex items-center gap-1 text-xs whitespace-nowrap min-w-0 w-full">
<span className="codicon codicon-law flex items-center" style={{ fontSize: "12.5px", marginBottom: 1 }} />
</div>
</VSCodeButton>
<Tooltip tipText="Manage Cline Rules">
<VSCodeButton
appearance="icon"
aria-label="Cline Rules"
onClick={() => setIsVisible(!isVisible)}
style={{ padding: "0px 0px", height: "20px" }}>
<div className="flex items-center gap-1 text-xs whitespace-nowrap min-w-0 w-full">
<span
className="codicon codicon-law flex items-center"
style={{ fontSize: "12.5px", marginBottom: 1 }}
/>
</div>
</VSCodeButton>
</Tooltip>
</div>
{isVisible && (
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"
import mermaid from "mermaid"
import { useDebounceEffect } from "@/utils/useDebounceEffect"
import styled from "styled-components"
import { vscode } from "@/utils/vscode"
import { FileServiceClient } from "@/services/grpc-client"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
const MERMAID_THEME = {
@@ -131,10 +131,7 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
try {
const pngDataUrl = await svgToPng(svgEl)
vscode.postMessage({
type: "openImage",
text: pngDataUrl,
})
FileServiceClient.openImage({ value: pngDataUrl }).catch((err) => console.error("Failed to open image:", err))
} catch (err) {
console.error("Error converting SVG to PNG:", err)
}
@@ -1,5 +1,6 @@
import React, { useState, useRef, useLayoutEffect, memo } from "react"
import { useWindowSize } from "react-use"
import { FileServiceClient } from "@/services/grpc-client"
import { vscode } from "@/utils/vscode"
interface ThumbnailsProps {
@@ -33,7 +34,7 @@ const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProp
const isDeletable = setImages !== undefined
const handleImageClick = (image: string) => {
vscode.postMessage({ type: "openImage", text: image })
FileServiceClient.openImage({ value: image }).catch((err) => console.error("Failed to open image:", err))
}
return (

Some files were not shown because too many files have changed in this diff Show More