Compare commits

..
Author SHA1 Message Date
0xtoshii ad636b0fb9 changeset 2025-04-18 19:27:55 -07:00
0xtoshii da36622f99 xml 2025-04-18 19:26:35 -07:00
0xtoshii ba0117de45 format 2025-04-18 13:14:40 -07:00
0xtoshii e2426e5735 css styles 2025-04-18 12:47:57 -07:00
0xtoshii b3e5588bce changeset 2025-04-17 16:51:45 -07:00
0xtoshii 85076ed39e rm 2025-04-17 16:38:06 -07:00
0xtoshii faf2361dbe formatting new call 2025-04-17 16:37:05 -07:00
Toshii d4c35a6877 Merge branch 'main' into ft/sl-commands 2025-04-17 16:30:36 -07:00
0xtoshii 93aa8e03a7 styles 2025-04-17 16:11:34 -07:00
Saoud Rizwan bc87fdb4b3 fix: BrowserSessionRow to include 'reasoning' message type handling (#2953) 2025-04-17 12:58:10 -10:00
0xtoshii 5fe9b3cb8b highlighting boxes 2025-04-17 15:57:45 -07:00
Trevor Hudson c021b6464b Trevor/eng 416 add editing ability to older user messages in chat (#2954)
* User message editing

* restore and send

* dont redo if the message is the same

* select by default

* resolve conflicts

* handle workspace restore

* add title to buttons

* don't allow restoring files if there is no workspace

* fix

* fix messaging

* fix text

* fix type
2025-04-17 15:38:30 -07:00
Evan 180ebdad74 Add edit cline rule button (#2956)
* add button to open rule file

* changeset
2025-04-17 13:33:11 -07:00
Andrei EdellandAndrei Edell 2a80fedf7d proto migration for testBrowserConnection.ts (#2922)
* proto migration for testBrowserConnection.ts

* format fix

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-04-17 09:31:48 -10:00
0xtoshii e557ea5e26 spacing 2025-04-17 12:31:45 -07:00
Evan 450583c81d à la mode (#2912)
* add fetching global cline rules files

* add toggle functionality to clinerules

* add toggles modal

* changeset

* change codicon

* fix bad merged files

* fix duplicate globalClineRulesToggles declaration in state.ts from merge

* refresh cline rules on modal open
2025-04-16 19:58:19 -07:00
Yaroslav HalchenkoandDennis Bartlett 45b1666325 Add codespell support (config, workflow to detect/not fix) and make it fix some typos (#2939)
* Add github action to codespell main on push and PRs

* Add rudimentary codespell config

* run codespell throughout fixing typos automagically (but ignoring overall fail due to ambigous ones)

=== Do not change lines below ===
{
 "chain": [],
 "cmd": "codespell -w || :",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [],
 "outputs": [],
 "pwd": "."
}
^^^ Do not change lines above ^^^

* Do interactive fixing of some ambigous typos

=== Do not change lines below ===
{
 "chain": [],
 "cmd": "codespell -w -i 3 -C 4",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [],
 "outputs": [],
 "pwd": "."
}
^^^ Do not change lines above ^^^

* Fix Formatting

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-16 19:53:16 -07:00
0xtoshii 718b826b19 color 2025-04-16 19:04:37 -07:00
0xtoshii 6b97490f9d cursor position 2025-04-16 18:56:35 -07:00
Matt Rubens 6c5b99d304 Safer check for xAI reasoning content (#2936) 2025-04-16 18:45:07 -07:00
0xtoshii 6b1314be24 consider cursor 2025-04-16 18:18:31 -07:00
Ara 02120810ce Adding o3 and o4-mini models (#2932)
* Revert "Add OpenAI o3 & 4o-mini (#2927)"

This reverts commit 89cbbe95e3.

* Adding O3 and O4-mini Models

* Adding O3 and O4-mini Models

* Adding O3 and O4-mini Models
2025-04-16 17:27:12 -07:00
0xtoshii 6facb3e2e6 menu wrap 2025-04-16 17:00:24 -07:00
Frostbourne 612744394a [ENG-417] Add command to focus chat input (#2910)
* Make command to focus on chat input

* Allow cmd to focus from anywhere

* changeset

* fix unit test

* Jump to chat input from anywhere

* fix focusChatInput call after opening ext
2025-04-16 16:00:10 -07:00
0xtoshii bc481d0c7f nits 2025-04-16 15:51:11 -07:00
Trevor Hudson 01a48736eb Add ability to send context with an options selection (#2379)
* - add ability to send context with an options selection
- add sourcemaps for debugging in the webview

* remove colon if there is no message

* resolve conflicts

* remove sourcemap
2025-04-16 15:40:30 -07:00
0xtoshii 12fde2f05a highlights 2025-04-16 13:06:49 -07:00
Evan 4add38032e Add Accurate Title (#2934)
* Add openrouter ranking

* changeset
2025-04-16 12:55:17 -07:00
Evan 73078d63ce Pirate Mode Activated (#2890)
* add fetching global cline rules files

* add toggle functionality to clinerules

* selectively filter out OS generated files from read directory

* remove .file filtering

* remove duplicate imports

* pass path to global rules directory in system prompt

* empty commit to trigger tests
2025-04-16 12:44:04 -07:00
0xtoshii d9272352e4 menu base 2025-04-16 12:08:03 -07:00
Peter Dave Hello 89cbbe95e3 Add OpenAI o3 & 4o-mini (#2927)
Reference:
- https://platform.openai.com/docs/models/o3
- https://platform.openai.com/docs/models/o4-mini
2025-04-16 11:16:55 -07:00
Andrei EdellandAndrei Edell 4d696f377c PROTOBUS: gRPC over vscode message passing (#2830)
* initial protobuf setup & rough domains

* delete old protos for now

* phase 1

* initial working demo

* simplify call a bit more

* remomve some comments

* use common.proto

* remove redundant browser-service layer, clean up naming

* delete mcp proto for now

* better client layout & easier service imports

* a reflection-based way to create grpc services automatically

* better code layout for grpc implementations

* switch to auto-generating the method registration via bash

* hook protobufs into package.json scripts

* make service implementations more generic

* warn user that they must install protoc deps

* delete old message passing for getBrowserConnectionInfo

* format fix

* format fix

* rewrite build-protos in node & update package.json

* don't protoc during package

* change how imports work based on feedback

* package lock seems necessary now

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-04-15 17:37:02 -07:00
Yusuke Mizushima dd84bdaa9e Fix/vertex token count (#2893)
* feat: add token usage metadata handling in VertexHandler

* feat: add cost calculation for API usage in VertexHandler

* fix: correct vertexai token count calculation
2025-04-15 17:22:46 -07:00
0xtoshii a2811c60c9 new model 2025-04-14 17:55:04 -07:00
0xtoshii e4c42a5dbf test base 2025-04-14 16:20:07 -07:00
0xtoshii 5e0a0deb82 format 2025-04-14 16:19:56 -07:00
0xtoshii e699103a2f base 2025-04-14 16:10:47 -07:00
80 changed files with 3170 additions and 281 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
new slash command menu, slash command to trigger new_task
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
structured xml
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
gRPC over vscode message bus to make messaging better
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Allow user to modify a previous message to continue chat
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add command to focus chat input
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix vertexai token count
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Allow user to send context with an option selection
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Safer check for xAI reasoning content
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add an edit button to the Cline Rules toggle modal to open rule files
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Readme update
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add OpenAI o3 & 4o-mini
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add modal UI for toggling Cline Rules
+6
View File
@@ -0,0 +1,6 @@
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = .git*,*.svg,package-lock.json,*.css,.codespellrc,locales
check-hidden = true
ignore-regex = (\b(optIn|isTaller)\b|https://\S+)
# ignore-words-list =
+25
View File
@@ -0,0 +1,25 @@
# Codespell configuration is within .codespellrc
---
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@v1
- name: Codespell
uses: codespell-project/actions-codespell@v2
+2 -2
View File
@@ -58,7 +58,7 @@
- Add recommended models for Cline provider
- Add ability to detect when user edits files manually so Cline knows to re-read, leading to reduced diff edit errors
- Add improvements to file mention searching for faster searching
- Add scoring logic to file mentions to sort and exlcude results based on relevance
- Add scoring logic to file mentions to sort and exclude results based on relevance
- Add Support for Bytedance Doubao (Thanks Tunixer!)
- Fix to prevent duplicate BOM (Thanks bamps53!)
@@ -536,7 +536,7 @@
- Adds "Always allow read-only operations" setting to let Claude read files and view directories without needing approval (off by default)
- Implement sliding window context management to keep tasks going past 200k tokens
- Adds Google Cloud Vertex AI support and updates Claude 3.5 Sonnet max output to 8192 tokens for all providers.
- Improves system prompt to gaurd against lazy edits (less "//rest of code here")
- Improves system prompt to guard against lazy edits (less "//rest of code here")
## [1.3.0]
+1 -1
View File
@@ -2,7 +2,7 @@
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
# Cline
# Cline \#1 on OpenRouter
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+164 -36
View File
@@ -1,20 +1,22 @@
{
"name": "claude-dev",
"version": "3.12.0",
"version": "3.12.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.12.0",
"version": "3.12.3",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@bufbuild/protobuf": "^2.2.5",
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@grpc/grpc-js": "^1.9.15",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"@opentelemetry/api": "^1.4.1",
@@ -79,15 +81,18 @@
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
"proxyquire": "^2.1.3",
"should": "^13.2.3",
"sinon": "^19.0.2",
"ts-node": "^10.9.2",
"ts-proto": "^2.6.1",
"typescript": "^5.4.5"
},
"engines": {
@@ -3957,6 +3962,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@bufbuild/protobuf": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz",
"integrity": "sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@changesets/apply-release-plan": {
"version": "7.0.8",
"resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.8.tgz",
@@ -5566,6 +5577,7 @@
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
"integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.8",
"@types/node": ">=12.12.47"
@@ -9919,6 +9931,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/case-anything": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz",
"integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/chai": {
"version": "4.3.10",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
@@ -9938,35 +9963,18 @@
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
@@ -10612,6 +10620,19 @@
"node": ">=8"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1342118",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1342118.tgz",
@@ -10722,6 +10743,16 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dprint-node": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz",
"integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3"
}
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
@@ -11192,6 +11223,23 @@
"concat-map": "0.0.1"
}
},
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -11240,6 +11288,19 @@
"node": ">=8"
}
},
"node_modules/eslint/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -13659,6 +13720,36 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/log-symbols/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/long": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz",
@@ -14768,19 +14859,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/ora/node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
@@ -15366,6 +15444,20 @@
"node": ">=12.0.0"
}
},
"node_modules/protoc-gen-ts": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/protoc-gen-ts/-/protoc-gen-ts-0.8.7.tgz",
"integrity": "sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==",
"dev": true,
"license": "MIT",
"bin": {
"protoc-gen-ts": "protoc-gen-ts.js"
},
"funding": {
"type": "individual",
"url": "https://www.buymeacoffee.com/thesayyn"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -16970,6 +17062,42 @@
"node": ">=0.3.1"
}
},
"node_modules/ts-poet": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.11.0.tgz",
"integrity": "sha512-r5AGF8vvb+GjBsnqiTqbLhN1/U2FJt6BI+k0dfCrkKzWvUhNlwMmq9nDHuucHs45LomgHjZPvYj96dD3JawjJA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dprint-node": "^1.0.8"
}
},
"node_modules/ts-proto": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.7.0.tgz",
"integrity": "sha512-BGHjse2wTOeswOqnnPKinpxmbaRd882so/e1En6ww59YMG7AO9Kg4vPpJcbVfrpBixPRDqHafXD/RDyd2T99GA==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"case-anything": "^2.1.13",
"ts-poet": "^6.7.0",
"ts-proto-descriptors": "2.0.0"
},
"bin": {
"protoc-gen-ts_proto": "protoc-gen-ts_proto"
}
},
"node_modules/ts-proto-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-2.0.0.tgz",
"integrity": "sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0"
}
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+11
View File
@@ -116,6 +116,11 @@
"command": "cline.fixWithCline",
"title": "Fix with Cline",
"category": "Cline"
},
{
"command": "cline.focusChatInput",
"title": "Jump to Chat Input",
"category": "Cline"
}
],
"keybindings": [
@@ -290,6 +295,7 @@
"watch:esbuild:test": "IS_TEST=true node esbuild.js --watch",
"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",
"package:test": "IS_TEST=true npm run build:webview:test && npm run check-types && npm run lint && IS_TEST=true node esbuild.js --production",
"build:webview:test": "cd webview-ui && IS_TEST=true npm run build",
"watch:test": "IS_TEST=true npm-run-all -p watch:tsc watch:esbuild:test",
@@ -334,15 +340,18 @@
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
"proxyquire": "^2.1.3",
"should": "^13.2.3",
"sinon": "^19.0.2",
"ts-node": "^10.9.2",
"ts-proto": "^2.6.1",
"typescript": "^5.4.5"
},
"dependencies": {
@@ -350,8 +359,10 @@
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@bufbuild/protobuf": "^2.2.5",
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@grpc/grpc-js": "^1.9.15",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"@opentelemetry/api": "^1.4.1",
+22
View File
@@ -0,0 +1,22 @@
syntax = "proto3";
package cline;
import "common.proto";
service BrowserService {
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
}
message BrowserConnectionInfo {
bool is_connected = 1;
bool is_remote = 2;
optional string host = 3;
}
message BrowserConnection {
bool success = 1;
string message = 2;
optional string endpoint = 3;
}
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env node
import * as fs from "fs/promises"
import * as path from "path"
import { execSync } from "child_process"
import { globby } from "globby"
import chalk from "chalk"
// Get script directory and root directory
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname)
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
// Check if protoc is installed
try {
const options = { stdio: "ignore" }
execSync("protoc --version", options)
} catch (error) {
console.warn(chalk.yellow("Warning: protoc is not installed. Skipping proto generation."))
console.warn(chalk.yellow("To install Protocol Buffers compiler, visit: https://grpc.io/docs/protoc-installation/"))
process.exit(0) // Exit with success as requested
}
// Check if ts-proto plugin is available
const TS_PROTO_PLUGIN = path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto")
try {
await fs.access(TS_PROTO_PLUGIN)
} catch (error) {
console.error(chalk.red("Error: ts-proto plugin not found at"), TS_PROTO_PLUGIN)
console.error(chalk.red('Please run "npm install" to install the required dependencies.'))
process.exit(1)
}
// Define output directories
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
// Create output directory if it doesn't exist
await fs.mkdir(TS_OUT_DIR, { recursive: true })
// Clean up existing generated files
console.log(chalk.cyan("Cleaning up existing generated TypeScript files..."))
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
// Process all proto files
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR })
for (const protoFile of protoFiles) {
console.log(chalk.cyan(`Generating TypeScript code for ${protoFile}...`))
// Build the protoc command with proper path handling for cross-platform
const protocCommand = [
"protoc",
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
`--proto_path="${SCRIPT_DIR}"`,
`"${path.join(SCRIPT_DIR, protoFile)}"`,
].join(" ")
try {
const execOptions = {
stdio: "inherit",
}
execSync(protocCommand, execOptions)
} catch (error) {
console.error(chalk.red(`Error generating TypeScript for ${protoFile}:`), error)
process.exit(1)
}
}
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
console.log(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
// Generate method registration files
await generateMethodRegistrations()
// Make the script executable
try {
await fs.chmod(path.join(SCRIPT_DIR, "build-proto.js"), 0o755)
} catch (error) {
console.warn(chalk.yellow("Warning: Could not make script executable:"), error)
}
}
async function generateMethodRegistrations() {
console.log(chalk.cyan("Generating method registration files..."))
const serviceDirs = [
path.join(ROOT_DIR, "src", "core", "controller", "browser"),
// Add more service directories here as needed
]
for (const serviceDir of serviceDirs) {
try {
await fs.access(serviceDir)
} catch (error) {
console.log(chalk.gray(`Skipping ${serviceDir} - directory does not exist`))
continue
}
const serviceName = path.basename(serviceDir)
const registryFile = path.join(serviceDir, "methods.ts")
console.log(chalk.cyan(`Generating method registrations for ${serviceName}...`))
// Get all TypeScript files in the service directory
const files = await globby("*.ts", { cwd: serviceDir })
// Filter out index.ts and methods.ts
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
// Create the output file with header
let content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"\n`
// Add imports for all implementation files
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
content += `import { ${baseName} } from "./${baseName}"\n`
}
// Add registration function
content += `\n// Register all ${serviceName} service methods
export function registerAllMethods(): void {
\t// Register each method with the registry\n`
// Add registration statements
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
content += `\tregisterMethod("${baseName}", ${baseName})\n`
}
// Close the function
content += `}`
// Write the file
await fs.writeFile(registryFile, content)
console.log(chalk.green(`Generated ${registryFile}`))
}
console.log(chalk.green("Method registration files generated successfully."))
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
process.exit(1)
})
+40
View File
@@ -0,0 +1,40 @@
syntax = "proto3";
package cline;
message Metadata {
}
message EmptyRequest {
Metadata metadata = 1;
}
message Empty {
}
message StringRequest {
Metadata metadata = 1;
string value = 2;
}
message String {
string value = 1;
}
message Int64Request {
Metadata metadata = 1;
int64 value = 2;
}
message Int64 {
int64 value = 1;
}
message BytesRequest {
Metadata metadata = 1;
bytes value = 2;
}
message Bytes {
bytes value = 1;
}
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+3 -1
View File
@@ -50,7 +50,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o1":
case "o1-preview":
case "o1-mini": {
// o1 doesnt support streaming, non-1 temp, or system prompt
// o1 doesn't support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
model: model.id,
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
@@ -64,6 +64,8 @@ export class OpenAiNativeHandler implements ApiHandler {
break
}
case "o4-mini":
case "o3":
case "o3-mini": {
const stream = await this.client.chat.completions.create({
model: model.id,
+2 -2
View File
@@ -35,7 +35,7 @@ export class OpenAiHandler implements ApiHandler {
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
const isO3Mini = modelId.includes("o3-mini")
const isReasoningModelFamily = modelId.includes("o3") || modelId.includes("o4")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -55,7 +55,7 @@ export class OpenAiHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
if (isO3Mini) {
if (isReasoningModelFamily) {
openAiMessages = [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
temperature = undefined // does not support temperature
reasoningEffort = (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium"
+12
View File
@@ -5,6 +5,7 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { VertexAI } from "@google-cloud/vertexai"
import { calculateApiCostOpenAI } from "../../utils/cost"
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
export class VertexHandler implements ApiHandler {
@@ -266,6 +267,17 @@ export class VertexHandler implements ApiHandler {
}
}
}
// Handle token usage metadata
const { usageMetadata } = await streamingResult.response
if (usageMetadata) {
const { promptTokenCount = 0, candidatesTokenCount = 0 } = usageMetadata
yield {
type: "usage",
inputTokens: promptTokenCount,
outputTokens: candidatesTokenCount,
totalCost: calculateApiCostOpenAI(model.info, promptTokenCount, candidatesTokenCount, 0, 0),
}
}
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
export class XAIHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -47,7 +47,7 @@ export class XAIHandler implements ApiHandler {
}
}
if ("reasoning_content" in delta && delta.reasoning_content) {
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
// @ts-ignore-next-line
@@ -123,7 +123,7 @@ export function parseAssistantMessage(assistantMessage: string) {
contentBlocks.push(currentToolUse)
}
// Note: it doesnt matter if check for currentToolUse or currentTextContent, only one of them will be defined since only one can be partial at a time
// Note: it doesn't matter if check for currentToolUse or currentTextContent, only one of them will be defined since only one can be partial at a time
if (currentTextContent) {
// stream did not complete text content, add it as partial
contentBlocks.push(currentTextContent)
@@ -537,7 +537,7 @@ export class ContextManager {
// we can assume that thisExistingFileReads does not have many entries
if (!thisExistingFileReads.includes(filePath)) {
// meaning we havent already replaced this file read
// meaning we haven't already replaced this file read
const entireMatch = match[0] // The entire matched string
@@ -590,7 +590,7 @@ export class ContextManager {
) {
const pattern = new RegExp(`(<final_file_content path="[^"]*">)[\\s\\S]*?(</final_file_content>)`)
// check if this exists in the text, it wont exist if the user rejects the file change for example
// check if this exists in the text, it won't exist if the user rejects the file change for example
if (pattern.test(secondBlockText)) {
const replacementText = secondBlockText.replace(pattern, `$1 ${formatResponse.duplicateFileReadNotice()} $2`)
const indices = fileReadIndices.get(filePath) || []
@@ -741,7 +741,7 @@ export class ContextManager {
let totalCharactersSaved = 0
for (let i = startIndex; i < endIndex; i++) {
// looping over the outer indicies of messages
// looping over the outer indices of messages
const message = apiMessages[i]
if (!message.content) {
@@ -782,7 +782,7 @@ export class ContextManager {
totalCharCount += originalTextLength
} else {
// meaning there was an update to this text previously, but we didnt just alter it
// meaning there was an update to this text previously, but we didn't just alter it
totalCharCount += latestUpdate[2][0].length
}
} else {
@@ -790,7 +790,7 @@ export class ContextManager {
totalCharCount += block.text.length
}
} else {
// reach here if there's no alterations for this outer index, meaning each inner index wont have any changes either
// reach here if there's no alterations for this outer index, meaning each inner index won't have any changes either
totalCharCount += block.text.length
}
} else if (block.type === "image" && block.source) {
@@ -1,17 +1,29 @@
import path from "path"
import { GlobalFileNames } from "../../../storage/disk"
import { ensureRulesDirectoryExists, GlobalFileNames } from "../../../storage/disk"
import { fileExistsAtPath, isDirectory, readDirectory } from "../../../../utils/fs"
import { formatResponse } from "../../../prompts/responses"
import fs from "fs/promises"
import { ClineRulesToggles } from "../../../../shared/cline-rules"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../../../storage/state"
import * as vscode from "vscode"
export const getGlobalClineRules = async (globalClineRulesFilePath: string) => {
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, globalClineRulesFilePath)
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(rulesFilesTotalContent)
return clineRulesFileInstructions
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(
rulesFilePaths,
globalClineRulesFilePath,
toggles,
)
if (rulesFilesTotalContent) {
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(
globalClineRulesFilePath,
rulesFilesTotalContent,
)
return clineRulesFileInstructions
}
} catch {
console.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
}
@@ -24,7 +36,7 @@ export const getGlobalClineRules = async (globalClineRulesFilePath: string) => {
return undefined
}
export const getLocalClineRules = async (cwd: string) => {
export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles) => {
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let clineRulesFileInstructions: string | undefined
@@ -32,17 +44,21 @@ export const getLocalClineRules = async (cwd: string) => {
if (await fileExistsAtPath(clineRulesFilePath)) {
if (await isDirectory(clineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(path.join(cwd, GlobalFileNames.clineRules))
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd)
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
const rulesFilePaths = await readDirectory(clineRulesFilePath)
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
}
} catch {
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
}
} else {
try {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalFileInstructions(cwd, ruleFileContent)
if (clineRulesFilePath in toggles && toggles[clineRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
@@ -53,13 +69,107 @@ export const getLocalClineRules = async (cwd: string) => {
return clineRulesFileInstructions
}
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string) => {
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
const ruleFilesTotalContent = await Promise.all(
rulesFilePaths.map(async (filePath) => {
const ruleFilePath = path.resolve(basePath, filePath)
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
if (ruleFilePath in toggles && toggles[ruleFilePath] === false) {
return null
}
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
}),
).then((contents) => contents.join("\n\n"))
).then((contents) => contents.filter(Boolean).join("\n\n"))
return ruleFilesTotalContent
}
export async function synchronizeRuleToggles(
rulesDirectoryPath: string,
currentToggles: ClineRulesToggles,
): Promise<ClineRulesToggles> {
// Create a copy of toggles to modify
const updatedToggles = { ...currentToggles }
try {
const pathExists = await fileExistsAtPath(rulesDirectoryPath)
if (pathExists) {
const isDir = await isDirectory(rulesDirectoryPath)
if (isDir) {
// DIRECTORY CASE
const filePaths = await readDirectory(rulesDirectoryPath)
const existingRulePaths = new Set<string>()
for (const filePath of filePaths) {
const ruleFilePath = path.resolve(rulesDirectoryPath, filePath)
existingRulePaths.add(ruleFilePath)
const pathHasToggle = ruleFilePath in updatedToggles
if (!pathHasToggle) {
updatedToggles[ruleFilePath] = true
}
}
// Clean up toggles for non-existent files
for (const togglePath in updatedToggles) {
const pathExists = existingRulePaths.has(togglePath)
if (!pathExists) {
delete updatedToggles[togglePath]
}
}
} else {
// FILE CASE
// Add toggle for this file
const pathHasToggle = rulesDirectoryPath in updatedToggles
if (!pathHasToggle) {
updatedToggles[rulesDirectoryPath] = true
}
// Remove toggles for any other paths
for (const togglePath in updatedToggles) {
if (togglePath !== rulesDirectoryPath) {
delete updatedToggles[togglePath]
}
}
}
} else {
// PATH DOESN'T EXIST CASE
// Clear all toggles since the path doesn't exist
for (const togglePath in updatedToggles) {
delete updatedToggles[togglePath]
}
}
} catch (error) {
console.error(`Failed to synchronize rule toggles for path: ${rulesDirectoryPath}`, error)
}
return updatedToggles
}
export async function refreshClineRulesToggles(
context: vscode.ExtensionContext,
workingDirectory: string,
): Promise<{
globalToggles: ClineRulesToggles
localToggles: ClineRulesToggles
}> {
// Global toggles
const globalClineRulesToggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
await updateGlobalState(context, "globalClineRulesToggles", updatedGlobalToggles)
// Local toggles
const localClineRulesToggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
return {
globalToggles: updatedGlobalToggles,
localToggles: updatedLocalToggles,
}
}
@@ -0,0 +1,47 @@
import { BrowserConnectionInfo } from "../../../shared/proto/browser"
import { EmptyRequest } from "../../../shared/proto/common"
import { Controller } from "../index"
import { getAllExtensionState } from "../../storage/state"
/**
* Get information about the current browser connection
* @param controller The controller instance
* @param request The request message
* @returns The browser connection info
*/
export async function getBrowserConnectionInfo(controller: Controller, request: EmptyRequest): Promise<BrowserConnectionInfo> {
try {
// Get browser settings from extension state
const { browserSettings } = await getAllExtensionState(controller.context)
// Check if there's an active browser session by using the controller's handleWebviewMessage approach
// This is similar to what's done in controller/index.ts for the "getBrowserConnectionInfo" message
if (controller.task?.browserSession) {
// Access the browser session through the controller's task property
// Using indexer notation to access private property
const browserSession = controller.task.browserSession
const connectionInfo = browserSession.getConnectionInfo()
// Convert from BrowserSession.BrowserConnectionInfo to proto.BrowserConnectionInfo
return {
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host || "", // Ensure host is never undefined
}
}
// Fallback to browser settings if no active browser session
return {
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost || "",
}
} catch (error: unknown) {
console.error("Error getting browser connection info:", error)
return {
isConnected: false,
isRemote: false,
host: "",
}
}
}
+15
View File
@@ -0,0 +1,15 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
import { registerAllMethods } from "./methods"
// Create browser service registry
const browserService = createServiceRegistry("browser")
// Export the method handler type and registration function
export type BrowserMethodHandler = ServiceMethodHandler
export const registerMethod = browserService.registerMethod
// Export the request handler
export const handleBrowserServiceRequest = browserService.handleRequest
// Register all browser methods
registerAllMethods()
+14
View File
@@ -0,0 +1,14 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { getBrowserConnectionInfo } from "./getBrowserConnectionInfo"
import { testBrowserConnection } from "./testBrowserConnection"
// Register all browser service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("getBrowserConnectionInfo", getBrowserConnectionInfo)
registerMethod("testBrowserConnection", testBrowserConnection)
}
@@ -0,0 +1,63 @@
import { BrowserConnection } from "../../../shared/proto/browser"
import { StringRequest } from "../../../shared/proto/common"
import { Controller } from "../index"
import { getAllExtensionState } from "../../storage/state"
import { BrowserSession } from "../../../services/browser/BrowserSession"
import { discoverChromeInstances } from "../../../services/browser/BrowserDiscovery"
/**
* Test connection to a browser instance
* @param controller The controller instance
* @param request The request message
* @returns The browser connection result
*/
export async function testBrowserConnection(controller: Controller, request: StringRequest): Promise<BrowserConnection> {
try {
const { browserSettings } = await getAllExtensionState(controller.context)
const browserSession = new BrowserSession(controller.context, browserSettings)
const text = request.value || ""
// If no text is provided, try auto-discovery
if (!text) {
try {
const discoveredHost = await discoverChromeInstances()
if (discoveredHost) {
// Test the connection to the discovered host
const result = await browserSession.testConnection(discoveredHost)
return {
success: result.success,
message: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
endpoint: result.endpoint || "",
}
} else {
return {
success: false,
message:
"No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
endpoint: "",
}
}
} catch (error) {
return {
success: false,
message: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
}
}
} else {
// Test the provided URL
const result = await browserSession.testConnection(text)
return {
success: result.success,
message: result.message,
endpoint: result.endpoint || "",
}
}
} catch (error) {
return {
success: false,
message: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
endpoint: "",
}
}
}
+81
View File
@@ -0,0 +1,81 @@
import { Controller } from "./index"
import { handleBrowserServiceRequest } from "./browser/index"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
/**
* Handles gRPC requests from the webview
*/
export class GrpcHandler {
constructor(private controller: Controller) {}
/**
* Handle a gRPC request from the webview
* @param service The service name
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
* @returns The response message or error
*/
async handleRequest(
service: string,
method: string,
message: any,
requestId: string,
): Promise<{
message?: any
error?: string
request_id: string
}> {
try {
// Handle BrowserService requests
if (service === "cline.BrowserService") {
return {
message: await handleBrowserServiceRequest(this.controller, method, message),
request_id: requestId,
}
}
throw new Error(`Unknown service: ${service}`)
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
request_id: requestId,
}
}
}
}
/**
* Handle a gRPC request from the webview
* @param controller The controller instance
* @param request The gRPC request
*/
export async function handleGrpcRequest(
controller: Controller,
request: {
service: string
method: string
message: any
request_id: string
},
) {
try {
const grpcHandler = new GrpcHandler(controller)
const response = await grpcHandler.handleRequest(request.service, request.method, request.message, request.request_id)
// Send the response back to the webview
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: response,
})
} catch (error) {
// Send error response
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
error: error instanceof Error ? error.message : String(error),
request_id: request.request_id,
},
})
}
}
+65
View File
@@ -0,0 +1,65 @@
import { Controller } from "./index"
/**
* Generic type for service method handlers
*/
export type ServiceMethodHandler = (controller: Controller, message: any) => Promise<any>
/**
* Generic service registry for gRPC services
*/
export class ServiceRegistry {
private serviceName: string
private methodRegistry: Record<string, ServiceMethodHandler> = {}
/**
* Create a new service registry
* @param serviceName The name of the service (used for logging)
*/
constructor(serviceName: string) {
this.serviceName = serviceName
}
/**
* Register a method handler
* @param methodName The name of the method to register
* @param handler The handler function for the method
*/
registerMethod(methodName: string, handler: ServiceMethodHandler): void {
this.methodRegistry[methodName] = handler
console.log(`Registered ${this.serviceName} method: ${methodName}`)
}
/**
* Handle a service request
* @param controller The controller instance
* @param method The method name
* @param message The request message
* @returns The response message
*/
async handleRequest(controller: Controller, method: string, message: any): Promise<any> {
const handler = this.methodRegistry[method]
if (!handler) {
throw new Error(`Unknown ${this.serviceName} method: ${method}`)
}
return handler(controller, message)
}
}
/**
* Create a service registry factory function
* @param serviceName The name of the service
* @returns An object with register and handle functions
*/
export function createServiceRegistry(serviceName: string) {
const registry = new ServiceRegistry(serviceName)
return {
registerMethod: (methodName: string, handler: ServiceMethodHandler) => registry.registerMethod(methodName, handler),
handleRequest: (controller: Controller, method: string, message: any) =>
registry.handleRequest(controller, method, message),
}
}
+58 -88
View File
@@ -7,6 +7,7 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { handleGrpcRequest } from "./grpc-handler"
import { buildApiHandler } from "../../api"
import { cleanupLegacyCheckpoints } from "../../integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "../../integrations/misc/export-markdown"
@@ -39,12 +40,16 @@ import {
getAllExtensionState,
getGlobalState,
getSecret,
getWorkspaceState,
resetExtensionState,
storeSecret,
updateApiConfiguration,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { Task } from "../task"
import { Task, cwd } from "../task"
import { ClineRulesToggles } from "../../shared/cline-rules"
import { refreshClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -56,7 +61,7 @@ export class Controller {
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
private disposables: vscode.Disposable[] = []
private task?: Task
task?: Task
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
@@ -254,6 +259,13 @@ export class Controller {
telemetryService.updateTelemetryState(isOptedIn)
})
break
case "showChatView": {
this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
break
}
case "newTask":
// Code that should run in response to the hello message command
//vscode.window.showInformationMessage(message.text!)
@@ -299,88 +311,6 @@ export class Controller {
await this.postStateToWebview()
}
break
case "getBrowserConnectionInfo":
try {
// Get the current browser session from Cline if it exists
if (this.task?.browserSession) {
const connectionInfo = this.task.browserSession.getConnectionInfo()
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host,
})
} else {
// If no active browser session, just return the settings
const { browserSettings } = await getAllExtensionState(this.context)
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost,
})
}
} catch (error) {
console.error("Error getting browser connection info:", error)
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: false,
})
}
break
case "testBrowserConnection":
try {
const { browserSettings } = await getAllExtensionState(this.context)
const browserSession = new BrowserSession(this.context, browserSettings)
// If no text is provided, try auto-discovery
if (!message.text) {
try {
const discoveredHost = await discoverChromeInstances()
if (discoveredHost) {
// Test the connection to the discovered host
const result = await browserSession.testConnection(discoveredHost)
// Send the result back to the webview
await this.postMessageToWebview({
type: "browserConnectionResult",
success: result.success,
text: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
endpoint: result.endpoint,
})
} else {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
})
}
} catch (error) {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
})
}
} else {
// Test the provided URL
const result = await browserSession.testConnection(message.text)
// Send the result back to the webview
await this.postMessageToWebview({
type: "browserConnectionResult",
success: result.success,
text: result.message,
endpoint: result.endpoint,
})
}
} catch (error) {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
})
}
break
case "discoverBrowser":
try {
const discoveredHost = await discoverChromeInstances()
@@ -499,6 +429,10 @@ export class Controller {
const openAiModels = await this.getOpenAiModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
this.postMessageToWebview({ type: "openAiModels", openAiModels })
break
case "refreshClineRules":
await refreshClineRulesToggles(this.context, cwd)
await this.postStateToWebview()
break
case "openImage":
openImage(message.text!)
break
@@ -526,7 +460,7 @@ export class Controller {
break
}
case "checkpointRestore": {
await this.cancelTask() // we cannot alter message history say if the task is active, as it could be in the middle of editing a file or running a command, which expect the ask to be responded to rather than being superceded by a new message eg add deleted_api_reqs
await this.cancelTask() // we cannot alter message history say if the task is active, as it could be in the middle of editing a file or running a command, which expect the ask to be responded to rather than being superseded by a new message eg add deleted_api_reqs
// cancel task waits for any open editor to be reverted and starts a new cline instance
if (message.number) {
// wait for messages to be loaded
@@ -536,7 +470,7 @@ export class Controller {
console.error("Failed to init new cline instance")
})
// NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
await this.task?.restoreCheckpoint(message.number, message.text! as ClineCheckpointRestore)
await this.task?.restoreCheckpoint(message.number, message.text! as ClineCheckpointRestore, message.offset)
}
break
}
@@ -676,6 +610,30 @@ export class Controller {
}
break
}
case "toggleClineRule": {
const { isGlobal, rulePath, enabled } = message
if (rulePath && typeof enabled === "boolean" && typeof isGlobal === "boolean") {
if (isGlobal) {
const toggles =
((await getGlobalState(this.context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
toggles[rulePath] = enabled
await updateGlobalState(this.context, "globalClineRulesToggles", toggles)
} else {
const toggles =
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
toggles[rulePath] = enabled
await updateWorkspaceState(this.context, "localClineRulesToggles", toggles)
}
await this.postStateToWebview()
} else {
console.error("toggleClineRule: Missing or invalid parameters", {
rulePath,
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
})
}
break
}
case "requestTotalTasksSize": {
this.refreshTotalTasksSize()
break
@@ -910,6 +868,12 @@ export class Controller {
}
break
}
case "grpc_request": {
if (message.grpc_request) {
await handleGrpcRequest(this, message.grpc_request)
}
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -1756,7 +1720,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
}
}
// if we tried to get a task that doesn't exist, remove it from state
// FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason
// FIXME: this seems to happen sometimes when the json file doesn't save to disk for some reason
await this.deleteTaskFromState(id)
throw new Error("Task not found")
}
@@ -1888,8 +1852,12 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
mcpMarketplaceEnabled,
telemetrySetting,
planActSeparateModelsSetting,
globalClineRulesToggles,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
@@ -1912,6 +1880,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
telemetrySetting,
planActSeparateModelsSetting,
vscMachineId: vscode.env.machineId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
}
}
@@ -1933,7 +1903,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
// conversation history to send in API requests
/*
It seems that some API messages do not comply with vscode state requirements. Either the Anthropic library is manipulating these values somehow in the backend in a way thats creating cyclic references, or the API returns a function or a Symbol as part of the message content.
It seems that some API messages do not comply with vscode state requirements. Either the Anthropic library is manipulating these values somehow in the backend in a way that's creating cyclic references, or the API returns a function or a Symbol as part of the message content.
VSCode docs about state: "The value must be JSON-stringifyable ... value — A value. MUST not contain cyclic references."
For now we'll store the conversation history in memory, and if we need to store in state directly we'd need to do a manual conversion to ensure proper json stringification.
*/
+43
View File
@@ -0,0 +1,43 @@
export const newTaskToolResponse = () =>
`<explicit_instructions type="new_task">
The user has explicitly asked you to help them create a new task with preloaded context, which you will create. In this message the user has potentially added instructions or context which you should consider, if given, when creating the new task.
Irrespective of whether additional information or instructions are given, you are only allowed to respond to this message by calling the new_task tool.
To refresh your memory, the tool definition for new_task and an example for calling the tool is described below:
## new_task tool definition:
Description: Request to create a new task with preloaded context. The user will be presented with a preview of the context and can choose to create a new task or keep chatting in the current conversation. The user may choose to start a new task at any point.
Parameters:
- context: (required) The context to preload the new task with. This should include:
* Comprehensively explain what has been accomplished in the current task - mention specific file names that are relevant
* The specific next steps or focus for the new task - mention specific file names that are relevant
* Any critical information needed to continue the work
* Clear indication of how this new task relates to the overall workflow
* This should be akin to a long handoff file, enough for a totally new developer to be able to pick up where you left off and know exactly what to do next and which files to look at.
Usage:
<new_task>
<context>context to preload new task with</context>
</new_task>
## Tool use example:
<new_task>
<context>
Authentication System Implementation:
- We've implemented the basic user model with email/password
- Password hashing is working with bcrypt
- Login endpoint is functional with proper validation
- JWT token generation is implemented
Next Steps:
- Implement refresh token functionality
- Add token validation middleware
- Create password reset flow
- Implement role-based access control
</context>
</new_task>
Below is the the user's input when they indicated that they wanted to create a new task.
</explicit_instructions>\n
`
+2 -2
View File
@@ -204,8 +204,8 @@ Otherwise, if you have not completed the task and do not need additional informa
clineIgnoreInstructions: (content: string) =>
`# .clineignore\n\n(The following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${content}\n.clineignore`,
clineRulesGlobalDirectoryInstructions: (content: string) =>
`# .clinerules/\n\nThe following is provided by a global .clinerules/ directory where the user has specified instructions:\n\n${content}`,
clineRulesGlobalDirectoryInstructions: (globalClineRulesFilePath: string, content: string) =>
`# .clinerules/\n\nThe following is provided by a global .clinerules/ directory, located at ${globalClineRulesFilePath.toPosix()}, where the user has specified instructions for all working directories:\n\n${content}`,
clineRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
`# .clinerules/\n\nThe following is provided by a root-level .clinerules/ directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
+55
View File
@@ -0,0 +1,55 @@
import { newTaskToolResponse } 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"]
const commandReplacements: Record<string, string> = {
newtask: newTaskToolResponse(),
}
// this currently allows matching prepended whitespace prior to /slash-command
const tagPatterns = [
{ tag: "task", regex: /<task>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/task>/is },
{ tag: "feedback", regex: /<feedback>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/feedback>/is },
{ tag: "answer", regex: /<answer>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/answer>/is },
{ tag: "user_message", regex: /<user_message>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/user_message>/is },
]
// if we find a valid match, we will return inside that block
for (const { tag, regex } of tagPatterns) {
const regexObj = new RegExp(regex.source, regex.flags)
const match = regexObj.exec(text)
if (match) {
// match[1] is the command with any leading whitespace (e.g. " /newtask")
// match[2] is just the command name (e.g. "newtask")
const commandName = match[2] // casing matters
if (SUPPORTED_COMMANDS.includes(commandName)) {
const fullMatchStartIndex = match.index
// find position of slash command within the full match
const fullMatch = match[0]
const relativeStartIndex = fullMatch.indexOf(match[1])
// calculate absolute indices in the original string
const slashCommandStartIndex = fullMatchStartIndex + relativeStartIndex
const slashCommandEndIndex = slashCommandStartIndex + match[1].length
// remove the slash command and add custom instructions at the top of this message
const textWithoutSlashCommand = text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
const processedText = commandReplacements[commandName] + textWithoutSlashCommand
return processedText
}
}
}
// if no supported commands are found, return the original text
return text
}
+4
View File
@@ -19,6 +19,7 @@ export type SecretKey =
| "asksageApiKey"
| "xaiApiKey"
| "sambanovaApiKey"
export type GlobalStateKey =
| "apiProvider"
| "apiModelId"
@@ -47,6 +48,7 @@ export type GlobalStateKey =
| "openRouterModelInfo"
| "openRouterProviderSorting"
| "autoApprovalSettings"
| "globalClineRulesToggles"
| "browserSettings"
| "chatSettings"
| "vsCodeLmModelSelector"
@@ -71,3 +73,5 @@ export type GlobalStateKey =
| "reasoningEffort"
| "planActSeparateModelsSetting"
| "favoritedModelIds"
export type LocalStateKey = "localClineRulesToggles"
+7
View File
@@ -10,6 +10,7 @@ import { BrowserSettings } from "../../shared/BrowserSettings"
import { ChatSettings } from "../../shared/ChatSettings"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { UserInfo } from "../../shared/UserInfo"
import { ClineRulesToggles } from "../../shared/cline-rules"
/*
Storage
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
@@ -121,6 +122,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sambanovaApiKey,
planActSeparateModelsSettingRaw,
favoritedModelIds,
globalClineRulesToggles,
] = await Promise.all([
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -191,6 +193,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
])
let apiProvider: ApiProvider
@@ -207,6 +210,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
}
}
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
const o3MiniReasoningEffort = vscode.workspace.getConfiguration("cline.modelSettings.o3Mini").get("reasoningEffort", "medium")
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
@@ -291,6 +296,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
customInstructions,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
userInfo,
+40 -22
View File
@@ -70,8 +70,6 @@ import {
checkIsAnthropicContextWindowError,
checkIsOpenRouterContextWindowError,
} from "../context/context-management/context-error-handling"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { ContextManager } from "../context/context-management/ContextManager"
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
import {
@@ -82,10 +80,18 @@ import {
saveApiConversationHistory,
saveClineMessages,
} from "../storage/disk"
import { getGlobalClineRules, getLocalClineRules } from "../context/instructions/user-instructions/cline-rules"
import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
} from "../context/instructions/user-instructions/cline-rules"
import { getGlobalState } from "../storage/state"
import { parseSlashCommands } from ".././slash-commands"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
export const cwd =
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
type UserContent = Array<Anthropic.ContentBlockParam>
@@ -302,9 +308,13 @@ export class Task {
}
}
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore) {
const messageIndex = this.clineMessages.findIndex((m) => m.ts === messageTs)
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
const messageIndex = this.clineMessages.findIndex((m) => m.ts === messageTs) - (offset || 0)
// Find the last message before messageIndex that has a lastCheckpointHash
const lastHashIndex = findLastIndex(this.clineMessages.slice(0, messageIndex), (m) => m.lastCheckpointHash !== undefined)
const message = this.clineMessages[messageIndex]
const lastMessageWithHash = this.clineMessages[lastHashIndex]
if (!message) {
console.error("Message not found", this.clineMessages)
return
@@ -337,6 +347,14 @@ export class Task {
vscode.window.showErrorMessage("Failed to restore checkpoint: " + errorMessage)
didWorkspaceRestoreFail = true
}
} else if (offset && lastMessageWithHash.lastCheckpointHash && this.checkpointTracker) {
try {
await this.checkpointTracker.resetHead(lastMessageWithHash.lastCheckpointHash)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
vscode.window.showErrorMessage("Failed to restore offsetcheckpoint: " + errorMessage)
didWorkspaceRestoreFail = true
}
}
break
}
@@ -434,7 +452,7 @@ export class Task {
return
}
// TODO: handle if this is called from outside original workspace, in which case we need to show user error message we cant show diff outside of workspace?
// TODO: handle if this is called from outside original workspace, in which case we need to show user error message we can't show diff outside of workspace?
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
@@ -879,7 +897,7 @@ export class Task {
await this.overwriteClineMessages(modifiedClineMessages)
this.clineMessages = await getSavedClineMessages(this.getContext(), this.taskId)
// Now present the cline messages to the user and ask if they want to resume (NOTE: we ran into a bug before where the apiconversationhistory wouldnt be initialized when opening a old task, and it was because we were waiting for resume)
// Now present the cline messages to the user and ask if they want to resume (NOTE: we ran into a bug before where the apiconversationhistory wouldn't be initialized when opening a old task, and it was because we were waiting for resume)
// This is important in case the user deletes messages without resuming the task first
this.apiConversationHistory = await getSavedApiConversationHistory(this.getContext(), this.taskId)
@@ -1280,10 +1298,12 @@ export class Task {
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
: ""
const localClineRulesFileInstructions = await getLocalClineRules(cwd)
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.getContext(), cwd)
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath)
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, globalToggles)
const localClineRulesFileInstructions = await getLocalClineRules(cwd, localToggles)
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
let clineIgnoreInstructions: string | undefined
@@ -2418,7 +2438,7 @@ export class Task {
try {
if (block.partial) {
if (this.shouldAutoApproveTool(block.name)) {
// since depending on an upcoming parameter, requiresApproval this may become an ask - we cant partially stream a say prematurely. So in this particular case we have to wait for the requiresApproval parameter to be completed before presenting it.
// since depending on an upcoming parameter, requiresApproval this may become an ask - we can't partially stream a say prematurely. So in this particular case we have to wait for the requiresApproval parameter to be completed before presenting it.
// await this.say(
// "command",
// removeClosingTag("command", command),
@@ -2464,7 +2484,7 @@ export class Task {
let didAutoApprove = false
// If the model says this command is safe and auto aproval for safe commands is true, execute the command
// If the model says this command is safe and auto approval for safe commands is true, execute the command
// If the model says the command is risky, but *BOTH* auto approve settings are true, execute the command
const autoApproveResult = this.shouldAutoApproveTool(block.name)
const [autoApproveSafe, autoApproveAll] = Array.isArray(autoApproveResult)
@@ -2988,7 +3008,7 @@ export class Task {
)
} else {
// last message is completion_result
// we have command string, which means we have the result as well, so finish it (doesnt have to exist yet)
// we have command string, which means we have the result as well, so finish it (doesn't have to exist yet)
await this.say("completion_result", removeClosingTag("result", result), undefined, false)
await this.saveCheckpoint(true)
await addNewChangesFlagToLastCompletionResultMessage()
@@ -3024,7 +3044,7 @@ export class Task {
let commandResult: ToolResponse | undefined
if (command) {
if (lastMessage && lastMessage.ask !== "command") {
// havent sent a command message yet so first send completion_result then command
// haven't sent a command message yet so first send completion_result then command
await this.say("completion_result", result, undefined, false)
await this.saveCheckpoint(true)
await addNewChangesFlagToLastCompletionResultMessage()
@@ -3393,7 +3413,7 @@ export class Task {
if (this.didRejectTool) {
// userContent has a tool rejection, so interrupt the assistant's response to present the user's feedback
assistantMessage += "\n\n[Response interrupted by user feedback]"
// this.userMessageContentReady = true // instead of setting this premptively, we allow the present iterator to finish and set userMessageContentReady when its ready
// this.userMessageContentReady = true // instead of setting this preemptively, we allow the present iterator to finish and set userMessageContentReady when its ready
break
}
@@ -3448,7 +3468,7 @@ export class Task {
partialBlocks.forEach((block) => {
block.partial = false
})
// this.assistantMessageContent.forEach((e) => (e.partial = false)) // cant just do this bc a tool could be in the middle of executing ()
// this.assistantMessageContent.forEach((e) => (e.partial = false)) // can't just do this bc a tool could be in the middle of executing ()
if (partialBlocks.length > 0) {
this.presentAssistantMessage() // if there is content to update then it will complete and update this.userMessageContentReady to true, which we pwaitfor before making the next request. all this is really doing is presenting the last partial message that we just set to complete
}
@@ -3531,12 +3551,10 @@ export class Task {
block.text.includes("<task>") ||
block.text.includes("<user_message>")
) {
const parsedText = await parseMentions(
block.text,
cwd,
this.urlContentFetcher,
this.fileContextTracker,
)
let 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)
return {
...block,
+1 -1
View File
@@ -193,7 +193,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
/*
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicitly allow for these resources. E.g.
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
+18
View File
@@ -397,6 +397,24 @@ export function activate(context: vscode.ExtensionContext) {
}),
)
// Register the focusChatInput command handler
context.subscriptions.push(
vscode.commands.registerCommand("cline.focusChatInput", () => {
let visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
visibleWebview = WebviewProvider.getSidebarInstance()
// showing the extension will call didBecomeVisible which focuses it already
// but it doesn't focus if a tab is selected which focusChatInput accounts for
}
visibleWebview?.controller.postMessageToWebview({
type: "action",
action: "focusChatInput",
})
}),
)
// Set up test server if in test mode
if (IS_TEST === "true") {
createTestServer(sidebarWebview)
@@ -61,7 +61,7 @@ class DiagnosticsMonitor {
return currentDiagnostics
}
let timeout = 300 // only way this happens is if theres no errors
let timeout = 300 // only way this happens is if there's no errors
// if diagnostics contain existing errors (since the check above didn't trigger) then it's likely cline just did something that should have fixed the error, so we'll give a longer grace period for diagnostics to catch up
const hasErrors = currentDiagnostics.some(([_, diagnostics]) =>
+1 -1
View File
@@ -289,7 +289,7 @@ export class DiffViewProvider {
updatedDocument.positionAt(updatedDocument.getText().length),
)
edit.replace(updatedDocument.uri, fullRange, this.originalContent ?? "")
// Apply the edit and save, since contents shouldnt have changed this wont show in local history unless of course the user made changes and saved during the edit
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of course the user made changes and saved during the edit
await vscode.workspace.applyEdit(edit)
await updatedDocument.save()
console.log(`File ${absolutePath} has been reverted to its original content.`)
+2 -2
View File
@@ -95,7 +95,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
}
// first few chunks could be the command being echoed back, so we must ignore
// note this means that 'echo' commands wont work
// note this means that 'echo' commands won't work
if (!didOutputNonCommand) {
const lines = data.split("\n")
for (let i = 0; i < lines.length; i++) {
@@ -145,7 +145,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL,
)
// For non-immediately returning commands we want to show loading spinner right away but this wouldnt happen until it emits a line break, so as soon as we get any output we emit "" to let webview know to show spinner
// For non-immediately returning commands we want to show loading spinner right away but this wouldn't happen until it emits a line break, so as soon as we get any output we emit "" to let webview know to show spinner
if (!didEmitEmptyLine && !this.fullOutput && data) {
this.emit("line", "") // empty line to indicate start of command output stream
didEmitEmptyLine = true
+1 -1
View File
@@ -120,7 +120,7 @@ export async function searchWorkspaceFiles(
limit: limit * 2,
})
// The min threshold value will require some testing and tuning as the scores are exponential, and exagerated
// The min threshold value will require some testing and tuning as the scores are exponential, and exaggerated
const MIN_SCORE_THRESHOLD = 100
// Filter results by score and map to original items
+1 -1
View File
@@ -157,7 +157,7 @@ async function parseFile(
formattedOutput += "|----\n"
}
// Only add the first line of the definition
// query captures includes the definition name and the definition implementation, but we only want the name (I found discrepencies in the naming structure for various languages, i.e. javascript names would be 'name' and typescript names would be 'name.definition)
// query captures includes the definition name and the definition implementation, but we only want the name (I found discrepancies in the naming structure for various languages, i.e. javascript names would be 'name' and typescript names would be 'name.definition)
if (name.includes("name") && lines[startLine]) {
formattedOutput += `${lines[startLine]}\n`
}
+10 -7
View File
@@ -9,6 +9,7 @@ import { HistoryItem } from "./HistoryItem"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse, McpViewTab } from "./mcp"
import { TelemetrySetting } from "./TelemetrySetting"
import type { BalanceResponse, UsageTransaction, PaymentTransaction } from "../shared/ClineAccount"
import { ClineRulesToggles } from "./cline-rules"
// webview will hold state
export interface ExtensionMessage {
@@ -43,12 +44,12 @@ export interface ExtensionMessage {
| "totalTasksSize"
| "addToInput"
| "browserConnectionResult"
| "browserConnectionInfo"
| "detectedChromePath"
| "scrollToSettings"
| "browserRelaunchResult"
| "relativePathsResponse" // Handles single and multiple path responses
| "fileSearchResults"
| "grpc_response" // New type for gRPC responses
text?: string
paths?: (string | null)[] // Used for relativePathsResponse
action?:
@@ -60,6 +61,7 @@ export interface ExtensionMessage {
| "accountLoginClicked"
| "accountLogoutClicked"
| "accountButtonClicked"
| "focusChatInput"
invoke?: Invoke
state?: ExtensionState
images?: string[]
@@ -109,6 +111,11 @@ export interface ExtensionMessage {
error?: string
}
tab?: McpViewTab
grpc_response?: {
message?: any // JSON serialized protobuf message
request_id: string // Same ID as the request
error?: string // Optional error message
}
}
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
@@ -141,6 +148,8 @@ export interface ExtensionState {
}
version: string
vscMachineId: string
globalClineRulesToggles: ClineRulesToggles
localClineRulesToggles: ClineRulesToggles
}
export interface ClineMessage {
@@ -236,12 +245,6 @@ export type BrowserActionResult = {
currentMousePosition?: string
}
export interface BrowserConnectionInfo {
isConnected: boolean
isRemote: boolean
host?: string
}
export interface ClineAskUseMcpServer {
serverName: string
type: "use_mcp_tool" | "access_mcp_resource"
+16 -3
View File
@@ -29,17 +29,17 @@ export interface WebviewMessage {
| "openFile"
| "openMention"
| "cancelTask"
| "showChatView"
| "refreshOpenRouterModels"
| "refreshRequestyModels"
| "refreshOpenAiModels"
| "refreshClineRules"
| "openMcpSettings"
| "restartMcpServer"
| "deleteMcpServer"
| "autoApprovalSettings"
| "browserSettings"
| "discoverBrowser"
| "testBrowserConnection"
| "browserConnectionResult"
| "browserRelaunchResult"
| "togglePlanActMode"
| "checkpointDiff"
@@ -74,13 +74,15 @@ export interface WebviewMessage {
| "requestTotalTasksSize"
| "relaunchChromeDebugMode"
| "taskFeedback"
| "getBrowserConnectionInfo"
| "getDetectedChromePath"
| "detectedChromePath"
| "scrollToSettings"
| "getRelativePaths" // Handles single and multiple URI resolution
| "searchFiles"
| "toggleFavoriteModel"
| "grpc_request"
| "toggleClineRule"
// | "relaunchChromeDebugMode"
text?: string
uris?: string[] // Used for getRelativePaths
@@ -117,6 +119,17 @@ export interface WebviewMessage {
query?: string
// For toggleFavoriteModel
modelId?: string
grpc_request?: {
service: string
method: string
message: any // JSON serialized protobuf message
request_id: string // For correlating requests and responses
}
// For toggleClineRule
isGlobal?: boolean
rulePath?: string
enabled?: boolean
offset?: number
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+18
View File
@@ -606,6 +606,24 @@ export const geminiModels = {
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4.1"
export const openAiNativeModels = {
o3: {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 10.0,
outputPrice: 40.0,
cacheReadsPrice: 2.5,
},
"o4-mini": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.1,
outputPrice: 4.4,
cacheReadsPrice: 0.275,
},
"gpt-4.1": {
maxTokens: 32_768,
contextWindow: 1_047_576,
+1
View File
@@ -0,0 +1 @@
export type ClineRulesToggles = Record<string, boolean> // filepath -> enabled/disabled
+261
View File
@@ -0,0 +1,261 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.1
// source: browser.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { EmptyRequest, StringRequest } from "./common"
export const protobufPackage = "cline"
export interface BrowserConnectionInfo {
isConnected: boolean
isRemote: boolean
host?: string | undefined
}
export interface BrowserConnection {
success: boolean
message: string
endpoint?: string | undefined
}
function createBaseBrowserConnectionInfo(): BrowserConnectionInfo {
return { isConnected: false, isRemote: false, host: undefined }
}
export const BrowserConnectionInfo: MessageFns<BrowserConnectionInfo> = {
encode(message: BrowserConnectionInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.isConnected !== false) {
writer.uint32(8).bool(message.isConnected)
}
if (message.isRemote !== false) {
writer.uint32(16).bool(message.isRemote)
}
if (message.host !== undefined) {
writer.uint32(26).string(message.host)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): BrowserConnectionInfo {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBrowserConnectionInfo()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.isConnected = reader.bool()
continue
}
case 2: {
if (tag !== 16) {
break
}
message.isRemote = reader.bool()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.host = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): BrowserConnectionInfo {
return {
isConnected: isSet(object.isConnected) ? globalThis.Boolean(object.isConnected) : false,
isRemote: isSet(object.isRemote) ? globalThis.Boolean(object.isRemote) : false,
host: isSet(object.host) ? globalThis.String(object.host) : undefined,
}
},
toJSON(message: BrowserConnectionInfo): unknown {
const obj: any = {}
if (message.isConnected !== false) {
obj.isConnected = message.isConnected
}
if (message.isRemote !== false) {
obj.isRemote = message.isRemote
}
if (message.host !== undefined) {
obj.host = message.host
}
return obj
},
create<I extends Exact<DeepPartial<BrowserConnectionInfo>, I>>(base?: I): BrowserConnectionInfo {
return BrowserConnectionInfo.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<BrowserConnectionInfo>, I>>(object: I): BrowserConnectionInfo {
const message = createBaseBrowserConnectionInfo()
message.isConnected = object.isConnected ?? false
message.isRemote = object.isRemote ?? false
message.host = object.host ?? undefined
return message
},
}
function createBaseBrowserConnection(): BrowserConnection {
return { success: false, message: "", endpoint: undefined }
}
export const BrowserConnection: MessageFns<BrowserConnection> = {
encode(message: BrowserConnection, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.success !== false) {
writer.uint32(8).bool(message.success)
}
if (message.message !== "") {
writer.uint32(18).string(message.message)
}
if (message.endpoint !== undefined) {
writer.uint32(26).string(message.endpoint)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): BrowserConnection {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBrowserConnection()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.success = reader.bool()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.message = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.endpoint = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): BrowserConnection {
return {
success: isSet(object.success) ? globalThis.Boolean(object.success) : false,
message: isSet(object.message) ? globalThis.String(object.message) : "",
endpoint: isSet(object.endpoint) ? globalThis.String(object.endpoint) : undefined,
}
},
toJSON(message: BrowserConnection): unknown {
const obj: any = {}
if (message.success !== false) {
obj.success = message.success
}
if (message.message !== "") {
obj.message = message.message
}
if (message.endpoint !== undefined) {
obj.endpoint = message.endpoint
}
return obj
},
create<I extends Exact<DeepPartial<BrowserConnection>, I>>(base?: I): BrowserConnection {
return BrowserConnection.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<BrowserConnection>, I>>(object: I): BrowserConnection {
const message = createBaseBrowserConnection()
message.success = object.success ?? false
message.message = object.message ?? ""
message.endpoint = object.endpoint ?? undefined
return message
},
}
export type BrowserServiceDefinition = typeof BrowserServiceDefinition
export const BrowserServiceDefinition = {
name: "BrowserService",
fullName: "cline.BrowserService",
methods: {
getBrowserConnectionInfo: {
name: "getBrowserConnectionInfo",
requestType: EmptyRequest,
requestStream: false,
responseType: BrowserConnectionInfo,
responseStream: false,
options: {},
},
testBrowserConnection: {
name: "testBrowserConnection",
requestType: StringRequest,
requestStream: false,
responseType: BrowserConnection,
responseStream: false,
options: {},
},
},
} as const
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
+644
View File
@@ -0,0 +1,644 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.1
// source: common.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
export const protobufPackage = "cline"
export interface Metadata {}
export interface EmptyRequest {
metadata?: Metadata | undefined
}
export interface Empty {}
export interface StringRequest {
metadata?: Metadata | undefined
value: string
}
export interface String {
value: string
}
export interface Int64Request {
metadata?: Metadata | undefined
value: number
}
export interface Int64 {
value: number
}
export interface BytesRequest {
metadata?: Metadata | undefined
value: Buffer
}
export interface Bytes {
value: Buffer
}
function createBaseMetadata(): Metadata {
return {}
}
export const Metadata: MessageFns<Metadata> = {
encode(_: Metadata, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Metadata {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseMetadata()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(_: any): Metadata {
return {}
},
toJSON(_: Metadata): unknown {
const obj: any = {}
return obj
},
create<I extends Exact<DeepPartial<Metadata>, I>>(base?: I): Metadata {
return Metadata.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Metadata>, I>>(_: I): Metadata {
const message = createBaseMetadata()
return message
},
}
function createBaseEmptyRequest(): EmptyRequest {
return { metadata: undefined }
}
export const EmptyRequest: MessageFns<EmptyRequest> = {
encode(message: EmptyRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): EmptyRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseEmptyRequest()
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
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): EmptyRequest {
return { metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined }
},
toJSON(message: EmptyRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
return obj
},
create<I extends Exact<DeepPartial<EmptyRequest>, I>>(base?: I): EmptyRequest {
return EmptyRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<EmptyRequest>, I>>(object: I): EmptyRequest {
const message = createBaseEmptyRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
return message
},
}
function createBaseEmpty(): Empty {
return {}
}
export const Empty: MessageFns<Empty> = {
encode(_: Empty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Empty {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseEmpty()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(_: any): Empty {
return {}
},
toJSON(_: Empty): unknown {
const obj: any = {}
return obj
},
create<I extends Exact<DeepPartial<Empty>, I>>(base?: I): Empty {
return Empty.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Empty>, I>>(_: I): Empty {
const message = createBaseEmpty()
return message
},
}
function createBaseStringRequest(): StringRequest {
return { metadata: undefined, value: "" }
}
export const StringRequest: MessageFns<StringRequest> = {
encode(message: StringRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.value !== "") {
writer.uint32(18).string(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): StringRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseStringRequest()
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.value = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): StringRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
value: isSet(object.value) ? globalThis.String(object.value) : "",
}
},
toJSON(message: StringRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.value !== "") {
obj.value = message.value
}
return obj
},
create<I extends Exact<DeepPartial<StringRequest>, I>>(base?: I): StringRequest {
return StringRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<StringRequest>, I>>(object: I): StringRequest {
const message = createBaseStringRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.value = object.value ?? ""
return message
},
}
function createBaseString(): String {
return { value: "" }
}
export const String: MessageFns<String> = {
encode(message: String, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.value !== "") {
writer.uint32(10).string(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): String {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseString()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.value = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): String {
return { value: isSet(object.value) ? globalThis.String(object.value) : "" }
},
toJSON(message: String): unknown {
const obj: any = {}
if (message.value !== "") {
obj.value = message.value
}
return obj
},
create<I extends Exact<DeepPartial<String>, I>>(base?: I): String {
return String.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<String>, I>>(object: I): String {
const message = createBaseString()
message.value = object.value ?? ""
return message
},
}
function createBaseInt64Request(): Int64Request {
return { metadata: undefined, value: 0 }
}
export const Int64Request: MessageFns<Int64Request> = {
encode(message: Int64Request, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.value !== 0) {
writer.uint32(16).int64(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Int64Request {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseInt64Request()
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 = longToNumber(reader.int64())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): Int64Request {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
value: isSet(object.value) ? globalThis.Number(object.value) : 0,
}
},
toJSON(message: Int64Request): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.value !== 0) {
obj.value = Math.round(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<Int64Request>, I>>(base?: I): Int64Request {
return Int64Request.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Int64Request>, I>>(object: I): Int64Request {
const message = createBaseInt64Request()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.value = object.value ?? 0
return message
},
}
function createBaseInt64(): Int64 {
return { value: 0 }
}
export const Int64: MessageFns<Int64> = {
encode(message: Int64, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.value !== 0) {
writer.uint32(8).int64(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Int64 {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseInt64()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.value = longToNumber(reader.int64())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): Int64 {
return { value: isSet(object.value) ? globalThis.Number(object.value) : 0 }
},
toJSON(message: Int64): unknown {
const obj: any = {}
if (message.value !== 0) {
obj.value = Math.round(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<Int64>, I>>(base?: I): Int64 {
return Int64.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Int64>, I>>(object: I): Int64 {
const message = createBaseInt64()
message.value = object.value ?? 0
return message
},
}
function createBaseBytesRequest(): BytesRequest {
return { metadata: undefined, value: Buffer.alloc(0) }
}
export const BytesRequest: MessageFns<BytesRequest> = {
encode(message: BytesRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.value.length !== 0) {
writer.uint32(18).bytes(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): BytesRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBytesRequest()
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.value = Buffer.from(reader.bytes())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): BytesRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
value: isSet(object.value) ? Buffer.from(bytesFromBase64(object.value)) : Buffer.alloc(0),
}
},
toJSON(message: BytesRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.value.length !== 0) {
obj.value = base64FromBytes(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<BytesRequest>, I>>(base?: I): BytesRequest {
return BytesRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<BytesRequest>, I>>(object: I): BytesRequest {
const message = createBaseBytesRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.value = object.value ?? Buffer.alloc(0)
return message
},
}
function createBaseBytes(): Bytes {
return { value: Buffer.alloc(0) }
}
export const Bytes: MessageFns<Bytes> = {
encode(message: Bytes, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.value.length !== 0) {
writer.uint32(10).bytes(message.value)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Bytes {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBytes()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.value = Buffer.from(reader.bytes())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): Bytes {
return { value: isSet(object.value) ? Buffer.from(bytesFromBase64(object.value)) : Buffer.alloc(0) }
},
toJSON(message: Bytes): unknown {
const obj: any = {}
if (message.value.length !== 0) {
obj.value = base64FromBytes(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<Bytes>, I>>(base?: I): Bytes {
return Bytes.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Bytes>, I>>(object: I): Bytes {
const message = createBaseBytes()
message.value = object.value ?? Buffer.alloc(0)
return message
},
}
function bytesFromBase64(b64: string): Uint8Array {
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"))
}
function base64FromBytes(arr: Uint8Array): string {
return globalThis.Buffer.from(arr).toString("base64")
}
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function longToNumber(int64: { toString(): string }): number {
const num = globalThis.Number(int64.toString())
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER")
}
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER")
}
return num
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
+9 -1
View File
@@ -75,6 +75,13 @@ export async function getFileSizeInKB(filePath: string): Promise<number> {
}
}
// Common OS-generated files that would appear in an otherwise clean directory
const OS_GENERATED_FILES = [
".DS_Store", // macOS Finder
"Thumbs.db", // Windows Explorer thumbnails
"desktop.ini", // Windows folder settings
]
/**
* Recursively reads a directory and returns an array of absolute file paths.
*
@@ -86,7 +93,8 @@ export const readDirectory = async (directoryPath: string) => {
try {
const filePaths = await fs
.readdir(directoryPath, { withFileTypes: true, recursive: true })
.then((files) => files.filter((file) => file.isFile()))
.then((entries) => entries.filter((entry) => !OS_GENERATED_FILES.includes(entry.name)))
.then((entries) => entries.filter((entry) => entry.isFile()))
.then((files) => files.map((file) => path.resolve(file.parentPath, file.name)))
return filePaths
} catch {
+1 -1
View File
@@ -91,7 +91,7 @@ function getWindowsShellFromVSCode(): string | null {
// If the profile name indicates PowerShell, do version-based detection.
// In testing it was found these typically do not have a path, and this
// implementation manages to deductively get the corect version of PowerShell
// implementation manages to deductively get the correct version of PowerShell
if (defaultProfileName.toLowerCase().includes("powershell")) {
if (profile?.path) {
// If there's an explicit PowerShell path, return that
+23 -13
View File
@@ -27,7 +27,8 @@
"react-use": "^17.6.0",
"react-virtuoso": "^4.12.3",
"rehype-highlight": "^7.0.1",
"styled-components": "^6.1.15"
"styled-components": "^6.1.15",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -40,6 +41,7 @@
"@types/node": "^22.13.4",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/uuid": "^9.0.8",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-v8": "^3.0.9",
@@ -3539,6 +3541,13 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/vscode-webview": {
"version": "1.57.5",
"resolved": "https://registry.npmjs.org/@types/vscode-webview/-/vscode-webview-1.57.5.tgz",
@@ -6936,18 +6945,6 @@
"uuid": "^9.0.1"
}
},
"node_modules/mermaid/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/micromark": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz",
@@ -8836,6 +8833,19 @@
}
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+3 -1
View File
@@ -33,7 +33,8 @@
"react-use": "^17.6.0",
"react-virtuoso": "^4.12.3",
"rehype-highlight": "^7.0.1",
"styled-components": "^6.1.15"
"styled-components": "^6.1.15",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -46,6 +47,7 @@
"@types/node": "^22.13.4",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/uuid": "^9.0.8",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-v8": "^3.0.9",
@@ -3,6 +3,8 @@ import { useEffect, useRef, useState } from "react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { BrowserServiceClient } from "../../services/grpc-client"
interface ConnectionInfo {
isConnected: boolean
@@ -21,29 +23,25 @@ export const BrowserSettingsMenu = () => {
})
const popoverRef = useRef<HTMLDivElement>(null)
// Get actual connection info from the browser session
// Get actual connection info from the browser session using gRPC
useEffect(() => {
// Request connection info when component mounts
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
// Listen for connection info updates
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "browserConnectionInfo") {
// Function to fetch connection info
;(async () => {
try {
console.log("[DEBUG] SENDING BROWSER CONNECTION INFO REQUEST")
const info = await BrowserServiceClient.getBrowserConnectionInfo({})
console.log("[DEBUG] GOT BROWSER REPLY:", info, typeof info)
setConnectionInfo({
isConnected: message.isConnected,
isRemote: message.isRemote,
host: message.host,
isConnected: info.isConnected,
isRemote: info.isRemote,
host: info.host,
})
} catch (error) {
console.error("Error fetching browser connection info:", error)
}
}
})()
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
// No need for message event listeners anymore!
}, [browserSettings.remoteBrowserHost, browserSettings.remoteBrowserEnabled])
// Close popover when clicking outside
@@ -84,11 +82,22 @@ export const BrowserSettingsMenu = () => {
const toggleInfoPopover = () => {
setShowInfoPopover(!showInfoPopover)
// Request updated connection info when opening the popover
// Request updated connection info when opening the popover using gRPC
if (!showInfoPopover) {
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
const fetchConnectionInfo = async () => {
try {
const info = await BrowserServiceClient.getBrowserConnectionInfo({})
setConnectionInfo({
isConnected: info.isConnected,
isRemote: info.isRemote,
host: info.host,
})
} catch (error) {
console.error("Error fetching browser connection info:", error)
}
}
fetchConnectionInfo()
}
}
@@ -112,19 +121,27 @@ export const BrowserSettingsMenu = () => {
}
}
// Check connection status every second to keep icon in sync
// Check connection status every second to keep icon in sync using gRPC
useEffect(() => {
// Function to fetch connection info
const fetchConnectionInfo = async () => {
try {
const info = await BrowserServiceClient.getBrowserConnectionInfo({})
setConnectionInfo({
isConnected: info.isConnected,
isRemote: info.isRemote,
host: info.host,
})
} catch (error) {
console.error("Error fetching browser connection info:", error)
}
}
// Request connection info immediately
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
fetchConnectionInfo()
// Set up interval to refresh every second
const intervalId = setInterval(() => {
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
}, 1000)
const intervalId = setInterval(fetchConnectionInfo, 1000)
return () => clearInterval(intervalId)
}, [])
@@ -29,7 +29,7 @@ const linkContainerStyle: CSSProperties = { margin: "0" }
const linkStyle: CSSProperties = { display: "inline" }
/*
You must update the latestAnnouncementId in ClineProvider for new announcements to show to users. This new id will be compared with whats in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves.
You must update the latestAnnouncementId in ClineProvider for new announcements to show to users. This new id will be compared with what's in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves.
*/
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
@@ -192,7 +192,12 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
// Reset for next page
currentStateMessages = []
nextActionMessages = []
} else if (message.say === "api_req_started" || message.say === "text" || message.say === "browser_action") {
} else if (
message.say === "api_req_started" ||
message.say === "text" ||
message.say === "reasoning" ||
message.say === "browser_action"
) {
// These messages lead to the next result, so they should always go in nextActionMessages
nextActionMessages.push(message)
} else {
@@ -511,6 +516,7 @@ const BrowserSessionRowContent = ({
switch (message.say) {
case "api_req_started":
case "text":
case "reasoning":
return (
<div style={chatRowContentContainerStyle}>
<ChatRowContent
+22 -17
View File
@@ -27,11 +27,12 @@ import McpToolRow from "@/components/mcp/configuration/tabs/installed/server-row
import McpResponseDisplay from "@/components/mcp/chat-display/McpResponseDisplay"
import CreditLimitError from "@/components/chat/CreditLimitError"
import { OptionsButtons } from "@/components/chat/OptionsButtons"
import { highlightMentions } from "./TaskHeader"
import { highlightText } from "./TaskHeader"
import SuccessButton from "@/components/common/SuccessButton"
import TaskFeedbackButtons from "@/components/chat/TaskFeedbackButtons"
import NewTaskPreview from "./NewTaskPreview"
import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server-row/McpResourceRow"
import UserMessage from "./UserMessage"
const ChatRowContainer = styled.div`
padding: 10px 6px 10px 15px;
@@ -49,6 +50,8 @@ interface ChatRowProps {
lastModifiedMessage?: ClineMessage
isLast: boolean
onHeightChange: (isTaller: boolean) => void
inputValue?: string
sendMessageFromChatRow?: (text: string, images: string[]) => void
}
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
@@ -84,7 +87,7 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
const ChatRow = memo(
(props: ChatRowProps) => {
const { isLast, onHeightChange, message, lastModifiedMessage } = props
const { isLast, onHeightChange, message, lastModifiedMessage, inputValue } = props
// Store the previous height to compare with the current height
// This allows us to detect changes without causing re-renders
const prevHeightRef = useRef(0)
@@ -117,7 +120,15 @@ const ChatRow = memo(
export default ChatRow
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
export const ChatRowContent = ({
message,
isExpanded,
onToggleExpand,
lastModifiedMessage,
isLast,
inputValue,
sendMessageFromChatRow,
}: ChatRowContentProps) => {
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
@@ -864,20 +875,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
)
case "user_feedback":
return (
<div
style={{
backgroundColor: "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "9px",
whiteSpace: "pre-line",
wordWrap: "break-word",
}}>
<span style={{ display: "block" }}>{highlightMentions(message.text)}</span>
{message.images && message.images.length > 0 && (
<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
)}
</div>
<UserMessage
text={message.text}
images={message.images}
messageTs={message.ts}
sendMessageFromChatRow={sendMessageFromChatRow}
/>
)
case "user_feedback_diff":
const tool = JSON.parse(message.text || "{}") as ClineSayTool
@@ -1248,6 +1251,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "followup"}
inputValue={inputValue}
/>
</div>
</>
@@ -1287,6 +1291,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "plan_mode_respond"}
inputValue={inputValue}
/>
</div>
)
+127 -4
View File
@@ -15,6 +15,13 @@ import {
shouldShowContextMenu,
SearchResult,
} from "@/utils/context-mentions"
import {
SlashCommand,
shouldShowSlashCommandsMenu,
getMatchingSlashCommands,
insertSlashCommand,
validateSlashCommand,
} from "@/utils/slash-commands"
import { useMetaKeyDetection, useShortcut } from "@/utils/hooks"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
@@ -24,8 +31,10 @@ import Tooltip from "@/components/common/Tooltip"
import ApiOptions, { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import { MAX_IMAGES_PER_MESSAGE } from "@/components/chat/ChatView"
import ContextMenu from "@/components/chat/ContextMenu"
import SlashCommandMenu from "@/components/chat/SlashCommandMenu"
import { ChatSettings } from "@shared/ChatSettings"
import ServersToggleModal from "./ServersToggleModal"
import ClineRulesToggleModal from "../cline-rules/ClineRulesToggleModal"
interface ChatTextAreaProps {
inputValue: string
@@ -228,6 +237,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
const [showSlashCommandsMenu, setShowSlashCommandsMenu] = useState(false)
const [selectedSlashCommandsIndex, setSelectedSlashCommandsIndex] = useState(0)
const [slashCommandsQuery, setSlashCommandsQuery] = useState("")
const slashCommandsMenuContainerRef = useRef<HTMLDivElement>(null)
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
const [showContextMenu, setShowContextMenu] = useState(false)
@@ -388,8 +402,62 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
[setInputValue, cursorPosition],
)
const handleSlashCommandsSelect = useCallback(
(command: SlashCommand) => {
setShowSlashCommandsMenu(false)
if (textAreaRef.current) {
const { newValue, commandIndex } = insertSlashCommand(textAreaRef.current.value, command.name)
const newCursorPosition = newValue.indexOf(" ", commandIndex + 1 + command.name.length) + 1
setInputValue(newValue)
setCursorPosition(newCursorPosition)
setIntendedCursorPosition(newCursorPosition)
setTimeout(() => {
if (textAreaRef.current) {
textAreaRef.current.blur()
textAreaRef.current.focus()
}
}, 0)
}
},
[setInputValue],
)
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (showSlashCommandsMenu) {
if (event.key === "Escape") {
setShowSlashCommandsMenu(false)
return
}
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
event.preventDefault()
setSelectedSlashCommandsIndex((prevIndex) => {
const direction = event.key === "ArrowUp" ? -1 : 1
const commands = getMatchingSlashCommands(slashCommandsQuery)
if (commands.length === 0) {
return prevIndex
}
const newIndex = (prevIndex + direction + commands.length) % commands.length
return newIndex
})
return
}
if ((event.key === "Enter" || event.key === "Tab") && selectedSlashCommandsIndex !== -1) {
event.preventDefault()
const commands = getMatchingSlashCommands(slashCommandsQuery)
if (commands.length > 0) {
handleSlashCommandsSelect(commands[selectedSlashCommandsIndex])
}
return
}
}
if (showContextMenu) {
if (event.key === "Escape") {
// event.preventDefault()
@@ -541,9 +609,28 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const newCursorPosition = e.target.selectionStart
setInputValue(newValue)
setCursorPosition(newCursorPosition)
const showMenu = shouldShowContextMenu(newValue, newCursorPosition)
let showMenu = shouldShowContextMenu(newValue, newCursorPosition)
const showSlashCommandsMenu = shouldShowSlashCommandsMenu(newValue, newCursorPosition)
// we do not allow both menus to be shown at the same time
// the slash commands menu has precedence bc its a narrower component
if (showSlashCommandsMenu) {
showMenu = false
}
setShowSlashCommandsMenu(showSlashCommandsMenu)
setShowContextMenu(showMenu)
if (showSlashCommandsMenu) {
const slashIndex = newValue.indexOf("/")
const query = newValue.slice(slashIndex + 1, newCursorPosition)
setSlashCommandsQuery(query)
setSelectedSlashCommandsIndex(0)
} else {
setSlashCommandsQuery("")
setSelectedSlashCommandsIndex(0)
}
if (showMenu) {
const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
@@ -590,6 +677,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// Only hide the context menu if the user didn't click on it
if (!isMouseDownOnMenu) {
setShowContextMenu(false)
setShowSlashCommandsMenu(false)
}
setIsTextAreaFocused(false)
}, [isMouseDownOnMenu])
@@ -681,13 +769,35 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const updateHighlights = useCallback(() => {
if (!textAreaRef.current || !highlightLayerRef.current) return
const text = textAreaRef.current.value
let processedText = textAreaRef.current.value
highlightLayerRef.current.innerHTML = text
processedText = processedText
.replace(/\n$/, "\n\n")
.replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c] || c)
// highlight @mentions
.replace(mentionRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
// check for highlighting /slash-commands
if (/^\s*\//.test(processedText)) {
const slashIndex = processedText.indexOf("/")
// end of command is end of text or first whitespace
const spaceIndex = processedText.indexOf(" ", slashIndex)
const endIndex = spaceIndex > -1 ? spaceIndex : processedText.length
// extract and validate the exact command text
const commandText = processedText.substring(slashIndex + 1, endIndex)
const isValidCommand = validateSlashCommand(commandText)
if (isValidCommand) {
const fullCommand = processedText.substring(slashIndex, endIndex) // includes slash
const highlighted = `<mark class="slash-command-match-textarea-highlight">${fullCommand}</mark>`
processedText = processedText.substring(0, slashIndex) + highlighted + processedText.substring(endIndex)
}
}
highlightLayerRef.current.innerHTML = processedText
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
}, [])
@@ -1011,6 +1121,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}}
onDrop={onDrop}
onDragOver={onDragOver}>
{showSlashCommandsMenu && (
<div ref={slashCommandsMenuContainerRef}>
<SlashCommandMenu
onSelect={handleSlashCommandsSelect}
selectedIndex={selectedSlashCommandsIndex}
setSelectedIndex={setSelectedSlashCommandsIndex}
onMouseDown={handleMenuMouseDown}
query={slashCommandsQuery}
/>
</div>
)}
{showContextMenu && (
<div ref={contextMenuContainerRef}>
<ContextMenu
@@ -1153,7 +1275,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
display: "flex",
alignItems: "flex-center",
height: textAreaBaseHeight || 31,
bottom: 9.5, // should be 10 but doesnt look good on mac
bottom: 9.5, // should be 10 but doesn't look good on mac
zIndex: 2,
}}>
<div
@@ -1225,6 +1347,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</ButtonContainer>
</VSCodeButton>
<ServersToggleModal />
<ClineRulesToggleModal />
<ModelContainer ref={modelSelectorRef}>
<ModelButtonWrapper ref={buttonRef}>
+11 -1
View File
@@ -461,6 +461,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
textAreaRef.current?.focus()
}
break
case "focusChatInput":
textAreaRef.current?.focus()
if (isHidden) {
// Send message back to extension to show chat view
vscode.postMessage({ type: "showChatView" })
}
break
}
break
case "selectedImages":
@@ -568,6 +575,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
"browser_action",
"browser_action_result",
"checkpoint_created",
"reasoning",
].includes(message.say!)
}
return false
@@ -789,10 +797,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
lastModifiedMessage={modifiedMessages.at(-1)}
isLast={index === groupedMessages.length - 1}
onHeightChange={handleRowHeightChange}
inputValue={inputValue}
sendMessageFromChatRow={handleSendMessage}
/>
)
},
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange],
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange, inputValue],
)
return (
@@ -26,10 +26,12 @@ export const OptionsButtons = ({
options,
selected,
isActive,
inputValue,
}: {
options?: string[]
selected?: string
isActive?: boolean
inputValue?: string
}) => {
if (!options?.length) return null
@@ -58,7 +60,7 @@ export const OptionsButtons = ({
}
vscode.postMessage({
type: "optionsResponse",
text: option,
text: option + (inputValue ? `: ${inputValue?.trim()}` : ""),
})
}}>
{option}
@@ -74,7 +74,7 @@ const ServersToggleModal: React.FC = () => {
/>
<div className="flex justify-between items-center mb-2.5">
<div className="m-0">MCP Servers</div>
<div className="m-0 text-base font-semibold">MCP Servers</div>
<VSCodeButton
appearance="icon"
onClick={() => {
@@ -0,0 +1,80 @@
import React, { useCallback, useRef, useEffect } from "react"
import { SlashCommand, getMatchingSlashCommands } from "@/utils/slash-commands"
interface SlashCommandMenuProps {
onSelect: (command: SlashCommand) => void
selectedIndex: number
setSelectedIndex: (index: number) => void
onMouseDown: () => void
query: string
}
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedIndex, setSelectedIndex, onMouseDown, query }) => {
const menuRef = useRef<HTMLDivElement>(null)
const handleClick = useCallback(
(command: SlashCommand) => {
onSelect(command)
},
[onSelect],
)
// Auto-scroll logic remains the same...
useEffect(() => {
if (menuRef.current) {
const selectedElement = menuRef.current.children[selectedIndex] as HTMLElement
if (selectedElement) {
const menuRect = menuRef.current.getBoundingClientRect()
const selectedRect = selectedElement.getBoundingClientRect()
if (selectedRect.bottom > menuRect.bottom) {
menuRef.current.scrollTop += selectedRect.bottom - menuRect.bottom
} else if (selectedRect.top < menuRect.top) {
menuRef.current.scrollTop -= menuRect.top - selectedRect.top
}
}
}
}, [selectedIndex])
// Filter commands based on query
const filteredCommands = getMatchingSlashCommands(query)
return (
<div
className="absolute bottom-[calc(100%-10px)] left-[15px] right-[15px] overflow-x-hidden z-[1000]"
onMouseDown={onMouseDown}>
<div
ref={menuRef}
className="bg-[var(--vscode-dropdown-background)] border border-[var(--vscode-editorGroup-border)] rounded-[3px] shadow-[0_4px_10px_rgba(0,0,0,0.25)] flex flex-col max-h-[200px] overflow-y-auto" // Corrected rounded and shadow
>
{filteredCommands.length > 0 ? (
filteredCommands.map((command, index) => (
<div
key={command.name}
className={`py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
// Corrected padding
index === selectedIndex
? "bg-[var(--vscode-quickInputList-focusBackground)] text-[var(--vscode-quickInputList-focusForeground)]"
: "" // Removed bg-transparent
} hover:bg-[var(--vscode-list-hoverBackground)]`}
onClick={() => handleClick(command)}
onMouseEnter={() => setSelectedIndex(index)}>
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">/{command.name}</div>
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)] whitespace-normal overflow-hidden text-ellipsis">
{command.description}
</div>
</div>
))
) : (
<div className="py-2 px-3 cursor-default flex flex-col">
{" "}
{/* Corrected padding, removed border, changed cursor */}
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)]">No matching commands found</div>
</div>
)}
</div>
</div>
)
}
export default SlashCommandMenu
+61 -4
View File
@@ -9,6 +9,7 @@ import { formatSize } from "@/utils/format"
import { vscode } from "@/utils/vscode"
import Thumbnails from "@/components/common/Thumbnails"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import { validateSlashCommand } from "@/utils/slash-commands"
interface TaskHeaderProps {
task: ClineMessage
@@ -254,7 +255,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
Task
{!isTaskExpanded && ":"}
</span>
{!isTaskExpanded && <span style={{ marginLeft: 4 }}>{highlightMentions(task.text, false)}</span>}
{!isTaskExpanded && <span style={{ marginLeft: 4 }}>{highlightText(task.text, false)}</span>}
</div>
</div>
{!isTaskExpanded && isCostAvailable && (
@@ -300,7 +301,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
{highlightMentions(task.text, false)}
{highlightText(task.text, false)}
</div>
{!isTextExpanded && showSeeMore && (
<div
@@ -554,9 +555,41 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
)
}
export const highlightMentions = (text?: string, withShadow = true) => {
if (!text) return text
/**
* Highlights slash-command in this text if it exists
*/
const highlightSlashCommands = (text: string, withShadow = true) => {
const match = text.match(/^\s*\/([a-zA-Z0-9_-]+)(\s*|$)/)
if (!match) {
return text
}
const commandName = match[1]
const validationResult = validateSlashCommand(commandName)
if (!validationResult || validationResult !== "full") {
return text
}
const commandEndIndex = match[0].length
const beforeCommand = text.substring(0, text.indexOf("/"))
const afterCommand = match[2] + text.substring(commandEndIndex)
return [
beforeCommand,
<span key="slashCommand" className={withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"}>
/{commandName}
</span>,
afterCommand,
]
}
/**
* Highlights & formats all mentions inside this text
*/
export const highlightMentions = (text: string, withShadow = true) => {
const parts = text.split(mentionRegexGlobal)
return parts.map((part, index) => {
if (index % 2 === 0) {
// This is regular text
@@ -576,6 +609,30 @@ export const highlightMentions = (text?: string, withShadow = true) => {
})
}
/**
* Handles parsing both mentions and slash-commands
*/
export const highlightText = (text?: string, withShadow = true) => {
if (!text) {
return text
}
const resultWithSlashHighlighting = highlightSlashCommands(text, withShadow)
if (resultWithSlashHighlighting === text) {
// no highlighting done
return highlightMentions(resultWithSlashHighlighting, withShadow)
}
if (Array.isArray(resultWithSlashHighlighting) && resultWithSlashHighlighting.length === 3) {
const [beforeCommand, commandElement, afterCommand] = resultWithSlashHighlighting as [string, JSX.Element, string]
return [beforeCommand, commandElement, ...highlightMentions(afterCommand, withShadow)]
}
return [text]
}
const DeleteButton: React.FC<{
taskSize: string
taskId?: string
@@ -0,0 +1,185 @@
import React, { useState, useRef, forwardRef, useCallback } from "react"
import Thumbnails from "@/components/common/Thumbnails"
import { highlightText } from "./TaskHeader"
import { vscode } from "@/utils/vscode"
import DynamicTextArea from "react-textarea-autosize"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface UserMessageProps {
text?: string
images?: string[]
messageTs?: number // Timestamp for the message, needed for checkpoint restore
sendMessageFromChatRow?: (text: string, images: string[]) => void
}
const UserMessage: React.FC<UserMessageProps> = ({ text, images, messageTs, sendMessageFromChatRow }) => {
const [isEditing, setIsEditing] = useState(false)
const [editedText, setEditedText] = useState(text || "")
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const { checkpointTrackerErrorMessage } = useExtensionState()
// Create refs for the buttons to check in the blur handler
const restoreAllButtonRef = useRef<HTMLButtonElement>(null)
const restoreChatButtonRef = useRef<HTMLButtonElement>(null)
const handleClick = () => {
if (!isEditing) {
setIsEditing(true)
}
}
// Select all text when entering edit mode
React.useEffect(() => {
if (isEditing && textAreaRef.current) {
textAreaRef.current.select()
}
}, [isEditing])
const handleRestoreWorkspace = (type: string) => {
const delay = type === "task" ? 500 : 1000 // Delay for task and workspace restore
setIsEditing(false)
if (text === editedText) {
return
}
vscode.postMessage({
type: "checkpointRestore",
number: messageTs,
text: type,
offset: 1,
})
setTimeout(() => {
sendMessageFromChatRow?.(editedText, images || [])
}, delay)
}
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
// Check if focus is moving to one of our button elements
if (e.relatedTarget === restoreAllButtonRef.current || e.relatedTarget === restoreChatButtonRef.current) {
// Don't close edit mode if focus is moving to one of our buttons
return
}
// Otherwise, close edit mode
setIsEditing(false)
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Escape") {
setIsEditing(false)
} else if (e.key === "Enter" && e.metaKey && !checkpointTrackerErrorMessage) {
handleRestoreWorkspace("taskAndWorkspace")
} else if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleRestoreWorkspace("task")
}
}
return (
<div
style={{
backgroundColor: isEditing ? "unset" : "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "9px",
whiteSpace: "pre-line",
wordWrap: "break-word",
}}
onClick={handleClick}>
{isEditing ? (
<>
<DynamicTextArea
ref={textAreaRef}
value={editedText}
onChange={(e) => setEditedText(e.target.value)}
onBlur={(e) => handleBlur(e)}
onKeyDown={handleKeyDown}
autoFocus
style={{
width: "100%",
backgroundColor: "var(--vscode-input-background)",
color: "var(--vscode-input-foreground)",
borderColor: "var(--vscode-input-border)",
border: "1px solid",
borderRadius: "2px",
padding: "6px",
fontFamily: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
boxSizing: "border-box",
resize: "none",
overflowX: "hidden",
overflowY: "scroll",
scrollbarWidth: "none",
}}
/>
<div style={{ display: "flex", gap: "8px", marginTop: "8px", justifyContent: "flex-end" }}>
{!checkpointTrackerErrorMessage && (
<RestoreButton
ref={restoreAllButtonRef}
type="taskAndWorkspace"
label="Restore All"
isPrimary={false}
onClick={handleRestoreWorkspace}
title="Restore both the chat and workspace files to this checkpoint and send your edited message"
/>
)}
<RestoreButton
ref={restoreChatButtonRef}
type="task"
label="Restore Chat"
isPrimary={true}
onClick={handleRestoreWorkspace}
title="Restore just the chat to this checkpoint and send your edited message"
/>
</div>
</>
) : (
<span style={{ display: "block" }}>{highlightText(editedText || text)}</span>
)}
{images && images.length > 0 && <Thumbnails images={images} style={{ marginTop: "8px" }} />}
</div>
)
}
// Reusable button component for restore actions
interface RestoreButtonProps {
type: string
label: string
isPrimary: boolean
onClick: (type: string) => void
title?: string
}
const RestoreButton = forwardRef<HTMLButtonElement, RestoreButtonProps>(({ type, label, isPrimary, onClick, title }, ref) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
onClick(type)
}
return (
<button
ref={ref}
onClick={handleClick}
title={title}
style={{
backgroundColor: isPrimary
? "var(--vscode-button-background)"
: "var(--vscode-button-secondaryBackground, var(--vscode-descriptionForeground))",
color: isPrimary
? "var(--vscode-button-foreground)"
: "var(--vscode-button-secondaryForeground, var(--vscode-foreground))",
border: "none",
padding: "4px 8px",
borderRadius: "2px",
fontSize: "9px",
cursor: "pointer",
}}>
{label}
</button>
)
})
export default UserMessage
@@ -0,0 +1,133 @@
import React, { useRef, useState, useEffect } from "react"
import { useClickAway, useWindowSize } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
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"
const ClineRulesToggleModal: React.FC = () => {
const { globalClineRulesToggles = {}, localClineRulesToggles = {} } = useExtensionState()
const [isVisible, setIsVisible] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
const modalRef = useRef<HTMLDivElement>(null)
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
const [arrowPosition, setArrowPosition] = useState(0)
const [menuPosition, setMenuPosition] = useState(0)
useEffect(() => {
if (isVisible) {
vscode.postMessage({ type: "refreshClineRules" })
}
}, [isVisible])
// Format global rules for display with proper typing
const globalRules = Object.entries(globalClineRulesToggles || {})
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
.sort(([a], [b]) => a.localeCompare(b))
// Format local rules for display with proper typing
const localRules = Object.entries(localClineRulesToggles || {})
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
.sort(([a], [b]) => a.localeCompare(b))
// Handle toggle rule
const toggleRule = (isGlobal: boolean, rulePath: string, enabled: boolean) => {
vscode.postMessage({
type: "toggleClineRule",
isGlobal,
rulePath,
enabled,
})
}
// Close modal when clicking outside
useClickAway(modalRef, () => {
setIsVisible(false)
})
// Calculate positions for modal and arrow
useEffect(() => {
if (isVisible && buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect()
const buttonCenter = buttonRect.left + buttonRect.width / 2
const rightPosition = document.documentElement.clientWidth - buttonCenter - 5
setArrowPosition(rightPosition)
setMenuPosition(buttonRect.top + 1)
}
}, [isVisible, viewportWidth, viewportHeight])
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>
</div>
{isVisible && (
<div
className="fixed left-[15px] right-[15px] border border-[var(--vscode-editorGroup-border)] p-3 rounded z-[1000] overflow-y-auto"
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
background: CODE_BLOCK_BG_COLOR,
maxHeight: "calc(100vh - 100px)",
overscrollBehavior: "contain",
}}>
<div
className="fixed w-[10px] h-[10px] z-[-1] rotate-45 border-r border-b border-[var(--vscode-editorGroup-border)]"
style={{
bottom: `calc(100vh - ${menuPosition}px)`,
right: arrowPosition,
background: CODE_BLOCK_BG_COLOR,
}}
/>
<div className="flex justify-between items-center mb-2.5">
<div className="m-0 text-base font-semibold">Cline Rules</div>
<VSCodeButton
appearance="icon"
onClick={() => {
vscode.postMessage({
type: "openExtensionSettings",
})
setIsVisible(false)
}}>
{/* <span className="codicon codicon-gear text-[10px]"></span> */}
</VSCodeButton>
</div>
{/* Global Rules Section */}
<div className="mb-3">
<div className="text-sm font-normal mb-2">Global Rules</div>
<RulesToggleList
rules={globalRules}
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
listGap="small"
/>
</div>
{/* Local Rules Section */}
<div style={{ marginBottom: -10 }}>
<div className="text-sm font-normal mb-2">Workspace Rules</div>
<RulesToggleList
rules={localRules}
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
listGap="small"
/>
</div>
</div>
)}
</div>
)
}
export default ClineRulesToggleModal
@@ -0,0 +1,67 @@
import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
const RuleRow: React.FC<{
rulePath: string
enabled: boolean
toggleRule: (rulePath: string, enabled: boolean) => void
}> = ({ rulePath, enabled, toggleRule }) => {
// Get the filename from the path for display
const displayName = rulePath.split("/").pop() || rulePath
const handleEditClick = () => {
vscode.postMessage({
type: "openFile",
text: rulePath,
})
}
return (
<div className="mb-2.5">
<div
className={`flex items-center p-2 rounded bg-[var(--vscode-textCodeBlock-background)] ${
enabled ? "opacity-100" : "opacity-60"
}`}>
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
{displayName}
</span>
{/* Toggle Switch */}
<div className="flex items-center ml-2 space-x-2">
<div
role="switch"
aria-checked={enabled}
tabIndex={0}
className={`w-[20px] h-[10px] rounded-[5px] relative cursor-pointer transition-colors duration-200 ${
enabled
? "bg-[var(--vscode-testing-iconPassed)] opacity-90"
: "bg-[var(--vscode-titleBar-inactiveForeground)] opacity-50"
}`}
onClick={() => toggleRule(rulePath, !enabled)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
toggleRule(rulePath, !enabled)
}
}}>
<div
className={`w-[6px] h-[6px] bg-white border border-[#66666699] rounded-full absolute top-[1px] transition-all duration-200 ${
enabled ? "left-[12px]" : "left-[2px]"
}`}
/>
</div>
<VSCodeButton
appearance="icon"
aria-label="Edit rule file"
title="Edit rule file"
onClick={handleEditClick}
style={{ height: "20px" }}>
<span className="codicon codicon-edit" style={{ fontSize: "14px" }} />
</VSCodeButton>
</div>
</div>
</div>
)
}
export default RuleRow
@@ -0,0 +1,31 @@
import RuleRow from "./RuleRow"
const RulesToggleList = ({
rules,
toggleRule,
listGap = "medium",
}: {
rules: [string, boolean][]
toggleRule: (rulePath: string, enabled: boolean) => void
listGap?: "small" | "medium" | "large"
}) => {
const gapClasses = {
small: "gap-0",
medium: "gap-2.5",
large: "gap-5",
}
const gapClass = gapClasses[listGap]
return rules.length > 0 ? (
<div className={`flex flex-col ${gapClass}`}>
{rules.map(([rulePath, enabled]) => (
<RuleRow key={rulePath} rulePath={rulePath} enabled={enabled} toggleRule={toggleRule} />
))}
</div>
) : (
<div className="flex flex-col items-center gap-3 my-5 text-[var(--vscode-descriptionForeground)]">No rules found</div>
)
}
export default RulesToggleList
@@ -114,7 +114,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
})
// If the field is the provider, save it immediately
// Neccesary for favorite model selection to work without undoing provider changes
// Necessary for favorite model selection to work without undoing provider changes
if (field === "apiProvider") {
vscode.postMessage({
type: "apiConfiguration",
@@ -5,6 +5,7 @@ import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import styled from "styled-components"
import { BrowserServiceClient } from "../../services/grpc-client"
const ConnectionStatusIndicator = ({
isChecking,
@@ -93,10 +94,24 @@ export const BrowserSettingsSection: React.FC = () => {
if (browserSettings.remoteBrowserEnabled) {
setIsCheckingConnection(true)
setConnectionStatus(null)
vscode.postMessage({
type: browserSettings.remoteBrowserHost ? "testBrowserConnection" : "discoverBrowser",
text: browserSettings.remoteBrowserHost,
})
if (browserSettings.remoteBrowserHost) {
// Use gRPC for testBrowserConnection
BrowserServiceClient.testBrowserConnection({ value: browserSettings.remoteBrowserHost })
.then((result) => {
setConnectionStatus(result.success)
setIsCheckingConnection(false)
})
.catch((error) => {
console.error("Error testing browser connection:", error)
setConnectionStatus(false)
setIsCheckingConnection(false)
})
} else {
// Use old message passing for discoverBrowser (not yet migrated)
vscode.postMessage({
type: "discoverBrowser",
})
}
}
}, 1000),
[browserSettings.remoteBrowserEnabled, browserSettings.remoteBrowserHost],
@@ -153,10 +168,22 @@ export const BrowserSettingsSection: React.FC = () => {
const checkConnectionOnce = useCallback(() => {
// Don't show the spinner for every check to avoid UI flicker
// We'll rely on the response to update the connectionStatus
vscode.postMessage({
type: browserSettings.remoteBrowserHost ? "testBrowserConnection" : "discoverBrowser",
text: browserSettings.remoteBrowserHost,
})
if (browserSettings.remoteBrowserHost) {
// Use gRPC for testBrowserConnection
BrowserServiceClient.testBrowserConnection({ value: browserSettings.remoteBrowserHost })
.then((result) => {
setConnectionStatus(result.success)
})
.catch((error) => {
console.error("Error testing browser connection:", error)
setConnectionStatus(false)
})
} else {
// Use old message passing for discoverBrowser (not yet migrated)
vscode.postMessage({
type: "discoverBrowser",
})
}
}, [browserSettings.remoteBrowserHost])
// Setup continuous polling for connection status when remote browser is enabled
@@ -17,7 +17,7 @@ import { vscode } from "../utils/vscode"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { ClineRulesToggles } from "@shared/cline-rules"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
@@ -53,6 +53,8 @@ export const ExtensionStateContextProvider: React.FC<{
telemetrySetting: "unset",
vscMachineId: "",
planActSeparateModelsSetting: true,
globalClineRulesToggles: {},
localClineRulesToggles: {},
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -128,6 +130,7 @@ export const ExtensionStateContextProvider: React.FC<{
})
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({
@@ -184,6 +187,8 @@ export const ExtensionStateContextProvider: React.FC<{
mcpMarketplaceCatalog,
filePaths,
totalTasksSize,
globalClineRulesToggles: state.globalClineRulesToggles || {},
localClineRulesToggles: state.localClineRulesToggles || {},
setApiConfiguration: (value) =>
setState((prevState) => ({
...prevState,
+7
View File
@@ -173,3 +173,10 @@ vscode-dropdown::part(listbox) {
border-radius: 3px;
box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
}
.slash-command-match-textarea-highlight {
background-color: color-mix(in srgb, var(--vscode-focusBorder) 30%, transparent);
border-radius: 3px;
box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-focusBorder) 30%, transparent);
color: transparent;
}
+100
View File
@@ -0,0 +1,100 @@
import { vscode } from "../utils/vscode"
import { v4 as uuidv4 } from "uuid"
import { BrowserServiceDefinition } from "@shared/proto/browser"
import { EmptyRequest } from "@shared/proto/common"
// Generic type for any protobuf service definition
type ProtoService = {
name: string
fullName: string
methods: {
[key: string]: {
name: string
requestType: any
responseType: any
requestStream: boolean
responseStream: boolean
options: any
}
}
}
// Define a generic type that extracts method signatures from a service definition
type GrpcClientType<T extends ProtoService> = {
[K in keyof T["methods"]]: (
request: InstanceType<T["methods"][K]["requestType"]>,
) => Promise<InstanceType<T["methods"][K]["responseType"]>>
}
// Create a client for any protobuf service with inferred types
function createGrpcClient<T extends ProtoService>(service: T): GrpcClientType<T> {
const client = {} as GrpcClientType<T>
// For each method in the service
Object.values(service.methods).forEach((method) => {
// Create a function that matches the method signature
client[method.name as keyof GrpcClientType<T>] = ((request: any) => {
return new Promise((resolve, reject) => {
const requestId = uuidv4()
// Set up one-time listener for this specific request
const handleResponse = (event: MessageEvent) => {
const message = event.data
if (message.type === "grpc_response" && message.grpc_response?.request_id === requestId) {
// Remove listener once we get our response
window.removeEventListener("message", handleResponse)
if (message.grpc_response.error) {
reject(new Error(message.grpc_response.error))
} else {
// Convert JSON back to protobuf message
const responseType = method.responseType
const response = responseType.fromJSON(message.grpc_response.message)
console.log("[DEBUG] grpc-client sending response:", response)
resolve(response)
}
}
}
window.addEventListener("message", handleResponse)
let encodedRequest = {}
// Handle different types of requests
if (request === null || request === undefined) {
// Empty request
encodedRequest = {}
} else if (typeof request.toJSON === "function") {
// Proper protobuf object
encodedRequest = request.toJSON()
} else if (typeof request === "object") {
// Plain JavaScript object
encodedRequest = { ...request }
} else {
// Fallback
encodedRequest = { value: request }
}
// Send the request
vscode.postMessage({
type: "grpc_request",
grpc_request: {
service: service.fullName,
method: method.name,
message: encodedRequest, // Convert protobuf to JSON
request_id: requestId,
},
})
})
}) as any
})
return client
}
// Create the Browser Service Client singleton with inferred types
// No need for manual interface definition - types are inferred from the service definition
const BrowserServiceClient = createGrpcClient(BrowserServiceDefinition)
// Export the Browser Service Client as a static object
export { BrowserServiceClient }
+98
View File
@@ -0,0 +1,98 @@
export interface SlashCommand {
name: string
description: string
}
export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
{
name: "newtask",
description: "Create a new task with context from the current task",
},
]
// Regex for detecting slash commands in text
export const slashCommandRegex = /\/([a-zA-Z0-9_-]+)(\s|$)/
export const slashCommandRegexGlobal = new RegExp(slashCommandRegex.source, "g")
/**
* Determines whether the slash command menu should be displayed based on text input
*/
export function shouldShowSlashCommandsMenu(text: string, cursorPosition: number): boolean {
const beforeCursor = text.slice(0, cursorPosition)
// first check if there is a slash before the cursor
const slashIndex = beforeCursor.lastIndexOf("/")
if (slashIndex === -1) {
return false
}
// check if slash is at the very beginning (with optional whitespace)
const textBeforeSlash = beforeCursor.slice(0, slashIndex)
if (!/^\s*$/.test(textBeforeSlash)) {
return false
}
// potential partial or full command
const textAfterSlash = beforeCursor.slice(slashIndex + 1)
// don't show menu if there's whitespace after the slash but before the cursor
if (/\s/.test(textAfterSlash)) {
return false
}
return true
}
/**
* Gets filtered slash commands that match the current input
*/
export function getMatchingSlashCommands(query: string): SlashCommand[] {
if (!query) {
return [...SUPPORTED_SLASH_COMMANDS]
}
// filter commands that start with the query (case sensitive)
return SUPPORTED_SLASH_COMMANDS.filter((cmd) => cmd.name.startsWith(query))
}
/**
* Insert a slash command at position or replace partial command
*/
export function insertSlashCommand(text: string, commandName: string): { newValue: string; commandIndex: number } {
const slashIndex = text.indexOf("/")
// where the command ends, at the end of entire text or first space
const commandEndIndex = text.indexOf(" ", slashIndex)
// replace the partial command with the full command
const newValue =
text.substring(0, slashIndex + 1) + commandName + (commandEndIndex > -1 ? text.substring(commandEndIndex) : " ") // add extra space at the end if only slash command
return { newValue, commandIndex: slashIndex }
}
/**
* Determines the validation state of a slash command
* Returns partial if we have a partial match against valid commands, or full for full match
*/
export function validateSlashCommand(command: string): "full" | "partial" | null {
if (!command) {
return null
}
// case sensitive matching
const exactMatch = SUPPORTED_SLASH_COMMANDS.some((cmd) => cmd.name === command)
if (exactMatch) {
return "full"
}
const partialMatch = SUPPORTED_SLASH_COMMANDS.some((cmd) => cmd.name.startsWith(command))
if (partialMatch) {
return "partial"
}
return null // no match
}
+1 -1
View File
@@ -27,7 +27,7 @@ class VSCodeAPIWrapper {
* @remarks When running webview code inside a web browser, postMessage will instead
* log the given message to the console.
*
* @param message Abitrary data (must be JSON serializable) to send to the extension context.
* @param message Arbitrary data (must be JSON serializable) to send to the extension context.
*/
public postMessage(message: WebviewMessage) {
if (this.vsCodeApi) {