mirror of
https://github.com/cline/cline.git
synced 2026-09-07 22:16:30 +08:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad636b0fb9 | ||
|
|
da36622f99 | ||
|
|
ba0117de45 | ||
|
|
e2426e5735 | ||
|
|
b3e5588bce | ||
|
|
85076ed39e | ||
|
|
faf2361dbe | ||
|
|
d4c35a6877 | ||
|
|
93aa8e03a7 | ||
|
|
bc87fdb4b3 | ||
|
|
5fe9b3cb8b | ||
|
|
c021b6464b | ||
|
|
180ebdad74 | ||
|
|
2a80fedf7d | ||
|
|
e557ea5e26 | ||
|
|
450583c81d | ||
|
|
45b1666325 | ||
|
|
718b826b19 | ||
|
|
6b97490f9d | ||
|
|
6c5b99d304 | ||
|
|
6b1314be24 | ||
|
|
02120810ce | ||
|
|
6facb3e2e6 | ||
|
|
612744394a | ||
|
|
bc481d0c7f | ||
|
|
01a48736eb | ||
|
|
12fde2f05a | ||
|
|
4add38032e | ||
|
|
73078d63ce | ||
|
|
d9272352e4 | ||
|
|
89cbbe95e3 | ||
|
|
4d696f377c | ||
|
|
dd84bdaa9e | ||
|
|
a2811c60c9 | ||
|
|
e4c42a5dbf | ||
|
|
5e0a0deb82 | ||
|
|
e699103a2f |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
new slash command menu, slash command to trigger new_task
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
structured xml
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
gRPC over vscode message bus to make messaging better
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Allow user to modify a previous message to continue chat
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add command to focus chat input
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix vertexai token count
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Allow user to send context with an option selection
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Safer check for xAI reasoning content
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add an edit button to the Cline Rules toggle modal to open rule files
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Readme update
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add OpenAI o3 & 4o-mini
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add modal UI for toggling Cline Rules
|
||||
@@ -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 =
|
||||
@@ -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
@@ -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]
|
||||
|
||||
|
||||
@@ -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%" />
|
||||
|
||||
Generated
+164
-36
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Executable
+157
@@ -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)
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
`
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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:;
|
||||
|
||||
@@ -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]) =>
|
||||
|
||||
@@ -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.`)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type ClineRulesToggles = Record<string, boolean> // filepath -> enabled/disabled
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Generated
+23
-13
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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) => ({ "<": "<", ">": ">", "&": "&" })[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}>
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user