Compare commits

...

26 Commits

Author SHA1 Message Date
Elephant Lumps db9e24a28b merge conflicts 2025-05-18 19:39:30 -07:00
Evan 43357c1100 Migrate restartMcpServer protobus (#3606)
* migrate restartMcpServer

* changeset

* remove markdown file

* use stringRequest

* fix type

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-18 17:13:18 -07:00
Elephant Lumps 161782659e changed to stringrequest 2025-05-18 15:07:20 -07:00
canvrno eb19731843 taskCompletionViewChanges protobus migration (#3588) 2025-05-18 13:30:33 -07:00
Elephant Lumps dd317fd3de changeset 2025-05-18 09:54:26 -07:00
Elephant Lumps 106a339849 migrate deleteMcpServer 2025-05-18 09:53:56 -07:00
github-actions[bot] 95750f8c9c v3.16.1 Release Notes
v3.16.1 Release Notes
2025-05-17 17:43:34 -07:00
Ara 12820a4042 Improve Gemini Retry Handling UI and UX (#3589)
* Feat: Display API auto-retry status in chat UI

This commit enhances user experience by providing real-time feedback
on automatic API request retries directly within the chat interface.
When an API request encounters a retriable error (e.g., 429), the UI
will now indicate that a retry is in progress, showing the current
attempt, maximum attempts, and delay until the next attempt.

Key changes:
- Modified the `withRetry` decorator in `src/api/retry.ts` to accept
  an `onRetryAttempt` callback. This callback is invoked before each
  retry, passing details like attempt number, max retries, delay, and
  the error that triggered the retry.
- `Task` (`src/core/task/index.ts`) now provides this callback to API
  handlers. It updates the `api_req_started` message in `clineMessages`
  with `retryStatus` information and posts the updated state to the
  webview. It also clears retry status if retries are exhausted.
- The `ChatRow.tsx` component in the webview UI has been updated to
  display this retry status (e.g., "Retrying (attempt X of Y, next in Zs)...").
  If retries are exhausted, the standard error display is shown.
- Data structures in `src/shared/` (ExtensionMessage, api, proto/file)
  were updated to include `retryStatus` and the `onRetryAttempt` callback.
- Added test code to `GeminiHandler` (`src/api/providers/gemini.ts`) to
  simulate 429 errors, allowing for easier testing and verification of
  the retry feedback mechanism.

* Remove TaskTimeLine altogether

* Remove TaskTimeLine altogether

* Update src/core/task/index.ts

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

---------

Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-17 17:28:25 -07:00
Frostbourne 8e3adb42d6 Auto approve toggle switch (#3592)
* Add Enable AA button, rename toggle all, rm icons from bar

* Fix auto-approve bar not working and centralize feature

* move tooltip

* changeset
2025-05-17 17:26:59 -07:00
Ara 8ab35a5b06 fix: Address memory leak by bypassing subscribeToState gRPC stream for state updates (#3597)
* fix: Address memory leak by bypassing subscribeToState gRPC stream for state updates

* Remove TaskTimeLine altogether

* Remove TaskTimeLine altogether

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-17 17:12:35 -07:00
Sarah Fortune ba64d9fafb Don't use symlinks in the standalone distribution zip. (#3582)
Don't install vscode with file:./vscode because it creates a
symlink which is not portable for the distribution.
2025-05-17 14:52:19 -07:00
canvrno 2ba2b5b264 fetchOpenGraphData protobus migration (#3549) 2025-05-17 14:52:02 -07:00
canvrno 0dad8e178a [PROTOBUS] Move resetState to protobus (#3573)
* resetState protobus migration

* changeset
2025-05-17 14:51:27 -07:00
canvrno 1470563142 taskFeedback protobus migration (#3590) 2025-05-17 14:49:55 -07:00
github-actions[bot] 0ca16961ee v3.16.0 Release Notes
v3.16.0 Release Notes
2025-05-16 16:54:28 -07:00
canvrno 8d8452e668 [PROTOBUS] Move askResponse to protobus (#3539)
* askResponse protobus migration

* Standalone script updated
2025-05-16 12:29:33 -07:00
Matthew Rogers 6c18d5154f fix: permit use of global endpoint for vertex ai (#3469) 2025-05-17 00:07:12 +05:30
Evan aabe4ae1e1 Check if new user (#3586)
* add detection for new users for intro component

* fix lint issue

* changeset

* remove redundant fragment

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-16 23:43:08 +05:30
Toshii 5147e28aaf workflows (#3540)
* remove workflows subdirectory from cline local toggles

* set workflow toggles

* pre-updating the rules deletion logic

* delete file logic

* integration with task

* words

* pre-updating storage structure of workflows

* workflow menu items, no regex

* slash menu scrolling

* menu buttons

* placeholder

* match command base

* regex

* nit

* slash menu click outside

* changeset

* fixing linter warning

* better UI/UX

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-15 22:20:20 -07:00
Dimosthenis Kaponis c6e8b04b86 feat: Enhance HistoryPreview component with collapsible/expandable ta… (#3534)
* feat: Enhance HistoryPreview component with collapsible/expandable task history view

* fix: Update font size for empty state/'No recent tasks' message in HistoryPreview component
2025-05-15 21:38:53 -07:00
Tomás Barreiro c0b3c69a8f Consider the previous message as last if the last is a checkpoint (#3571) 2025-05-15 20:55:38 -07:00
Evan 080ed7c1c6 Add Extension Recommendation (#3530)
* add tailwind css intelliSense rec

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-15 20:53:14 -07:00
canvrno 570ece3284 selectImages protobus migration (#3575) 2025-05-15 18:58:58 -07:00
Frostbourne 8f6f6464a0 Inject react-devtools (#3569) 2025-05-15 15:43:06 -07:00
Sarah Fortune 8c565b5a7c Run the cline extension as a standalone process outside of vscode. (#3535)
* Add standalone cline server.

Add directory standalone/ with the scripts to generate
a cline instance that runs a gRPC service for the proto bus.

* Rm unused dependencies

* Build standalone extension

Build stubs for the whole vscode SDK.

Import extension.js instead of putting everything in one file.

Move all the files the extension needs at runtime in files/
  Use local packages for vscode and stub-utils instead of module alias.
  Move vscode-impls into the vscode module.
  Create separate package.json for the standalone extension in files/.

* Handlers for gRPC requests

Add code to the bottom of extension.js to export the gRPC handlers.
Add a wrapper to the handlers to catch and log extensions, otherwise the whole server process fails.
Fix use of open module.

* Standalone gRPC server

Export handers from the extension.
Add reflection and healthcheck to the server.
Add vscode launch file for standalone server.

* Fix formatting

* Better error handling in the server template.

Exit if the server could not bind to the port.
Use internal error code if exception is thrown.

* Formatting

* Stop using google-protobuf npm module to generate JS for protos

The code generated by google-protobuf cannot serialize protos from plain objects. It needs the protos to be class instances created with ProtoExample.create().
But, the protos created in the extension are just POJOs.
Use protoLoader instead which is fine with plain objects.
Protoloader is also the method used in the grpc JS documentation: https://grpc.io/docs/languages/node/basics/#loading-service-descriptors-from-proto-files

* Rm proto that was removed in cline/cline

* Rm old protos when building standalone extension.

* Log gRPC requests

* feat(standalone): implement TypeScript gRPC-based standalone extension

The major improvement is that the gRPC implementation is now written in TypeScript instead of JavaScript, and the standalone extension is compiled together with the original extension rather than using the compiled JS output. This provides full type safety throughout the codebase and prevents issues with the TypeScript compiler renaming handlers during compilation, making the system more robust and maintainable.

- Add new standalone implementation files in src/standalone/ directory using TypeScript
- Implement gRPC server setup in extension-standalone.ts with full type safety
- Generate server setup code with service registrations
- Update build script to support the new standalone architecture
- Reorganize runtime files from standalone/files/ to standalone/runtime-files/
- Replace template-based server generation with gRPC service registration

* Fix issues when doing clean build

Use correct build dir in esbuild.js
Remove undefined type.

* Add handler for gRPC methods with streaming response.

Add a handler-wrapper for rpc's with streaming responses.

Fix issue where grpc-js won't deserialize protos in camelcase. It is the default
for generated code for protos to use camelcase (keepCase: false), but I cannot find
where is being set for the proto serializations to keep the case. For now, just convert the
properties of the proto messages to snake case. This is not a good
solution, but trying to fix this is time sink.

* Formatting

* Add streaming response support to the script that generates setup-server.ts

Add types for the handlers.

* Formatting

* Fix case conversion for gRPC requset protos as well.

Convert snake case to camelcase for incoming request protos.

* formatting

* Improve build process / building for standalone extension

Add separate configs for the extension and the standalone in the esbuild config.
Modules that use __dirname to load files at runtime are marked as external in the build config.
Rename vscode-impls to vscode-context.
Remove unecessary files from the standalone runtime.

* Rename extension-standalone.js to standalone.js

* Move generate-server-setup script to protos dir.

Add the script the npm target `protos`, so it is run when the protos are regenerated.

* formatting

* Add a post build step for the npm run target `protos` to format the generated files.

* Move generate-server-setup to scripts directory

* Add a JS script to package the standalone build, replacing the shell script.

Add a post build step for the standalone target that:
    * copies the vscode module files into the output directory.
    * checks that native modules are not included in the output
    * creates a zip of the build.

* Rm files that were included from merge by mistake

* Move scripts from standalone in scripts directory

Remove unused package.json files from standalone/

* Update scripts and launch.json to use correct paths

* During build install external modules in the dist directory.

Add package.json for the distribution.
Set the node path for the vscode launch config.
Make the prettier silent during `npm run protos`

* Fix ellipsis suggestions
2025-05-15 12:04:46 -07:00
Ara cd1ff2ad25 Refactor reasoning effort option and checkpoint handling (#3454)
• Replace "o3MiniReasoningEffort" with "reasoningEffort" in API providers
• Remove deprecated configuration properties from package.json
• Guard checkpoint tracker initialization and saving using the enableCheckpoints flag

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-15 22:24:16 +05:30
112 changed files with 8712 additions and 1014 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
taskCompletionViewChanges protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate restartMcpServer to protobus
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate the deleteMcpServer message to protobus
+1 -1
View File
@@ -19,4 +19,4 @@ coverage
# But don't ignore the coverage scripts in .github/scripts/
!.github/scripts/coverage/
*evals.env
*evals.env
+6 -1
View File
@@ -1,5 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
"recommendations": [
"dbaeumer.vscode-eslint",
"connor4312.esbuild-problem-matchers",
"ms-vscode.extension-test-runner",
"bradlc.vscode-tailwindcss"
]
}
+17
View File
@@ -38,6 +38,23 @@
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
{
"type": "node",
"request": "launch",
"name": "Run Standalone Extension",
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true,
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
"cwd": "${workspaceFolder}/dist-standalone",
"outFiles": ["${workspaceFolder}/dist-standalone/**/*.js"],
"preLaunchTask": "compile-standalone",
"env": {
"GRPC_TRACE": "all",
"GRPC_VERBOSITY": "DEBUG",
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
},
"program": "standalone.js"
}
]
}
+10
View File
@@ -3,6 +3,16 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "compile-standalone",
"type": "npm",
"script": "compile-standalone",
"group": "build",
"problemMatcher": [],
"presentation": {
"reveal": "always"
}
},
{
"label": "npm: protos",
"type": "npm",
+20 -1
View File
@@ -1,8 +1,27 @@
# Changelog
## [3.16.1]
- Add Enable auto approve toggle switch, allowing users to easily turn auto-approve functionality on or off without losing their action settings
- Improve Gemini retry handling with better UI feedback, showing retry progress during API request attempts
- Fix memory leak issue that could occur during long sessions with multiple tasks
- Improve UI for Gemini model retry attempts with clearer status updates
- Fix quick actions functionality in auto-approve settings
- Update UI styling for auto-approve menu items to conserve space
## [3.16.0]
- Add new workflow feature allowing users to create and manage workflow files that can be injected into conversations via slash commands
- Add collapsible recent task list, allowing users to hide their task history when sharing their screen (Thanks @cosmix!)
- Add global endpoint option for Vertex AI users, providing higher availability and reducing 429 errors (Thanks @soniqua!)
- Add detection for new users to display special components and guidance
- Add Tailwind CSS IntelliSense to the recommended extensions list
- Fix eternal loading states when the last message is a checkpoint (Thanks @BarreiroT!)
- Improve settings organization by migrating VSCode Advanced settings to Settings Webview
## [3.15.5]
- Fix inefficient memory management in the task timeline
- Fix inefficient memory management in the task timeline
- Fix Gemini rate limitation response not being handled properly (Thanks @BarreiroT!)
## [3.15.4]
+5 -5
View File
@@ -44,16 +44,16 @@ This guide is tailored for organizations with established GCP environments (leve
#### 2.1 Choose and Confirm a Region
Vertex AI supports eight regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
Vertex AI supports multiple regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
- **us-east5 (Columbus, Ohio)**
- **us-east1 (South Carolina)**
- **us-east4 (Northern Virginia)**
- **us-central1 (Iowa)**
- **us-west1 (The Dalles, Oregon)**
- **us-west4 (Las Vegas, Nevada)**
- **europe-west1 (Belgium)**
- **europe-west4 (Netherlands)**
- **asia-southeast1 (Singapore)**
- **global (Global)**
The Global endpoint may offer higher availability and reduce resource exhausted errors. Only Gemini models are supported.
#### 2.2 Enable the Claude 3.5 Sonnet v2 Model
+24 -5
View File
@@ -4,6 +4,8 @@ const path = require("path")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
const standalone = process.argv.includes("--standalone")
const destDir = standalone ? "dist-standalone" : "dist"
/**
* @type {import('esbuild').Plugin}
@@ -85,7 +87,7 @@ const copyWasmFiles = {
build.onEnd(() => {
// tree sitter
const sourceDir = path.join(__dirname, "node_modules", "web-tree-sitter")
const targetDir = path.join(__dirname, "dist")
const targetDir = path.join(__dirname, destDir)
// Copy tree-sitter.wasm
fs.copyFileSync(path.join(sourceDir, "tree-sitter.wasm"), path.join(targetDir, "tree-sitter.wasm"))
@@ -117,7 +119,8 @@ const copyWasmFiles = {
},
}
const extensionConfig = {
// Base configuration shared between extension and standalone builds
const baseConfig = {
bundle: true,
minify: production,
sourcemap: !production,
@@ -140,16 +143,32 @@ const extensionConfig = {
},
},
],
entryPoints: ["src/extension.ts"],
format: "cjs",
sourcesContent: false,
platform: "node",
outfile: "dist/extension.js",
}
// Extension-specific configuration
const extensionConfig = {
...baseConfig,
entryPoints: ["src/extension.ts"],
outfile: `${destDir}/extension.js`,
external: ["vscode"],
}
// Standalone-specific configuration
const standaloneConfig = {
...baseConfig,
entryPoints: ["src/standalone/standalone.ts"],
outfile: `${destDir}/standalone.js`,
// These gRPC protos need to load files from the module directory at runtime,
// so they cannot be bundled.
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
}
async function main() {
const extensionCtx = await esbuild.context(extensionConfig)
const config = standalone ? standaloneConfig : extensionConfig
const extensionCtx = await esbuild.context(config)
if (watch) {
await extensionCtx.watch()
} else {
+1039 -101
View File
File diff suppressed because it is too large Load Diff
+12 -40
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.15.5",
"version": "3.16.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -236,53 +236,20 @@
},
"configuration": {
"title": "Cline",
"properties": {
"cline.enableCheckpoints": {
"type": "boolean",
"default": true,
"description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces."
},
"cline.preferredLanguage": {
"type": "string",
"enum": [
"English",
"Arabic - العربية",
"Portuguese - Português (Brasil)",
"Czech - Čeština",
"French - Français",
"German - Deutsch",
"Hindi - हिन्दी",
"Hungarian - Magyar",
"Italian - Italiano",
"Japanese - 日本語",
"Korean - 한국어",
"Polish - Polski",
"Portuguese - Português (Portugal)",
"Russian - Русский",
"Simplified Chinese - 简体中文",
"Spanish - Español",
"Traditional Chinese - 繁體中文",
"Turkish - Türkçe"
],
"default": "English",
"description": "The language that Cline should use for communication."
},
"cline.mcpMarketplace.enabled": {
"type": "boolean",
"default": true,
"description": "Controls whether the MCP Marketplace is enabled."
}
}
"properties": {}
}
},
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.js",
"compile-standalone": "npm run protos && npm run check-types && npm run lint && node esbuild.js --standalone",
"postcompile-standalone": "node scripts/package-standalone.mjs",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "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 src/core/controller webview-ui/src/services --write",
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs",
"postprotos": "prettier src/shared/proto src/core/controller webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
@@ -352,8 +319,9 @@
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@bufbuild/protobuf": "^2.2.5",
"@google-cloud/vertexai": "^1.9.3",
"@google/genai": "^0.9.0",
"@google/genai": "^0.13.0",
"@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"@opentelemetry/api": "^1.4.1",
@@ -364,6 +332,7 @@
"@opentelemetry/semantic-conventions": "^1.30.0",
"@sentry/browser": "^9.12.0",
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
"axios": "^1.8.2",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
@@ -377,6 +346,7 @@
"fzf": "^0.5.2",
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"grpc-health-check": "^2.0.2",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
@@ -385,6 +355,7 @@
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"ollama": "^0.5.13",
"open": "^10.1.2",
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
@@ -398,6 +369,7 @@
"simple-git": "^3.27.0",
"strip-ansi": "^7.1.0",
"tree-sitter-wasms": "^0.1.11",
"ts-morph": "^25.0.1",
"turndown": "^7.2.0",
"web-tree-sitter": "^0.22.6",
"zod": "^3.24.2"
+4
View File
@@ -22,6 +22,9 @@ service FileService {
// Search git commits in the workspace
rpc searchCommits(StringRequest) returns (GitCommits);
// Select images from the file system and return as data URLs
rpc selectImages(EmptyRequest) returns (StringArray);
// Convert URIs to workspace-relative paths
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
@@ -82,6 +85,7 @@ message RuleFileRequest {
bool is_global = 2; // Common field for all operations
optional string rule_path = 3; // Path field for deleteRuleFile (optional)
optional string filename = 4; // Filename field for createRuleFile (optional)
optional string type = 5; // Type of the file to create (optional)
}
// Result for rule file operations with meaningful data only
+2
View File
@@ -11,6 +11,8 @@ service McpService {
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
rpc downloadMcp(StringRequest) returns (Empty);
rpc restartMcpServer(StringRequest) returns (McpServers);
rpc deleteMcpServer(StringRequest) returns (McpServers);
}
message ToggleMcpServerRequest {
+1
View File
@@ -7,6 +7,7 @@ service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc subscribeToState(EmptyRequest) returns (stream State);
rpc toggleFavoriteModel(StringRequest) returns (Empty);
rpc resetState(EmptyRequest) returns (Empty);
}
message State {
+14
View File
@@ -25,6 +25,12 @@ service TaskService {
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
// Gets filtered task history
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
// Sends a response to a previous ask operation
rpc askResponse(AskResponseRequest) returns (Empty);
// Records task feedback (thumbs up/down)
rpc taskFeedback(StringRequest) returns (Empty);
// Shows task completion changes diff in a view
rpc taskCompletionViewChanges(Int64Request) returns (Empty);
}
// Request message for creating a new task
@@ -89,3 +95,11 @@ message TaskItem {
int32 cache_writes = 9;
int32 cache_reads = 10;
}
// Request for ask response operation
message AskResponseRequest {
Metadata metadata = 1;
string response_type = 2;
string text = 3;
repeated string images = 4;
}
+10
View File
@@ -8,9 +8,19 @@ import "common.proto";
service WebService {
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
rpc fetchOpenGraphData(StringRequest) returns (OpenGraphData);
}
message IsImageUrl {
bool is_image = 1;
string url = 2;
}
message OpenGraphData {
string title = 1;
string description = 2;
string image = 3;
string url = 4;
string site_name = 5;
string type = 6;
}
+79
View File
@@ -0,0 +1,79 @@
import * as fs from "fs"
import * as grpc from "@grpc/grpc-js"
import * as protoLoader from "@grpc/proto-loader"
import * as health from "grpc-health-check"
import { fileURLToPath } from "url"
import path from "path"
const OUT_FILE = path.resolve("src/standalone/server-setup.ts")
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
// Load service definitions.
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(fs.readFileSync(DESCRIPTOR_SET))
const healthDef = protoLoader.loadSync(health.protoPath)
const packageDefinition = { ...clineDef, ...healthDef }
const proto = grpc.loadPackageDefinition(packageDefinition)
/**
* Generate imports and function to add all the handlers to the server for all services defined in the proto files.
*/
function generateHandlersAndExports() {
let imports = []
let handlerSetup = []
for (const [name, def] of Object.entries(proto.cline)) {
if (!def || !("service" in def)) {
continue
}
const domain = name.replace(/Service$/, "")
const dir = domain.charAt(0).toLowerCase() + domain.slice(1)
imports.push(`// ${domain} Service`)
handlerSetup.push(` // ${domain} Service`)
handlerSetup.push(` server.addService(proto.cline.${name}.service, {`)
for (const [rpcName, rpc] of Object.entries(def.service)) {
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
if (rpc.requestStream) {
throw new Error("Request streaming is not supported")
}
if (rpc.responseStream) {
handlerSetup.push(` ${rpcName}: wrapStreamingResponse(${rpcName}, controller),`)
} else {
handlerSetup.push(` ${rpcName}: wrapper(${rpcName}, controller),`)
}
}
handlerSetup.push(` });`)
imports.push("")
handlerSetup.push("")
}
return {
imports: imports.join("\n"),
handlerSetup: handlerSetup.join("\n"),
}
}
const { imports, handlerSetup } = generateHandlersAndExports()
const scriptName = path.basename(fileURLToPath(import.meta.url))
// Create output file
let output = `// GENERATED CODE -- DO NOT EDIT!
// Generated by ${scriptName}
import * as grpc from "@grpc/grpc-js"
import { Controller } from "../core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
${imports}
export function addServices(
server: grpc.Server,
proto: any,
controller: Controller,
wrapper: GrpcHandlerWrapper,
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
): void {
${handlerSetup}
}
`
// Write output file
fs.writeFileSync(OUT_FILE, output)
console.log(`Generated service handlers in ${OUT_FILE}.`)
+96
View File
@@ -0,0 +1,96 @@
const fs = require("fs")
const path = require("path")
const { Project, SyntaxKind } = require("ts-morph")
function traverse(container, output, prefix = "") {
for (const node of container.getStatements()) {
const kind = node.getKind()
if (kind === SyntaxKind.ModuleDeclaration) {
const name = node.getName().replace(/^['"]|['"]$/g, "")
var fullPrefix
if (prefix) {
fullPrefix = `${prefix}.${name}`
} else {
fullPrefix = name
}
output.push(`${fullPrefix} = {};`)
const body = node.getBody()
if (body && body.getKind() === SyntaxKind.ModuleBlock) {
traverse(body, output, fullPrefix)
}
} else if (kind === SyntaxKind.FunctionDeclaration) {
const name = node.getName()
const params = node.getParameters().map((p, i) => sanitizeParam(p.getName(), i))
const typeNode = node.getReturnTypeNode()
const returnType = typeNode ? typeNode.getText() : ""
const ret = mapReturn(returnType)
output.push(
`${prefix}.${name} = function(${params.join(", ")}) { console.log('Called stubbed function: ${prefix}.${name}'); ${ret} };`,
)
} else if (kind === SyntaxKind.EnumDeclaration) {
const name = node.getName()
const members = node.getMembers().map((m) => m.getName())
output.push(`${prefix}.${name} = { ${members.map((m) => `${m}: 0`).join(", ")} };`)
} else if (kind === SyntaxKind.VariableStatement) {
for (const decl of node.getDeclarations()) {
const name = decl.getName()
output.push(`${prefix}.${name} = createStub("${prefix}.${name}");`)
}
} else if (kind == SyntaxKind.ClassDeclaration) {
const name = node.getName()
output.push(
`${prefix}.${name} = class { constructor(...args) {
console.log('Constructed stubbed class: new ${prefix}.${name}(', args, ')');
return createStub(${prefix}.${name});
}};`,
)
} else if (kind === SyntaxKind.TypeAliasDeclaration || kind === SyntaxKind.InterfaceDeclaration) {
//console.log("Skipping", SyntaxKind[kind], node.getName())
// Skip interfaces and type aliases because they are only used at compile time by typescript.
} else {
console.log("Can't handle: ", SyntaxKind[kind])
}
}
}
function mapReturn(typeStr) {
if (!typeStr) return ""
if (typeStr.includes("void")) return ""
if (typeStr.includes("string")) return `return '';`
if (typeStr.includes("number")) return `return 0;`
if (typeStr.includes("boolean")) return `return false;`
if (typeStr.includes("[]")) return `return [];`
if (typeStr.includes("Thenable")) return `return Promise.resolve(null);`
return `return createStub("unknown");`
}
function sanitizeParam(name, index) {
return name || `arg${index}`
}
async function main() {
const inputPath = "node_modules/@types/vscode/index.d.ts"
const outputPath = "standalone/runtime-files/vscode/vscode-stubs.js"
const project = new Project()
const sourceFile = project.addSourceFileAtPath(inputPath)
const output = []
output.push("// GENERATED CODE -- DO NOT EDIT!")
output.push('console.log("Loading stubs...");')
output.push('const { createStub } = require("./stub-utils")')
traverse(sourceFile, output)
output.push("module.exports = vscode;")
output.push('console.log("Finished loading stubs");')
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
fs.writeFileSync(outputPath, output.join("\n"))
console.log(`Wrote vscode SDK stubs to ${outputPath}`)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -eu
DIR=${1:-src/}
DEST_DIR=dist-standalone
DEST=dist-standalone/vscode-uses.txt
mkdir -p $DEST_DIR
{
git grep -h 'vscode\.' $DIR |
grep -Ev '//.*vscode' | # remove commented out code
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
sort | uniq > $DEST
}
echo Done, wrote uses of the vscode SDK to $(realpath $DEST)
+54
View File
@@ -0,0 +1,54 @@
import fs from "fs"
import path from "path"
import { glob } from "glob"
import archiver from "archiver"
import { cp } from "fs/promises"
import { execSync } from "child_process"
const BUILD_DIR = "dist-standalone"
const SOURCE_DIR = "standalone/runtime-files"
await cp(SOURCE_DIR, BUILD_DIR, { recursive: true })
// Run npm install in the distribution directory
console.log("Running npm install in distribution directory...")
const cwd = process.cwd()
process.chdir(BUILD_DIR)
try {
execSync("npm install", { stdio: "inherit" })
// Move the vscode directory into node_modules.
// It can't be installed using npm because it will create a symlink which is not portable.
fs.renameSync("vscode", path.join("node_modules", "vscode"))
} catch (error) {
console.error("Error during setup:", error)
process.exit(1)
} finally {
process.chdir(cwd)
}
// Check for native .node modules.
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
if (nativeModules.length > 0) {
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
process.exit(1)
}
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 9 } })
output.on("close", () => {
console.log(`Created ${zipPath} (${archive.pointer()} bytes)`)
})
archive.on("error", (err) => {
throw err
})
archive.pipe(output)
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ["standalone.zip"],
})
await archive.finalize()
+1 -1
View File
@@ -33,7 +33,7 @@ export class ClineHandler implements ApiHandler {
systemPrompt,
messages,
this.getModel(),
this.options.o3MiniReasoningEffort,
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
+5 -1
View File
@@ -73,7 +73,11 @@ export class GeminiHandler implements ApiHandler {
* @param messages The conversation history to include in the message
* @returns An async generator that yields chunks of the response with accurate immediate costs
*/
@withRetry()
@withRetry({
maxRetries: 4,
baseDelay: 2000,
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const { id: modelId, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
+2 -1
View File
@@ -66,8 +66,9 @@ export class OpenAiNativeHandler implements ApiHandler {
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
reasoning_effort: (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium",
reasoning_effort: (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium",
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
+1 -1
View File
@@ -65,7 +65,7 @@ export class OpenAiHandler implements ApiHandler {
if (isReasoningModelFamily) {
openAiMessages = [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
temperature = undefined // does not support temperature
reasoningEffort = (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium"
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
}
const stream = await this.client.chat.completions.create({
+1 -1
View File
@@ -35,7 +35,7 @@ export class OpenRouterHandler implements ApiHandler {
systemPrompt,
messages,
this.getModel(),
this.options.o3MiniReasoningEffort,
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
+1 -1
View File
@@ -32,7 +32,7 @@ export class RequestyHandler implements ApiHandler {
...convertToOpenAiMessages(messages),
]
const reasoningEffort = this.options.o3MiniReasoningEffort || "medium"
const reasoningEffort = this.options.reasoningEffort || "medium"
const reasoning = { reasoning_effort: reasoningEffort }
const reasoningArgs = model.id.startsWith("openai/o") ? reasoning : {}
+9
View File
@@ -54,6 +54,15 @@ export function withRetry(options: RetryOptions = {}) {
delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt))
}
const handlerInstance = this as any
if (handlerInstance.options?.onRetryAttempt) {
try {
handlerInstance.options.onRetryAttempt(attempt + 1, maxRetries, delay, error)
} catch (e) {
console.error("Error in onRetryAttempt callback:", e)
}
}
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ export async function createOpenRouterStream(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
model: { id: string; info: ModelInfo },
o3MiniReasoningEffort?: string,
reasoningEffort?: string,
thinkingBudgetTokens?: number,
openRouterProviderSorting?: string,
) {
@@ -144,7 +144,7 @@ export async function createOpenRouterStream(
stream_options: { include_usage: true },
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
include_reasoning: true,
...(model.id.startsWith("openai/o") ? { reasoning_effort: o3MiniReasoningEffort || "medium" } : {}),
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
})
@@ -8,45 +8,6 @@ import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceSt
import * as vscode from "vscode"
import { synchronizeRuleToggles, getRuleFilesTotalContent } from "@core/context/instructions/user-instructions/rule-helpers"
/**
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
* Doesn't do anything if .clinerules dir already exists or doesn't exist
* Returns whether there are any uncaught errors
*/
export async function ensureLocalClinerulesDirExists(cwd: string): Promise<boolean> {
const clinerulePath = path.resolve(cwd, GlobalFileNames.clineRules)
const defaultRuleFilename = "default-rules.md"
try {
const exists = await fileExistsAtPath(clinerulePath)
if (exists && !(await isDirectory(clinerulePath))) {
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
const content = await fs.readFile(clinerulePath, "utf8")
const tempPath = clinerulePath + ".bak"
await fs.rename(clinerulePath, tempPath) // create backup
try {
await fs.mkdir(clinerulePath, { recursive: true })
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
await fs.unlink(tempPath).catch(() => {}) // delete backup
return false // conversion successful with no errors
} catch (conversionError) {
// attempt to restore backup on conversion failure
try {
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
await fs.rename(tempPath, clinerulePath) // restore backup
} catch (restoreError) {}
return true // in either case here we consider this an error
}
}
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
return false
} catch (error) {
return true
}
}
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
@@ -80,7 +41,8 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
if (await fileExistsAtPath(clineRulesFilePath)) {
if (await isDirectory(clineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(clineRulesFilePath)
const rulesFilePaths = await readDirectory(clineRulesFilePath, [[".clinerules", "workflows"]])
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
@@ -121,7 +83,9 @@ export async function refreshClineRulesToggles(
// Local toggles
const localClineRulesToggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
[".clinerules", "workflows"],
])
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
return {
@@ -129,82 +93,3 @@ export async function refreshClineRulesToggles(
localToggles: updatedLocalToggles,
}
}
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string) => {
try {
let filePath: string
if (isGlobal) {
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
filePath = path.join(globalClineRulesFilePath, filename)
} else {
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
const hasError = await ensureLocalClinerulesDirExists(cwd)
if (hasError === true) {
return { filePath: null, fileExists: false }
}
await fs.mkdir(localClineRulesFilePath, { recursive: true })
filePath = path.join(localClineRulesFilePath, filename)
}
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return { filePath, fileExists }
}
await fs.writeFile(filePath, "", "utf8")
return { filePath, fileExists: false }
} catch (error) {
return { filePath: null, fileExists: false }
}
}
export async function deleteRuleFile(
context: vscode.ExtensionContext,
rulePath: string,
isGlobal: boolean,
): Promise<{ success: boolean; message: string }> {
try {
// Check if file exists
const fileExists = await fileExistsAtPath(rulePath)
if (!fileExists) {
return {
success: false,
message: `Rule file does not exist: ${rulePath}`,
}
}
// Delete the file from disk
await fs.unlink(rulePath)
// Get the filename for messages
const fileName = path.basename(rulePath)
// Update the appropriate toggles
if (isGlobal) {
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateGlobalState(context, "globalClineRulesToggles", toggles)
} else {
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
}
return {
success: true,
message: `Rule file "${fileName}" deleted successfully`,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`Error deleting rule file: ${errorMessage}`, error)
return {
success: false,
message: `Failed to delete rule file.`,
}
}
}
@@ -1,14 +1,21 @@
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
import * as path from "path"
import fs from "fs/promises"
import { ClineRulesToggles } from "@shared/cline-rules"
import * as vscode from "vscode"
/**
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
*/
export async function readDirectoryRecursive(directoryPath: string, allowedFileExtension: string): Promise<string[]> {
export async function readDirectoryRecursive(
directoryPath: string,
allowedFileExtension: string,
excludedPaths: string[][] = [],
): Promise<string[]> {
try {
const entries = await readDirectory(directoryPath)
const entries = await readDirectory(directoryPath, excludedPaths)
let results: string[] = []
for (const entry of entries) {
if (allowedFileExtension !== "") {
@@ -33,6 +40,7 @@ export async function synchronizeRuleToggles(
rulesDirectoryPath: string,
currentToggles: ClineRulesToggles,
allowedFileExtension: string = "",
excludedPaths: string[][] = [],
): Promise<ClineRulesToggles> {
// Create a copy of toggles to modify
const updatedToggles = { ...currentToggles }
@@ -45,7 +53,7 @@ export async function synchronizeRuleToggles(
if (isDir) {
// DIRECTORY CASE
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension)
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension, excludedPaths)
const existingRulePaths = new Set<string>()
for (const filePath of filePaths) {
@@ -119,3 +127,155 @@ export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePat
).then((contents) => contents.filter(Boolean).join("\n\n"))
return ruleFilesTotalContent
}
/**
* Handles converting any directory into a file (specifically used for .clinerules and .clinerules/workflows)
* The old .clinerules file or .clinerules/workflows file will be renamed to a default filename
* Doesn't do anything if the dir already exists or doesn't exist
* Returns whether there are any uncaught errors
*/
export async function ensureLocalClineDirExists(clinerulePath: string, defaultRuleFilename: string): Promise<boolean> {
try {
const exists = await fileExistsAtPath(clinerulePath)
if (exists && !(await isDirectory(clinerulePath))) {
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
const content = await fs.readFile(clinerulePath, "utf8")
const tempPath = clinerulePath + ".bak"
await fs.rename(clinerulePath, tempPath) // create backup
try {
await fs.mkdir(clinerulePath, { recursive: true })
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
await fs.unlink(tempPath).catch(() => {}) // delete backup
return false // conversion successful with no errors
} catch (conversionError) {
// attempt to restore backup on conversion failure
try {
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
await fs.rename(tempPath, clinerulePath) // restore backup
} catch (restoreError) {}
return true // in either case here we consider this an error
}
}
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
return false
} catch (error) {
return true
}
}
/**
* Create a rule file or workflow file
*/
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string, type: string) => {
try {
let filePath: string
if (isGlobal) {
// global means its implicitly clinerules
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
filePath = path.join(globalClineRulesFilePath, filename)
} else {
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
const hasError = await ensureLocalClineDirExists(localClineRulesFilePath, "default-rules.md")
if (hasError === true) {
return { filePath: null, fileExists: false }
}
await fs.mkdir(localClineRulesFilePath, { recursive: true })
if (type === "workflow") {
const localWorkflowsFilePath = path.resolve(cwd, GlobalFileNames.workflows)
const hasError = await ensureLocalClineDirExists(localWorkflowsFilePath, "default-workflows.md")
if (hasError === true) {
return { filePath: null, fileExists: false }
}
await fs.mkdir(localWorkflowsFilePath, { recursive: true })
filePath = path.join(localWorkflowsFilePath, filename)
} else {
// clinerules file creation
filePath = path.join(localClineRulesFilePath, filename)
}
}
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return { filePath, fileExists }
}
await fs.writeFile(filePath, "", "utf8")
return { filePath, fileExists: false }
} catch (error) {
return { filePath: null, fileExists: false }
}
}
/**
* Delete a rule file or workflow file
*/
export async function deleteRuleFile(
context: vscode.ExtensionContext,
rulePath: string,
isGlobal: boolean,
type: string,
): Promise<{ success: boolean; message: string }> {
try {
// Check if file exists
const fileExists = await fileExistsAtPath(rulePath)
if (!fileExists) {
return {
success: false,
message: `File does not exist: ${rulePath}`,
}
}
// Delete the file from disk
await fs.unlink(rulePath)
// Get the filename for messages
const fileName = path.basename(rulePath)
// Update the appropriate toggles
if (isGlobal) {
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateGlobalState(context, "globalClineRulesToggles", toggles)
} else {
if (type === "workflow") {
const toggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "workflowToggles", toggles)
} else if (type === "cursor") {
const toggles = ((await getWorkspaceState(context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "localCursorRulesToggles", toggles)
} else if (type === "windsurf") {
const toggles = ((await getWorkspaceState(context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "localWindsurfRulesToggles", toggles)
} else {
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
}
}
return {
success: true,
message: `File "${fileName}" deleted successfully`,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`Error deleting file: ${errorMessage}`, error)
return {
success: false,
message: `Failed to delete file.`,
}
}
}
@@ -0,0 +1,20 @@
import path from "path"
import { GlobalFileNames } from "@core/storage/disk"
import { ClineRulesToggles } from "@shared/cline-rules"
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
import * as vscode from "vscode"
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
/**
* Refresh the workflow toggles
*/
export async function refreshWorkflowToggles(
context: vscode.ExtensionContext,
workingDirectory: string,
): Promise<ClineRulesToggles> {
const workflowRulesToggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
await updateWorkspaceState(context, "workflowToggles", updatedWorkflowToggles)
return updatedWorkflowToggles
}
+22 -10
View File
@@ -1,14 +1,13 @@
import { Controller } from ".."
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import {
createRuleFile as createRuleFileImpl,
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import * as vscode from "vscode"
import * as path from "path"
import { handleFileServiceRequest } from "./index"
import { cwd } from "@core/task"
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
/**
* Creates a rule file in either global or workspace rules directory
@@ -18,32 +17,45 @@ import { cwd } from "@core/task"
* @throws Error if operation fails
*/
export const createRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
if (typeof request.isGlobal !== "boolean" || typeof request.filename !== "string" || !request.filename) {
if (
typeof request.isGlobal !== "boolean" ||
!request.filename ||
typeof request.filename !== "string" ||
!request.type ||
typeof request.type !== "string"
) {
console.error("createRuleFile: Missing or invalid parameters", {
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
filename: typeof request.filename === "string" ? request.filename : `Invalid: ${typeof request.filename}`,
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
})
throw new Error("Missing or invalid parameters")
}
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd)
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd, request.type)
if (!filePath) {
throw new Error("Failed to create rule file.")
throw new Error("Failed to create file.")
}
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
if (fileExists) {
vscode.window.showWarningMessage(`Rule file "${request.filename}" already exists.`)
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
// Still open it for editing
await handleFileServiceRequest(controller, "openFile", { value: filePath })
} else {
await refreshClineRulesToggles(controller.context, cwd)
if (request.type === "workflow") {
await refreshWorkflowToggles(controller.context, cwd)
} else {
await refreshClineRulesToggles(controller.context, cwd)
}
await controller.postStateToWebview()
await handleFileServiceRequest(controller, "openFile", { value: filePath })
vscode.window.showInformationMessage(
`Created new ${request.isGlobal ? "global" : "workspace"} rule file: ${request.filename}`,
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
)
}
+20 -9
View File
@@ -1,11 +1,10 @@
import { Controller } from ".."
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import {
deleteRuleFile as deleteRuleFileImpl,
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
import * as vscode from "vscode"
import * as path from "path"
import { cwd } from "@core/task"
@@ -18,26 +17,38 @@ import { cwd } from "@core/task"
* @throws Error if operation fails
*/
export const deleteRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
if (typeof request.isGlobal !== "boolean" || typeof request.rulePath !== "string" || !request.rulePath) {
if (
typeof request.isGlobal !== "boolean" ||
typeof request.rulePath !== "string" ||
!request.rulePath ||
!request.type ||
typeof request.type !== "string"
) {
console.error("deleteRuleFile: Missing or invalid parameters", {
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
rulePath: typeof request.rulePath === "string" ? request.rulePath : `Invalid: ${typeof request.rulePath}`,
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
})
throw new Error("Missing or invalid parameters")
}
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal)
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal, request.type)
if (!result.success) {
throw new Error(result.message || "Failed to delete rule file")
}
await refreshClineRulesToggles(controller.context, cwd)
await refreshExternalRulesToggles(controller.context, cwd)
// we refresh inside of the deleteRuleFileImpl(..) call
//await refreshClineRulesToggles(controller.context, cwd)
//await refreshExternalRulesToggles(controller.context, cwd)
//await refreshWorkflowToggles(controller.context, cwd)
await controller.postStateToWebview()
const fileName = path.basename(request.rulePath)
vscode.window.showInformationMessage(`Rule file "${fileName}" deleted successfully`)
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
return RuleFile.create({
filePath: request.rulePath,
+2
View File
@@ -10,6 +10,7 @@ import { openFile } from "./openFile"
import { openImage } from "./openImage"
import { searchCommits } from "./searchCommits"
import { searchFiles } from "./searchFiles"
import { selectImages } from "./selectImages"
// Register all file service methods
export function registerAllMethods(): void {
@@ -21,4 +22,5 @@ export function registerAllMethods(): void {
registerMethod("openImage", openImage)
registerMethod("searchCommits", searchCommits)
registerMethod("searchFiles", searchFiles)
registerMethod("selectImages", selectImages)
}
+21
View File
@@ -0,0 +1,21 @@
import { Controller } from ".."
import { EmptyRequest, StringArray } from "@shared/proto/common"
import { selectImages as selectImagesIntegration } from "@integrations/misc/process-images"
import { FileMethodHandler } from "./index"
/**
* Prompts the user to select images from the file system and returns them as data URLs
* @param controller The controller instance
* @param request Empty request, no parameters needed
* @returns Array of image data URLs
*/
export const selectImages: FileMethodHandler = async (controller: Controller, request: EmptyRequest): Promise<StringArray> => {
try {
const images = await selectImagesIntegration()
return StringArray.create({ values: images })
} catch (error) {
console.error("Error selecting images:", error)
// Return empty array on error
return StringArray.create({ values: [] })
}
}
+53 -83
View File
@@ -14,7 +14,6 @@ import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMi
import { downloadTask } from "@integrations/misc/export-markdown"
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
import { handleFileServiceRequest } from "./file"
import { selectImages } from "@integrations/misc/process-images"
import { getTheme } from "@integrations/theme/getTheme"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
@@ -51,6 +50,7 @@ import { ClineRulesToggles } from "@shared/cline-rules"
import { sendStateUpdate } from "./state/subscribeToState"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -66,7 +66,7 @@ export class Controller {
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
private latestAnnouncementId = "may-09-2025_17:11:00" // update to some unique identifier when we add a new announcement
private latestAnnouncementId = "may-16-2025_16:11:00" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
@@ -145,8 +145,19 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
enableCheckpointsSetting,
isNewUser,
taskHistory,
} = await getAllExtensionState(this.context)
const NEW_USER_TASK_COUNT_THRESHOLD = 10
// Check if the user has completed enough tasks to no longer be considered a "new user"
if (isNewUser && !historyItem && taskHistory && taskHistory.length >= NEW_USER_TASK_COUNT_THRESHOLD) {
await updateGlobalState(this.context, "isNewUser", false)
await this.postStateToWebview()
}
if (autoApprovalSettings) {
const updatedAutoApprovalSettings = {
...autoApprovalSettings,
@@ -168,6 +179,7 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
enableCheckpointsSetting ?? true,
customInstructions,
task,
images,
@@ -311,26 +323,14 @@ export class Controller {
const browserSession = new BrowserSession(this.context, browserSettings)
await browserSession.relaunchChromeDebugMode(this)
break
case "askResponse":
this.task?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
break
case "didShowAnnouncement":
await updateGlobalState(this.context, "lastShownAnnouncementId", this.latestAnnouncementId)
await this.postStateToWebview()
break
case "selectImages":
const images = await selectImages()
await this.postMessageToWebview({
type: "selectedImages",
images,
})
break
case "resetState":
await this.resetState()
break
case "refreshClineRules":
await refreshClineRulesToggles(this.context, cwd)
await refreshExternalRulesToggles(this.context, cwd)
await refreshWorkflowToggles(this.context, cwd)
await this.postStateToWebview()
break
case "openInBrowser":
@@ -338,18 +338,9 @@ export class Controller {
vscode.env.openExternal(vscode.Uri.parse(message.url))
}
break
case "fetchOpenGraphData":
this.fetchOpenGraphData(message.text!)
break
case "openMention":
openMention(message.text)
break
case "taskCompletionViewChanges": {
if (message.number) {
await this.task?.presentMultifileDiff(message.number, true)
}
break
}
case "accountLogoutClicked": {
await this.handleSignOut()
break
@@ -377,11 +368,6 @@ export class Controller {
await this.silentlyRefreshMcpMarketplace()
break
}
case "taskFeedback":
if (message.feedbackType && this.task?.taskId) {
telemetryService.captureTaskFeedback(this.task.taskId, message.feedbackType)
}
break
// case "openMcpMarketplaceServerDetails": {
// if (message.text) {
// const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`)
@@ -482,24 +468,21 @@ export class Controller {
}
break
}
case "toggleWorkflow": {
const { workflowPath, enabled } = message
if (workflowPath && typeof enabled === "boolean") {
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
toggles[workflowPath] = enabled
await updateWorkspaceState(this.context, "workflowToggles", toggles)
await this.postStateToWebview()
}
break
}
case "requestTotalTasksSize": {
this.refreshTotalTasksSize()
break
}
case "restartMcpServer": {
try {
await this.mcpHub?.restartConnection(message.text!)
} catch (error) {
console.error(`Failed to retry connection for ${message.text}:`, error)
}
break
}
case "deleteMcpServer": {
if (message.serverName) {
this.mcpHub?.deleteServer(message.serverName)
}
break
}
case "fetchLatestMcpServersFromHub": {
this.mcpHub?.sendLatestMcpServers()
break
@@ -563,6 +546,22 @@ export class Controller {
// plan act setting
await updateGlobalState(this.context, "planActSeparateModelsSetting", message.planActSeparateModelsSetting)
if (typeof message.enableCheckpointsSetting === "boolean") {
await updateGlobalState(this.context, "enableCheckpointsSetting", message.enableCheckpointsSetting)
}
if (typeof message.mcpMarketplaceEnabled === "boolean") {
await updateGlobalState(this.context, "mcpMarketplaceEnabled", message.mcpMarketplaceEnabled)
}
// chat settings (including preferredLanguage and openAIReasoningEffort)
if (message.chatSettings) {
await updateGlobalState(this.context, "chatSettings", message.chatSettings)
if (this.task) {
this.task.chatSettings = message.chatSettings
}
}
// after settings are updated, post state to webview
await this.postStateToWebview()
@@ -1316,7 +1315,10 @@ export class Controller {
async postStateToWebview() {
const state = await this.getStateToPostToWebview()
await sendStateUpdate(state)
// For testing: Bypass gRPC stream and send state directly
console.log("[Controller Test Revert] Posting full state via direct 'state' message.")
await this.postMessageToWebview({ type: "state", state: state })
// await sendStateUpdate(state) // Original line for the GrPC stream
}
async getStateToPostToWebview(): Promise<ExtensionState> {
@@ -1332,8 +1334,10 @@ export class Controller {
mcpMarketplaceEnabled,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
globalClineRulesToggles,
shellIntegrationTimeout,
isNewUser,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
@@ -1345,6 +1349,8 @@ export class Controller {
const localCursorRulesToggles =
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
const workflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
@@ -1366,12 +1372,15 @@ export class Controller {
mcpMarketplaceEnabled,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
vscMachineId: vscode.env.machineId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
localCursorRulesToggles: localCursorRulesToggles || {},
workflowToggles: workflowToggles || {},
shellIntegrationTimeout,
isNewUser,
}
}
@@ -1447,30 +1456,6 @@ export class Controller {
// secrets
// Open Graph Data
async fetchOpenGraphData(url: string) {
try {
// Use the fetchOpenGraphData function from link-preview.ts
const ogData = await fetchOpenGraphData(url)
// Send the data back to the webview
await this.postMessageToWebview({
type: "openGraphData",
openGraphData: ogData,
url: url,
})
} catch (error) {
console.error(`Error fetching Open Graph data for ${url}:`, error)
// Send an error response
await this.postMessageToWebview({
type: "openGraphData",
error: `Failed to fetch Open Graph data: ${error}`,
url: url,
})
}
}
// Git commit message generation
async generateGitCommitMessage() {
@@ -1575,19 +1560,4 @@ Commit message:`
}
// dev
async resetState() {
vscode.window.showInformationMessage("Resetting state...")
await resetExtensionState(this.context)
if (this.task) {
this.task.abortTask()
this.task = undefined
}
vscode.window.showInformationMessage("State reset")
await this.postStateToWebview()
await this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
}
}
@@ -0,0 +1,25 @@
import type { Controller } from "../index"
import type { McpServers } from "../../../shared/proto/mcp"
import { convertMcpServersToProtoMcpServers } from "../../../shared/proto-conversions/mcp/mcp-server-conversion"
import { StringRequest } from "@/shared/proto/common"
/**
* Deletes an MCP server
* @param controller The controller instance
* @param request The delete server request
* @returns The list of remaining MCP servers after deletion
*/
export async function deleteMcpServer(controller: Controller, request: StringRequest): Promise<McpServers> {
try {
// Call the RPC variant to delete the server and get updated server list
const mcpServers = (await controller.mcpHub?.deleteServerRPC(request.value)) || []
// Convert application types to protobuf types
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
return { mcpServers: protoServers }
} catch (error) {
console.error(`Failed to delete MCP server: ${error}`)
throw error
}
}
+4
View File
@@ -4,7 +4,9 @@
// Import all method implementations
import { registerMethod } from "./index"
import { addRemoteMcpServer } from "./addRemoteMcpServer"
import { deleteMcpServer } from "./deleteMcpServer"
import { downloadMcp } from "./downloadMcp"
import { restartMcpServer } from "./restartMcpServer"
import { toggleMcpServer } from "./toggleMcpServer"
import { updateMcpTimeout } from "./updateMcpTimeout"
@@ -12,7 +14,9 @@ import { updateMcpTimeout } from "./updateMcpTimeout"
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
registerMethod("deleteMcpServer", deleteMcpServer)
registerMethod("downloadMcp", downloadMcp)
registerMethod("restartMcpServer", restartMcpServer)
registerMethod("toggleMcpServer", toggleMcpServer)
registerMethod("updateMcpTimeout", updateMcpTimeout)
}
@@ -0,0 +1,24 @@
import type { McpServers } from "@shared/proto/mcp"
import type { Controller } from "../index"
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { StringRequest } from "@/shared/proto/common"
/**
* Restarts an MCP server connection
* @param controller The controller instance
* @param request The request containing the server name
* @returns The updated list of MCP servers
*/
export async function restartMcpServer(controller: Controller, request: StringRequest): Promise<McpServers> {
try {
const mcpServers = await controller.mcpHub?.restartConnectionRPC(request.value)
// Convert from McpServer[] to ProtoMcpServer[] ensuring all required fields are set
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
return { mcpServers: protoServers }
} catch (error) {
console.error(`Failed to restart MCP server ${request.value}:`, error)
throw error
}
}
+2
View File
@@ -4,6 +4,7 @@
// Import all method implementations
import { registerMethod } from "./index"
import { getLatestState } from "./getLatestState"
import { resetState } from "./resetState"
import { subscribeToState } from "./subscribeToState"
import { toggleFavoriteModel } from "./toggleFavoriteModel"
@@ -14,6 +15,7 @@ export const streamingMethods = ["subscribeToState"]
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("getLatestState", getLatestState)
registerMethod("resetState", resetState)
registerMethod("subscribeToState", subscribeToState, { isStreaming: true })
registerMethod("toggleFavoriteModel", toggleFavoriteModel)
}
+36
View File
@@ -0,0 +1,36 @@
import { Controller } from ".."
import { Empty, EmptyRequest } from "../../../shared/proto/common"
import { resetExtensionState } from "../../../core/storage/state"
import * as vscode from "vscode"
/**
* Resets the extension state to its defaults
* @param controller The controller instance
* @param request An empty request (no parameters needed)
* @returns An empty response
*/
export async function resetState(controller: Controller, request: EmptyRequest): Promise<Empty> {
try {
vscode.window.showInformationMessage("Resetting state...")
await resetExtensionState(controller.context)
if (controller.task) {
controller.task.abortTask()
controller.task = undefined
}
vscode.window.showInformationMessage("State reset")
await controller.postStateToWebview()
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
return Empty.create()
} catch (error) {
console.error("Error resetting state:", error)
vscode.window.showErrorMessage(`Failed to reset state: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
}
+45
View File
@@ -0,0 +1,45 @@
import { Controller } from ".."
import { Empty } from "../../../shared/proto/common"
import { AskResponseRequest } from "../../../shared/proto/task"
import { ClineAskResponse } from "../../../shared/WebviewMessage"
/**
* Handles a response from the webview for a previous ask operation
*
* @param controller The controller instance
* @param request The request containing response type, optional text and optional images
* @returns Empty response
*/
export async function askResponse(controller: Controller, request: AskResponseRequest): Promise<Empty> {
try {
if (!controller.task) {
console.warn("askResponse: No active task to receive response")
return Empty.create()
}
// Map the string responseType to the ClineAskResponse enum
let responseType: ClineAskResponse
switch (request.responseType) {
case "yesButtonClicked":
responseType = "yesButtonClicked"
break
case "noButtonClicked":
responseType = "noButtonClicked"
break
case "messageResponse":
responseType = "messageResponse"
break
default:
console.warn(`askResponse: Unknown response type: ${request.responseType}`)
return Empty.create()
}
// Call the task's handler for webview responses
await controller.task.handleWebviewAskResponse(responseType, request.text, request.images)
return Empty.create()
} catch (error) {
console.error("Error in askResponse handler:", error)
throw error
}
}
+6
View File
@@ -3,6 +3,7 @@
// Import all method implementations
import { registerMethod } from "./index"
import { askResponse } from "./askResponse"
import { cancelTask } from "./cancelTask"
import { clearTask } from "./clearTask"
import { deleteNonFavoritedTasks } from "./deleteNonFavoritedTasks"
@@ -11,11 +12,14 @@ import { exportTaskWithId } from "./exportTaskWithId"
import { getTaskHistory } from "./getTaskHistory"
import { newTask } from "./newTask"
import { showTaskWithId } from "./showTaskWithId"
import { taskCompletionViewChanges } from "./taskCompletionViewChanges"
import { taskFeedback } from "./taskFeedback"
import { toggleTaskFavorite } from "./toggleTaskFavorite"
// Register all task service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("askResponse", askResponse)
registerMethod("cancelTask", cancelTask)
registerMethod("clearTask", clearTask)
registerMethod("deleteNonFavoritedTasks", deleteNonFavoritedTasks)
@@ -24,5 +28,7 @@ export function registerAllMethods(): void {
registerMethod("getTaskHistory", getTaskHistory)
registerMethod("newTask", newTask)
registerMethod("showTaskWithId", showTaskWithId)
registerMethod("taskCompletionViewChanges", taskCompletionViewChanges)
registerMethod("taskFeedback", taskFeedback)
registerMethod("toggleTaskFavorite", toggleTaskFavorite)
}
@@ -0,0 +1,21 @@
import { Controller } from ".."
import { Empty } from "../../../shared/proto/common"
import { Int64Request } from "../../../shared/proto/common"
/**
* Shows task completion changes in a diff view
* @param controller The controller instance
* @param request The request containing the timestamp of the message
* @returns Empty response
*/
export async function taskCompletionViewChanges(controller: Controller, request: Int64Request): Promise<Empty> {
try {
if (request.value && controller.task) {
await controller.task.presentMultifileDiff(request.value, true)
}
return Empty.create()
} catch (error) {
console.error("Error in taskCompletionViewChanges handler:", error)
throw error
}
}
+28
View File
@@ -0,0 +1,28 @@
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
/**
* Handles task feedback submission (thumbs up/down)
* @param controller The controller instance
* @param request The StringRequest containing the feedback type ("thumbs_up" or "thumbs_down") in the value field
* @returns Empty response
*/
export async function taskFeedback(controller: Controller, request: StringRequest): Promise<Empty> {
if (!request.value) {
console.warn("taskFeedback: Missing feedback type value")
return Empty.create()
}
try {
if (controller.task?.taskId) {
telemetryService.captureTaskFeedback(controller.task.taskId, request.value as any)
} else {
console.warn("taskFeedback: No active task to receive feedback")
}
} catch (error) {
console.error("Error in taskFeedback handler:", error)
}
return Empty.create()
}
@@ -0,0 +1,26 @@
import { Controller } from ".."
import { StringRequest } from "../../../shared/proto/common"
import { OpenGraphData } from "../../../shared/proto/web"
import { fetchOpenGraphData as fetchOGData } from "../../../integrations/misc/link-preview"
import { convertDomainOpenGraphDataToProto } from "../../../shared/proto-conversions/web/open-graph-conversion"
/**
* Fetches Open Graph metadata from a URL
* @param controller The controller instance
* @param request The request containing the URL to fetch metadata from
* @returns Promise resolving to OpenGraphData
*/
export async function fetchOpenGraphData(controller: Controller, request: StringRequest): Promise<OpenGraphData> {
try {
const url = request.value || ""
// Fetch open graph data using the existing utility
const ogData = await fetchOGData(url)
// Convert domain model to proto model
return convertDomainOpenGraphDataToProto(ogData)
} catch (error) {
console.error(`Error fetching Open Graph data: ${request.value}`, error)
// Return empty OpenGraphData object
return OpenGraphData.create({})
}
}
+2
View File
@@ -4,9 +4,11 @@
// Import all method implementations
import { registerMethod } from "./index"
import { checkIsImageUrl } from "./checkIsImageUrl"
import { fetchOpenGraphData } from "./fetchOpenGraphData"
// Register all web service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("checkIsImageUrl", checkIsImageUrl)
registerMethod("fetchOpenGraphData", fetchOpenGraphData)
}
+55 -7
View File
@@ -1,11 +1,16 @@
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse, reportBugToolResponse } from "../prompts/commands"
import { ClineRulesToggles } from "@shared/cline-rules"
import fs from "fs/promises"
/**
* 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): { processedText: string; needsClinerulesFileCheck: boolean } {
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
export async function parseSlashCommands(
text: string,
workflowToggles: ClineRulesToggles,
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
const SUPPORTED_DEFAULT_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
const commandReplacements: Record<string, string> = {
newtask: newTaskToolResponse(),
@@ -17,10 +22,10 @@ export function parseSlashCommands(text: string): { processedText: string; needs
// 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 },
{ 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
@@ -34,7 +39,8 @@ export function parseSlashCommands(text: string): { processedText: string; needs
const commandName = match[2] // casing matters
if (SUPPORTED_COMMANDS.includes(commandName)) {
// we give preference to the default commands if the user has a file with the same name
if (SUPPORTED_DEFAULT_COMMANDS.includes(commandName)) {
const fullMatchStartIndex = match.index
// find position of slash command within the full match
@@ -51,6 +57,48 @@ export function parseSlashCommands(text: string): { processedText: string; needs
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
}
// in practice we want to minimize this work, so we only do it if theres a possible match
const enabledWorkflows = Object.entries(workflowToggles)
.filter(([_, enabled]) => enabled)
.map(([filePath, _]) => {
const fileName = filePath.replace(/^.*[/\\]/, "")
return {
fullPath: filePath,
fileName: fileName,
}
})
// Then check if the command matches any enabled workflow filename
const matchingWorkflow = enabledWorkflows.find((workflow) => workflow.fileName === commandName)
if (matchingWorkflow) {
try {
// Read workflow file content from the full path
const workflowContent = (await fs.readFile(matchingWorkflow.fullPath, "utf8")).trim()
// find position of slash command within the full match
const fullMatchStartIndex = match.index
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 =
`<explicit_instructions type="${matchingWorkflow.fileName}">\n${workflowContent}\n</explicit_instructions>\n` +
textWithoutSlashCommand
return { processedText, needsClinerulesFileCheck: false }
} catch (error) {
console.error(`Error reading workflow file ${matchingWorkflow.fullPath}: ${error}`)
}
}
}
}
+1
View File
@@ -15,6 +15,7 @@ export const GlobalFileNames = {
openRouterModels: "openrouter_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
workflows: ".clinerules/workflows",
cursorRulesDir: ".cursor/rules",
cursorRulesFile: ".cursorrules",
windsurfRules: ".windsurfrules",
+3
View File
@@ -83,8 +83,11 @@ export type GlobalStateKey =
| "thinkingBudgetTokens"
| "reasoningEffort"
| "planActSeparateModelsSetting"
| "enableCheckpointsSetting"
| "mcpMarketplaceEnabled"
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
| "isNewUser"
export type LocalStateKey = "localClineRulesToggles"
+40 -7
View File
@@ -1,5 +1,5 @@
import * as vscode from "vscode"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { DEFAULT_CHAT_SETTINGS, OpenAIReasoningEffort } from "@shared/ChatSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { GlobalStateKey, SecretKey } from "./state-keys"
@@ -51,8 +51,32 @@ export async function getWorkspaceState(context: vscode.ExtensionContext, key: s
return await context.workspaceState.get(key)
}
async function migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw: boolean | undefined): Promise<boolean> {
const config = vscode.workspace.getConfiguration("cline")
const mcpMarketplaceEnabled = config.get<boolean>("mcpMarketplace.enabled")
if (mcpMarketplaceEnabled !== undefined) {
// Remove from VSCode configuration
await config.update("mcpMarketplace.enabled", undefined, true)
return !mcpMarketplaceEnabled
}
return mcpMarketplaceEnabledRaw ?? true
}
async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: boolean | undefined): Promise<boolean> {
const config = vscode.workspace.getConfiguration("cline")
const enableCheckpoints = config.get<boolean>("enableCheckpoints")
if (enableCheckpoints !== undefined) {
// Remove from VSCode configuration
await config.update("enableCheckpoints", undefined, true)
return enableCheckpoints
}
return enableCheckpointsSettingRaw ?? true
}
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
isNewUser,
storedApiProvider,
apiModelId,
apiKey,
@@ -136,7 +160,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
globalClineRulesToggles,
requestTimeoutMs,
shellIntegrationTimeout,
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
@@ -220,6 +247,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "requestTimeoutMs") as Promise<number | undefined>,
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
fetch,
])
let apiProvider: ApiProvider
@@ -238,9 +268,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)
const mcpMarketplaceEnabled = await migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw)
const enableCheckpointsSetting = await migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw)
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
// On win11 state sometimes initializes as empty string instead of undefined
@@ -309,7 +338,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
o3MiniReasoningEffort,
thinkingBudgetTokens,
reasoningEffort,
liteLlmBaseUrl,
@@ -328,6 +356,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
favoritedModelIds,
requestTimeoutMs,
},
isNewUser: isNewUser ?? true,
lastShownAnnouncementId,
customInstructions,
taskHistory,
@@ -335,7 +364,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
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,
chatSettings: {
...DEFAULT_CHAT_SETTINGS, // Apply defaults first
...(chatSettings || {}), // Spread fetched chatSettings, which includes preferredLanguage, and openAIReasoningEffort
},
userInfo,
previousModeApiProvider,
previousModeModelId,
@@ -345,9 +377,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
mcpMarketplaceEnabled,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting,
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
}
}
+137 -29
View File
@@ -80,6 +80,7 @@ import {
ensureTaskDirectoryExists,
getSavedApiConversationHistory,
getSavedClineMessages,
GlobalFileNames,
saveApiConversationHistory,
saveClineMessages,
} from "@core/storage/disk"
@@ -87,13 +88,14 @@ import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
ensureLocalClinerulesDirExists,
} from "@core/context/instructions/user-instructions/cline-rules"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
import {
refreshExternalRulesToggles,
getLocalWindsurfRules,
getLocalCursorRules,
} from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
import { getGlobalState } from "@core/storage/state"
import { parseSlashCommands } from "@core/slash-commands"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
@@ -167,6 +169,7 @@ export class Task {
private didAlreadyUseTool = false
private didCompleteReadingStream = false
private didAutomaticallyRetryFailedApiRequest = false
private enableCheckpoints: boolean
constructor(
context: vscode.ExtensionContext,
@@ -182,6 +185,7 @@ export class Task {
browserSettings: BrowserSettings,
chatSettings: ChatSettings,
shellIntegrationTimeout: number,
enableCheckpointsSetting: boolean,
customInstructions?: string,
task?: string,
images?: string[],
@@ -207,6 +211,7 @@ export class Task {
this.autoApprovalSettings = autoApprovalSettings
this.browserSettings = browserSettings
this.chatSettings = chatSettings
this.enableCheckpoints = enableCheckpointsSetting
// Initialize taskId first
if (historyItem) {
@@ -222,11 +227,50 @@ export class Task {
// Initialize file context tracker
this.fileContextTracker = new FileContextTracker(context, this.taskId)
this.modelContextTracker = new ModelContextTracker(context, this.taskId)
// Now that taskId is initialized, we can build the API handler
this.api = buildApiHandler({
// Prepare effective API configuration
let effectiveApiConfiguration: ApiConfiguration = {
...apiConfiguration,
taskId: this.taskId,
})
onRetryAttempt: (attempt: number, maxRetries: number, delay: number, error: any) => {
const lastApiReqStartedIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
if (lastApiReqStartedIndex !== -1) {
try {
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(
this.clineMessages[lastApiReqStartedIndex].text || "{}",
)
currentApiReqInfo.retryStatus = {
attempt: attempt, // attempt is already 1-indexed from retry.ts
maxAttempts: maxRetries, // total attempts
delaySec: Math.round(delay / 1000),
errorSnippet: error?.message ? `${String(error.message).substring(0, 50)}...` : undefined,
}
// Clear previous cancelReason and streamingFailedMessage if we are retrying
delete currentApiReqInfo.cancelReason
delete currentApiReqInfo.streamingFailedMessage
this.clineMessages[lastApiReqStartedIndex].text = JSON.stringify(currentApiReqInfo)
// Post the updated state to the webview so the UI reflects the retry attempt
this.postStateToWebview().catch((e) =>
console.error("Error posting state to webview in onRetryAttempt:", e),
)
console.log(
`[Task ${this.taskId}] API Auto-Retry Status Update: Attempt ${attempt}/${maxRetries}, Delay: ${delay}ms`,
)
} catch (e) {
console.error(`[Task ${this.taskId}] Error updating api_req_started with retryStatus:`, e)
}
}
},
}
if (apiConfiguration.apiProvider === "openai" || apiConfiguration.apiProvider === "openai-native") {
effectiveApiConfiguration.reasoningEffort = chatSettings.openAIReasoningEffort
}
// Now that taskId is initialized, we can build the API handler
this.api = buildApiHandler(effectiveApiConfiguration)
// Set taskId on browserSession for telemetry tracking
this.browserSession.setTaskId(this.taskId)
@@ -342,9 +386,19 @@ export class Task {
break
case "taskAndWorkspace":
case "workspace":
if (!this.enableCheckpoints) {
vscode.window.showErrorMessage("Checkpoints are disabled in settings.")
didWorkspaceRestoreFail = true
break
}
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -451,6 +505,11 @@ export class Task {
const relinquishButton = () => {
this.postMessageToWebview({ type: "relinquishControl" })
}
if (!this.enableCheckpoints) {
vscode.window.showInformationMessage("Checkpoints are disabled in settings. Cannot show diff.")
relinquishButton()
return
}
console.log("presentMultifileDiff", messageTs)
const messageIndex = this.clineMessages.findIndex((m) => m.ts === messageTs)
@@ -468,9 +527,13 @@ export class Task {
}
// 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) {
if (!this.checkpointTracker && this.enableCheckpoints && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -568,6 +631,10 @@ export class Task {
}
async doesLatestTaskCompletionHaveNewChanges() {
if (!this.enableCheckpoints) {
return false
}
const messageIndex = findLastIndex(this.clineMessages, (m) => m.say === "completion_result")
const message = this.clineMessages[messageIndex]
if (!message) {
@@ -580,9 +647,13 @@ export class Task {
return false
}
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
if (this.enableCheckpoints && !this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -1081,6 +1152,10 @@ export class Task {
// Checkpoints
async saveCheckpoint(isAttemptCompletionMessage: boolean = false) {
if (!this.enableCheckpoints) {
// If checkpoints are disabled, do nothing.
return
}
// Set isCheckpointCheckedOut to false for all checkpoint_created messages
this.clineMessages.forEach((message) => {
if (message.say === "checkpoint_created") {
@@ -1448,7 +1523,7 @@ export class Task {
*/
private async migrateDisableBrowserToolSetting(): Promise<void> {
const config = vscode.workspace.getConfiguration("cline")
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool")
const disableBrowserTool = config.get<boolean>("disableBrowserTool")
if (disableBrowserTool !== undefined) {
this.browserSettings.disableToolUse = disableBrowserTool
@@ -1457,6 +1532,16 @@ export class Task {
}
}
private async migratePreferredLanguageToolSetting(): Promise<void> {
const config = vscode.workspace.getConfiguration("cline")
const preferredLanguage = config.get<LanguageDisplay>("preferredLanguage")
if (preferredLanguage !== undefined) {
this.chatSettings.preferredLanguage = preferredLanguage
// Remove from VSCode configuration
await config.update("preferredLanguage", undefined, true)
}
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
@@ -1473,9 +1558,8 @@ export class Task {
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings)
let settingsCustomInstructions = this.customInstructions?.trim()
const preferredLanguage = getLanguageKey(
vscode.workspace.getConfiguration("cline").get<LanguageDisplay>("preferredLanguage"),
)
await this.migratePreferredLanguageToolSetting()
const preferredLanguage = getLanguageKey(this.chatSettings.preferredLanguage as LanguageDisplay)
const preferredLanguageInstructions =
preferredLanguage && preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
@@ -1603,6 +1687,20 @@ export class Task {
const errorMessage = this.formatErrorWithStatusCode(error)
// Update the 'api_req_started' message to reflect final failure before asking user to manually retry
const lastApiReqStartedIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
if (lastApiReqStartedIndex !== -1) {
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(this.clineMessages[lastApiReqStartedIndex].text || "{}")
delete currentApiReqInfo.retryStatus
this.clineMessages[lastApiReqStartedIndex].text = JSON.stringify({
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
cancelReason: "retries_exhausted", // Indicate that automatic retries failed
streamingFailedMessage: errorMessage,
} satisfies ClineApiReqInfo)
// this.ask will trigger postStateToWebview, so this change should be picked up.
}
const { response } = await this.ask("api_req_failed", errorMessage)
if (response !== "yesButtonClicked") {
@@ -3670,17 +3768,11 @@ export class Task {
}),
)
if (isFirstRequest) {
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
}
// use this opportunity to initialize the checkpoint tracker (can be expensive to initialize in the constructor)
// FIXME: right now we're letting users init checkpoints for old tasks, but this could be a problem if opening a task in the wrong workspace
// isNewTask &&
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
// Initialize checkpoint tracker first if enabled and it's the first request
if (isFirstRequest && this.enableCheckpoints && !this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await pTimeout(
CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath),
CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath, this.enableCheckpoints),
{
milliseconds: 15_000,
message:
@@ -3694,14 +3786,22 @@ export class Task {
}
}
// Now that checkpoint tracker is initialized, update the dummy checkpoint_created message with the commit hash. (This is necessary since we use the API request loading as an opportunity to initialize the checkpoint tracker, which can take some time)
if (isFirstRequest) {
const commitHash = await this.checkpointTracker?.commit()
// Now, if it's the first request AND checkpoints are enabled AND tracker was successfully initialized,
// then say "checkpoint_created" and perform the commit.
if (isFirstRequest && this.enableCheckpoints && this.checkpointTracker) {
await this.say("checkpoint_created") // Now this is conditional
const commitHash = await this.checkpointTracker.commit() // Actual commit
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
if (lastCheckpointMessage) {
lastCheckpointMessage.lastCheckpointHash = commitHash
await this.saveClineMessagesAndUpdateHistory()
// saveClineMessagesAndUpdateHistory will be called later after API response,
// so no need to call it here unless this is the only modification to this message.
// For now, assuming it's handled later.
}
} else if (isFirstRequest && this.enableCheckpoints && !this.checkpointTracker && this.checkpointTrackerErrorMessage) {
// Checkpoints are enabled, but tracker failed to initialize.
// checkpointTrackerErrorMessage is already set and will be part of the state.
// No explicit UI message here, error message will be in ExtensionState.
}
const [parsedUserContent, environmentDetails, clinerulesError] = await this.loadContext(userContent, includeFileDetails)
@@ -3744,8 +3844,11 @@ export class Task {
// fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy purposes to keep track of prices in tasks from history
// (it's worth removing a few months from now)
const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}")
delete currentApiReqInfo.retryStatus // Clear retry status when request is finalized
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
...JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}"),
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWrites: cacheWriteTokens,
@@ -4014,6 +4117,8 @@ export class Task {
// Track if we need to check clinerulesFile
let needsClinerulesFileCheck = false
const workflowToggles = await refreshWorkflowToggles(this.getContext(), cwd)
const processUserContent = async () => {
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
// (Note: this caused the @/ import alias bug where file contents were being parsed as well, since v2 converted tool results to text blocks)
@@ -4036,7 +4141,10 @@ export class Task {
)
// when parsing slash commands, we still want to allow the user to provide their desired context
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
parsedText,
workflowToggles,
)
if (needsCheck) {
needsClinerulesFileCheck = true
@@ -4062,7 +4170,7 @@ export class Task {
// After processing content, check clinerulesData if needed
let clinerulesError = false
if (needsClinerulesFileCheck) {
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
clinerulesError = await ensureLocalClineDirExists(cwd, GlobalFileNames.clineRules)
}
// Return all results
+1
View File
@@ -328,6 +328,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
<!DOCTYPE html>
<html lang="en">
<head>
<script src="http://localhost:8097"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
@@ -21,15 +21,17 @@ class CheckpointTracker {
this.cwd = cwd
}
public static async create(taskId: string, provider?: ClineProvider): Promise<CheckpointTracker | undefined> {
public static async create(
taskId: string,
enableCheckpointsSetting: boolean,
provider?: ClineProvider,
): Promise<CheckpointTracker | undefined> {
try {
if (!provider) {
throw new Error("Provider is required to create a checkpoint tracker")
}
// Check if checkpoints are disabled in VS Code settings
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
if (!enableCheckpoints) {
if (!enableCheckpointsSetting) {
return undefined // Don't create tracker when disabled
}
@@ -90,7 +90,11 @@ class CheckpointTracker {
* Configuration:
* - Respects 'cline.enableCheckpoints' VS Code setting
*/
public static async create(taskId: string, globalStoragePath: string | undefined): Promise<CheckpointTracker | undefined> {
public static async create(
taskId: string,
globalStoragePath: string | undefined,
enableCheckpointsSetting: boolean,
): Promise<CheckpointTracker | undefined> {
if (!globalStoragePath) {
throw new Error("Global storage path is required to create a checkpoint tracker")
}
@@ -98,9 +102,9 @@ class CheckpointTracker {
console.info(`Creating new CheckpointTracker for task ${taskId}`)
const startTime = performance.now()
// Check if checkpoints are disabled in VS Code settings
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
if (!enableCheckpoints) {
// Check if checkpoints are disabled by setting
if (!enableCheckpointsSetting) {
console.info(`Checkpoints disabled by setting for task ${taskId}`)
return undefined // Don't create tracker when disabled
}
+159 -7
View File
@@ -103,6 +103,7 @@ export class McpHub {
getServers(): McpServer[] {
// Only return enabled servers
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
}
@@ -180,6 +181,120 @@ export class McpHub {
}
}
private async connectToServerRPC(
name: string,
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
): Promise<void> {
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
this.connections = this.connections.filter((conn) => conn.server.name !== name)
try {
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
const client = new Client(
{
name: "Cline",
version: this.clientVersion,
},
{
capabilities: {},
},
)
let transport: StdioClientTransport | SSEClientTransport
if (config.transportType === "sse") {
transport = new SSEClientTransport(new URL(config.url), {})
} else {
transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: {
...config.env,
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
},
stderr: "pipe", // necessary for stderr to be available
})
}
transport.onerror = async (error) => {
console.error(`Transport error for "${name}":`, error)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
this.appendErrorMessage(connection, error.message)
}
}
transport.onclose = async () => {
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
}
}
const connection: McpConnection = {
server: {
name,
config: JSON.stringify(config),
status: "connecting",
disabled: config.disabled,
},
client,
transport,
}
this.connections.push(connection)
if (config.transportType === "stdio") {
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
await transport.start()
const stderrStream = (transport as StdioClientTransport).stderr
if (stderrStream) {
stderrStream.on("data", async (data: Buffer) => {
const output = data.toString()
// Check if output contains INFO level log
const isInfoLog = !/\berror\b/i.test(output)
if (isInfoLog) {
// Log normal informational messages
console.info(`Server "${name}" info:`, output)
} else {
// Treat as error log
console.error(`Server "${name}" stderr:`, output)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
this.appendErrorMessage(connection, output)
}
}
})
} else {
console.error(`No stderr stream for ${name}`)
}
transport.start = async () => {} // No-op now, .connect() won't fail
}
// Connect
await client.connect(transport)
connection.server.status = "connected"
connection.server.error = ""
// Initial fetch of tools and resources
connection.server.tools = await this.fetchToolsList(name)
connection.server.resources = await this.fetchResourcesList(name)
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
} catch (error) {
// Update status with error
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error))
}
throw error
}
}
private async connectToServer(
name: string,
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>,
@@ -494,6 +609,36 @@ export class McpHub {
this.fileWatchers.clear()
}
async restartConnectionRPC(serverName: string): Promise<McpServer[]> {
this.isConnecting = true
// Get existing connection and update its status
const connection = this.connections.find((conn) => conn.server.name === serverName)
const inMemoryConfig = connection?.server.config
if (inMemoryConfig) {
connection.server.status = "connecting"
connection.server.error = ""
await setTimeoutPromise(500) // artificial delay to show user that server is restarting
try {
await this.deleteConnection(serverName)
// Try to connect again using existing config
await this.connectToServerRPC(serverName, JSON.parse(inMemoryConfig))
} catch (error) {
console.error(`Failed to restart connection for ${serverName}:`, error)
}
}
this.isConnecting = false
const config = await this.readAndValidateMcpSettingsFile()
if (!config) {
throw new Error("Failed to read or validate MCP settings")
}
const serverOrder = Object.keys(config.mcpServers || {})
return this.getSortedMcpServers(serverOrder)
}
async restartConnection(serverName: string): Promise<void> {
this.isConnecting = true
@@ -738,7 +883,12 @@ export class McpHub {
}
}
public async deleteServer(serverName: string) {
/**
* RPC variant of deleteServer that returns the updated server list directly
* @param serverName The name of the server to delete
* @returns Array of remaining MCP servers
*/
public async deleteServerRPC(serverName: string): Promise<McpServer[]> {
try {
const settingsPath = await this.getMcpSettingsFilePath()
const content = await fs.readFile(settingsPath, "utf-8")
@@ -746,21 +896,23 @@ export class McpHub {
if (!config.mcpServers || typeof config.mcpServers !== "object") {
config.mcpServers = {}
}
if (config.mcpServers[serverName]) {
delete config.mcpServers[serverName]
const updatedConfig = {
mcpServers: config.mcpServers,
}
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
await this.updateServerConnections(config.mcpServers)
vscode.window.showInformationMessage(`Deleted ${serverName} MCP server`)
await this.updateServerConnectionsRPC(config.mcpServers)
// Get the servers in their correct order from settings
const serverOrder = Object.keys(config.mcpServers || {})
return this.getSortedMcpServers(serverOrder)
} else {
vscode.window.showWarningMessage(`${serverName} not found in MCP configuration`)
throw new Error(`${serverName} not found in MCP configuration`)
}
} catch (error) {
vscode.window.showErrorMessage(
`Failed to delete MCP server: ${error instanceof Error ? error.message : String(error)}`,
)
console.error(`Failed to delete MCP server: ${error instanceof Error ? error.message : String(error)}`)
throw error
}
}
+31 -24
View File
@@ -5,6 +5,7 @@ import { execa } from "execa"
import { Logger } from "@services/logging/Logger"
import { WebviewProvider } from "@core/webview"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { TaskServiceClient } from "webview-ui/src/services/grpc-client"
import {
getWorkspacePath,
validateWorkspacePath,
@@ -15,7 +16,6 @@ import {
import { updateGlobalState, getAllExtensionState, updateApiConfiguration, storeSecret } from "@core/storage/state"
import { ClineAsk, ExtensionMessage } from "@shared/ExtensionMessage"
import { ApiProvider } from "@shared/api"
import { WebviewMessage } from "@shared/WebviewMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { getSavedClineMessages, getSavedApiConversationHistory } from "@core/storage/disk"
@@ -515,8 +515,8 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
const askText = message.partialMessage.text
// Automatically respond to different types of asks
setTimeout(() => {
autoRespondToAsk(webviewProvider, askType, askText)
setTimeout(async () => {
await autoRespondToAsk(webviewProvider, askType, askText)
}, 100) // Small delay to ensure the message is processed first
}
@@ -538,65 +538,64 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
* @param askType The type of ask message
* @param askText The text content of the ask message
*/
function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, askText?: string): void {
async function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, askText?: string): Promise<void> {
if (!webviewProvider.controller) {
return
}
Logger.log(`Auto-responding to ask type: ${askType}`)
// Create a response message based on the ask type
const response: WebviewMessage = {
type: "askResponse",
askResponse: "yesButtonClicked", // Default to approving most actions
}
// Default to approving most actions
let responseType = "yesButtonClicked"
let responseText: string | undefined
let responseImages: string[] | undefined
// Handle specific ask types differently if needed
switch (askType) {
case "followup":
// For follow-up questions, provide a generic response
response.askResponse = "messageResponse"
response.text = "I can't answer any questions right now, use your best judgment."
responseType = "messageResponse"
responseText = "I can't answer any questions right now, use your best judgment."
break
case "api_req_failed":
// Always retry API requests
response.askResponse = "yesButtonClicked" // "Retry" button
responseType = "yesButtonClicked" // "Retry" button
break
case "completion_result":
// Accept the completion
response.askResponse = "messageResponse"
response.text = "Task completed successfully."
responseType = "messageResponse"
responseText = "Task completed successfully."
break
case "mistake_limit_reached":
// Provide guidance to continue
response.askResponse = "messageResponse"
response.text = "Try breaking down the task into smaller steps."
responseType = "messageResponse"
responseText = "Try breaking down the task into smaller steps."
break
case "auto_approval_max_req_reached":
// Reset the count to continue
response.askResponse = "yesButtonClicked" // "Reset and continue" button
responseType = "yesButtonClicked" // "Reset and continue" button
break
case "resume_task":
case "resume_completed_task":
// Resume the task
response.askResponse = "messageResponse"
responseType = "messageResponse"
break
case "new_task":
// Decline creating a new task to keep the current task running
response.askResponse = "messageResponse"
response.text = "Continue with the current task."
responseType = "messageResponse"
responseText = "Continue with the current task."
break
case "plan_mode_respond":
// Respond to plan mode with a message to toggle to Act mode
response.askResponse = "messageResponse"
response.text = "PLAN_MODE_TOGGLE_RESPONSE" // Special marker to toggle to Act mode
responseType = "messageResponse"
responseText = "PLAN_MODE_TOGGLE_RESPONSE" // Special marker to toggle to Act mode
// Automatically toggle to Act mode after responding
setTimeout(async () => {
@@ -616,8 +615,16 @@ function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, a
}
// Send the response message
webviewProvider.controller.handleWebviewMessage(response)
Logger.log(`Auto-responded to ${askType} with ${response.askResponse}`)
try {
await TaskServiceClient.askResponse({
responseType,
text: responseText,
images: responseImages,
})
Logger.log(`Auto-responded to ${askType} with ${responseType}`)
} catch (error) {
Logger.log(`Error sending askResponse: ${error}`)
}
}
/**
+1 -1
View File
@@ -35,5 +35,5 @@ export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
},
maxRequests: 20,
enableNotifications: false,
favorites: ["enableAll", "readFiles", "editFiles"],
favorites: ["enableAutoApprove", "readFiles", "editFiles"],
}
+8
View File
@@ -1,7 +1,15 @@
export type OpenAIReasoningEffort = "low" | "medium" | "high"
export interface ChatSettings {
mode: "plan" | "act"
preferredLanguage?: string
openAIReasoningEffort?: OpenAIReasoningEffort
}
export type PartialChatSettings = Partial<ChatSettings>
export const DEFAULT_CHAT_SETTINGS: ChatSettings = {
mode: "act",
preferredLanguage: "English",
openAIReasoningEffort: "medium",
}
+11 -1
View File
@@ -115,6 +115,7 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
export const DEFAULT_PLATFORM = "unknown"
export interface ExtensionState {
isNewUser: boolean
apiConfiguration?: ApiConfiguration
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
@@ -126,6 +127,7 @@ export interface ExtensionState {
customInstructions?: string
mcpMarketplaceEnabled?: boolean
planActSeparateModelsSetting: boolean
enableCheckpointsSetting?: boolean
platform: Platform
shouldShowAnnouncement: boolean
taskHistory: HistoryItem[]
@@ -141,6 +143,7 @@ export interface ExtensionState {
vscMachineId: string
globalClineRulesToggles: ClineRulesToggles
localClineRulesToggles: ClineRulesToggles
workflowToggles: ClineRulesToggles
localCursorRulesToggles: ClineRulesToggles
localWindsurfRulesToggles: ClineRulesToggles
}
@@ -205,6 +208,7 @@ export type ClineSay =
| "clineignore_error"
| "checkpoint_created"
| "load_mcp_documentation"
| "info" // Added for general informational messages like retry status
export interface ClineSayTool {
tool:
@@ -273,8 +277,14 @@ export interface ClineApiReqInfo {
cost?: number
cancelReason?: ClineApiReqCancelReason
streamingFailedMessage?: string
retryStatus?: {
attempt: number
maxAttempts: number
delaySec: number
errorSnippet?: string
}
}
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
+5 -12
View File
@@ -14,21 +14,15 @@ export interface WebviewMessage {
| "newTask"
| "condense"
| "reportBug"
| "askResponse"
| "didShowAnnouncement"
| "selectImages"
| "resetState"
| "openInBrowser"
| "openMention"
| "showChatView"
| "refreshClineRules"
| "openMcpSettings"
| "restartMcpServer"
| "deleteMcpServer"
| "autoApprovalSettings"
| "browserRelaunchResult"
| "togglePlanActMode"
| "taskCompletionViewChanges"
| "openExtensionSettings"
| "requestVsCodeLmModels"
| "toggleToolAutoApprove"
@@ -42,7 +36,6 @@ export interface WebviewMessage {
| "fetchLatestMcpServersFromHub"
| "telemetrySetting"
| "openSettings"
| "fetchOpenGraphData"
| "invoke"
| "updateSettings"
| "clearAllTaskHistory"
@@ -50,7 +43,6 @@ export interface WebviewMessage {
| "optionsResponse"
| "requestTotalTasksSize"
| "relaunchChromeDebugMode"
| "taskFeedback"
| "scrollToSettings"
| "searchFiles"
| "grpc_request"
@@ -58,6 +50,7 @@ export interface WebviewMessage {
| "toggleClineRule"
| "toggleCursorRule"
| "toggleWindsurfRule"
| "toggleWorkflow"
| "deleteClineRule"
| "copyToClipboard"
| "updateTerminalConnectionTimeout"
@@ -66,7 +59,6 @@ export interface WebviewMessage {
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
apiConfiguration?: ApiConfiguration
images?: string[]
bool?: boolean
@@ -90,10 +82,10 @@ export interface WebviewMessage {
// For openInBrowser
url?: string
planActSeparateModelsSetting?: boolean
enableCheckpointsSetting?: boolean
mcpMarketplaceEnabled?: boolean
telemetrySetting?: TelemetrySetting
customInstructionsSetting?: string
// For task feedback
feedbackType?: TaskFeedbackType
mentionsRequestId?: string
query?: string
// For toggleFavoriteModel
@@ -108,9 +100,10 @@ export interface WebviewMessage {
grpc_request_cancel?: {
request_id: string // ID of the request to cancel
}
// For cline rules
// For cline rules and workflows
isGlobal?: boolean
rulePath?: string
workflowPath?: string
enabled?: boolean
filename?: string
+13 -1
View File
@@ -80,7 +80,6 @@ export interface ApiHandlerOptions {
mistralApiKey?: string
azureApiVersion?: string
vsCodeLmModelSelector?: LanguageModelChatSelector
o3MiniReasoningEffort?: string
qwenApiLine?: string
asksageApiUrl?: string
asksageApiKey?: string
@@ -89,6 +88,7 @@ export interface ApiHandlerOptions {
reasoningEffort?: string
sambanovaApiKey?: string
requestTimeoutMs?: number
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -115,6 +115,7 @@ export interface ModelInfo {
outputPrice?: number // Output price per million tokens when budget > 0
outputPriceTiers?: PriceTier[] // Optional: Tiered output price when budget > 0
}
supportsGlobalEndpoint?: boolean // Whether the model supports a global endpoint with Vertex AI
cacheWritesPrice?: number
cacheReadsPrice?: number
description?: string
@@ -404,6 +405,7 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 0.15,
outputPrice: 0.6,
cacheWritesPrice: 1.0,
@@ -414,6 +416,7 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0.075,
outputPrice: 0.3,
},
@@ -422,6 +425,7 @@ export const vertexModels = {
contextWindow: 32_767,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0,
outputPrice: 0,
},
@@ -430,6 +434,7 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0,
outputPrice: 0,
},
@@ -446,6 +451,7 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
@@ -469,6 +475,7 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0.15,
outputPrice: 0.6,
thinkingConfig: {
@@ -481,6 +488,7 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0,
outputPrice: 0,
},
@@ -550,6 +558,10 @@ export const vertexModels = {
},
} as const satisfies Record<string, ModelInfo>
export const vertexGlobalModels: Record<string, ModelInfo> = Object.fromEntries(
Object.entries(vertexModels).filter(([_k, v]) => v.hasOwnProperty("supportsGlobalEndpoint")),
) as Record<string, ModelInfo>
export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
maxTokens: -1,
contextWindow: 128_000,
@@ -2,22 +2,24 @@ import { RuleFileRequest } from "../../proto/file"
// Helper for creating delete requests
export const DeleteRuleFileRequest = {
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
return RuleFileRequest.create({
rulePath: params.rulePath,
isGlobal: params.isGlobal,
metadata: params.metadata,
type: params.type,
})
},
}
// Helper for creating create requests
export const CreateRuleFileRequest = {
create: (params: { filename: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
create: (params: { filename: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
return RuleFileRequest.create({
filename: params.filename,
isGlobal: params.isGlobal,
metadata: params.metadata,
type: params.type,
})
},
}
@@ -0,0 +1,18 @@
import { OpenGraphData as DomainOpenGraphData } from "@integrations/misc/link-preview"
import { OpenGraphData as ProtoOpenGraphData } from "@shared/proto/web"
/**
* Converts domain OpenGraphData objects to proto OpenGraphData objects
* @param ogData Domain OpenGraphData object
* @returns Proto OpenGraphData object
*/
export function convertDomainOpenGraphDataToProto(ogData: DomainOpenGraphData): ProtoOpenGraphData {
return ProtoOpenGraphData.create({
title: ogData.title || "",
description: ogData.description || "",
image: ogData.image || "",
url: ogData.url || "",
siteName: ogData.siteName || "",
type: ogData.type || "",
})
}
+29 -2
View File
@@ -6,7 +6,7 @@
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { Empty, Metadata, StringRequest } from "./common"
import { Empty, EmptyRequest, Metadata, StringArray, StringRequest } from "./common"
export const protobufPackage = "cline"
@@ -73,6 +73,8 @@ export interface RuleFileRequest {
rulePath?: string | undefined
/** Filename field for createRuleFile (optional) */
filename?: string | undefined
/** Type of the file to create (optional) */
type?: string | undefined
}
/** Result for rule file operations with meaningful data only */
@@ -682,7 +684,7 @@ export const GitCommit: MessageFns<GitCommit> = {
}
function createBaseRuleFileRequest(): RuleFileRequest {
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined }
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined, type: undefined }
}
export const RuleFileRequest: MessageFns<RuleFileRequest> = {
@@ -699,6 +701,9 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
if (message.filename !== undefined) {
writer.uint32(34).string(message.filename)
}
if (message.type !== undefined) {
writer.uint32(42).string(message.type)
}
return writer
},
@@ -741,6 +746,14 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
message.filename = reader.string()
continue
}
case 5: {
if (tag !== 42) {
break
}
message.type = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
@@ -756,6 +769,7 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
isGlobal: isSet(object.isGlobal) ? globalThis.Boolean(object.isGlobal) : false,
rulePath: isSet(object.rulePath) ? globalThis.String(object.rulePath) : undefined,
filename: isSet(object.filename) ? globalThis.String(object.filename) : undefined,
type: isSet(object.type) ? globalThis.String(object.type) : undefined,
}
},
@@ -773,6 +787,9 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
if (message.filename !== undefined) {
obj.filename = message.filename
}
if (message.type !== undefined) {
obj.type = message.type
}
return obj
},
@@ -786,6 +803,7 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
message.isGlobal = object.isGlobal ?? false
message.rulePath = object.rulePath ?? undefined
message.filename = object.filename ?? undefined
message.type = object.type ?? undefined
return message
},
}
@@ -933,6 +951,15 @@ export const FileServiceDefinition = {
responseStream: false,
options: {},
},
/** Select images from the file system and return as data URLs */
selectImages: {
name: "selectImages",
requestType: EmptyRequest,
requestStream: false,
responseType: StringArray,
responseStream: false,
options: {},
},
/** Convert URIs to workspace-relative paths */
getRelativePaths: {
name: "getRelativePaths",
+16
View File
@@ -1012,6 +1012,22 @@ export const McpServiceDefinition = {
responseStream: false,
options: {},
},
restartMcpServer: {
name: "restartMcpServer",
requestType: StringRequest,
requestStream: false,
responseType: McpServers,
responseStream: false,
options: {},
},
deleteMcpServer: {
name: "deleteMcpServer",
requestType: StringRequest,
requestStream: false,
responseType: McpServers,
responseStream: false,
options: {},
},
},
} as const
+8
View File
@@ -101,6 +101,14 @@ export const StateServiceDefinition = {
responseStream: false,
options: {},
},
resetState: {
name: "resetState",
requestType: EmptyRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
},
} as const
+145 -1
View File
@@ -6,7 +6,7 @@
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { Empty, EmptyRequest, Metadata, StringArrayRequest, StringRequest } from "./common"
import { Empty, EmptyRequest, Int64Request, Metadata, StringArrayRequest, StringRequest } from "./common"
export const protobufPackage = "cline"
@@ -73,6 +73,14 @@ export interface TaskItem {
cacheReads: number
}
/** Request for ask response operation */
export interface AskResponseRequest {
metadata?: Metadata | undefined
responseType: string
text: string
images: string[]
}
function createBaseNewTaskRequest(): NewTaskRequest {
return { metadata: undefined, text: "", images: [] }
}
@@ -966,6 +974,115 @@ export const TaskItem: MessageFns<TaskItem> = {
},
}
function createBaseAskResponseRequest(): AskResponseRequest {
return { metadata: undefined, responseType: "", text: "", images: [] }
}
export const AskResponseRequest: MessageFns<AskResponseRequest> = {
encode(message: AskResponseRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.responseType !== "") {
writer.uint32(18).string(message.responseType)
}
if (message.text !== "") {
writer.uint32(26).string(message.text)
}
for (const v of message.images) {
writer.uint32(34).string(v!)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): AskResponseRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseAskResponseRequest()
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.responseType = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.text = reader.string()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.images.push(reader.string())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): AskResponseRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
responseType: isSet(object.responseType) ? globalThis.String(object.responseType) : "",
text: isSet(object.text) ? globalThis.String(object.text) : "",
images: globalThis.Array.isArray(object?.images) ? object.images.map((e: any) => globalThis.String(e)) : [],
}
},
toJSON(message: AskResponseRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.responseType !== "") {
obj.responseType = message.responseType
}
if (message.text !== "") {
obj.text = message.text
}
if (message.images?.length) {
obj.images = message.images
}
return obj
},
create<I extends Exact<DeepPartial<AskResponseRequest>, I>>(base?: I): AskResponseRequest {
return AskResponseRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<AskResponseRequest>, I>>(object: I): AskResponseRequest {
const message = createBaseAskResponseRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.responseType = object.responseType ?? ""
message.text = object.text ?? ""
message.images = object.images?.map((e) => e) || []
return message
},
}
export type TaskServiceDefinition = typeof TaskServiceDefinition
export const TaskServiceDefinition = {
name: "TaskService",
@@ -1052,6 +1169,33 @@ export const TaskServiceDefinition = {
responseStream: false,
options: {},
},
/** Sends a response to a previous ask operation */
askResponse: {
name: "askResponse",
requestType: AskResponseRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
/** Records task feedback (thumbs up/down) */
taskFeedback: {
name: "taskFeedback",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
/** Shows task completion changes diff in a view */
taskCompletionViewChanges: {
name: "taskCompletionViewChanges",
requestType: Int64Request,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
},
} as const
+157
View File
@@ -15,6 +15,15 @@ export interface IsImageUrl {
url: string
}
export interface OpenGraphData {
title: string
description: string
image: string
url: string
siteName: string
type: string
}
function createBaseIsImageUrl(): IsImageUrl {
return { isImage: false, url: "" }
}
@@ -91,6 +100,146 @@ export const IsImageUrl: MessageFns<IsImageUrl> = {
},
}
function createBaseOpenGraphData(): OpenGraphData {
return { title: "", description: "", image: "", url: "", siteName: "", type: "" }
}
export const OpenGraphData: MessageFns<OpenGraphData> = {
encode(message: OpenGraphData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.title !== "") {
writer.uint32(10).string(message.title)
}
if (message.description !== "") {
writer.uint32(18).string(message.description)
}
if (message.image !== "") {
writer.uint32(26).string(message.image)
}
if (message.url !== "") {
writer.uint32(34).string(message.url)
}
if (message.siteName !== "") {
writer.uint32(42).string(message.siteName)
}
if (message.type !== "") {
writer.uint32(50).string(message.type)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenGraphData {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseOpenGraphData()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.title = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.description = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.image = reader.string()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.url = reader.string()
continue
}
case 5: {
if (tag !== 42) {
break
}
message.siteName = reader.string()
continue
}
case 6: {
if (tag !== 50) {
break
}
message.type = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): OpenGraphData {
return {
title: isSet(object.title) ? globalThis.String(object.title) : "",
description: isSet(object.description) ? globalThis.String(object.description) : "",
image: isSet(object.image) ? globalThis.String(object.image) : "",
url: isSet(object.url) ? globalThis.String(object.url) : "",
siteName: isSet(object.siteName) ? globalThis.String(object.siteName) : "",
type: isSet(object.type) ? globalThis.String(object.type) : "",
}
},
toJSON(message: OpenGraphData): unknown {
const obj: any = {}
if (message.title !== "") {
obj.title = message.title
}
if (message.description !== "") {
obj.description = message.description
}
if (message.image !== "") {
obj.image = message.image
}
if (message.url !== "") {
obj.url = message.url
}
if (message.siteName !== "") {
obj.siteName = message.siteName
}
if (message.type !== "") {
obj.type = message.type
}
return obj
},
create<I extends Exact<DeepPartial<OpenGraphData>, I>>(base?: I): OpenGraphData {
return OpenGraphData.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenGraphData>, I>>(object: I): OpenGraphData {
const message = createBaseOpenGraphData()
message.title = object.title ?? ""
message.description = object.description ?? ""
message.image = object.image ?? ""
message.url = object.url ?? ""
message.siteName = object.siteName ?? ""
message.type = object.type ?? ""
return message
},
}
export type WebServiceDefinition = typeof WebServiceDefinition
export const WebServiceDefinition = {
name: "WebService",
@@ -104,6 +253,14 @@ export const WebServiceDefinition = {
responseStream: false,
options: {},
},
fetchOpenGraphData: {
name: "fetchOpenGraphData",
requestType: StringRequest,
requestStream: false,
responseType: OpenGraphData,
responseStream: false,
options: {},
},
},
} as const
+38
View File
@@ -0,0 +1,38 @@
import * as grpc from "@grpc/grpc-js"
import { Controller } from "../core/controller"
/**
* Type definition for a gRPC handler function.
* This represents a function that takes a Controller instance and a request object,
* and returns a Promise of the response type.
*
* @template TRequest - The type of the request object
* @template TResponse - The type of the response object
*/
export type GrpcHandler<TRequest, TResponse> = (controller: Controller, req: TRequest) => Promise<TResponse>
export type GrpcStreamingResponseHandler<TRequest, TResponse> = (
controller: Controller,
req: TRequest,
streamResponseHandler: StreamingResponseWriter<TResponse>,
requestId?: string,
) => Promise<TResponse>
/**
* Type definition for the wrapper function that converts a Promise-based handler
* to a gRPC callback-style handler.
*
* @template TRequest - The type of the request object
* @template TResponse - The type of the response object
*/
export type GrpcHandlerWrapper = <TRequest, TResponse>(
handler: GrpcHandler<TRequest, TResponse>,
controller: Controller,
) => grpc.handleUnaryCall<TRequest, TResponse>
export type GrpcStreamingResponseHandlerWrapper = <TRequest, TResponse>(
handler: GrpcStreamingResponseHandler<TRequest, TResponse>,
controller: Controller,
) => grpc.handleServerStreamingCall<TRequest, TResponse>
export type StreamingResponseWriter<TResponse> = (response: TResponse, isLast?: boolean, sequenceNumber?: number) => Promise<void>
+169
View File
@@ -0,0 +1,169 @@
// GENERATED CODE -- DO NOT EDIT!
// Generated by generate-server-setup.mjs
import * as grpc from "@grpc/grpc-js"
import { Controller } from "../core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
// Account Service
import { accountLoginClicked } from "../core/controller/account/accountLoginClicked"
// Browser Service
import { getBrowserConnectionInfo } from "../core/controller/browser/getBrowserConnectionInfo"
import { testBrowserConnection } from "../core/controller/browser/testBrowserConnection"
import { discoverBrowser } from "../core/controller/browser/discoverBrowser"
import { getDetectedChromePath } from "../core/controller/browser/getDetectedChromePath"
import { updateBrowserSettings } from "../core/controller/browser/updateBrowserSettings"
// Checkpoints Service
import { checkpointDiff } from "../core/controller/checkpoints/checkpointDiff"
import { checkpointRestore } from "../core/controller/checkpoints/checkpointRestore"
// File Service
import { openFile } from "../core/controller/file/openFile"
import { openImage } from "../core/controller/file/openImage"
import { deleteRuleFile } from "../core/controller/file/deleteRuleFile"
import { createRuleFile } from "../core/controller/file/createRuleFile"
import { searchCommits } from "../core/controller/file/searchCommits"
import { selectImages } from "../core/controller/file/selectImages"
import { getRelativePaths } from "../core/controller/file/getRelativePaths"
import { searchFiles } from "../core/controller/file/searchFiles"
// Mcp Service
import { toggleMcpServer } from "../core/controller/mcp/toggleMcpServer"
import { updateMcpTimeout } from "../core/controller/mcp/updateMcpTimeout"
import { addRemoteMcpServer } from "../core/controller/mcp/addRemoteMcpServer"
import { downloadMcp } from "../core/controller/mcp/downloadMcp"
import { restartMcpServer } from "../core/controller/mcp/restartMcpServer"
import { deleteMcpServer } from "../core/controller/mcp/deleteMcpServer"
// Models Service
import { getOllamaModels } from "../core/controller/models/getOllamaModels"
import { getLmStudioModels } from "../core/controller/models/getLmStudioModels"
import { getVsCodeLmModels } from "../core/controller/models/getVsCodeLmModels"
import { refreshOpenRouterModels } from "../core/controller/models/refreshOpenRouterModels"
import { refreshOpenAiModels } from "../core/controller/models/refreshOpenAiModels"
import { refreshRequestyModels } from "../core/controller/models/refreshRequestyModels"
// Slash Service
import { reportBug } from "../core/controller/slash/reportBug"
import { condense } from "../core/controller/slash/condense"
// State Service
import { getLatestState } from "../core/controller/state/getLatestState"
import { subscribeToState } from "../core/controller/state/subscribeToState"
import { toggleFavoriteModel } from "../core/controller/state/toggleFavoriteModel"
import { resetState } from "../core/controller/state/resetState"
// Task Service
import { cancelTask } from "../core/controller/task/cancelTask"
import { clearTask } from "../core/controller/task/clearTask"
import { deleteTasksWithIds } from "../core/controller/task/deleteTasksWithIds"
import { newTask } from "../core/controller/task/newTask"
import { showTaskWithId } from "../core/controller/task/showTaskWithId"
import { exportTaskWithId } from "../core/controller/task/exportTaskWithId"
import { toggleTaskFavorite } from "../core/controller/task/toggleTaskFavorite"
import { deleteNonFavoritedTasks } from "../core/controller/task/deleteNonFavoritedTasks"
import { getTaskHistory } from "../core/controller/task/getTaskHistory"
import { askResponse } from "../core/controller/task/askResponse"
import { taskFeedback } from "../core/controller/task/taskFeedback"
import { taskCompletionViewChanges } from "../core/controller/task/taskCompletionViewChanges"
// Web Service
import { checkIsImageUrl } from "../core/controller/web/checkIsImageUrl"
import { fetchOpenGraphData } from "../core/controller/web/fetchOpenGraphData"
export function addServices(
server: grpc.Server,
proto: any,
controller: Controller,
wrapper: GrpcHandlerWrapper,
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
): void {
// Account Service
server.addService(proto.cline.AccountService.service, {
accountLoginClicked: wrapper(accountLoginClicked, controller),
})
// Browser Service
server.addService(proto.cline.BrowserService.service, {
getBrowserConnectionInfo: wrapper(getBrowserConnectionInfo, controller),
testBrowserConnection: wrapper(testBrowserConnection, controller),
discoverBrowser: wrapper(discoverBrowser, controller),
getDetectedChromePath: wrapper(getDetectedChromePath, controller),
updateBrowserSettings: wrapper(updateBrowserSettings, controller),
})
// Checkpoints Service
server.addService(proto.cline.CheckpointsService.service, {
checkpointDiff: wrapper(checkpointDiff, controller),
checkpointRestore: wrapper(checkpointRestore, controller),
})
// File Service
server.addService(proto.cline.FileService.service, {
openFile: wrapper(openFile, controller),
openImage: wrapper(openImage, controller),
deleteRuleFile: wrapper(deleteRuleFile, controller),
createRuleFile: wrapper(createRuleFile, controller),
searchCommits: wrapper(searchCommits, controller),
selectImages: wrapper(selectImages, controller),
getRelativePaths: wrapper(getRelativePaths, controller),
searchFiles: wrapper(searchFiles, controller),
})
// Mcp Service
server.addService(proto.cline.McpService.service, {
toggleMcpServer: wrapper(toggleMcpServer, controller),
updateMcpTimeout: wrapper(updateMcpTimeout, controller),
addRemoteMcpServer: wrapper(addRemoteMcpServer, controller),
downloadMcp: wrapper(downloadMcp, controller),
restartMcpServer: wrapper(restartMcpServer, controller),
deleteMcpServer: wrapper(deleteMcpServer, controller),
})
// Models Service
server.addService(proto.cline.ModelsService.service, {
getOllamaModels: wrapper(getOllamaModels, controller),
getLmStudioModels: wrapper(getLmStudioModels, controller),
getVsCodeLmModels: wrapper(getVsCodeLmModels, controller),
refreshOpenRouterModels: wrapper(refreshOpenRouterModels, controller),
refreshOpenAiModels: wrapper(refreshOpenAiModels, controller),
refreshRequestyModels: wrapper(refreshRequestyModels, controller),
})
// Slash Service
server.addService(proto.cline.SlashService.service, {
reportBug: wrapper(reportBug, controller),
condense: wrapper(condense, controller),
})
// State Service
server.addService(proto.cline.StateService.service, {
getLatestState: wrapper(getLatestState, controller),
subscribeToState: wrapStreamingResponse(subscribeToState, controller),
toggleFavoriteModel: wrapper(toggleFavoriteModel, controller),
resetState: wrapper(resetState, controller),
})
// Task Service
server.addService(proto.cline.TaskService.service, {
cancelTask: wrapper(cancelTask, controller),
clearTask: wrapper(clearTask, controller),
deleteTasksWithIds: wrapper(deleteTasksWithIds, controller),
newTask: wrapper(newTask, controller),
showTaskWithId: wrapper(showTaskWithId, controller),
exportTaskWithId: wrapper(exportTaskWithId, controller),
toggleTaskFavorite: wrapper(toggleTaskFavorite, controller),
deleteNonFavoritedTasks: wrapper(deleteNonFavoritedTasks, controller),
getTaskHistory: wrapper(getTaskHistory, controller),
askResponse: wrapper(askResponse, controller),
taskFeedback: wrapper(taskFeedback, controller),
taskCompletionViewChanges: wrapper(taskCompletionViewChanges, controller),
})
// Web Service
server.addService(proto.cline.WebService.service, {
checkIsImageUrl: wrapper(checkIsImageUrl, controller),
fetchOpenGraphData: wrapper(fetchOpenGraphData, controller),
})
}
+111
View File
@@ -0,0 +1,111 @@
import * as grpc from "@grpc/grpc-js"
import { ReflectionService } from "@grpc/reflection"
import * as health from "grpc-health-check"
import { activate } from "../extension"
import { Controller } from "../core/controller"
import { extensionContext, outputChannel, postMessage } from "./vscode-context"
import { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase } from "./utils"
import { GrpcHandler, GrpcStreamingResponseHandler } from "./grpc-types"
import { addServices } from "./server-setup"
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
function main() {
log("Starting service...")
activate(extensionContext)
const controller = new Controller(extensionContext, outputChannel, postMessage)
const server = new grpc.Server()
// Set up health check.
const healthImpl = new health.HealthImplementation({ "": "SERVING" })
healthImpl.addToServer(server)
// Add all the handlers for the ProtoBus services to the server.
addServices(server, proto, controller, wrapHandler, wrapStreamingResponseHandler)
// Set up reflection.
const reflection = new ReflectionService(packageDefinition)
reflection.addToServer(server)
// Start the server.
const host = "127.0.0.1:50051"
server.bindAsync(host, grpc.ServerCredentials.createInsecure(), (err) => {
if (err) {
log(`Error: Failed to bind to ${host}, port may be unavailable ${err.message}`)
process.exit(1)
} else {
server.start()
log(`gRPC server listening on ${host}`)
}
})
}
/**
* Wraps a Promise-based handler function to make it compatible with gRPC's callback-based API.
* This function converts an async handler that returns a Promise into a function that uses
* the gRPC callback pattern.
*
* @template TRequest - The type of the request object
* @template TResponse - The type of the response object
* @param handler - The Promise-based handler function to wrap
* @param controllerInstance - The controller instance to pass to the handler
* @returns A gRPC-compatible callback-style handler function
*/
function wrapHandler<TRequest, TResponse>(
handler: GrpcHandler<TRequest, TResponse>,
controller: Controller,
): grpc.handleUnaryCall<TRequest, TResponse> {
return async (call: grpc.ServerUnaryCall<TRequest, TResponse>, callback: grpc.sendUnaryData<TResponse>) => {
try {
log(`gRPC request: ${call.getPath()}`)
const result = await handler(controller, snakeToCamelCase(call.request))
// The grpc-js serializer expects the proto message to be in the same
// case as the proto file. This is a work around until we find a solution.
callback(null, camelToSnakeCase(result))
} catch (err: any) {
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
callback({
code: grpc.status.INTERNAL,
message: err.message || "Internal error",
} as grpc.ServiceError)
}
}
}
function wrapStreamingResponseHandler<TRequest, TResponse>(
handler: GrpcStreamingResponseHandler<TRequest, TResponse>,
controller: Controller,
): grpc.handleServerStreamingCall<TRequest, TResponse> {
return async (call: grpc.ServerWritableStream<TRequest, TResponse>) => {
try {
const requestId = call.metadata.get("request-id").pop()?.toString()
log(`gRPC streaming request: ${call.getPath()}`)
const responseHandler: StreamingResponseHandler = (response, isLast, sequenceNumber) => {
try {
// The grpc-js serializer expects the proto message to be in the same
// case as the proto file. This is a work around until we find a solution.
call.write(camelToSnakeCase(response)) // Use a bound version of call.write to maintain proper 'this' context
if (isLast === true) {
log(`Closing stream for ${requestId}`)
call.end()
}
return Promise.resolve()
} catch (error) {
return Promise.reject(error)
}
}
await handler(controller, snakeToCamelCase(call.request), responseHandler, requestId)
} catch (err: any) {
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
call.destroy({
code: grpc.status.INTERNAL,
message: err.message || "Internal error",
} as grpc.ServiceError)
}
}
}
main()
+65
View File
@@ -0,0 +1,65 @@
import * as fs from "fs"
import * as grpc from "@grpc/grpc-js"
import * as protoLoader from "@grpc/proto-loader"
import * as health from "grpc-health-check"
const log = (...args: unknown[]) => {
const timestamp = new Date().toISOString()
console.log(`[${timestamp}]`, "#bot.cline.server.ts", ...args)
}
// Load service definitions.
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet)
const healthDef = protoLoader.loadSync(health.protoPath)
const packageDefinition = { ...clineDef, ...healthDef }
const proto = grpc.loadPackageDefinition(packageDefinition) as unknown
// Helper function to convert camelCase to snake_case
function camelToSnakeCase(obj: any): any {
if (obj === null || typeof obj !== "object") {
return obj
}
if (Array.isArray(obj)) {
return obj.map(camelToSnakeCase)
}
return Object.keys(obj).reduce((acc: any, key: string) => {
// Convert key from camelCase to snake_case
const snakeKey = key
.replace(/([A-Z])/g, "_$1")
.replace(/^_+/, "")
.toLowerCase()
// Convert value recursively if it's an object
const value = obj[key]
acc[snakeKey] = camelToSnakeCase(value)
return acc
}, {})
}
// Helper function to convert snake_case to camelCase
function snakeToCamelCase(obj: any): any {
if (obj === null || typeof obj !== "object") {
return obj
}
if (Array.isArray(obj)) {
return obj.map(snakeToCamelCase)
}
return Object.keys(obj).reduce((acc: any, key: string) => {
// Convert key from snake_case to camelCase
const camelKey = key.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase())
// Convert value recursively if it's an object
const value = obj[key]
acc[camelKey] = snakeToCamelCase(value)
return acc
}, {})
}
export { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase }
+164
View File
@@ -0,0 +1,164 @@
// @ts-nocheck
import * as vscode from "vscode"
import open from "open"
import { log } from "./utils"
function stubUri(path: string): vscode.Uri {
console.log(`Using file path: ${path}`)
return {
fsPath: path,
scheme: "",
authority: "",
path: "",
query: "",
fragment: "",
with: function (change: {
scheme?: string
authority?: string
path?: string
query?: string
fragment?: string
}): vscode.Uri {
return stubUri(path)
},
toString: function (skipEncoding?: boolean): string {
return path
},
toJSON: function () {
return {}
},
}
}
function createMemento(): vscode.Memento {
const store = {}
return {
keys: function (): readonly string[] {
return Object.keys(store)
},
get: function <T>(key: string): T | undefined {
return key in store ? store[key] : undefined
},
update: function (key: string, value: any): Thenable<void> {
store[key] = value
return Promise.resolve()
},
}
}
const extensionContext: vscode.ExtensionContext = {
extensionPath: "/tmp/vscode/extension",
extensionUri: stubUri("/tmp/vscode/extension"),
globalStoragePath: "/tmp/vscode/global",
globalStorageUri: stubUri("/tmp/vscode/global"),
storagePath: "/tmp/vscode/storage",
storageUri: stubUri("/tmp/vscode/storage"),
logPath: "/tmp/vscode/log",
logUri: stubUri("/tmp/vscode/log"),
globalState: createMemento(),
workspaceState: createMemento(),
storageState: createMemento(),
environmentVariableCollection: {
getScoped: function (scope: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableCollection {
return {
persistent: false,
description: undefined,
replace: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
append: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
prepend: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
get: function (variable: string): vscode.EnvironmentVariableMutator | undefined {
return undefined
},
forEach: function (
callback: (
variable: string,
mutator: vscode.EnvironmentVariableMutator,
collection: vscode.EnvironmentVariableCollection,
) => any,
thisArg?: any,
): void {},
delete: function (variable: string): void {},
clear: function (): void {},
[Symbol.iterator]: function (): Iterator<
[variable: string, mutator: vscode.EnvironmentVariableMutator],
any,
any
> {
throw new Error("environmentVariableCollection.getScoped.Iterator not implemented")
},
}
},
persistent: false,
description: undefined,
replace: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
append: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
prepend: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
get: function (variable: string): vscode.EnvironmentVariableMutator | undefined {
return undefined
},
forEach: function (
callback: (
variable: string,
mutator: vscode.EnvironmentVariableMutator,
collection: vscode.EnvironmentVariableCollection,
) => any,
thisArg?: any,
): void {
throw new Error("environmentVariableCollection.forEach not implemented")
},
delete: function (variable: string): void {},
clear: function (): void {},
[Symbol.iterator]: function (): Iterator<[variable: string, mutator: vscode.EnvironmentVariableMutator], any, any> {
throw new Error("environmentVariableCollection.Iterator not implemented")
},
},
extensionMode: 1, // Development
extension: {
id: "your.extension.id",
isActive: true,
extensionPath: "/tmp/vscode/extension",
extensionUri: stubUri("/tmp/vscode/extension"),
packageJSON: {},
exports: {},
activate: async () => {},
extensionKind: vscode.ExtensionKind.UI,
},
subscriptions: [],
asAbsolutePath: (relPath) => `/tmp/vscode/extension/${relPath}`,
secrets: {
store: async () => {},
get: async () => undefined,
delete: async () => {},
onDidChange: {},
},
}
const outputChannel: vscode.OutputChannel = {
append: (text) => process.stdout.write(text),
appendLine: (line) => console.log(line),
clear: () => {},
show: () => {},
hide: () => {},
dispose: () => {},
name: "",
replace: function (value: string): void {},
}
function postMessage(message: ExtensionMessage): Promise<boolean> {
log("postMessage called:", message)
return Promise.resolve(true)
}
console.log("Finished loading vscode context...")
export { extensionContext, outputChannel, postMessage }
+23 -6
View File
@@ -37,7 +37,18 @@ describe("Chat Integration Tests", () => {
break;
case 'invoke':
if (message.invoke === 'primaryButtonClick') {
vscode.postMessage({ type: 'askResponse', askResponse: 'yesButtonClicked' });
vscode.postMessage({
type: 'grpc_request',
grpc_request: {
service: 'cline.TaskService',
method: 'askResponse',
message: {
responseType: 'yesButtonClicked'
},
request_id: 'test-request-id',
is_streaming: false
}
});
}
break;
}
@@ -117,10 +128,14 @@ describe("Chat Integration Tests", () => {
})
it("should handle tool approval flow", async () => {
// Set up approval listener
// Set up approval listener for gRPC request
const approvalPromise = new Promise<any>((resolve) => {
panel.webview.onDidReceiveMessage((message) => {
if (message.type === "askResponse") {
if (
message.type === "grpc_request" &&
message.grpc_request?.service === "cline.TaskService" &&
message.grpc_request?.method === "askResponse"
) {
resolve(message)
}
})
@@ -132,9 +147,11 @@ describe("Chat Integration Tests", () => {
invoke: "primaryButtonClick",
})
// Verify approval was sent
// Verify gRPC request was sent with correct parameters
const response = await approvalPromise
assert.equal(response.type, "askResponse")
assert.equal(response.askResponse, "yesButtonClicked")
assert.equal(response.type, "grpc_request")
assert.equal(response.grpc_request.service, "cline.TaskService")
assert.equal(response.grpc_request.method, "askResponse")
assert.equal(response.grpc_request.message.responseType, "yesButtonClicked")
})
})
+169 -1
View File
@@ -3,7 +3,7 @@ import { after, describe, it } from "mocha"
import * as os from "os"
import * as path from "path"
import "should"
import { createDirectoriesForFile, fileExistsAtPath, isDirectory } from "./fs"
import { createDirectoriesForFile, fileExistsAtPath, isDirectory, readDirectory } from "./fs"
describe("Filesystem Utilities", () => {
const tmpDir = path.join(os.tmpdir(), "cline-test-" + Math.random().toString(36).slice(2))
@@ -88,4 +88,172 @@ describe("Filesystem Utilities", () => {
isDir.should.be.false()
})
})
describe("readDirectory", () => {
it("should list files in a directory", async () => {
// Create test directory with files
const testDir = path.join(tmpDir, "read-test")
await fs.mkdir(testDir, { recursive: true })
await fs.writeFile(path.join(testDir, "file1.txt"), "content")
await fs.writeFile(path.join(testDir, "file2.txt"), "content")
// Get files
const files = await readDirectory(testDir)
files.length.should.equal(2)
files.should.containDeep([path.resolve(testDir, "file1.txt"), path.resolve(testDir, "file2.txt")])
})
it("should exclude specified directories", async () => {
// Create test directory with files and an excluded directory
const testDir = path.join(tmpDir, "exclude-test")
const excludeDir = path.join(testDir, "exclude-me")
await fs.mkdir(excludeDir, { recursive: true })
await fs.writeFile(path.join(testDir, "include.txt"), "content")
await fs.writeFile(path.join(excludeDir, "excluded.txt"), "content")
// Get files, excluding the "exclude-me" directory
const files = await readDirectory(testDir, [["exclude-me"]])
files.length.should.equal(1)
files.should.containDeep([path.resolve(testDir, "include.txt")])
files.should.not.containDeep([path.resolve(excludeDir, "excluded.txt")])
})
})
it("should correctly handle complex nested directory structures", async () => {
// Create a complex directory structure
const complexDir = path.join(tmpDir, "complex-test")
// Create main dir
await fs.mkdir(complexDir, { recursive: true })
await fs.writeFile(path.join(complexDir, "root.txt"), "content")
// Create first branch
await fs.mkdir(path.join(complexDir, "dir1"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir1", "file1.txt"), "content")
// Create second branch with nested structure
await fs.mkdir(path.join(complexDir, "dir2", "subdir1"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir2", "file2.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir2", "subdir1", "file3.txt"), "content")
// Create third branch with deep nesting
await fs.mkdir(path.join(complexDir, "dir3", "subdir2", "deepdir"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir3", "file4.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "file5.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"), "content")
// Get all files
const files = await readDirectory(complexDir)
const expectedFiles = [
path.resolve(complexDir, "root.txt"),
path.resolve(complexDir, "dir1", "file1.txt"),
path.resolve(complexDir, "dir2", "file2.txt"),
path.resolve(complexDir, "dir2", "subdir1", "file3.txt"),
path.resolve(complexDir, "dir3", "file4.txt"),
path.resolve(complexDir, "dir3", "subdir2", "file5.txt"),
path.resolve(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"),
]
files.length.should.equal(expectedFiles.length)
files.sort().should.deepEqual(expectedFiles.sort())
})
it("should correctly exclude multiple directories in complex structures", async () => {
// Use the same complex directory structure
const complexDir = path.join(tmpDir, "complex-exclude-test")
// Create main dir
await fs.mkdir(complexDir, { recursive: true })
await fs.writeFile(path.join(complexDir, "root.txt"), "content")
// Create first branch
await fs.mkdir(path.join(complexDir, "dir1"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir1", "file1.txt"), "content")
// Create second branch with nested structure
await fs.mkdir(path.join(complexDir, "dir2", "subdir1"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir2", "file2.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir2", "subdir1", "file3.txt"), "content")
// Create third branch with deep nesting
await fs.mkdir(path.join(complexDir, "dir3", "subdir2", "deepdir"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir3", "file4.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "file5.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"), "content")
// Get files excluding multiple directories
const files = await readDirectory(complexDir, [["dir1"], ["subdir2"]])
const expectedFiles = [
path.resolve(complexDir, "root.txt"),
path.resolve(complexDir, "dir2", "file2.txt"),
path.resolve(complexDir, "dir2", "subdir1", "file3.txt"),
path.resolve(complexDir, "dir3", "file4.txt"),
]
files.length.should.equal(expectedFiles.length)
files.sort().should.deepEqual(expectedFiles.sort())
})
it("should exclude .clinerules/workflows directory specifically", async () => {
// Create a test directory structure
const clinerulesDirTest = path.join(tmpDir, "clinerules-test")
const clinerulesDirPath = path.join(clinerulesDirTest, ".clinerules")
// Create .clinerules directory and root files
await fs.mkdir(clinerulesDirPath, { recursive: true })
await fs.writeFile(path.join(clinerulesDirPath, "config.json"), "{}")
await fs.writeFile(path.join(clinerulesDirPath, "settings.js"), "// settings")
// Create .clinerules/other directory and files
const otherDirPath = path.join(clinerulesDirPath, "other")
await fs.mkdir(otherDirPath, { recursive: true })
await fs.writeFile(path.join(otherDirPath, "helper.js"), "// helper code")
await fs.writeFile(path.join(otherDirPath, "util.js"), "// util functions")
// Create .clinerules/workflows directory and files
const workflowsDirPath = path.join(clinerulesDirPath, "workflows")
await fs.mkdir(workflowsDirPath, { recursive: true })
await fs.writeFile(path.join(workflowsDirPath, "workflow1.js"), "// workflow1")
await fs.writeFile(path.join(workflowsDirPath, "workflow2.js"), "// workflow2")
// Get all files WITHOUT exclusion
const allFiles = await readDirectory(clinerulesDirPath)
// Verify all files are included
allFiles.length.should.equal(6) // 2 in root + 2 in other + 2 in workflows
allFiles.some((file) => file.includes("workflow1.js")).should.be.true()
allFiles.some((file) => file.includes("workflow2.js")).should.be.true()
// Get files WITH workflows directory excluded
const filteredFiles = await readDirectory(clinerulesDirPath, [[".clinerules", "workflows"]])
// Verify workflows files are excluded but others remain
filteredFiles.length.should.equal(4) // 2 in root + 2 in other
const expectedFiles = [
path.resolve(clinerulesDirPath, "config.json"),
path.resolve(clinerulesDirPath, "settings.js"),
path.resolve(otherDirPath, "helper.js"),
path.resolve(otherDirPath, "util.js"),
]
filteredFiles.sort().should.deepEqual(expectedFiles.sort())
// Test with multiple exclusions
const multiExcludeFiles = await readDirectory(clinerulesDirPath, [
[".clinerules", "workflows"],
[".clinerules", "other"],
])
// Verify both workflows and other directories are excluded
multiExcludeFiles.length.should.equal(2) // only the 2 files in root
const rootOnlyFiles = [path.resolve(clinerulesDirPath, "config.json"), path.resolve(clinerulesDirPath, "settings.js")]
multiExcludeFiles.sort().should.deepEqual(rootOnlyFiles.sort())
})
})
+19 -1
View File
@@ -86,16 +86,34 @@ const OS_GENERATED_FILES = [
* Recursively reads a directory and returns an array of absolute file paths.
*
* @param directoryPath - The path to the directory to read.
* @param excludedPaths - Nested array of paths to ignore.
* @returns A promise that resolves to an array of absolute file paths.
* @throws Error if the directory cannot be read.
*/
export const readDirectory = async (directoryPath: string) => {
export const readDirectory = async (directoryPath: string, excludedPaths: string[][] = []) => {
try {
const filePaths = await fs
.readdir(directoryPath, { withFileTypes: true, recursive: true })
.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)))
.then((filePaths) =>
filePaths.filter((filePath) => {
if (excludedPaths.length === 0) {
return true
}
for (const excludedPathList of excludedPaths) {
const pathToSearchFor = path.sep + excludedPathList.join(path.sep) + path.sep
if (filePath.includes(pathToSearchFor)) {
return false
}
}
return true
}),
)
return filePaths
} catch {
throw new Error(`Error reading directory at ${directoryPath}`)
+514
View File
@@ -0,0 +1,514 @@
{
"name": "Cline standalone",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Cline standalone",
"version": "1.0.0",
"dependencies": {
"@grpc/grpc-js": "^1.13.3",
"@grpc/reflection": "^1.0.4",
"grpc-health-check": "^2.0.2",
"open": "^10.1.2",
"vscode": "file:./vscode"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
"integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
},
"engines": {
"node": ">=12.10.0"
}
},
"node_modules/@grpc/proto-loader": {
"version": "0.7.15",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.2.5",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@grpc/reflection": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@grpc/reflection/-/reflection-1.0.4.tgz",
"integrity": "sha512-znA8v4AviOD3OPOxy11pxrtP8k8DanpefeTymS8iGW1fVr1U2cHuzfhYqDPHnVNDf4qvF9E25KtSihPy2DBWfQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"protobufjs": "^7.2.5"
},
"peerDependencies": {
"@grpc/grpc-js": "^1.8.21"
}
},
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@types/node": {
"version": "22.15.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/bundle-name": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
"license": "MIT",
"dependencies": {
"run-applescript": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/default-browser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
"integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
"license": "MIT",
"dependencies": {
"bundle-name": "^4.1.0",
"default-browser-id": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/default-browser-id": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/grpc-health-check": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/grpc-health-check/-/grpc-health-check-2.0.2.tgz",
"integrity": "sha512-5XKOdg/gIlTsZZR+8QJjzmW2CHBnn+NfM8zevIpzg+96i9dAuoH+Guu/L/5vuUkSbVZG69wVPnLpEnW+Smon1A==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13"
}
},
"node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
"license": "MIT",
"dependencies": {
"is-docker": "^3.0.0"
},
"bin": {
"is-inside-container": "cli.js"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-wsl": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
"integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
"license": "MIT",
"dependencies": {
"is-inside-container": "^1.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/open": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",
"integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==",
"license": "MIT",
"dependencies": {
"default-browser": "^5.2.1",
"define-lazy-prop": "^3.0.0",
"is-inside-container": "^1.0.0",
"is-wsl": "^3.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/protobufjs": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz",
"integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/run-applescript": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
"integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/vscode": {
"resolved": "vscode",
"link": true
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"vscode": {
"version": "1.0.0"
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "Cline standalone",
"version": "1.0.0",
"main": "standalone.js",
"dependencies": {
"@grpc/grpc-js": "^1.13.3",
"@grpc/reflection": "^1.0.4",
"grpc-health-check": "^2.0.2",
"open": "^10.1.2"
}
}
+7
View File
@@ -0,0 +1,7 @@
const stubs = require("./vscode-stubs.js")
const impls = require("./vscode-impls.js")
module.exports = {
...stubs,
...impls,
}
@@ -0,0 +1,5 @@
{
"name": "vscode",
"version": "1.0.0",
"main": "index.js"
}
@@ -0,0 +1,19 @@
function createStub(path) {
return new Proxy(function () {}, {
get: (target, prop) => {
const fullPath = `${path}.${String(prop)}`
console.log(`Accessed stub: ${fullPath}`)
return createStub(fullPath)
},
apply: (target, thisArg, args) => {
console.log(`Called stub: ${path} with args:`, args)
return createStub(path)
},
construct: (target, args) => {
console.log(`Constructed stub: ${path} with args:`, args)
return createStub(path)
},
})
}
module.exports = { createStub }
@@ -0,0 +1,149 @@
console.log("Loading stub impls...")
const { createStub } = require("./stub-utils")
const open = require("open").default
vscode.window = {
showInformationMessage: (...args) => {
console.log("Stubbed showInformationMessage:", ...args)
return Promise.resolve(undefined)
},
showWarningMessage: (...args) => {
console.log("Stubbed showWarningMessage:", ...args)
return Promise.resolve(undefined)
},
showErrorMessage: (...args) => {
console.log("Stubbed showErrorMessage:", ...args)
return Promise.resolve(undefined)
},
showInputBox: async (options) => {
console.log("Stubbed showInputBox:", options)
return ""
},
showOpenDialog: async (options) => {
console.log("Stubbed showOpenDialog:", options)
return []
},
showSaveDialog: async (options) => {
console.log("Stubbed showSaveDialog:", options)
return undefined
},
showTextDocument: async (...args) => {
console.log("Stubbed showTextDocument:", ...args)
return {}
},
createOutputChannel: (name) => {
console.log("Stubbed createOutputChannel:", name)
return {
appendLine: console.log,
show: () => {},
dispose: () => {},
}
},
createTerminal: (...args) => {
console.log("Stubbed createTerminal:", ...args)
return {
sendText: console.log,
show: () => {},
dispose: () => {},
}
},
activeTextEditor: undefined,
visibleTextEditors: [],
tabGroups: {
all: [],
close: async () => {},
},
withProgress: async (_options, task) => {
console.log("Stubbed withProgress")
return task({ report: () => {} })
},
registerUriHandler: () => ({ dispose: () => {} }),
registerWebviewViewProvider: () => ({ dispose: () => {} }),
onDidChangeActiveTextEditor: () => ({ dispose: () => {} }),
createTextEditorDecorationType: () => ({ dispose: () => {} }),
createWebviewPanel: (...args) => {
console.log("Stubbed createWebviewPanel:", ...args)
return {
webview: {},
reveal: () => {},
dispose: () => {},
}
},
}
vscode.env = {
uriScheme: "vscode",
appName: "Visual Studio Code",
appRoot: "/tmp/vscode/appRoot",
language: "en",
machineId: "stub-machine-id",
remoteName: undefined,
sessionId: "stub-session-id",
shell: "/bin/bash",
clipboard: createStub("vscode.env.clipboard"),
openExternal: createStub("vscode.env.openExternal"),
getQueryParameter: createStub("vscode.env.getQueryParameter"),
onDidChangeTelemetryEnabled: createStub("vscode.env.onDidChangeTelemetryEnabled"),
isTelemetryEnabled: createStub("vscode.env.isTelemetryEnabled"),
telemetryConfiguration: createStub("vscode.env.telemetryConfiguration"),
onDidChangeTelemetryConfiguration: createStub("vscode.env.onDidChangeTelemetryConfiguration"),
createTelemetryLogger: createStub("vscode.env.createTelemetryLogger"),
}
vscode.Uri = {
parse: (uriString) => {
const url = new URL(uriString)
return {
scheme: url.protocol.replace(":", ""),
authority: url.hostname,
path: url.pathname,
query: url.search.slice(1),
fragment: url.hash.slice(1),
fsPath: `/tmp${url.pathname}`,
toString: () => uriString,
toJSON: () => uriString,
with: (change) => {
const newUrl = new URL(uriString)
if (change.scheme) newUrl.protocol = change.scheme + ":"
if (change.authority) newUrl.hostname = change.authority
if (change.path) newUrl.pathname = change.path
if (change.query) newUrl.search = "?" + change.query
if (change.fragment) newUrl.hash = "#" + change.fragment
return vscode.Uri.parse(newUrl.toString())
},
}
},
file: (path) => {
return {
scheme: "file",
authority: "",
path,
fsPath: path,
query: "",
fragment: "",
toString: () => `file://${path}`,
toJSON: () => `file://${path}`,
with: (change) => {
const modified = Object.assign({}, vscode.Uri.file(path), change)
return modified
},
}
},
joinPath: (...segments) => {
const joined = segments.map((s) => (typeof s === "string" ? s : s.path)).join("/")
return vscode.Uri.file("/" + joined.replace(/\/+/g, "/"))
},
}
vscode.env.openExternal = async (uri) => {
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
console.log("Opening browser:", url)
await open(url)
return true
}
console.log("Finished loading stub impls...")
File diff suppressed because it is too large Load Diff
+2001
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -11,7 +11,8 @@
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest dev",
"test:coverage": "vitest run --coverage"
"test:coverage": "vitest run --coverage",
"devtools": "react-devtools"
},
"dependencies": {
"@floating-ui/react": "^0.27.4",
@@ -67,6 +68,7 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"jsdom": "^26.0.0",
"react-devtools": "^6.1.2",
"tailwindcss": "^4.1.5",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
+14 -16
View File
@@ -44,22 +44,21 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
</h3>
<ul style={ulStyle}>
<li>
<b>Task Timeline:</b> See the history of your coding journey with a visual timeline of checkpoints, letting
you understand what Cline did at a glance.
<b>Workflows:</b> Create and manage workflow files that can be injected into conversations via slash commands,
making it easy to automate repetitive tasks.
</li>
<li>
<b>UX Improvements:</b> Type while Cline works, smarter auto-scrolling, new copy buttons for task headers and
messages, and a simplified home interface for a smoother experience.
<b>Collapsible Task List:</b> Hide your recent tasks when sharing your screen to keep your prompts private.
</li>
<li>
<b>Commit Message Generation:</b> Let Cline help craft meaningful commit messages based on your changes.
<b>Global Endpoint for Vertex AI:</b> Improved availability and reduced rate limiting errors for Vertex AI
users.
</li>
<li>
<b>Quote Replies:</b> Easily reference previous messages with new quote reply support for clearer
conversations.
<b>New User Experience:</b> Special components and guidance for new users to help them get started with Cline.
</li>
<li>
<b>Auto Caching for Gemini:</b> Native support for Gemini's recently released Implicit Caching.
<b>UI Improvements:</b> Fixed loading states and improved settings organization for a smoother experience.
</li>
</ul>
<Accordion isCompact className="pl-0">
@@ -75,21 +74,20 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
}}>
<ul style={ulStyle}>
<li>
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price
tracking for Gemini models.
<b>Task Timeline:</b> See the history of your coding journey with a visual timeline of checkpoints.
</li>
<li>
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their
contents easily.
<b>UX Improvements:</b> Type while Cline works, smarter auto-scrolling, and copy buttons for task
headers and messages.
</li>
<li>
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price
tracking.
</li>
<li>
<b>Global Cline Rules:</b> Store multiple rules files in Documents/Cline/Rules to share between
projects.
</li>
<li>
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
new task.
</li>
</ul>
</AccordionItem>
</Accordion>
+23 -12
View File
@@ -18,7 +18,7 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCo
import { useExtensionState } from "@/context/ExtensionStateContext"
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
import { vscode } from "@/utils/vscode"
import { FileServiceClient } from "@/services/grpc-client"
import { FileServiceClient, TaskServiceClient } from "@/services/grpc-client"
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
interface CopyButtonProps {
@@ -208,12 +208,12 @@ export const ChatRowContent = ({
selectedText: "",
})
const contentRef = useRef<HTMLDivElement>(null)
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage, retryStatus] = useMemo(() => {
if (message.text != null && message.say === "api_req_started") {
const info: ClineApiReqInfo = JSON.parse(message.text)
return [info.cost, info.cancelReason, info.streamingFailedMessage]
return [info.cost, info.cancelReason, info.streamingFailedMessage, info.retryStatus]
}
return [undefined, undefined, undefined]
return [undefined, undefined, undefined, undefined]
}, [message.text, message.say])
// when resuming task last won't be api_req_failed but a resume_task message so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
@@ -445,6 +445,17 @@ export const ChatRowContent = ({
if (apiRequestFailedMessage) {
return <span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
}
// New: Check for retryStatus to modify the title
if (retryStatus && cost == null && !apiReqCancelReason) {
const retryOperations = retryStatus.maxAttempts > 0 ? retryStatus.maxAttempts - 1 : 0
return (
<span
style={{
color: normalColor,
fontWeight: "bold",
}}>{`API Request (Retrying failed attempt ${retryStatus.attempt}/${retryOperations})...`}</span>
)
}
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
})(),
@@ -1215,10 +1226,9 @@ export const ChatRowContent = ({
disabled={seeNewChangesDisabled}
onClick={() => {
setSeeNewChangesDisabled(true)
vscode.postMessage({
type: "taskCompletionViewChanges",
number: message.ts,
})
TaskServiceClient.taskCompletionViewChanges({
value: message.ts,
}).catch((err) => console.error("Failed to show task completion view changes:", err))
}}
style={{
cursor: seeNewChangesDisabled ? "wait" : "pointer",
@@ -1379,10 +1389,11 @@ export const ChatRowContent = ({
disabled={seeNewChangesDisabled}
onClick={() => {
setSeeNewChangesDisabled(true)
vscode.postMessage({
type: "taskCompletionViewChanges",
number: message.ts,
})
TaskServiceClient.taskCompletionViewChanges({
value: message.ts,
}).catch((err) =>
console.error("Failed to show task completion view changes:", err),
)
}}>
<i
className="codicon codicon-new-file"
@@ -259,7 +259,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
},
ref,
) => {
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform, workflowToggles } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [isDraggingOver, setIsDraggingOver] = useState(false)
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
@@ -373,6 +373,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [showContextMenu, setShowContextMenu])
useEffect(() => {
const handleClickOutsideSlashMenu = (event: MouseEvent) => {
if (
slashCommandsMenuContainerRef.current &&
!slashCommandsMenuContainerRef.current.contains(event.target as Node)
) {
setShowSlashCommandsMenu(false)
}
}
if (showSlashCommandsMenu) {
document.addEventListener("mousedown", handleClickOutsideSlashMenu)
}
return () => {
document.removeEventListener("mousedown", handleClickOutsideSlashMenu)
}
}, [showSlashCommandsMenu])
const handleMentionSelect = useCallback(
(type: ContextMenuOptionType, value?: string) => {
if (type === ContextMenuOptionType.NoResults) {
@@ -463,13 +482,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
event.preventDefault()
setSelectedSlashCommandsIndex((prevIndex) => {
const direction = event.key === "ArrowUp" ? -1 : 1
const commands = getMatchingSlashCommands(slashCommandsQuery)
// Get commands with workflow toggles
const allCommands = getMatchingSlashCommands(slashCommandsQuery, workflowToggles)
if (commands.length === 0) {
if (allCommands.length === 0) {
return prevIndex
}
const newIndex = (prevIndex + direction + commands.length) % commands.length
// Calculate total command count
const totalCommandCount = allCommands.length
// Create wraparound navigation - moves from last item to first and vice versa
const newIndex = (prevIndex + direction + totalCommandCount) % totalCommandCount
return newIndex
})
return
@@ -477,7 +501,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
if ((event.key === "Enter" || event.key === "Tab") && selectedSlashCommandsIndex !== -1) {
event.preventDefault()
const commands = getMatchingSlashCommands(slashCommandsQuery)
const commands = getMatchingSlashCommands(slashCommandsQuery, workflowToggles)
if (commands.length > 0) {
handleSlashCommandsSelect(commands[selectedSlashCommandsIndex])
}
@@ -880,7 +904,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// extract and validate the exact command text
const commandText = processedText.substring(slashIndex + 1, endIndex)
const isValidCommand = validateSlashCommand(commandText)
const isValidCommand = validateSlashCommand(commandText, workflowToggles)
if (isValidCommand) {
const fullCommand = processedText.substring(slashIndex, endIndex) // includes slash
@@ -893,7 +917,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
highlightLayerRef.current.innerHTML = processedText
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
}, [])
}, [workflowToggles])
useLayoutEffect(() => {
updateHighlights()
@@ -1373,6 +1397,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setSelectedIndex={setSelectedSlashCommandsIndex}
onMouseDown={handleMenuMouseDown}
query={slashCommandsQuery}
workflowToggles={workflowToggles}
/>
</div>
)}
+33 -25
View File
@@ -18,7 +18,7 @@ import { combineCommandSequences } from "@shared/combineCommandSequences"
import { getApiMetrics } from "@shared/getApiMetrics"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { TaskServiceClient, SlashServiceClient } from "@/services/grpc-client"
import { TaskServiceClient, SlashServiceClient, FileServiceClient } from "@/services/grpc-client"
import HistoryPreview from "@/components/history/HistoryPreview"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import Announcement from "@/components/chat/Announcement"
@@ -463,25 +463,22 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "resume_completed_task":
case "mistake_limit_reached":
case "new_task": // user can provide feedback or reject the new task suggestion
vscode.postMessage({
type: "askResponse",
askResponse: "messageResponse",
await TaskServiceClient.askResponse({
responseType: "messageResponse",
text: messageToSend,
images,
})
break
case "condense":
vscode.postMessage({
type: "askResponse",
askResponse: "messageResponse",
await TaskServiceClient.askResponse({
responseType: "messageResponse",
text: messageToSend,
images,
})
break
case "report_bug":
vscode.postMessage({
type: "askResponse",
askResponse: "messageResponse",
await TaskServiceClient.askResponse({
responseType: "messageResponse",
text: messageToSend,
images,
})
@@ -525,16 +522,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
await TaskServiceClient.askResponse({
responseType: "yesButtonClicked",
text: trimmedInput,
images: images,
})
} else {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
await TaskServiceClient.askResponse({
responseType: "yesButtonClicked",
})
}
// Clear input state after sending
@@ -591,17 +586,15 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "browser_action_launch":
case "use_mcp_server":
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
await TaskServiceClient.askResponse({
responseType: "noButtonClicked",
text: trimmedInput,
images: images,
})
} else {
// responds to the API with a "This operation failed" and lets it try again
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
await TaskServiceClient.askResponse({
responseType: "noButtonClicked",
})
}
// Clear input state after sending
@@ -632,8 +625,15 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
const selectImages = useCallback(() => {
vscode.postMessage({ type: "selectImages" })
const selectImages = useCallback(async () => {
try {
const response = await FileServiceClient.selectImages({})
if (response && response.values && response.values.length > 0) {
setSelectedImages((prevImages) => [...prevImages, ...response.values].slice(0, MAX_IMAGES_PER_MESSAGE))
}
} catch (error) {
console.error("Error selecting images:", error)
}
}, [])
const shouldDisableImages = !selectedModelInfo.supportsImages || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
@@ -976,6 +976,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
)
}
// We display certain statuses for the last message only
// If the last message is a checkpoint, we want to show the status of the previous message
const nextMessage = index < groupedMessages.length - 1 && groupedMessages[index + 1]
const isNextCheckpoint = !Array.isArray(nextMessage) && nextMessage && nextMessage?.say === "checkpoint_created"
const isLastMessageGroup = isNextCheckpoint && index === groupedMessages.length - 2
const isLast = index === groupedMessages.length - 1 || isLastMessageGroup
// regular message
return (
<ChatRow
@@ -984,7 +992,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
isExpanded={expandedRows[messageOrGroup.ts] || false}
onToggleExpand={() => toggleRowExpansion(messageOrGroup.ts)}
lastModifiedMessage={modifiedMessages.at(-1)}
isLast={index === groupedMessages.length - 1}
isLast={isLast}
onHeightChange={handleRowHeightChange}
inputValue={inputValue}
sendMessageFromChatRow={handleSendMessage}
@@ -7,9 +7,17 @@ interface SlashCommandMenuProps {
setSelectedIndex: (index: number) => void
onMouseDown: () => void
query: string
workflowToggles?: Record<string, boolean>
}
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedIndex, setSelectedIndex, onMouseDown, query }) => {
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
onSelect,
selectedIndex,
setSelectedIndex,
onMouseDown,
query,
workflowToggles = {},
}) => {
const menuRef = useRef<HTMLDivElement>(null)
const handleClick = useCallback(
@@ -19,10 +27,9 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
[onSelect],
)
// Auto-scroll logic remains the same...
useEffect(() => {
if (menuRef.current) {
const selectedElement = menuRef.current.children[selectedIndex] as HTMLElement
const selectedElement = menuRef.current.querySelector(`#slash-command-menu-item-${selectedIndex}`) as HTMLElement
if (selectedElement) {
const menuRect = menuRef.current.getBoundingClientRect()
const selectedRect = selectedElement.getBoundingClientRect()
@@ -37,7 +44,46 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
}, [selectedIndex])
// Filter commands based on query
const filteredCommands = getMatchingSlashCommands(query)
const filteredCommands = getMatchingSlashCommands(query, workflowToggles)
const defaultCommands = filteredCommands.filter((cmd) => cmd.section === "default" || !cmd.section)
const workflowCommands = filteredCommands.filter((cmd) => cmd.section === "custom")
// Create a reusable function for rendering a command section
const renderCommandSection = (commands: SlashCommand[], title: string, indexOffset: number, showDescriptions: boolean) => {
if (commands.length === 0) return null
return (
<>
<div className="text-xs text-[var(--vscode-descriptionForeground)] px-3 py-1 font-bold border-b border-[var(--vscode-editorGroup-border)]">
{title}
</div>
{commands.map((command, index) => {
const itemIndex = index + indexOffset
return (
<div
key={command.name}
id={`slash-command-menu-item-${itemIndex}`}
className={`slash-command-menu-item py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
itemIndex === selectedIndex
? "bg-[var(--vscode-quickInputList-focusBackground)] text-[var(--vscode-quickInputList-focusForeground)]"
: ""
} hover:bg-[var(--vscode-list-hoverBackground)]`}
onClick={() => handleClick(command)}
onMouseEnter={() => setSelectedIndex(itemIndex)}>
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">
<span className="ph-no-capture">/{command.name}</span>
</div>
{showDescriptions && command.description && (
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)] whitespace-normal overflow-hidden text-ellipsis">
<span className="ph-no-capture">{command.description}</span>
</div>
)}
</div>
)
})}
</>
)
}
return (
<div
@@ -45,33 +91,15 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
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
>
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 overflow-y-auto"
style={{ maxHeight: "min(200px, calc(50vh))", overscrollBehavior: "contain" }}>
{filteredCommands.length > 0 ? (
filteredCommands.map((command, index) => (
<div
key={command.name}
id={`slash-command-menu-item-${index}`}
className={`slash-command-menu-item 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">
<span className="ph-no-capture">/{command.name}</span>
</div>
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)] whitespace-normal overflow-hidden text-ellipsis">
<span className="ph-no-capture">{command.description}</span>
</div>
</div>
))
<>
{renderCommandSection(defaultCommands, "Default Commands", 0, true)}
{renderCommandSection(workflowCommands, "Workflow Commands", defaultCommands.length, false)}
</>
) : (
<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>
)}
@@ -1,8 +1,8 @@
import React, { useState, useEffect } from "react"
import styled from "styled-components"
import { vscode } from "@/utils/vscode"
import { TaskFeedbackType } from "@shared/WebviewMessage"
import { TaskServiceClient } from "@/services/grpc-client"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { TaskFeedbackType } from "@shared/WebviewMessage"
interface TaskFeedbackButtonsProps {
messageTs: number
@@ -41,25 +41,27 @@ const TaskFeedbackButtons: React.FC<TaskFeedbackButtonsProps> = ({ messageTs, is
return null
}
const handleFeedback = (type: TaskFeedbackType) => {
const handleFeedback = async (type: TaskFeedbackType) => {
if (feedback !== null) return // Already provided feedback
setFeedback(type)
// Send feedback to extension
vscode.postMessage({
type: "taskFeedback",
feedbackType: type,
})
// Store in localStorage that feedback was provided for this message
try {
const feedbackHistory = localStorage.getItem("taskFeedbackHistory") || "{}"
const history = JSON.parse(feedbackHistory)
history[messageTs] = true
localStorage.setItem("taskFeedbackHistory", JSON.stringify(history))
} catch (e) {
console.error("Error updating feedback history:", e)
await TaskServiceClient.taskFeedback({
value: type,
})
// Store in localStorage that feedback was provided for this message
try {
const feedbackHistory = localStorage.getItem("taskFeedbackHistory") || "{}"
const history = JSON.parse(feedbackHistory)
history[messageTs] = true
localStorage.setItem("taskFeedbackHistory", JSON.stringify(history))
} catch (e) {
console.error("Error updating feedback history:", e)
}
} catch (error) {
console.error("Error sending task feedback:", error)
}
}
@@ -1,11 +1,11 @@
import { useCallback, useRef, useState, useMemo } from "react"
import { useRef, useState, useMemo } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useAutoApproveActions } from "@/hooks/useAutoApproveActions"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
import AutoApproveMenuItem from "./AutoApproveMenuItem"
import AutoApproveModal from "./AutoApproveModal"
import { ACTION_METADATA, NOTIFICATIONS_SETTING } from "./constants"
import { ActionMetadata } from "./types"
interface AutoApproveBarProps {
style?: React.CSSProperties
@@ -13,10 +13,11 @@ interface AutoApproveBarProps {
const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
const { autoApprovalSettings } = useExtensionState()
const { isChecked, isFavorited, updateAction } = useAutoApproveActions()
const [isModalVisible, setIsModalVisible] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
// Favorites are derived from autoApprovalSettings
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
// Render a favorited item with a checkbox
@@ -32,6 +33,7 @@ const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
isFavorited={isFavorited}
onToggle={updateAction}
condensed={true}
showIcon={false}
/>
)
}
@@ -67,25 +69,6 @@ const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
]
}
const isChecked = (action: ActionMetadata): boolean => {
if (action.id === "enableNotifications") {
return autoApprovalSettings.enableNotifications
}
if (action.id === "enableAll") {
return Object.values(autoApprovalSettings.actions).every(Boolean)
}
return autoApprovalSettings.actions[action.id] ?? false
}
const isFavorited = (action: ActionMetadata): boolean => {
return favorites.includes(action.id)
}
const updateAction = useCallback(() => {
// This is just a placeholder since we need to pass it to AutoApproveMenuItem
// The actual implementation is in the modal component
}, [])
return (
<div
className="px-[10px] mx-[5px] select-none rounded-[10px_10px_0_0]"
@@ -11,6 +11,7 @@ interface AutoApproveMenuItemProps {
onToggle: (action: ActionMetadata, checked: boolean) => void
onToggleFavorite?: (actionId: string) => void
condensed?: boolean
showIcon?: boolean
}
const CheckboxContainer = styled.div<{
@@ -77,6 +78,7 @@ const AutoApproveMenuItem = ({
onToggle,
onToggleFavorite,
condensed = false,
showIcon = true,
}: AutoApproveMenuItemProps) => {
const checked = isChecked(action)
const favorited = isFavorited?.(action)
@@ -93,7 +95,7 @@ const AutoApproveMenuItem = ({
<CheckboxContainer isFavorited={favorited} onClick={onChange}>
<div className="left-content">
<VSCodeCheckbox checked={checked} />
<span className={`codicon ${action.icon} icon`}></span>
{showIcon && <span className={`codicon ${action.icon} icon`}></span>}
<span className="label">{condensed ? action.shortName : action.label}</span>
</div>
{onToggleFavorite && !condensed && (
@@ -1,11 +1,10 @@
import React, { useRef, useState, useEffect, useMemo, useCallback } from "react"
import React, { useRef, useState, useEffect } from "react"
import { useClickAway, useWindowSize } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useAutoApproveActions } from "@/hooks/useAutoApproveActions"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { vscode } from "@/utils/vscode"
import { VSCodeTextField, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
import HeroTooltip from "@/components/common/HeroTooltip"
import AutoApproveMenuItem from "./AutoApproveMenuItem"
import { ActionMetadata } from "./types"
@@ -28,6 +27,8 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
NOTIFICATIONS_SETTING,
}) => {
const { autoApprovalSettings } = useExtensionState()
const { isChecked, isFavorited, toggleFavorite, updateAction, updateMaxRequests } = useAutoApproveActions()
const modalRef = useRef<HTMLDivElement>(null)
const itemsContainerRef = useRef<HTMLDivElement>(null)
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
@@ -35,9 +36,6 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
const [menuPosition, setMenuPosition] = useState(0)
const [containerWidth, setContainerWidth] = useState(0)
// Favorites are derived from autoApprovalSettings
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
useClickAway(modalRef, (e) => {
// Skip if click was on the button that toggles the modal
if (buttonRef.current && buttonRef.current.contains(e.target as Node)) {
@@ -83,141 +81,6 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
}
}, [isVisible])
const toggleFavorite = useCallback(
(actionId: string) => {
const currentFavorites = autoApprovalSettings.favorites || []
let newFavorites: string[]
if (currentFavorites.includes(actionId)) {
newFavorites = currentFavorites.filter((id) => id !== actionId)
} else {
newFavorites = [...currentFavorites, actionId]
}
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
favorites: newFavorites,
},
})
},
[autoApprovalSettings],
)
const updateAction = useCallback(
(action: ActionMetadata, value: boolean) => {
const actionId = action.id
const subActionId = action.subAction?.id
if (actionId === "enableAll" || subActionId === "enableAll") {
toggleAll(action, value)
return
}
if (actionId === "enableNotifications" || subActionId === "enableNotifications") {
updateNotifications(action, value)
return
}
let newActions = {
...autoApprovalSettings.actions,
[actionId]: value,
}
if (value === false && subActionId) {
newActions[subActionId] = false
}
if (value === true && action.parentActionId) {
newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true
}
// Check if this will result in any enabled actions
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
actions: newActions,
enabled: willHaveEnabledActions,
},
})
},
[autoApprovalSettings],
)
const updateMaxRequests = useCallback(
(maxRequests: number) => {
const currentSettings = autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
maxRequests,
},
})
},
[autoApprovalSettings],
)
const updateNotifications = useCallback(
(action: ActionMetadata, checked: boolean) => {
if (action.id === "enableNotifications") {
const currentSettings = autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
enableNotifications: checked,
},
})
}
},
[autoApprovalSettings],
)
const toggleAll = useCallback(
(action: ActionMetadata, checked: boolean) => {
let actions = { ...autoApprovalSettings.actions }
for (const action of Object.keys(actions)) {
actions[action as keyof AutoApprovalSettings["actions"]] = checked
}
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
actions,
},
})
},
[autoApprovalSettings],
)
// Check if action is enabled
const isChecked = (action: ActionMetadata): boolean => {
if (action.id === "enableNotifications") {
return autoApprovalSettings.enableNotifications
}
if (action.id === "enableAll") {
return Object.values(autoApprovalSettings.actions).every(Boolean)
}
return autoApprovalSettings.actions[action.id] ?? false
}
// Check if action is favorited
const isFavorited = (action: ActionMetadata): boolean => {
return favorites.includes(action.id)
}
if (!isVisible) return null
return (
@@ -240,19 +103,19 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
/>
<div className="flex justify-between items-center mb-3">
<div className="m-0 text-base font-semibold">Auto-approve Settings</div>
<HeroTooltip
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
placement="top">
<div className="text-base font-semibold mb-1">Auto-approve Settings</div>
</HeroTooltip>
<VSCodeButton appearance="icon" onClick={() => setIsVisible(false)}>
<span className="codicon codicon-close text-[10px]"></span>
</VSCodeButton>
</div>
<HeroTooltip
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
placement="top">
<div className="mb-3">
<span className="text-[color:var(--vscode-foreground)] font-medium">Actions:</span>
</div>
</HeroTooltip>
<div className="mb-2.5">
<span className="text-[color:var(--vscode-foreground)] font-medium">Actions:</span>
</div>
<div
ref={itemsContainerRef}
@@ -285,7 +148,7 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
))}
</div>
<div className="mb-3">
<div className="mb-2.5">
<span className="text-[color:var(--vscode-foreground)] font-medium">Quick Settings:</span>
</div>
@@ -1,11 +1,18 @@
import { ActionMetadata } from "./types"
export const ACTION_METADATA: ActionMetadata[] = [
{
id: "enableAutoApprove",
label: "Enable auto-approve",
shortName: "Enabled",
description: "Toggle the auto-approve feature on or off.",
icon: "codicon-play-circle",
},
{
id: "enableAll",
label: "Enable all",
label: "Toggle all",
shortName: "All",
description: "Enable all actions.",
description: "Toggle all actions on or off.",
icon: "codicon-checklist",
},
{
@@ -1,7 +1,7 @@
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
export interface ActionMetadata {
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll"
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll" | "enableAutoApprove"
label: string
shortName: string
description: string
@@ -6,6 +6,7 @@ import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import RulesToggleList from "./RulesToggleList"
import Tooltip from "@/components/common/Tooltip"
import styled from "styled-components"
const ClineRulesToggleModal: React.FC = () => {
const {
@@ -13,6 +14,7 @@ const ClineRulesToggleModal: React.FC = () => {
localClineRulesToggles = {},
localCursorRulesToggles = {},
localWindsurfRulesToggles = {},
workflowToggles = {},
} = useExtensionState()
const [isVisible, setIsVisible] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
@@ -20,6 +22,7 @@ const ClineRulesToggleModal: React.FC = () => {
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
const [arrowPosition, setArrowPosition] = useState(0)
const [menuPosition, setMenuPosition] = useState(0)
const [currentView, setCurrentView] = useState<"rules" | "workflows">("rules")
useEffect(() => {
if (isVisible) {
@@ -45,6 +48,10 @@ const ClineRulesToggleModal: React.FC = () => {
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
.sort(([a], [b]) => a.localeCompare(b))
const workflows = Object.entries(workflowToggles || {})
.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({
@@ -71,6 +78,14 @@ const ClineRulesToggleModal: React.FC = () => {
})
}
const toggleWorkflow = (workflowPath: string, enabled: boolean) => {
vscode.postMessage({
type: "toggleWorkflow",
workflowPath,
enabled,
})
}
// Close modal when clicking outside
useClickAway(modalRef, () => {
setIsVisible(false)
@@ -91,7 +106,7 @@ const ClineRulesToggleModal: React.FC = () => {
return (
<div ref={modalRef}>
<div ref={buttonRef} className="inline-flex min-w-0 max-w-full">
<Tooltip tipText="Manage Cline Rules" visible={isVisible ? false : undefined}>
<Tooltip tipText="Manage Cline Rules & Workflows" visible={isVisible ? false : undefined}>
<VSCodeButton
appearance="icon"
aria-label="Cline Rules"
@@ -125,68 +140,146 @@ const ClineRulesToggleModal: React.FC = () => {
}}
/>
<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)
}}></VSCodeButton>
{/* Tabs container */}
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "10px",
}}>
<div
style={{
display: "flex",
gap: "1px",
borderBottom: "1px solid var(--vscode-panel-border)",
}}>
<TabButton isActive={currentView === "rules"} onClick={() => setCurrentView("rules")}>
Rules
</TabButton>
<TabButton isActive={currentView === "workflows"} onClick={() => setCurrentView("workflows")}>
Workflows
</TabButton>
</div>
</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"
isGlobal={true}
ruleType={"cline"}
showNewRule={true}
showNoRules={true}
/>
{/* Description text */}
<div className="text-xs text-[var(--vscode-descriptionForeground)] mb-4">
{currentView === "rules" ? (
<p>
Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to
include context and preferences for your projects or globally for every conversation.
</p>
) : (
<p>
Workflows allow you to define a series of steps to guide Cline through a repetitive set of tasks,
such as deploying a service or submitting a PR. To invoke a workflow, type{" "}
<span
className="
text-[var(--vscode-foreground)] font-bold">
/workflow-name
</span>{" "}
in the chat.
</p>
)}
</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"
isGlobal={false}
ruleType={"cline"}
showNewRule={false}
showNoRules={false}
/>
<RulesToggleList
rules={cursorRules}
toggleRule={toggleCursorRule}
listGap="small"
isGlobal={false}
ruleType={"cursor"}
showNewRule={false}
showNoRules={false}
/>
<RulesToggleList
rules={windsurfRules}
toggleRule={toggleWindsurfRule}
listGap="small"
isGlobal={false}
ruleType={"windsurf"}
showNewRule={true}
showNoRules={localRules.length === 0 && cursorRules.length === 0 && windsurfRules.length === 0}
/>
</div>
{currentView === "rules" ? (
<>
{/* 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"
isGlobal={true}
ruleType={"cline"}
showNewRule={true}
showNoRules={false}
/>
</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"
isGlobal={false}
ruleType={"cline"}
showNewRule={false}
showNoRules={false}
/>
<RulesToggleList
rules={cursorRules}
toggleRule={toggleCursorRule}
listGap="small"
isGlobal={false}
ruleType={"cursor"}
showNewRule={false}
showNoRules={false}
/>
<RulesToggleList
rules={windsurfRules}
toggleRule={toggleWindsurfRule}
listGap="small"
isGlobal={false}
ruleType={"windsurf"}
showNewRule={true}
showNoRules={false}
/>
</div>
</>
) : (
/* Workflows section */
<div style={{ marginBottom: -10 }}>
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
<RulesToggleList
rules={workflows}
toggleRule={toggleWorkflow}
listGap="small"
isGlobal={false}
ruleType={"workflow"}
showNewRule={true}
showNoRules={false}
/>
</div>
)}
</div>
)}
</div>
)
}
const StyledTabButton = styled.button<{ isActive: boolean }>`
background: none;
border: none;
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
margin-bottom: -1px;
font-family: inherit;
&:hover {
color: var(--vscode-foreground);
}
`
export const TabButton = ({
children,
isActive,
onClick,
}: {
children: React.ReactNode
isActive: boolean
onClick: () => void
}) => (
<StyledTabButton isActive={isActive} onClick={onClick}>
{children}
</StyledTabButton>
)
export default ClineRulesToggleModal
@@ -7,9 +7,10 @@ import { CreateRuleFileRequest } from "@shared/proto-conversions/file/rule-files
interface NewRuleRowProps {
isGlobal: boolean
ruleType?: string
}
const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal, ruleType }) => {
const [isExpanded, setIsExpanded] = useState(false)
const [filename, setFilename] = useState("")
const inputRef = useRef<HTMLInputElement>(null)
@@ -64,6 +65,7 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
CreateRuleFileRequest.create({
isGlobal,
filename: finalFilename,
type: ruleType || "cline",
}),
)
} catch (err) {
@@ -97,7 +99,11 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
<input
ref={inputRef}
type="text"
placeholder="rule-name (.md, .txt, or no extension)"
placeholder={
ruleType === "workflow"
? "workflow-name (.md, .txt, or no extension)"
: "rule-name (.md, .txt, or no extension)"
}
value={filename}
onChange={(e) => setFilename(e.target.value)}
onKeyDown={handleKeyDown}
@@ -121,7 +127,7 @@ const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
) : (
<>
<span className="flex-1 text-[var(--vscode-descriptionForeground)] bg-[var(--vscode-input-background)] italic text-xs">
New rule file...
{ruleType === "workflow" ? "New workflow file..." : "New rule file..."}
</span>
<div className="flex items-center ml-2 space-x-2">
<VSCodeButton

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