From 9de7253998bd225b682d4b8e5f630d2b2449038c Mon Sep 17 00:00:00 2001
From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Date: Sat, 4 Jan 2025 19:50:35 -0800
Subject: [PATCH] refactor: code cleanup, formatting updates, improved
workspace handling, checkpoints feature
Add instructions
Fix completion
Refactor
Rename reset to restore
add haschanges flag
Remove log
Better error handling
Better error handling
Fix wording
Fix
Fix
Fix
Comment
Add hash for only latest tool
Prepare for release
Fix
Fix delete
Format fix
---
.vscode/tasks.json | 6 +-
CHANGELOG.md | 8 +
README.md | 12 +
esbuild.js | 27 +-
package-lock.json | 58 +-
package.json | 5 +-
src/api/index.ts | 5 +-
src/api/providers/anthropic.ts | 64 +-
src/api/providers/bedrock.ts | 30 +-
src/api/providers/deepseek.ts | 23 +-
src/api/providers/gemini.ts | 18 +-
src/api/providers/lmstudio.ts | 15 +-
src/api/providers/ollama.ts | 15 +-
src/api/providers/openai-native.ts | 20 +-
src/api/providers/openai.ts | 9 +-
src/api/providers/openrouter.ts | 58 +-
src/api/providers/vertex.ts | 18 +-
src/api/transform/gemini-format.ts | 51 +-
src/api/transform/o1-format.ts | 45 +-
src/api/transform/openai-format.ts | 130 +-
src/core/Cline.ts | 2263 ++++++++++++++---
src/core/assistant-message/diff.ts | 26 +-
src/core/assistant-message/index.ts | 22 +-
.../parse-assistant-message.ts | 47 +-
src/core/mentions/index.ts | 50 +-
src/core/prompts/responses.ts | 50 +-
src/core/prompts/system.ts | 23 +-
src/core/sliding-window/index.ts | 83 +-
src/core/webview/ClineProvider.ts | 554 +++-
src/core/webview/getNonce.ts | 3 +-
src/core/webview/getUri.ts | 6 +-
src/exports/README.md | 8 +-
src/exports/index.ts | 14 +-
src/extension.ts | 98 +-
.../checkpoints/CheckpointTracker.ts | 435 ++++
src/integrations/diagnostics/index.ts | 9 +-
.../editor/DecorationController.ts | 31 +-
src/integrations/editor/DiffViewProvider.ts | 133 +-
src/integrations/editor/detect-omission.ts | 20 +-
src/integrations/misc/export-markdown.ts | 35 +-
src/integrations/misc/extract-text.ts | 9 +-
src/integrations/misc/open-file.ts | 31 +-
src/integrations/notifications/index.ts | 16 +-
src/integrations/terminal/TerminalManager.ts | 58 +-
src/integrations/terminal/TerminalProcess.ts | 53 +-
src/integrations/terminal/TerminalRegistry.ts | 4 +-
.../theme/default-themes/dark_plus.json | 5 +-
.../theme/default-themes/dark_vs.json | 5 +-
.../theme/default-themes/hc_black.json | 6 +-
.../theme/default-themes/hc_light.json | 21 +-
.../theme/default-themes/light_plus.json | 5 +-
.../theme/default-themes/light_vs.json | 10 +-
src/integrations/theme/getTheme.ts | 48 +-
.../workspace/WorkspaceTracker.ts | 38 +-
src/integrations/workspace/get-python-env.ts | 4 +-
src/services/browser/BrowserSession.ts | 17 +-
src/services/browser/UrlContentFetcher.ts | 5 +-
src/services/glob/list-files.ts | 13 +-
src/services/mcp/McpHub.ts | 180 +-
src/services/ripgrep/index.ts | 21 +-
src/services/tree-sitter/index.ts | 26 +-
src/services/tree-sitter/languageParser.ts | 12 +-
src/shared/ExtensionMessage.ts | 18 +-
src/shared/HistoryItem.ts | 4 +
src/shared/WebviewMessage.ts | 11 +-
src/shared/api.ts | 12 +-
src/shared/array.ts | 10 +-
src/shared/combineApiRequests.ts | 23 +-
src/shared/combineCommandSequences.ts | 34 +-
src/shared/context-mentions.ts | 3 +-
src/shared/getApiMetrics.ts | 18 +-
src/utils/cost.ts | 12 +-
src/utils/fs.test.ts | 22 +-
src/utils/fs.ts | 4 +-
src/utils/path.test.ts | 4 +-
src/utils/path.ts | 5 +-
webview-ui/package-lock.json | 33 +-
webview-ui/package.json | 1 +
webview-ui/public/index.html | 4 +-
webview-ui/src/App.tsx | 16 +-
.../src/components/chat/Announcement.tsx | 72 +-
.../src/components/chat/AutoApproveMenu.tsx | 88 +-
.../src/components/chat/BrowserSessionRow.tsx | 190 +-
webview-ui/src/components/chat/ChatRow.tsx | 590 ++++-
.../src/components/chat/ChatTextArea.tsx | 216 +-
webview-ui/src/components/chat/ChatView.tsx | 210 +-
.../src/components/chat/ContextMenu.tsx | 75 +-
webview-ui/src/components/chat/TaskHeader.tsx | 288 ++-
.../components/common/CheckpointControls.tsx | 294 +++
.../src/components/common/CodeAccordian.tsx | 13 +-
.../src/components/common/CodeBlock.tsx | 16 +-
webview-ui/src/components/common/Demo.tsx | 48 +-
.../src/components/common/MarkdownBlock.tsx | 9 +-
.../src/components/common/SuccessButton.tsx | 31 +
.../src/components/common/Thumbnails.tsx | 10 +-
.../components/common/VSCodeButtonLink.tsx | 6 +-
.../src/components/history/HistoryPreview.tsx | 37 +-
.../src/components/history/HistoryView.tsx | 192 +-
.../src/components/mcp/McpResourceRow.tsx | 9 +-
webview-ui/src/components/mcp/McpToolRow.tsx | 89 +-
webview-ui/src/components/mcp/McpView.tsx | 128 +-
.../src/components/settings/ApiOptions.tsx | 442 +++-
.../settings/OpenRouterModelPicker.tsx | 77 +-
.../src/components/settings/SettingsView.tsx | 90 +-
.../src/components/settings/TabNavbar.tsx | 28 +-
.../src/components/welcome/WelcomeView.tsx | 34 +-
.../src/context/ExtensionStateContext.tsx | 58 +-
webview-ui/src/index.css | 24 +-
webview-ui/src/reportWebVitals.ts | 16 +-
webview-ui/src/utils/context-mentions.ts | 38 +-
webview-ui/src/utils/size.ts | 9 +
webview-ui/src/utils/textMateToHljs.ts | 27 +-
webview-ui/src/utils/validate.ts | 23 +-
113 files changed, 7141 insertions(+), 1684 deletions(-)
create mode 100644 src/integrations/checkpoints/CheckpointTracker.ts
create mode 100644 webview-ui/src/components/common/CheckpointControls.tsx
create mode 100644 webview-ui/src/components/common/SuccessButton.tsx
create mode 100644 webview-ui/src/utils/size.ts
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index e1413836d1..6878c4156c 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -5,7 +5,11 @@
"tasks": [
{
"label": "watch",
- "dependsOn": ["npm: build:webview", "npm: watch:tsc", "npm: watch:esbuild"],
+ "dependsOn": [
+ "npm: build:webview",
+ "npm: watch:tsc",
+ "npm: watch:esbuild"
+ ],
"presentation": {
"reveal": "never"
},
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4baaf087ea..e78dd13dbe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Change Log
+## [3.1.0]
+
+- Added checkpoints: Snapshots of workspace are automatically created whenever Cline uses a tool
+ - Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
+ - Restore options: Choose to restore just the task state, just the workspace files, or both
+- New 'See new changes' button appears after task completion, providing an overview of all workspace changes
+- Task header now shows disk space usage with a delete button to help manage snapshot storage
+
## [3.0.12]
- Fix DeepSeek API cost reporting (input price is 0 since it's all either a cache read or write, different than how Anthropic reports cache usage)
diff --git a/README.md b/README.md
index 6819183bda..0da41abe92 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,18 @@ Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol),
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
+
+
+
+
+
+
+### Checkpoints: Compare and Restore
+
+As Cline works through a task, the extension takes a snapshot of your workspace at each step. You can use the 'Compare' button to see a diff between the snapshot and your current workspace, and the 'Restore' button to roll back to that point.
+
+For example, when working with a local web server, you can use 'Restore Workspace Only' to quickly test different versions of your app, then use 'Restore Task and Workspace' when you find the version you want to continue building from. This lets you safely explore different approaches without losing progress.
+
## Contributing
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
diff --git a/esbuild.js b/esbuild.js
index 8b203076e4..f4ddcc9f9f 100644
--- a/esbuild.js
+++ b/esbuild.js
@@ -18,7 +18,9 @@ const esbuildProblemMatcherPlugin = {
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
- console.error(` ${location.file}:${location.line}:${location.column}:`)
+ console.error(
+ ` ${location.file}:${location.line}:${location.column}:`,
+ )
})
console.log("[watch] build finished")
})
@@ -30,14 +32,26 @@ const copyWasmFiles = {
setup(build) {
build.onEnd(() => {
// tree sitter
- const sourceDir = path.join(__dirname, "node_modules", "web-tree-sitter")
+ const sourceDir = path.join(
+ __dirname,
+ "node_modules",
+ "web-tree-sitter",
+ )
const targetDir = path.join(__dirname, "dist")
// Copy tree-sitter.wasm
- fs.copyFileSync(path.join(sourceDir, "tree-sitter.wasm"), path.join(targetDir, "tree-sitter.wasm"))
+ fs.copyFileSync(
+ path.join(sourceDir, "tree-sitter.wasm"),
+ path.join(targetDir, "tree-sitter.wasm"),
+ )
// Copy language-specific WASM files
- const languageWasmDir = path.join(__dirname, "node_modules", "tree-sitter-wasms", "out")
+ const languageWasmDir = path.join(
+ __dirname,
+ "node_modules",
+ "tree-sitter-wasms",
+ "out",
+ )
const languages = [
"typescript",
"tsx",
@@ -56,7 +70,10 @@ const copyWasmFiles = {
languages.forEach((lang) => {
const filename = `tree-sitter-${lang}.wasm`
- fs.copyFileSync(path.join(languageWasmDir, filename), path.join(targetDir, filename))
+ fs.copyFileSync(
+ path.join(languageWasmDir, filename),
+ path.join(targetDir, filename),
+ )
})
})
},
diff --git a/package-lock.json b/package-lock.json
index 5723f20b33..42e106c3bc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
- "version": "3.0.9",
+ "version": "3.0.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
- "version": "3.0.9",
+ "version": "3.0.12",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
@@ -15,6 +15,7 @@
"@google/generative-ai": "^0.18.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@types/clone-deep": "^4.0.4",
+ "@types/get-folder-size": "^3.0.4",
"@types/pdf-parse": "^1.1.4",
"@types/turndown": "^5.0.5",
"@vscode/codicons": "^0.0.36",
@@ -27,6 +28,7 @@
"diff": "^5.2.0",
"execa": "^9.5.2",
"fast-deep-equal": "^3.1.3",
+ "get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
@@ -38,6 +40,7 @@
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"serialize-error": "^11.0.3",
+ "simple-git": "^3.27.0",
"strip-ansi": "^7.1.0",
"tree-sitter-wasms": "^0.1.11",
"turndown": "^7.2.0",
@@ -2777,6 +2780,21 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kwsites/file-exists": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
+ "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1"
+ }
+ },
+ "node_modules/@kwsites/promise-deferred": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
+ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
+ "license": "MIT"
+ },
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
@@ -4546,6 +4564,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/get-folder-size": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/get-folder-size/-/get-folder-size-3.0.4.tgz",
+ "integrity": "sha512-tSf/k7Undx6jKRwpChR9tl+0ZPf0BVwkjBRtJ5qSnz6iWm2ZRYMAS2MktC2u7YaTAFHmxpL/LBxI85M7ioJCSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -7198,6 +7225,18 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-folder-size": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-5.0.0.tgz",
+ "integrity": "sha512-+fgtvbL83tSDypEK+T411GDBQVQtxv+qtQgbV+HVa/TYubqDhNd5ghH/D6cOHY9iC5/88GtOZB7WI8PXy2A3bg==",
+ "license": "MIT",
+ "bin": {
+ "get-folder-size": "bin/get-folder-size.js"
+ },
+ "engines": {
+ "node": ">=18.11.0"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -10464,6 +10503,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-git": {
+ "version": "3.27.0",
+ "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz",
+ "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==",
+ "license": "MIT",
+ "dependencies": {
+ "@kwsites/file-exists": "^1.1.1",
+ "@kwsites/promise-deferred": "^1.1.1",
+ "debug": "^4.3.5"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/steveukx/git-js?sponsor=1"
+ }
+ },
"node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
diff --git a/package.json b/package.json
index aef1c202bf..281e236242 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline (prev. Claude Dev)",
"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.0.12",
+ "version": "3.1.0",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@@ -171,6 +171,7 @@
"@google/generative-ai": "^0.18.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@types/clone-deep": "^4.0.4",
+ "@types/get-folder-size": "^3.0.4",
"@types/pdf-parse": "^1.1.4",
"@types/turndown": "^5.0.5",
"@vscode/codicons": "^0.0.36",
@@ -183,6 +184,7 @@
"diff": "^5.2.0",
"execa": "^9.5.2",
"fast-deep-equal": "^3.1.3",
+ "get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
@@ -194,6 +196,7 @@
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"serialize-error": "^11.0.3",
+ "simple-git": "^3.27.0",
"strip-ansi": "^7.1.0",
"tree-sitter-wasms": "^0.1.11",
"turndown": "^7.2.0",
diff --git a/src/api/index.ts b/src/api/index.ts
index 287f843642..ce75fecd68 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -13,7 +13,10 @@ import { ApiStream } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
export interface ApiHandler {
- createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
+ createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream
getModel(): { id: string; info: ModelInfo }
}
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index c090f17c63..944883ff9b 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -22,7 +22,10 @@ export class AnthropicHandler implements ApiHandler {
})
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
let stream: AnthropicStream
const modelId = this.getModel().id
switch (modelId) {
@@ -35,19 +38,31 @@ export class AnthropicHandler implements ApiHandler {
The latest message will be the new user message, one before will be the assistant message from a previous request, and the user message before that will be a previously cached user message. So we need to mark the latest user message as ephemeral to cache it for the next request, and mark the second to last user message as ephemeral to let the server know the last message to retrieve from the cache for the current request..
*/
const userMsgIndices = messages.reduce(
- (acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
+ (acc, msg, index) =>
+ msg.role === "user" ? [...acc, index] : acc,
[] as number[],
)
- const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
- const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
+ const lastUserMsgIndex =
+ userMsgIndices[userMsgIndices.length - 1] ?? -1
+ const secondLastMsgUserIndex =
+ userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.client.beta.promptCaching.messages.create(
{
model: modelId,
max_tokens: this.getModel().info.maxTokens || 8192,
temperature: 0,
- system: [{ text: systemPrompt, type: "text", cache_control: { type: "ephemeral" } }], // setting cache breakpoint for system prompt so new tasks can reuse it
+ system: [
+ {
+ text: systemPrompt,
+ type: "text",
+ cache_control: { type: "ephemeral" },
+ },
+ ], // setting cache breakpoint for system prompt so new tasks can reuse it
messages: messages.map((message, index) => {
- if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
+ if (
+ index === lastUserMsgIndex ||
+ index === secondLastMsgUserIndex
+ ) {
return {
...message,
content:
@@ -56,13 +71,24 @@ export class AnthropicHandler implements ApiHandler {
{
type: "text",
text: message.content,
- cache_control: { type: "ephemeral" },
+ cache_control: {
+ type: "ephemeral",
+ },
},
]
- : message.content.map((content, contentIndex) =>
- contentIndex === message.content.length - 1
- ? { ...content, cache_control: { type: "ephemeral" } }
- : content,
+ : message.content.map(
+ (content, contentIndex) =>
+ contentIndex ===
+ message.content.length -
+ 1
+ ? {
+ ...content,
+ cache_control:
+ {
+ type: "ephemeral",
+ },
+ }
+ : content,
),
}
}
@@ -83,7 +109,10 @@ export class AnthropicHandler implements ApiHandler {
case "claude-3-opus-20240229":
case "claude-3-haiku-20240307":
return {
- headers: { "anthropic-beta": "prompt-caching-2024-07-31" },
+ headers: {
+ "anthropic-beta":
+ "prompt-caching-2024-07-31",
+ },
}
default:
return undefined
@@ -116,8 +145,10 @@ export class AnthropicHandler implements ApiHandler {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
- cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
- cacheReadTokens: usage.cache_read_input_tokens || undefined,
+ cacheWriteTokens:
+ usage.cache_creation_input_tokens || undefined,
+ cacheReadTokens:
+ usage.cache_read_input_tokens || undefined,
}
break
case "message_delta":
@@ -171,6 +202,9 @@ export class AnthropicHandler implements ApiHandler {
const id = modelId as AnthropicModelId
return { id, info: anthropicModels[id] }
}
- return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
+ return {
+ id: anthropicDefaultModelId,
+ info: anthropicModels[anthropicDefaultModelId],
+ }
}
}
diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts
index 58f75ad4ac..b02fbfa9e6 100644
--- a/src/api/providers/bedrock.ts
+++ b/src/api/providers/bedrock.ts
@@ -1,7 +1,13 @@
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler } from "../"
-import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api"
+import {
+ ApiHandlerOptions,
+ bedrockDefaultModelId,
+ BedrockModelId,
+ bedrockModels,
+ ModelInfo,
+} from "../../shared/api"
import { ApiStream } from "../transform/stream"
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
@@ -14,9 +20,15 @@ export class AwsBedrockHandler implements ApiHandler {
this.client = new AnthropicBedrock({
// Authenticate by either providing the keys below or use the default AWS credential providers, such as
// using ~/.aws/credentials or the "AWS_SECRET_ACCESS_KEY" and "AWS_ACCESS_KEY_ID" environment variables.
- ...(this.options.awsAccessKey ? { awsAccessKey: this.options.awsAccessKey } : {}),
- ...(this.options.awsSecretKey ? { awsSecretKey: this.options.awsSecretKey } : {}),
- ...(this.options.awsSessionToken ? { awsSessionToken: this.options.awsSessionToken } : {}),
+ ...(this.options.awsAccessKey
+ ? { awsAccessKey: this.options.awsAccessKey }
+ : {}),
+ ...(this.options.awsSecretKey
+ ? { awsSecretKey: this.options.awsSecretKey }
+ : {}),
+ ...(this.options.awsSessionToken
+ ? { awsSessionToken: this.options.awsSessionToken }
+ : {}),
// awsRegion changes the aws region to which the request is made. By default, we read AWS_REGION,
// and if that's not present, we default to us-east-1. Note that we do not read ~/.aws/config for the region.
@@ -24,7 +36,10 @@ export class AwsBedrockHandler implements ApiHandler {
})
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
// cross region inference requires prefixing the model id with the region
let modelId: string
if (this.options.awsUseCrossRegionInference) {
@@ -107,6 +122,9 @@ export class AwsBedrockHandler implements ApiHandler {
const id = modelId as BedrockModelId
return { id, info: bedrockModels[id] }
}
- return { id: bedrockDefaultModelId, info: bedrockModels[bedrockDefaultModelId] }
+ return {
+ id: bedrockDefaultModelId,
+ info: bedrockModels[bedrockDefaultModelId],
+ }
}
}
diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts
index 9539ce35aa..9601ca2242 100644
--- a/src/api/providers/deepseek.ts
+++ b/src/api/providers/deepseek.ts
@@ -1,7 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
-import { ApiHandlerOptions, DeepSeekModelId, ModelInfo, deepSeekDefaultModelId, deepSeekModels } from "../../shared/api"
+import {
+ ApiHandlerOptions,
+ DeepSeekModelId,
+ ModelInfo,
+ deepSeekDefaultModelId,
+ deepSeekModels,
+} from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@@ -17,12 +23,18 @@ export class DeepSeekHandler implements ApiHandler {
})
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
- messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
+ messages: [
+ { role: "system", content: systemPrompt },
+ ...convertToOpenAiMessages(messages),
+ ],
stream: true,
stream_options: { include_usage: true },
})
@@ -56,6 +68,9 @@ export class DeepSeekHandler implements ApiHandler {
const id = modelId as DeepSeekModelId
return { id, info: deepSeekModels[id] }
}
- return { id: deepSeekDefaultModelId, info: deepSeekModels[deepSeekDefaultModelId] }
+ return {
+ id: deepSeekDefaultModelId,
+ info: deepSeekModels[deepSeekDefaultModelId],
+ }
}
}
diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts
index d7ac5ec67d..de6bf394ec 100644
--- a/src/api/providers/gemini.ts
+++ b/src/api/providers/gemini.ts
@@ -1,7 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { GoogleGenerativeAI } from "@google/generative-ai"
import { ApiHandler } from "../"
-import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "../../shared/api"
+import {
+ ApiHandlerOptions,
+ geminiDefaultModelId,
+ GeminiModelId,
+ geminiModels,
+ ModelInfo,
+} from "../../shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
@@ -17,7 +23,10 @@ export class GeminiHandler implements ApiHandler {
this.client = new GoogleGenerativeAI(options.geminiApiKey)
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
const model = this.client.getGenerativeModel({
model: this.getModel().id,
systemInstruction: systemPrompt,
@@ -51,6 +60,9 @@ export class GeminiHandler implements ApiHandler {
const id = modelId as GeminiModelId
return { id, info: geminiModels[id] }
}
- return { id: geminiDefaultModelId, info: geminiModels[geminiDefaultModelId] }
+ return {
+ id: geminiDefaultModelId,
+ info: geminiModels[geminiDefaultModelId],
+ }
}
}
diff --git a/src/api/providers/lmstudio.ts b/src/api/providers/lmstudio.ts
index 868ef7da13..37fa67cea7 100644
--- a/src/api/providers/lmstudio.ts
+++ b/src/api/providers/lmstudio.ts
@@ -1,7 +1,11 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
-import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
+import {
+ ApiHandlerOptions,
+ ModelInfo,
+ openAiModelInfoSaneDefaults,
+} from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@@ -12,12 +16,17 @@ export class LmStudioHandler implements ApiHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
- baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
+ baseURL:
+ (this.options.lmStudioBaseUrl || "http://localhost:1234") +
+ "/v1",
apiKey: "noop",
})
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
diff --git a/src/api/providers/ollama.ts b/src/api/providers/ollama.ts
index 7668bd395f..01d4f73fe8 100644
--- a/src/api/providers/ollama.ts
+++ b/src/api/providers/ollama.ts
@@ -1,7 +1,11 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
-import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
+import {
+ ApiHandlerOptions,
+ ModelInfo,
+ openAiModelInfoSaneDefaults,
+} from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@@ -12,12 +16,17 @@ export class OllamaHandler implements ApiHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
- baseURL: (this.options.ollamaBaseUrl || "http://localhost:11434") + "/v1",
+ baseURL:
+ (this.options.ollamaBaseUrl || "http://localhost:11434") +
+ "/v1",
apiKey: "ollama",
})
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts
index 70d55b7abe..3e7dc86b2b 100644
--- a/src/api/providers/openai-native.ts
+++ b/src/api/providers/openai-native.ts
@@ -22,14 +22,20 @@ export class OpenAiNativeHandler implements ApiHandler {
})
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
switch (this.getModel().id) {
case "o1-preview":
case "o1-mini": {
// o1 doesnt support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
model: this.getModel().id,
- messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
+ messages: [
+ { role: "user", content: systemPrompt },
+ ...convertToOpenAiMessages(messages),
+ ],
})
yield {
type: "text",
@@ -47,7 +53,10 @@ export class OpenAiNativeHandler implements ApiHandler {
model: this.getModel().id,
// max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
- messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
+ messages: [
+ { role: "system", content: systemPrompt },
+ ...convertToOpenAiMessages(messages),
+ ],
stream: true,
stream_options: { include_usage: true },
})
@@ -80,6 +89,9 @@ export class OpenAiNativeHandler implements ApiHandler {
const id = modelId as OpenAiNativeModelId
return { id, info: openAiNativeModels[id] }
}
- return { id: openAiNativeDefaultModelId, info: openAiNativeModels[openAiNativeDefaultModelId] }
+ return {
+ id: openAiNativeDefaultModelId,
+ info: openAiNativeModels[openAiNativeDefaultModelId],
+ }
}
}
diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts
index 57cab17e68..241f621831 100644
--- a/src/api/providers/openai.ts
+++ b/src/api/providers/openai.ts
@@ -21,7 +21,9 @@ export class OpenAiHandler implements ApiHandler {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
- apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
+ apiVersion:
+ this.options.azureApiVersion ||
+ azureOpenAiDefaultApiVersion,
})
} else {
this.client = new OpenAI({
@@ -31,7 +33,10 @@ export class OpenAiHandler implements ApiHandler {
}
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 32e50de5d7..85b27f0df1 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -2,7 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler } from "../"
-import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
+import {
+ ApiHandlerOptions,
+ ModelInfo,
+ openRouterDefaultModelId,
+ openRouterDefaultModelInfo,
+} from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import delay from "delay"
@@ -23,7 +28,10 @@ export class OpenRouterHandler implements ApiHandler {
})
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
// Convert Anthropic messages to OpenAI format
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -58,14 +66,18 @@ export class OpenRouterHandler implements ApiHandler {
}
// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
- const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
+ const lastTwoUserMessages = openAiMessages
+ .filter((msg) => msg.role === "user")
+ .slice(-2)
lastTwoUserMessages.forEach((msg) => {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
- let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
+ let lastTextPart = msg.content
+ .filter((part) => part.type === "text")
+ .pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
@@ -97,7 +109,8 @@ export class OpenRouterHandler implements ApiHandler {
}
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
- let shouldApplyMiddleOutTransform = !this.getModel().info.supportsPromptCache
+ let shouldApplyMiddleOutTransform =
+ !this.getModel().info.supportsPromptCache
// except for deepseek (which we set supportsPromptCache to true for), where because the context window is so small our truncation algo might miss and we should use openrouter's middle-out transform as a fallback to ensure we don't exceed the context window (FIXME: once we have a more robust token estimator we should not rely on this)
if (this.getModel().id === "deepseek/deepseek-chat") {
shouldApplyMiddleOutTransform = true
@@ -110,7 +123,9 @@ export class OpenRouterHandler implements ApiHandler {
temperature: 0,
messages: openAiMessages,
stream: true,
- transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
+ transforms: shouldApplyMiddleOutTransform
+ ? ["middle-out"]
+ : undefined,
})
let genId: string | undefined
@@ -119,8 +134,12 @@ export class OpenRouterHandler implements ApiHandler {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as { message?: string; code?: number }
- console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
- throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
+ console.error(
+ `OpenRouter API Error: ${error?.code} - ${error?.message}`,
+ )
+ throw new Error(
+ `OpenRouter API Error ${error?.code}: ${error?.message}`,
+ )
}
if (!genId && chunk.id) {
@@ -146,12 +165,15 @@ export class OpenRouterHandler implements ApiHandler {
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
try {
- const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
- headers: {
- Authorization: `Bearer ${this.options.openRouterApiKey}`,
+ const response = await axios.get(
+ `https://openrouter.ai/api/v1/generation?id=${genId}`,
+ {
+ headers: {
+ Authorization: `Bearer ${this.options.openRouterApiKey}`,
+ },
+ timeout: 5_000, // this request hangs sometimes
},
- timeout: 5_000, // this request hangs sometimes
- })
+ )
const generation = response.data?.data
console.log("OpenRouter generation details:", response.data)
@@ -166,7 +188,10 @@ export class OpenRouterHandler implements ApiHandler {
}
} catch (error) {
// ignore if fails
- console.error("Error fetching OpenRouter generation details:", error)
+ console.error(
+ "Error fetching OpenRouter generation details:",
+ error,
+ )
}
}
@@ -176,6 +201,9 @@ export class OpenRouterHandler implements ApiHandler {
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
}
- return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
+ return {
+ id: openRouterDefaultModelId,
+ info: openRouterDefaultModelInfo,
+ }
}
}
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 60e6967dd6..304a491859 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -1,7 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { ApiHandler } from "../"
-import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
+import {
+ ApiHandlerOptions,
+ ModelInfo,
+ vertexDefaultModelId,
+ VertexModelId,
+ vertexModels,
+} from "../../shared/api"
import { ApiStream } from "../transform/stream"
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
@@ -18,7 +24,10 @@ export class VertexHandler implements ApiHandler {
})
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ async *createMessage(
+ systemPrompt: string,
+ messages: Anthropic.Messages.MessageParam[],
+ ): ApiStream {
const stream = await this.client.messages.create({
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens || 8192,
@@ -81,6 +90,9 @@ export class VertexHandler implements ApiHandler {
const id = modelId as VertexModelId
return { id, info: vertexModels[id] }
}
- return { id: vertexDefaultModelId, info: vertexModels[vertexDefaultModelId] }
+ return {
+ id: vertexDefaultModelId,
+ info: vertexModels[vertexDefaultModelId],
+ }
}
}
diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts
index 935e47147a..16332c3640 100644
--- a/src/api/transform/gemini-format.ts
+++ b/src/api/transform/gemini-format.ts
@@ -62,10 +62,20 @@ export function convertAnthropicContentToGemini(
} as FunctionResponsePart
} else {
// The only case when tool_result could be array is when the tool failed and we're providing ie user feedback potentially with images
- const textParts = block.content.filter((part) => part.type === "text")
- const imageParts = block.content.filter((part) => part.type === "image")
- const text = textParts.length > 0 ? textParts.map((part) => part.text).join("\n\n") : ""
- const imageText = imageParts.length > 0 ? "\n\n(See next part for image)" : ""
+ const textParts = block.content.filter(
+ (part) => part.type === "text",
+ )
+ const imageParts = block.content.filter(
+ (part) => part.type === "image",
+ )
+ const text =
+ textParts.length > 0
+ ? textParts.map((part) => part.text).join("\n\n")
+ : ""
+ const imageText =
+ imageParts.length > 0
+ ? "\n\n(See next part for image)"
+ : ""
return [
{
functionResponse: {
@@ -88,32 +98,40 @@ export function convertAnthropicContentToGemini(
]
}
default:
- throw new Error(`Unsupported content block type: ${(block as any).type}`)
+ throw new Error(
+ `Unsupported content block type: ${(block as any).type}`,
+ )
}
})
}
-export function convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam): Content {
+export function convertAnthropicMessageToGemini(
+ message: Anthropic.Messages.MessageParam,
+): Content {
return {
role: message.role === "assistant" ? "model" : "user",
parts: convertAnthropicContentToGemini(message.content),
}
}
-export function convertAnthropicToolToGemini(tool: Anthropic.Messages.Tool): FunctionDeclaration {
+export function convertAnthropicToolToGemini(
+ tool: Anthropic.Messages.Tool,
+): FunctionDeclaration {
return {
name: tool.name,
description: tool.description || "",
parameters: {
type: SchemaType.OBJECT,
properties: Object.fromEntries(
- Object.entries(tool.input_schema.properties || {}).map(([key, value]) => [
- key,
- {
- type: (value as any).type.toUpperCase(),
- description: (value as any).description || "",
- },
- ]),
+ Object.entries(tool.input_schema.properties || {}).map(
+ ([key, value]) => [
+ key,
+ {
+ type: (value as any).type.toUpperCase(),
+ description: (value as any).description || "",
+ },
+ ],
+ ),
),
required: (tool.input_schema.required as string[]) || [],
},
@@ -147,7 +165,10 @@ export function convertGeminiResponseToAnthropic(
const functionCalls = response.functionCalls()
if (functionCalls) {
functionCalls.forEach((call, index) => {
- if ("content" in call.args && typeof call.args.content === "string") {
+ if (
+ "content" in call.args &&
+ typeof call.args.content === "string"
+ ) {
call.args.content = unescapeGeminiContent(call.args.content)
}
content.push({
diff --git a/src/api/transform/o1-format.ts b/src/api/transform/o1-format.ts
index 1346fdbd54..16e4de4b6d 100644
--- a/src/api/transform/o1-format.ts
+++ b/src/api/transform/o1-format.ts
@@ -244,7 +244,10 @@ const toolNames = [
"attempt_completion",
]
-function parseAIResponse(response: string): { normalText: string; toolCalls: ToolCall[] } {
+function parseAIResponse(response: string): {
+ normalText: string
+ toolCalls: ToolCall[]
+} {
// Create a regex pattern to match any tool call opening tag
const toolCallPattern = new RegExp(`<(${toolNames.join("|")})`, "i")
const match = response.match(toolCallPattern)
@@ -269,7 +272,9 @@ function parseToolCalls(toolCallsText: string): ToolCall[] {
let remainingText = toolCallsText
while (remainingText.length > 0) {
- const toolMatch = toolNames.find((tool) => new RegExp(`<${tool}`, "i").test(remainingText))
+ const toolMatch = toolNames.find((tool) =>
+ new RegExp(`<${tool}`, "i").test(remainingText),
+ )
if (!toolMatch) {
break // No more tool calls found
@@ -284,7 +289,10 @@ function parseToolCalls(toolCallsText: string): ToolCall[] {
break // Malformed XML, no closing tag found
}
- const toolCallContent = remainingText.slice(startIndex, endIndex + endTag.length)
+ const toolCallContent = remainingText.slice(
+ startIndex,
+ endIndex + endTag.length,
+ )
remainingText = remainingText.slice(endIndex + endTag.length).trim()
const toolCall = parseToolCall(toolMatch, toolCallContent)
@@ -300,7 +308,9 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
const tool_input: Record = {}
// Remove the outer tool tags
- const innerContent = content.replace(new RegExp(`^<${toolName}>|${toolName}>$`, "g"), "").trim()
+ const innerContent = content
+ .replace(new RegExp(`^<${toolName}>|${toolName}>$`, "g"), "")
+ .trim()
// Parse nested XML elements
const paramRegex = /<(\w+)>([\s\S]*?)<\/\1>/gs
@@ -321,7 +331,10 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
return { tool: toolName, tool_input }
}
-function validateToolInput(toolName: string, tool_input: Record): boolean {
+function validateToolInput(
+ toolName: string,
+ tool_input: Record,
+): boolean {
switch (toolName) {
case "execute_command":
return "command" in tool_input
@@ -363,7 +376,9 @@ export function convertO1ResponseToAnthropicMessage(
completion: OpenAI.Chat.Completions.ChatCompletion,
): Anthropic.Messages.Message {
const openAiMessage = completion.choices[0].message
- const { normalText, toolCalls } = parseAIResponse(openAiMessage.content || "")
+ const { normalText, toolCalls } = parseAIResponse(
+ openAiMessage.content || "",
+ )
const anthropicMessage: Anthropic.Messages.Message = {
id: completion.id,
@@ -398,14 +413,16 @@ export function convertO1ResponseToAnthropicMessage(
if (toolCalls.length > 0) {
anthropicMessage.content.push(
- ...toolCalls.map((toolCall: ToolCall, index: number): Anthropic.ToolUseBlock => {
- return {
- type: "tool_use",
- id: `call_${index}_${Date.now()}`, // Generate a unique ID for each tool call
- name: toolCall.tool,
- input: toolCall.tool_input,
- }
- }),
+ ...toolCalls.map(
+ (toolCall: ToolCall, index: number): Anthropic.ToolUseBlock => {
+ return {
+ type: "tool_use",
+ id: `call_${index}_${Date.now()}`, // Generate a unique ID for each tool call
+ name: toolCall.tool,
+ input: toolCall.tool_input,
+ }
+ },
+ ),
)
}
diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts
index fe23b9b2ff..51ba165b58 100644
--- a/src/api/transform/openai-format.ts
+++ b/src/api/transform/openai-format.ts
@@ -8,7 +8,10 @@ export function convertToOpenAiMessages(
for (const anthropicMessage of anthropicMessages) {
if (typeof anthropicMessage.content === "string") {
- openAiMessages.push({ role: anthropicMessage.role, content: anthropicMessage.content })
+ openAiMessages.push({
+ role: anthropicMessage.role,
+ content: anthropicMessage.content,
+ })
} else {
// image_url.url is base64 encoded image data
// ensure it contains the content-type of the image: data:image/png;base64,
@@ -19,20 +22,27 @@ export function convertToOpenAiMessages(
{ role: "tool", tool_call_id: "", content: ""}
*/
if (anthropicMessage.role === "user") {
- const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
- nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
- toolMessages: Anthropic.ToolResultBlockParam[]
- }>(
- (acc, part) => {
- if (part.type === "tool_result") {
- acc.toolMessages.push(part)
- } else if (part.type === "text" || part.type === "image") {
- acc.nonToolMessages.push(part)
- } // user cannot send tool_use messages
- return acc
- },
- { nonToolMessages: [], toolMessages: [] },
- )
+ const { nonToolMessages, toolMessages } =
+ anthropicMessage.content.reduce<{
+ nonToolMessages: (
+ | Anthropic.TextBlockParam
+ | Anthropic.ImageBlockParam
+ )[]
+ toolMessages: Anthropic.ToolResultBlockParam[]
+ }>(
+ (acc, part) => {
+ if (part.type === "tool_result") {
+ acc.toolMessages.push(part)
+ } else if (
+ part.type === "text" ||
+ part.type === "image"
+ ) {
+ acc.nonToolMessages.push(part)
+ } // user cannot send tool_use messages
+ return acc
+ },
+ { nonToolMessages: [], toolMessages: [] },
+ )
// Process tool result messages FIRST since they must follow the tool use messages
let toolResultImages: Anthropic.Messages.ImageBlockParam[] = []
@@ -85,7 +95,9 @@ export function convertToOpenAiMessages(
if (part.type === "image") {
return {
type: "image_url",
- image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` },
+ image_url: {
+ url: `data:${part.source.media_type};base64,${part.source.data}`,
+ },
}
}
return { type: "text", text: part.text }
@@ -93,20 +105,27 @@ export function convertToOpenAiMessages(
})
}
} else if (anthropicMessage.role === "assistant") {
- const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
- nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
- toolMessages: Anthropic.ToolUseBlockParam[]
- }>(
- (acc, part) => {
- if (part.type === "tool_use") {
- acc.toolMessages.push(part)
- } else if (part.type === "text" || part.type === "image") {
- acc.nonToolMessages.push(part)
- } // assistant cannot send tool_result messages
- return acc
- },
- { nonToolMessages: [], toolMessages: [] },
- )
+ const { nonToolMessages, toolMessages } =
+ anthropicMessage.content.reduce<{
+ nonToolMessages: (
+ | Anthropic.TextBlockParam
+ | Anthropic.ImageBlockParam
+ )[]
+ toolMessages: Anthropic.ToolUseBlockParam[]
+ }>(
+ (acc, part) => {
+ if (part.type === "tool_use") {
+ acc.toolMessages.push(part)
+ } else if (
+ part.type === "text" ||
+ part.type === "image"
+ ) {
+ acc.nonToolMessages.push(part)
+ } // assistant cannot send tool_result messages
+ return acc
+ },
+ { nonToolMessages: [], toolMessages: [] },
+ )
// Process non-tool messages
let content: string | undefined
@@ -122,15 +141,16 @@ export function convertToOpenAiMessages(
}
// Process tool use messages
- let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
- id: toolMessage.id,
- type: "function",
- function: {
- name: toolMessage.name,
- // json string
- arguments: JSON.stringify(toolMessage.input),
- },
- }))
+ let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] =
+ toolMessages.map((toolMessage) => ({
+ id: toolMessage.id,
+ type: "function",
+ function: {
+ name: toolMessage.name,
+ // json string
+ arguments: JSON.stringify(toolMessage.input),
+ },
+ }))
openAiMessages.push({
role: "assistant",
@@ -183,20 +203,24 @@ export function convertToAnthropicMessage(
if (openAiMessage.tool_calls && openAiMessage.tool_calls.length > 0) {
anthropicMessage.content.push(
- ...openAiMessage.tool_calls.map((toolCall): Anthropic.ToolUseBlock => {
- let parsedInput = {}
- try {
- parsedInput = JSON.parse(toolCall.function.arguments || "{}")
- } catch (error) {
- console.error("Failed to parse tool arguments:", error)
- }
- return {
- type: "tool_use",
- id: toolCall.id,
- name: toolCall.function.name,
- input: parsedInput,
- }
- }),
+ ...openAiMessage.tool_calls.map(
+ (toolCall): Anthropic.ToolUseBlock => {
+ let parsedInput = {}
+ try {
+ parsedInput = JSON.parse(
+ toolCall.function.arguments || "{}",
+ )
+ } catch (error) {
+ console.error("Failed to parse tool arguments:", error)
+ }
+ return {
+ type: "tool_use",
+ id: toolCall.id,
+ name: toolCall.function.name,
+ input: parsedInput,
+ }
+ },
+ ),
)
}
return anthropicMessage
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 60bb309038..de94b62f89 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -9,8 +9,14 @@ import { serializeError } from "serialize-error"
import * as vscode from "vscode"
import { ApiHandler, buildApiHandler } from "../api"
import { ApiStream } from "../api/transform/stream"
-import { DiffViewProvider } from "../integrations/editor/DiffViewProvider"
-import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
+import {
+ DIFF_VIEW_URI_SCHEME,
+ DiffViewProvider,
+} from "../integrations/editor/DiffViewProvider"
+import {
+ findToolName,
+ formatContentBlockToMarkdown,
+} from "../integrations/misc/export-markdown"
import { extractTextFromFile } from "../integrations/misc/extract-text"
import { TerminalManager } from "../integrations/terminal/TerminalManager"
import { BrowserSession } from "../services/browser/BrowserSession"
@@ -19,10 +25,13 @@ import { listFiles } from "../services/glob/list-files"
import { regexSearchFiles } from "../services/ripgrep"
import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
import { ApiConfiguration } from "../shared/api"
-import { findLastIndex } from "../shared/array"
+import { findLast, findLastIndex } from "../shared/array"
import { AutoApprovalSettings } from "../shared/AutoApprovalSettings"
import { combineApiRequests } from "../shared/combineApiRequests"
-import { combineCommandSequences, COMMAND_REQ_APP_STRING } from "../shared/combineCommandSequences"
+import {
+ combineCommandSequences,
+ COMMAND_REQ_APP_STRING,
+} from "../shared/combineCommandSequences"
import {
BrowserAction,
BrowserActionResult,
@@ -35,31 +44,49 @@ import {
ClineSay,
ClineSayBrowserAction,
ClineSayTool,
+ COMPLETION_RESULT_CHANGES_FLAG,
} from "../shared/ExtensionMessage"
import { getApiMetrics } from "../shared/getApiMetrics"
import { HistoryItem } from "../shared/HistoryItem"
-import { ClineAskResponse } from "../shared/WebviewMessage"
+import {
+ ClineAskResponse,
+ ClineCheckpointRestore,
+} from "../shared/WebviewMessage"
import { calculateApiCost } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
-import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
+import {
+ AssistantMessageContent,
+ parseAssistantMessage,
+ ToolParamName,
+ ToolUseName,
+} from "./assistant-message"
import { constructNewFileContent } from "./assistant-message/diff"
import { parseMentions } from "./mentions"
import { formatResponse } from "./prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system"
-import { truncateHalfConversation } from "./sliding-window"
+import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
import { showSystemNotification } from "../integrations/notifications"
import { removeInvalidChars } from "../utils/string"
import { fixModelHtmlEscaping } from "../utils/string"
import { OpenAiHandler } from "../api/providers/openai"
+import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker"
+import getFolderSize from "get-folder-size"
const cwd =
- vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
+ vscode.workspace.workspaceFolders
+ ?.map((folder) => folder.uri.fsPath)
+ .at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
-type ToolResponse = string | Array
+type ToolResponse =
+ | string
+ | Array
type UserContent = Array<
- Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
+ | Anthropic.TextBlockParam
+ | Anthropic.ImageBlockParam
+ | Anthropic.ToolUseBlockParam
+ | Anthropic.ToolResultBlockParam
>
export class Cline {
@@ -81,16 +108,24 @@ export class Cline {
private consecutiveMistakeCount: number = 0
private providerRef: WeakRef
private abort: boolean = false
- didFinishAborting = false
+ didFinishAbortingStream = false
abandoned = false
private diffViewProvider: DiffViewProvider
+ private checkpointTracker?: CheckpointTracker
+ checkpointTrackerErrorMessage?: string
+ conversationHistoryDeletedRange?: [number, number]
+ isInitialized = false
// streaming
+ isStreaming = false
private currentStreamingContentIndex = 0
private assistantMessageContent: AssistantMessageContent[] = []
private presentAssistantMessageLocked = false
private presentAssistantMessageHasPendingUpdates = false
- private userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
+ private userMessageContent: (
+ | Anthropic.TextBlockParam
+ | Anthropic.ImageBlockParam
+ )[] = []
private userMessageContentReady = false
private didRejectTool = false
private didAlreadyUseTool = false
@@ -115,19 +150,24 @@ export class Cline {
this.autoApprovalSettings = autoApprovalSettings
if (historyItem) {
this.taskId = historyItem.id
+ this.conversationHistoryDeletedRange =
+ historyItem.conversationHistoryDeletedRange
this.resumeTaskFromHistory()
} else if (task || images) {
this.taskId = Date.now().toString()
this.startTask(task, images)
} else {
- throw new Error("Either historyItem or task/images must be provided")
+ throw new Error(
+ "Either historyItem or task/images must be provided",
+ )
}
}
// Storing task to disk for history
private async ensureTaskDirectoryExists(): Promise {
- const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
+ const globalStoragePath =
+ this.providerRef.deref()?.context.globalStorageUri.fsPath
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
@@ -136,8 +176,13 @@ export class Cline {
return taskDir
}
- private async getSavedApiConversationHistory(): Promise {
- const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
+ private async getSavedApiConversationHistory(): Promise<
+ Anthropic.MessageParam[]
+ > {
+ const filePath = path.join(
+ await this.ensureTaskDirectoryExists(),
+ GlobalFileNames.apiConversationHistory,
+ )
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
@@ -150,15 +195,23 @@ export class Cline {
await this.saveApiConversationHistory()
}
- private async overwriteApiConversationHistory(newHistory: Anthropic.MessageParam[]) {
+ private async overwriteApiConversationHistory(
+ newHistory: Anthropic.MessageParam[],
+ ) {
this.apiConversationHistory = newHistory
await this.saveApiConversationHistory()
}
private async saveApiConversationHistory() {
try {
- const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
- await fs.writeFile(filePath, JSON.stringify(this.apiConversationHistory))
+ const filePath = path.join(
+ await this.ensureTaskDirectoryExists(),
+ GlobalFileNames.apiConversationHistory,
+ )
+ await fs.writeFile(
+ filePath,
+ JSON.stringify(this.apiConversationHistory),
+ )
} catch (error) {
// in the off chance this fails, we don't want to stop the task
console.error("Failed to save API conversation history:", error)
@@ -166,12 +219,18 @@ export class Cline {
}
private async getSavedClineMessages(): Promise {
- const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages)
+ const filePath = path.join(
+ await this.ensureTaskDirectoryExists(),
+ GlobalFileNames.uiMessages,
+ )
if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
} else {
// check old location
- const oldPath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json")
+ const oldPath = path.join(
+ await this.ensureTaskDirectoryExists(),
+ "claude_messages.json",
+ )
if (await fileExistsAtPath(oldPath)) {
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
await fs.unlink(oldPath) // remove old file
@@ -182,6 +241,12 @@ export class Cline {
}
private async addToClineMessages(message: ClineMessage) {
+ // these values allow us to reconstruct the conversation history at the time this cline message was created
+ // it's important that apiConversationHistory is initialized before we add cline messages
+ message.conversationHistoryIndex =
+ this.apiConversationHistory.length - 1 // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when reseting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to
+ message.conversationHistoryDeletedRange =
+ this.conversationHistoryDeletedRange
this.clineMessages.push(message)
await this.saveClineMessages()
}
@@ -193,18 +258,39 @@ export class Cline {
private async saveClineMessages() {
try {
- const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages)
+ const taskDir = await this.ensureTaskDirectoryExists()
+ const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
await fs.writeFile(filePath, JSON.stringify(this.clineMessages))
// combined as they are in ChatView
- const apiMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(this.clineMessages.slice(1))))
+ const apiMetrics = getApiMetrics(
+ combineApiRequests(
+ combineCommandSequences(this.clineMessages.slice(1)),
+ ),
+ )
const taskMessage = this.clineMessages[0] // first message is always the task say
const lastRelevantMessage =
this.clineMessages[
findLastIndex(
this.clineMessages,
- (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"),
+ (m) =>
+ !(
+ m.ask === "resume_task" ||
+ m.ask === "resume_completed_task"
+ ),
)
]
+ let taskDirSize = 0
+ try {
+ // getFolderSize.loose silently ignores errors
+ // returns # of bytes, size/1000/1000 = MB
+ taskDirSize = await getFolderSize.loose(taskDir)
+ } catch (error) {
+ console.error(
+ "Failed to get task directory size:",
+ taskDir,
+ error,
+ )
+ }
await this.providerRef.deref()?.updateTaskHistory({
id: this.taskId,
ts: lastRelevantMessage.ts,
@@ -214,12 +300,347 @@ export class Cline {
cacheWrites: apiMetrics.totalCacheWrites,
cacheReads: apiMetrics.totalCacheReads,
totalCost: apiMetrics.totalCost,
+ size: taskDirSize,
+ shadowGitConfigWorkTree:
+ await this.checkpointTracker?.getShadowGitConfigWorkTree(),
+ conversationHistoryDeletedRange:
+ this.conversationHistoryDeletedRange,
})
} catch (error) {
console.error("Failed to save cline messages:", error)
}
}
+ async restoreCheckpoint(
+ messageTs: number,
+ restoreType: ClineCheckpointRestore,
+ ) {
+ const messageIndex = this.clineMessages.findIndex(
+ (m) => m.ts === messageTs,
+ )
+ const message = this.clineMessages[messageIndex]
+ if (!message) {
+ console.error("Message not found", this.clineMessages)
+ return
+ }
+
+ let didWorkspaceRestoreFail = false
+
+ switch (restoreType) {
+ case "task":
+ break
+ case "taskAndWorkspace":
+ case "workspace":
+ if (!this.checkpointTracker) {
+ try {
+ this.checkpointTracker = await CheckpointTracker.create(
+ this.taskId,
+ this.providerRef.deref(),
+ )
+ this.checkpointTrackerErrorMessage = undefined
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : "Unknown error"
+ console.error(
+ "Failed to initialize checkpoint tracker:",
+ errorMessage,
+ )
+ this.checkpointTrackerErrorMessage = errorMessage
+ await this.providerRef.deref()?.postStateToWebview()
+ vscode.window.showErrorMessage(errorMessage)
+ didWorkspaceRestoreFail = true
+ }
+ }
+ if (message.lastCheckpointHash && this.checkpointTracker) {
+ try {
+ await this.checkpointTracker.resetHead(
+ message.lastCheckpointHash,
+ )
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : "Unknown error"
+ vscode.window.showErrorMessage(
+ "Failed to restore checkpoint: " + errorMessage,
+ )
+ didWorkspaceRestoreFail = true
+ }
+ }
+ break
+ }
+
+ if (!didWorkspaceRestoreFail) {
+ switch (restoreType) {
+ case "task":
+ case "taskAndWorkspace":
+ this.conversationHistoryDeletedRange =
+ message.conversationHistoryDeletedRange
+ const newConversationHistory =
+ this.apiConversationHistory.slice(
+ 0,
+ (message.conversationHistoryIndex || 0) + 2,
+ ) // +1 since this index corresponds to the last user message, and another +1 since slice end index is exclusive
+ await this.overwriteApiConversationHistory(
+ newConversationHistory,
+ )
+
+ // aggregate deleted api reqs info so we don't lose costs/tokens
+ const deletedMessages = this.clineMessages.slice(
+ messageIndex + 1,
+ )
+ const deletedApiReqsMetrics = getApiMetrics(
+ combineApiRequests(
+ combineCommandSequences(deletedMessages),
+ ),
+ )
+
+ const newClineMessages = this.clineMessages.slice(
+ 0,
+ messageIndex + 1,
+ )
+ await this.overwriteClineMessages(newClineMessages) // calls saveClineMessages which saves historyItem
+
+ await this.say(
+ "deleted_api_reqs",
+ JSON.stringify({
+ tokensIn: deletedApiReqsMetrics.totalTokensIn,
+ tokensOut: deletedApiReqsMetrics.totalTokensOut,
+ cacheWrites: deletedApiReqsMetrics.totalCacheWrites,
+ cacheReads: deletedApiReqsMetrics.totalCacheReads,
+ cost: deletedApiReqsMetrics.totalCost,
+ } satisfies ClineApiReqInfo),
+ )
+ break
+ case "workspace":
+ break
+ }
+
+ switch (restoreType) {
+ case "task":
+ vscode.window.showInformationMessage(
+ "Task messages have been restored to the checkpoint",
+ )
+ break
+ case "workspace":
+ vscode.window.showInformationMessage(
+ "Workspace files have been restored to the checkpoint",
+ )
+ break
+ case "taskAndWorkspace":
+ vscode.window.showInformationMessage(
+ "Task and workspace have been restored to the checkpoint",
+ )
+ break
+ }
+
+ await this.providerRef
+ .deref()
+ ?.postMessageToWebview({ type: "relinquishControl" })
+
+ this.providerRef.deref()?.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
+ } else {
+ await this.providerRef
+ .deref()
+ ?.postMessageToWebview({ type: "relinquishControl" })
+ }
+ }
+
+ async presentMultifileDiff(
+ messageTs: number,
+ seeNewChangesSinceLastTaskCompletion: boolean,
+ ) {
+ const relinquishButton = () => {
+ this.providerRef
+ .deref()
+ ?.postMessageToWebview({ type: "relinquishControl" })
+ }
+
+ console.log("presentMultifileDiff", messageTs)
+ const messageIndex = this.clineMessages.findIndex(
+ (m) => m.ts === messageTs,
+ )
+ const message = this.clineMessages[messageIndex]
+ if (!message) {
+ console.error("Message not found")
+ relinquishButton()
+ return
+ }
+ const hash = message.lastCheckpointHash
+ if (!hash) {
+ console.error("No checkpoint hash found")
+ relinquishButton()
+ return
+ }
+
+ // TODO: handle if this is called from outside original workspace, in which case we need to show user error message we cant show diff outside of workspace?
+ if (!this.checkpointTracker) {
+ try {
+ this.checkpointTracker = await CheckpointTracker.create(
+ this.taskId,
+ this.providerRef.deref(),
+ )
+ this.checkpointTrackerErrorMessage = undefined
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error"
+ console.error(
+ "Failed to initialize checkpoint tracker:",
+ errorMessage,
+ )
+ this.checkpointTrackerErrorMessage = errorMessage
+ await this.providerRef.deref()?.postStateToWebview()
+ vscode.window.showErrorMessage(errorMessage)
+ relinquishButton()
+ return
+ }
+ }
+
+ let changedFiles:
+ | {
+ relativePath: string
+ absolutePath: string
+ before: string
+ after: string
+ }[]
+ | undefined
+
+ try {
+ if (seeNewChangesSinceLastTaskCompletion) {
+ // Get last task completed
+ const lastTaskCompletedMessage = findLast(
+ this.clineMessages.slice(0, messageIndex),
+ (m) => m.say === "completion_result",
+ ) // ask is only used to relinquish control, its the last say we care about
+ // if undefined, then we get diff from beginning of git
+ // if (!lastTaskCompletedMessage) {
+ // console.error("No previous task completion message found")
+ // return
+ // }
+
+ // Get changed files between current state and commit
+ changedFiles = await this.checkpointTracker?.getDiffSet(
+ lastTaskCompletedMessage?.lastCheckpointHash, // if undefined, then we get diff from beginning of git history, AKA when the task was started
+ hash,
+ )
+ if (!changedFiles?.length) {
+ vscode.window.showInformationMessage("No changes found")
+ relinquishButton()
+ return
+ }
+ } else {
+ // Get changed files between current state and commit
+ changedFiles = await this.checkpointTracker?.getDiffSet(hash)
+ if (!changedFiles?.length) {
+ vscode.window.showInformationMessage("No changes found")
+ relinquishButton()
+ return
+ }
+ }
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error"
+ vscode.window.showErrorMessage(
+ "Failed to retrieve diff set: " + errorMessage,
+ )
+ relinquishButton()
+ return
+ }
+
+ // Check if multi-diff editor is enabled in VS Code settings
+ // const config = vscode.workspace.getConfiguration()
+ // const isMultiDiffEnabled = config.get("multiDiffEditor.experimental.enabled")
+
+ // if (!isMultiDiffEnabled) {
+ // vscode.window.showErrorMessage(
+ // "Please enable 'multiDiffEditor.experimental.enabled' in your VS Code settings to use this feature.",
+ // )
+ // relinquishButton()
+ // return
+ // }
+ // Open multi-diff editor
+ await vscode.commands.executeCommand(
+ "vscode.changes",
+ seeNewChangesSinceLastTaskCompletion
+ ? "New changes"
+ : "Changes since snapshot",
+ changedFiles.map((file) => [
+ vscode.Uri.file(file.absolutePath),
+ vscode.Uri.parse(
+ `${DIFF_VIEW_URI_SCHEME}:${file.relativePath}`,
+ ).with({
+ query: Buffer.from(file.before ?? "").toString("base64"),
+ }),
+ vscode.Uri.parse(
+ `${DIFF_VIEW_URI_SCHEME}:${file.relativePath}`,
+ ).with({
+ query: Buffer.from(file.after ?? "").toString("base64"),
+ }),
+ ]),
+ )
+ relinquishButton()
+ }
+
+ async doesLatestTaskCompletionHaveNewChanges() {
+ const messageIndex = findLastIndex(
+ this.clineMessages,
+ (m) => m.say === "completion_result",
+ )
+ const message = this.clineMessages[messageIndex]
+ if (!message) {
+ console.error("Completion message not found")
+ return false
+ }
+ const hash = message.lastCheckpointHash
+ if (!hash) {
+ console.error("No checkpoint hash found")
+ return false
+ }
+
+ if (!this.checkpointTracker) {
+ try {
+ this.checkpointTracker = await CheckpointTracker.create(
+ this.taskId,
+ this.providerRef.deref(),
+ )
+ this.checkpointTrackerErrorMessage = undefined
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error"
+ console.error(
+ "Failed to initialize checkpoint tracker:",
+ errorMessage,
+ )
+ return false
+ }
+ }
+
+ // Get last task completed
+ const lastTaskCompletedMessage = findLast(
+ this.clineMessages.slice(0, messageIndex),
+ (m) => m.say === "completion_result",
+ )
+
+ try {
+ // Get changed files between current state and commit
+ const changedFiles = await this.checkpointTracker?.getDiffSet(
+ lastTaskCompletedMessage?.lastCheckpointHash, // if undefined, then we get diff from beginning of git history, AKA when the task was started
+ hash,
+ )
+ const changedFilesCount = changedFiles?.length || 0
+ if (changedFilesCount > 0) {
+ return true
+ }
+ } catch (error) {
+ console.error("Failed to get diff set:", error)
+ return false
+ }
+
+ return false
+ }
+
// Communicate with webview
// partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message)
@@ -227,7 +648,11 @@ export class Cline {
type: ClineAsk,
text?: string,
partial?: boolean,
- ): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
+ ): Promise<{
+ response: ClineAskResponse
+ text?: string
+ images?: string[]
+ }> {
// If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of Cline now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
if (this.abort) {
throw new Error("Cline instance aborted")
@@ -236,7 +661,10 @@ export class Cline {
if (partial !== undefined) {
const lastMessage = this.clineMessages.at(-1)
const isUpdatingPreviousPartial =
- lastMessage && lastMessage.partial && lastMessage.type === "ask" && lastMessage.ask === type
+ lastMessage &&
+ lastMessage.partial &&
+ lastMessage.type === "ask" &&
+ lastMessage.ask === type
if (partial) {
if (isUpdatingPreviousPartial) {
// existing partial message, so update it
@@ -247,7 +675,10 @@ export class Cline {
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef
.deref()
- ?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
+ ?.postMessageToWebview({
+ type: "partialMessage",
+ partialMessage: lastMessage,
+ })
throw new Error("Current ask promise was ignored 1")
} else {
// this is a new partial message, so add it with partial state
@@ -256,7 +687,13 @@ export class Cline {
// this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
- await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial })
+ await this.addToClineMessages({
+ ts: askTs,
+ type: "ask",
+ ask: type,
+ text,
+ partial,
+ })
await this.providerRef.deref()?.postStateToWebview()
throw new Error("Current ask promise was ignored 2")
}
@@ -283,7 +720,10 @@ export class Cline {
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef
.deref()
- ?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
+ ?.postMessageToWebview({
+ type: "partialMessage",
+ partialMessage: lastMessage,
+ })
} else {
// this is a new partial=false message, so add it like normal
this.askResponse = undefined
@@ -291,7 +731,12 @@ export class Cline {
this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
- await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
+ await this.addToClineMessages({
+ ts: askTs,
+ type: "ask",
+ ask: type,
+ text,
+ })
await this.providerRef.deref()?.postStateToWebview()
}
}
@@ -303,28 +748,50 @@ export class Cline {
this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
- await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
+ await this.addToClineMessages({
+ ts: askTs,
+ type: "ask",
+ ask: type,
+ text,
+ })
await this.providerRef.deref()?.postStateToWebview()
}
- await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
+ await pWaitFor(
+ () =>
+ this.askResponse !== undefined || this.lastMessageTs !== askTs,
+ { interval: 100 },
+ )
if (this.lastMessageTs !== askTs) {
throw new Error("Current ask promise was ignored") // could happen if we send multiple asks in a row i.e. with command_output. It's important that when we know an ask could fail, it is handled gracefully
}
- const result = { response: this.askResponse!, text: this.askResponseText, images: this.askResponseImages }
+ const result = {
+ response: this.askResponse!,
+ text: this.askResponseText,
+ images: this.askResponseImages,
+ }
this.askResponse = undefined
this.askResponseText = undefined
this.askResponseImages = undefined
return result
}
- async handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) {
+ async handleWebviewAskResponse(
+ askResponse: ClineAskResponse,
+ text?: string,
+ images?: string[],
+ ) {
this.askResponse = askResponse
this.askResponseText = text
this.askResponseImages = images
}
- async say(type: ClineSay, text?: string, images?: string[], partial?: boolean): Promise {
+ async say(
+ type: ClineSay,
+ text?: string,
+ images?: string[],
+ partial?: boolean,
+ ): Promise {
if (this.abort) {
throw new Error("Cline instance aborted")
}
@@ -332,7 +799,10 @@ export class Cline {
if (partial !== undefined) {
const lastMessage = this.clineMessages.at(-1)
const isUpdatingPreviousPartial =
- lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type
+ lastMessage &&
+ lastMessage.partial &&
+ lastMessage.type === "say" &&
+ lastMessage.say === type
if (partial) {
if (isUpdatingPreviousPartial) {
// existing partial message, so update it
@@ -341,12 +811,22 @@ export class Cline {
lastMessage.partial = partial
await this.providerRef
.deref()
- ?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
+ ?.postMessageToWebview({
+ type: "partialMessage",
+ partialMessage: lastMessage,
+ })
} else {
// this is a new partial message, so add it with partial state
const sayTs = Date.now()
this.lastMessageTs = sayTs
- await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images, partial })
+ await this.addToClineMessages({
+ ts: sayTs,
+ type: "say",
+ say: type,
+ text,
+ images,
+ partial,
+ })
await this.providerRef.deref()?.postStateToWebview()
}
} else {
@@ -364,12 +844,21 @@ export class Cline {
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef
.deref()
- ?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage }) // more performant than an entire postStateToWebview
+ ?.postMessageToWebview({
+ type: "partialMessage",
+ partialMessage: lastMessage,
+ }) // more performant than an entire postStateToWebview
} else {
// this is a new partial=false message, so add it like normal
const sayTs = Date.now()
this.lastMessageTs = sayTs
- await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images })
+ await this.addToClineMessages({
+ ts: sayTs,
+ type: "say",
+ say: type,
+ text,
+ images,
+ })
await this.providerRef.deref()?.postStateToWebview()
}
}
@@ -377,22 +866,37 @@ export class Cline {
// this is a new non-partial message, so add it like normal
const sayTs = Date.now()
this.lastMessageTs = sayTs
- await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images })
+ await this.addToClineMessages({
+ ts: sayTs,
+ type: "say",
+ say: type,
+ text,
+ images,
+ })
await this.providerRef.deref()?.postStateToWebview()
}
}
- async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) {
+ async sayAndCreateMissingParamError(
+ toolName: ToolUseName,
+ paramName: string,
+ relPath?: string,
+ ) {
await this.say(
"error",
`Cline tried to use ${toolName}${
relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter '${paramName}'. Retrying...`,
)
- return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
+ return formatResponse.toolError(
+ formatResponse.missingToolParameterError(paramName),
+ )
}
- async removeLastPartialMessageIfExistsWithType(type: "ask" | "say", askOrSay: ClineAsk | ClineSay) {
+ async removeLastPartialMessageIfExistsWithType(
+ type: "ask" | "say",
+ askOrSay: ClineAsk | ClineSay,
+ ) {
const lastMessage = this.clineMessages.at(-1)
if (
lastMessage?.partial &&
@@ -416,23 +920,36 @@ export class Cline {
await this.say("text", task, images)
- let imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
- await this.initiateTaskLoop([
- {
- type: "text",
- text: `\n${task}\n`,
- },
- ...imageBlocks,
- ])
+ this.isInitialized = true
+
+ let imageBlocks: Anthropic.ImageBlockParam[] =
+ formatResponse.imageBlocks(images)
+ await this.initiateTaskLoop(
+ [
+ {
+ type: "text",
+ text: `\n${task}\n`,
+ },
+ ...imageBlocks,
+ ],
+ true,
+ )
}
private async resumeTaskFromHistory() {
+ // TODO: right now we let users init checkpoints for old tasks, assuming they're continuing them from the same workspace (which we never tied to tasks, so no way for us to know if it's opened in the right workspace)
+ // const doesShadowGitExist = await CheckpointTracker.doesShadowGitExist(this.taskId, this.providerRef.deref())
+ // if (!doesShadowGitExist) {
+ // this.checkpointTrackerErrorMessage = "Checkpoints are only available for new tasks"
+ // }
+
const modifiedClineMessages = await this.getSavedClineMessages()
// Remove any resume messages that may have been added before
const lastRelevantMessageIndex = findLastIndex(
modifiedClineMessages,
- (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"),
+ (m) =>
+ !(m.ask === "resume_task" || m.ask === "resume_completed_task"),
)
if (lastRelevantMessageIndex !== -1) {
modifiedClineMessages.splice(lastRelevantMessageIndex + 1)
@@ -444,8 +961,11 @@ export class Cline {
(m) => m.type === "say" && m.say === "api_req_started",
)
if (lastApiReqStartedIndex !== -1) {
- const lastApiReqStarted = modifiedClineMessages[lastApiReqStartedIndex]
- const { cost, cancelReason }: ClineApiReqInfo = JSON.parse(lastApiReqStarted.text || "{}")
+ const lastApiReqStarted =
+ modifiedClineMessages[lastApiReqStartedIndex]
+ const { cost, cancelReason }: ClineApiReqInfo = JSON.parse(
+ lastApiReqStarted.text || "{}",
+ )
if (cost === undefined && cancelReason === undefined) {
modifiedClineMessages.splice(lastApiReqStartedIndex, 1)
}
@@ -454,12 +974,21 @@ export class Cline {
await this.overwriteClineMessages(modifiedClineMessages)
this.clineMessages = await this.getSavedClineMessages()
- // Now present the cline messages to the user and ask if they want to resume
+ // Now present the cline messages to the user and ask if they want to resume (NOTE: we ran into a bug before where the apiconversationhistory wouldnt be initialized when opening a old task, and it was because we were waiting for resume)
+ // This is important in case the user deletes messages without resuming the task first
+ this.apiConversationHistory =
+ await this.getSavedApiConversationHistory()
const lastClineMessage = this.clineMessages
.slice()
.reverse()
- .find((m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")) // could be multiple resume tasks
+ .find(
+ (m) =>
+ !(
+ m.ask === "resume_task" ||
+ m.ask === "resume_completed_task"
+ ),
+ ) // could be multiple resume tasks
// const lastClineMessage = this.clineMessages[lastClineMessageIndex]
// could be a completion result with a command
// const secondLastClineMessage = this.clineMessages
@@ -478,6 +1007,8 @@ export class Cline {
askType = "resume_task"
}
+ this.isInitialized = true
+
const { response, text, images } = await this.ask(askType) // calls poststatetowebview
let responseText: string | undefined
let responseImages: string[] | undefined
@@ -493,36 +1024,51 @@ export class Cline {
await this.getSavedApiConversationHistory()
// v2.0 xml tags refactor caveat: since we don't use tools anymore, we need to replace all tool use blocks with a text block since the API disallows conversations with tool uses and no tool schema
- const conversationWithoutToolBlocks = existingApiConversationHistory.map((message) => {
- if (Array.isArray(message.content)) {
- const newContent = message.content.map((block) => {
- if (block.type === "tool_use") {
- // it's important we convert to the new tool schema format so the model doesn't get confused about how to invoke tools
- const inputAsXml = Object.entries(block.input as Record)
- .map(([key, value]) => `<${key}>\n${value}\n${key}>`)
- .join("\n")
- return {
- type: "text",
- text: `<${block.name}>\n${inputAsXml}\n${block.name}>`,
- } as Anthropic.Messages.TextBlockParam
- } else if (block.type === "tool_result") {
- // Convert block.content to text block array, removing images
- const contentAsTextBlocks = Array.isArray(block.content)
- ? block.content.filter((item) => item.type === "text")
- : [{ type: "text", text: block.content }]
- const textContent = contentAsTextBlocks.map((item) => item.text).join("\n\n")
- const toolName = findToolName(block.tool_use_id, existingApiConversationHistory)
- return {
- type: "text",
- text: `[${toolName} Result]\n\n${textContent}`,
- } as Anthropic.Messages.TextBlockParam
- }
- return block
- })
- return { ...message, content: newContent }
- }
- return message
- })
+ const conversationWithoutToolBlocks =
+ existingApiConversationHistory.map((message) => {
+ if (Array.isArray(message.content)) {
+ const newContent = message.content.map((block) => {
+ if (block.type === "tool_use") {
+ // it's important we convert to the new tool schema format so the model doesn't get confused about how to invoke tools
+ const inputAsXml = Object.entries(
+ block.input as Record,
+ )
+ .map(
+ ([key, value]) =>
+ `<${key}>\n${value}\n${key}>`,
+ )
+ .join("\n")
+ return {
+ type: "text",
+ text: `<${block.name}>\n${inputAsXml}\n${block.name}>`,
+ } as Anthropic.Messages.TextBlockParam
+ } else if (block.type === "tool_result") {
+ // Convert block.content to text block array, removing images
+ const contentAsTextBlocks = Array.isArray(
+ block.content,
+ )
+ ? block.content.filter(
+ (item) => item.type === "text",
+ )
+ : [{ type: "text", text: block.content }]
+ const textContent = contentAsTextBlocks
+ .map((item) => item.text)
+ .join("\n\n")
+ const toolName = findToolName(
+ block.tool_use_id,
+ existingApiConversationHistory,
+ )
+ return {
+ type: "text",
+ text: `[${toolName} Result]\n\n${textContent}`,
+ } as Anthropic.Messages.TextBlockParam
+ }
+ return block
+ })
+ return { ...message, content: newContent }
+ }
+ return message
+ })
existingApiConversationHistory = conversationWithoutToolBlocks
// FIXME: remove tool use blocks altogether
@@ -536,40 +1082,67 @@ export class Cline {
let modifiedOldUserContent: UserContent // either the last message if its user message, or the user message before the last (assistant) message
let modifiedApiConversationHistory: Anthropic.Messages.MessageParam[] // need to remove the last user message to replace with new modified user message
if (existingApiConversationHistory.length > 0) {
- const lastMessage = existingApiConversationHistory[existingApiConversationHistory.length - 1]
+ const lastMessage =
+ existingApiConversationHistory[
+ existingApiConversationHistory.length - 1
+ ]
if (lastMessage.role === "assistant") {
const content = Array.isArray(lastMessage.content)
? lastMessage.content
: [{ type: "text", text: lastMessage.content }]
- const hasToolUse = content.some((block) => block.type === "tool_use")
+ const hasToolUse = content.some(
+ (block) => block.type === "tool_use",
+ )
if (hasToolUse) {
const toolUseBlocks = content.filter(
(block) => block.type === "tool_use",
) as Anthropic.Messages.ToolUseBlock[]
- const toolResponses: Anthropic.ToolResultBlockParam[] = toolUseBlocks.map((block) => ({
- type: "tool_result",
- tool_use_id: block.id,
- content: "Task was interrupted before this tool call could be completed.",
- }))
- modifiedApiConversationHistory = [...existingApiConversationHistory] // no changes
+ const toolResponses: Anthropic.ToolResultBlockParam[] =
+ toolUseBlocks.map((block) => ({
+ type: "tool_result",
+ tool_use_id: block.id,
+ content:
+ "Task was interrupted before this tool call could be completed.",
+ }))
+ modifiedApiConversationHistory = [
+ ...existingApiConversationHistory,
+ ] // no changes
modifiedOldUserContent = [...toolResponses]
} else {
- modifiedApiConversationHistory = [...existingApiConversationHistory]
+ modifiedApiConversationHistory = [
+ ...existingApiConversationHistory,
+ ]
modifiedOldUserContent = []
}
} else if (lastMessage.role === "user") {
- const previousAssistantMessage: Anthropic.Messages.MessageParam | undefined =
- existingApiConversationHistory[existingApiConversationHistory.length - 2]
-
- const existingUserContent: UserContent = Array.isArray(lastMessage.content)
+ const previousAssistantMessage:
+ | Anthropic.Messages.MessageParam
+ | undefined =
+ existingApiConversationHistory[
+ existingApiConversationHistory.length - 2
+ ]
+
+ const existingUserContent: UserContent = Array.isArray(
+ lastMessage.content,
+ )
? lastMessage.content
: [{ type: "text", text: lastMessage.content }]
- if (previousAssistantMessage && previousAssistantMessage.role === "assistant") {
- const assistantContent = Array.isArray(previousAssistantMessage.content)
+ if (
+ previousAssistantMessage &&
+ previousAssistantMessage.role === "assistant"
+ ) {
+ const assistantContent = Array.isArray(
+ previousAssistantMessage.content,
+ )
? previousAssistantMessage.content
- : [{ type: "text", text: previousAssistantMessage.content }]
+ : [
+ {
+ type: "text",
+ text: previousAssistantMessage.content,
+ },
+ ]
const toolUseBlocks = assistantContent.filter(
(block) => block.type === "tool_use",
@@ -580,31 +1153,49 @@ export class Cline {
(block) => block.type === "tool_result",
) as Anthropic.ToolResultBlockParam[]
- const missingToolResponses: Anthropic.ToolResultBlockParam[] = toolUseBlocks
- .filter(
- (toolUse) => !existingToolResults.some((result) => result.tool_use_id === toolUse.id),
- )
- .map((toolUse) => ({
- type: "tool_result",
- tool_use_id: toolUse.id,
- content: "Task was interrupted before this tool call could be completed.",
- }))
-
- modifiedApiConversationHistory = existingApiConversationHistory.slice(0, -1) // removes the last user message
- modifiedOldUserContent = [...existingUserContent, ...missingToolResponses]
+ const missingToolResponses: Anthropic.ToolResultBlockParam[] =
+ toolUseBlocks
+ .filter(
+ (toolUse) =>
+ !existingToolResults.some(
+ (result) =>
+ result.tool_use_id ===
+ toolUse.id,
+ ),
+ )
+ .map((toolUse) => ({
+ type: "tool_result",
+ tool_use_id: toolUse.id,
+ content:
+ "Task was interrupted before this tool call could be completed.",
+ }))
+
+ modifiedApiConversationHistory =
+ existingApiConversationHistory.slice(0, -1) // removes the last user message
+ modifiedOldUserContent = [
+ ...existingUserContent,
+ ...missingToolResponses,
+ ]
} else {
- modifiedApiConversationHistory = existingApiConversationHistory.slice(0, -1)
+ modifiedApiConversationHistory =
+ existingApiConversationHistory.slice(0, -1)
modifiedOldUserContent = [...existingUserContent]
}
} else {
- modifiedApiConversationHistory = existingApiConversationHistory.slice(0, -1)
+ modifiedApiConversationHistory =
+ existingApiConversationHistory.slice(0, -1)
modifiedOldUserContent = [...existingUserContent]
}
} else {
- throw new Error("Unexpected: Last message is not a user or assistant message")
+ throw new Error(
+ "Unexpected: Last message is not a user or assistant message",
+ )
}
} else {
throw new Error("Unexpected: No existing API conversation history")
+ // console.error("Unexpected: No existing API conversation history")
+ // modifiedApiConversationHistory = []
+ // modifiedOldUserContent = []
}
let newUserContent: UserContent = [...modifiedOldUserContent]
@@ -629,7 +1220,8 @@ export class Cline {
return "just now"
})()
- const wasRecent = lastClineMessage?.ts && Date.now() - lastClineMessage.ts < 30_000
+ const wasRecent =
+ lastClineMessage?.ts && Date.now() - lastClineMessage.ts < 30_000
newUserContent.push({
type: "text",
@@ -648,15 +1240,24 @@ export class Cline {
newUserContent.push(...formatResponse.imageBlocks(responseImages))
}
- await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
- await this.initiateTaskLoop(newUserContent)
+ await this.overwriteApiConversationHistory(
+ modifiedApiConversationHistory,
+ )
+ await this.initiateTaskLoop(newUserContent, false)
}
- private async initiateTaskLoop(userContent: UserContent): Promise {
+ private async initiateTaskLoop(
+ userContent: UserContent,
+ isNewTask: boolean,
+ ): Promise {
let nextUserContent = userContent
let includeFileDetails = true
while (!this.abort) {
- const didEndLoop = await this.recursivelyMakeClineRequests(nextUserContent, includeFileDetails)
+ const didEndLoop = await this.recursivelyMakeClineRequests(
+ nextUserContent,
+ includeFileDetails,
+ isNewTask,
+ )
includeFileDetails = false // we only need file details the first time
// The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
@@ -683,17 +1284,58 @@ export class Cline {
}
}
- abortTask() {
+ async abortTask() {
this.abort = true // will stop any autonomously running promises
this.terminalManager.disposeAll()
this.urlContentFetcher.closeBrowser()
this.browserSession.closeBrowser()
- this.diffViewProvider.revertChanges()
+ await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint
+ }
+
+ // Checkpoints
+
+ async saveCheckpoint() {
+ const commitHash = await this.checkpointTracker?.commit() // silently fails for now
+ if (commitHash) {
+ // Start from the end and work backwards until we find a tool use or another message with a hash
+ for (let i = this.clineMessages.length - 1; i >= 0; i--) {
+ const message = this.clineMessages[i]
+ if (message.lastCheckpointHash) {
+ // Found a message with a hash, so we can stop
+ break
+ }
+ // Update this message with a hash
+ message.lastCheckpointHash = commitHash
+
+ // We only care about adding the hash to the last tool use (we don't want to add this hash to every prior message ie for tasks pre-checkpoint)
+ const isToolUse =
+ message.say === "tool" ||
+ message.ask === "tool" ||
+ message.say === "command" ||
+ message.ask === "command" ||
+ message.say === "completion_result" ||
+ message.ask === "completion_result" ||
+ message.ask === "followup" ||
+ message.say === "use_mcp_server" ||
+ message.ask === "use_mcp_server" ||
+ message.say === "browser_action" ||
+ message.say === "browser_action_launch" ||
+ message.ask === "browser_action_launch"
+
+ if (isToolUse) {
+ break
+ }
+ }
+ // Save the updated messages
+ await this.saveClineMessages()
+ }
}
// Tools
- async executeCommandTool(command: string): Promise<[boolean, ToolResponse]> {
+ async executeCommandTool(
+ command: string,
+ ): Promise<[boolean, ToolResponse]> {
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
const process = this.terminalManager.runCommand(terminalInfo, command)
@@ -702,7 +1344,10 @@ export class Cline {
let didContinue = false
const sendCommandOutput = async (line: string): Promise => {
try {
- const { response, text, images } = await this.ask("command_output", line)
+ const { response, text, images } = await this.ask(
+ "command_output",
+ line,
+ )
if (response === "yesButtonClicked") {
// proceed while running
} else {
@@ -746,12 +1391,18 @@ export class Cline {
result = result.trim()
if (userFeedback) {
- await this.say("user_feedback", userFeedback.text, userFeedback.images)
+ await this.say(
+ "user_feedback",
+ userFeedback.text,
+ userFeedback.images,
+ )
return [
true,
formatResponse.toolResult(
`Command is still running in the user's terminal.${
- result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
+ result.length > 0
+ ? `\nHere's the output so far:\n${result}`
+ : ""
}\n\nThe user provided the following feedback:\n\n${userFeedback.text}\n`,
userFeedback.images,
),
@@ -759,12 +1410,17 @@ export class Cline {
}
if (completed) {
- return [false, `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`]
+ return [
+ false,
+ `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`,
+ ]
} else {
return [
false,
`Command is still running in the user's terminal.${
- result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
+ result.length > 0
+ ? `\nHere's the output so far:\n${result}`
+ : ""
}\n\nYou will be updated on the terminal status and new output in the future.`,
]
}
@@ -795,7 +1451,10 @@ export class Cline {
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
- await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
+ await pWaitFor(
+ () => this.providerRef.deref()?.mcpHub?.isConnecting !== true,
+ { timeout: 10_000 },
+ ).catch(() => {
console.error("MCP servers failed to connect in time")
})
@@ -804,37 +1463,59 @@ export class Cline {
throw new Error("MCP hub not available")
}
- let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub)
+ let systemPrompt = await SYSTEM_PROMPT(
+ cwd,
+ this.api.getModel().info.supportsComputerUse ?? false,
+ mcpHub,
+ )
let settingsCustomInstructions = this.customInstructions?.trim()
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let clineRulesFileInstructions: string | undefined
if (await fileExistsAtPath(clineRulesFilePath)) {
try {
- const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
+ const ruleFileContent = (
+ await fs.readFile(clineRulesFilePath, "utf8")
+ ).trim()
if (ruleFileContent) {
clineRulesFileInstructions = `# .clinerules\n\nThe following is provided by a root-level .clinerules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${ruleFileContent}`
}
} catch {
- console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
+ console.error(
+ `Failed to read .clinerules file at ${clineRulesFilePath}`,
+ )
}
}
if (settingsCustomInstructions || clineRulesFileInstructions) {
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with
- systemPrompt += addUserInstructions(settingsCustomInstructions, clineRulesFileInstructions)
+ systemPrompt += addUserInstructions(
+ settingsCustomInstructions,
+ clineRulesFileInstructions,
+ )
}
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
const previousRequest = this.clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
- const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(
- previousRequest.text,
- )
- const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
- let contextWindow = this.api.getModel().info.contextWindow || 128_000
+ const {
+ tokensIn,
+ tokensOut,
+ cacheWrites,
+ cacheReads,
+ }: ClineApiReqInfo = JSON.parse(previousRequest.text)
+ const totalTokens =
+ (tokensIn || 0) +
+ (tokensOut || 0) +
+ (cacheWrites || 0) +
+ (cacheReads || 0)
+ let contextWindow =
+ this.api.getModel().info.contextWindow || 128_000
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
- if (this.api instanceof OpenAiHandler && this.api.getModel().id.toLowerCase().includes("deepseek")) {
+ if (
+ this.api instanceof OpenAiHandler &&
+ this.api.getModel().id.toLowerCase().includes("deepseek")
+ ) {
contextWindow = 64_000
}
let maxAllowedSize: number
@@ -849,17 +1530,36 @@ export class Cline {
maxAllowedSize = contextWindow - 40_000
break
default:
- maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
+ maxAllowedSize = Math.max(
+ contextWindow - 40_000,
+ contextWindow * 0.8,
+ ) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
}
+ // This is the most reliable way to know when we're close to hitting the context window.
if (totalTokens >= maxAllowedSize) {
- const truncatedMessages = truncateHalfConversation(this.apiConversationHistory)
- await this.overwriteApiConversationHistory(truncatedMessages)
+ // NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range
+ this.conversationHistoryDeletedRange =
+ getNextTruncationRange(
+ this.apiConversationHistory,
+ this.conversationHistoryDeletedRange,
+ )
+ await this.saveClineMessages() // saves task history item which we use to keep track of conversation history deleted range
+ // await this.overwriteApiConversationHistory(truncatedMessages)
}
}
}
- const stream = this.api.createMessage(systemPrompt, this.apiConversationHistory)
+ // conversationHistoryDeletedRange is updated only when we're close to hitting the context window, so we don't continuously break the prompt cache
+ const truncatedConversationHistory = getTruncatedMessages(
+ this.apiConversationHistory,
+ this.conversationHistoryDeletedRange,
+ )
+
+ const stream = this.api.createMessage(
+ systemPrompt,
+ truncatedConversationHistory,
+ )
const iterator = stream[Symbol.asyncIterator]()
try {
@@ -900,7 +1600,10 @@ export class Cline {
this.presentAssistantMessageLocked = true
this.presentAssistantMessageHasPendingUpdates = false
- if (this.currentStreamingContentIndex >= this.assistantMessageContent.length) {
+ if (
+ this.currentStreamingContentIndex >=
+ this.assistantMessageContent.length
+ ) {
// this may happen if the last content block was completed before streaming could finish. if streaming is finished, and we're out of bounds then this means we already presented/executed the last content block and are ready to continue to next request
if (this.didCompleteReadingStream) {
this.userMessageContentReady = true
@@ -911,7 +1614,9 @@ export class Cline {
//throw new Error("No more content blocks to stream! This shouldn't happen...") // remove and just return after testing
}
- const block = cloneDeep(this.assistantMessageContent[this.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
+ const block = cloneDeep(
+ this.assistantMessageContent[this.currentStreamingContentIndex],
+ ) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
switch (block.type) {
case "text": {
if (this.didRejectTool || this.didAlreadyUseTool) {
@@ -945,12 +1650,17 @@ export class Cline {
tagContent = possibleTag.slice(1).trim()
}
// Check if tagContent is likely an incomplete tag name (letters and underscores only)
- const isLikelyTagName = /^[a-zA-Z_]+$/.test(tagContent)
+ const isLikelyTagName = /^[a-zA-Z_]+$/.test(
+ tagContent,
+ )
// Preemptively remove < or to keep from these artifacts showing up in chat (also handles closing thinking tags)
- const isOpeningOrClosing = possibleTag === "<" || possibleTag === ""
+ const isOpeningOrClosing =
+ possibleTag === "<" || possibleTag === ""
// If the tag is incomplete and at the end, remove it from the content
if (isOpeningOrClosing || isLikelyTagName) {
- content = content.slice(0, lastOpenBracketIndex).trim()
+ content = content
+ .slice(0, lastOpenBracketIndex)
+ .trim()
}
}
}
@@ -982,7 +1692,9 @@ export class Cline {
return `[${block.name} for '${block.params.path}']`
case "search_files":
return `[${block.name} for '${block.params.regex}'${
- block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
+ block.params.file_pattern
+ ? ` in '${block.params.file_pattern}'`
+ : ""
}]`
case "list_files":
return `[${block.name} for '${block.params.path}']`
@@ -1044,13 +1756,23 @@ export class Cline {
this.didAlreadyUseTool = true
}
- const askApproval = async (type: ClineAsk, partialMessage?: string) => {
- const { response, text, images } = await this.ask(type, partialMessage, false)
+ const askApproval = async (
+ type: ClineAsk,
+ partialMessage?: string,
+ ) => {
+ const { response, text, images } = await this.ask(
+ type,
+ partialMessage,
+ false,
+ )
if (response !== "yesButtonClicked") {
if (response === "messageResponse") {
await this.say("user_feedback", text, images)
pushToolResult(
- formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images),
+ formatResponse.toolResult(
+ formatResponse.toolDeniedWithFeedback(text),
+ images,
+ ),
)
// this.userMessageContent.push({
// type: "text",
@@ -1079,8 +1801,13 @@ export class Cline {
return true
}
- const showNotificationForApprovalIfAutoApprovalEnabled = (message: string) => {
- if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
+ const showNotificationForApprovalIfAutoApprovalEnabled = (
+ message: string,
+ ) => {
+ if (
+ this.autoApprovalSettings.enabled &&
+ this.autoApprovalSettings.enableNotifications
+ ) {
showSystemNotification({
subtitle: "Approval Required",
message,
@@ -1089,6 +1816,12 @@ export class Cline {
}
const handleError = async (action: string, error: Error) => {
+ if (this.abandoned) {
+ console.log(
+ "Ignoring error since task was abandoned (i.e. from task cancellation after resetting)",
+ )
+ return
+ }
const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
await this.say(
"error",
@@ -1103,7 +1836,10 @@ export class Cline {
}
// If block is partial, remove partial closing tag so its not presented to user
- const removeClosingTag = (tag: ToolParamName, text?: string) => {
+ const removeClosingTag = (
+ tag: ToolParamName,
+ text?: string,
+ ) => {
if (!block.partial) {
return text || ""
}
@@ -1141,18 +1877,23 @@ export class Cline {
// Check if file exists using cached map or fs.access
let fileExists: boolean
if (this.diffViewProvider.editType !== undefined) {
- fileExists = this.diffViewProvider.editType === "modify"
+ fileExists =
+ this.diffViewProvider.editType === "modify"
} else {
const absolutePath = path.resolve(cwd, relPath)
fileExists = await fileExistsAtPath(absolutePath)
- this.diffViewProvider.editType = fileExists ? "modify" : "create"
+ this.diffViewProvider.editType = fileExists
+ ? "modify"
+ : "create"
}
try {
// Construct newContent from diff
let newContent: string
if (diff) {
- if (!this.api.getModel().id.includes("claude")) {
+ if (
+ !this.api.getModel().id.includes("claude")
+ ) {
// deepseek models tend to use unescaped html entities in diffs
diff = fixModelHtmlEscaping(diff)
diff = removeInvalidChars(diff)
@@ -1160,7 +1901,8 @@ export class Cline {
try {
newContent = await constructNewFileContent(
diff,
- this.diffViewProvider.originalContent || "",
+ this.diffViewProvider.originalContent ||
+ "",
!block.partial,
)
} catch (error) {
@@ -1184,15 +1926,26 @@ export class Cline {
// pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini)
if (newContent.startsWith("```")) {
// this handles cases where it includes language specifiers like ```python ```js
- newContent = newContent.split("\n").slice(1).join("\n").trim()
+ newContent = newContent
+ .split("\n")
+ .slice(1)
+ .join("\n")
+ .trim()
}
if (newContent.endsWith("```")) {
- newContent = newContent.split("\n").slice(0, -1).join("\n").trim()
+ newContent = newContent
+ .split("\n")
+ .slice(0, -1)
+ .join("\n")
+ .trim()
}
- if (!this.api.getModel().id.includes("claude")) {
+ if (
+ !this.api.getModel().id.includes("claude")
+ ) {
// it seems not just llama models are doing this, but also gemini and potentially others
- newContent = fixModelHtmlEscaping(newContent)
+ newContent =
+ fixModelHtmlEscaping(newContent)
newContent = removeInvalidChars(newContent)
}
} else {
@@ -1203,20 +1956,41 @@ export class Cline {
newContent = newContent.trimEnd() // remove any trailing newlines, since it's automatically inserted by the editor
const sharedMessageProps: ClineSayTool = {
- tool: fileExists ? "editedExistingFile" : "newFileCreated",
- path: getReadablePath(cwd, removeClosingTag("path", relPath)),
+ tool: fileExists
+ ? "editedExistingFile"
+ : "newFileCreated",
+ path: getReadablePath(
+ cwd,
+ removeClosingTag("path", relPath),
+ ),
content: diff || content,
}
if (block.partial) {
// update gui message
- const partialMessage = JSON.stringify(sharedMessageProps)
+ const partialMessage =
+ JSON.stringify(sharedMessageProps)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool") // in case the user changes auto-approval settings mid stream
- await this.say("tool", partialMessage, undefined, block.partial)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ ) // in case the user changes auto-approval settings mid stream
+ await this.say(
+ "tool",
+ partialMessage,
+ undefined,
+ block.partial,
+ )
} else {
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
- await this.ask("tool", partialMessage, block.partial).catch(() => {})
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
+ await this.ask(
+ "tool",
+ partialMessage,
+ block.partial,
+ ).catch(() => {})
}
// update editor
if (!this.diffViewProvider.isEditing) {
@@ -1224,25 +1998,49 @@ export class Cline {
await this.diffViewProvider.open(relPath)
}
// editor is open, stream content in
- await this.diffViewProvider.update(newContent, false)
+ await this.diffViewProvider.update(
+ newContent,
+ false,
+ )
break
} else {
if (!relPath) {
this.consecutiveMistakeCount++
- pushToolResult(await this.sayAndCreateMissingParamError(block.name, "path"))
+ pushToolResult(
+ await this.sayAndCreateMissingParamError(
+ block.name,
+ "path",
+ ),
+ )
await this.diffViewProvider.reset()
+ await this.saveCheckpoint()
break
}
if (block.name === "replace_in_file" && !diff) {
this.consecutiveMistakeCount++
- pushToolResult(await this.sayAndCreateMissingParamError("replace_in_file", "diff"))
+ pushToolResult(
+ await this.sayAndCreateMissingParamError(
+ "replace_in_file",
+ "diff",
+ ),
+ )
await this.diffViewProvider.reset()
+ await this.saveCheckpoint()
break
}
- if (block.name === "write_to_file" && !content) {
+ if (
+ block.name === "write_to_file" &&
+ !content
+ ) {
this.consecutiveMistakeCount++
- pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "content"))
+ pushToolResult(
+ await this.sayAndCreateMissingParamError(
+ "write_to_file",
+ "content",
+ ),
+ )
await this.diffViewProvider.reset()
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -1252,11 +2050,19 @@ export class Cline {
// in other words, you must always repeat the block.partial logic here
if (!this.diffViewProvider.isEditing) {
// show gui message before showing edit animation
- const partialMessage = JSON.stringify(sharedMessageProps)
- await this.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
+ const partialMessage =
+ JSON.stringify(sharedMessageProps)
+ await this.ask(
+ "tool",
+ partialMessage,
+ true,
+ ).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
await this.diffViewProvider.open(relPath)
}
- await this.diffViewProvider.update(newContent, true)
+ await this.diffViewProvider.update(
+ newContent,
+ true,
+ )
await delay(300) // wait for diff view to update
this.diffViewProvider.scrollToFirstDiff()
// showOmissionWarning(this.diffViewProvider.originalContent || "", newContent)
@@ -1273,8 +2079,16 @@ export class Cline {
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool")
- await this.say("tool", completeMessage, undefined, false)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ )
+ await this.say(
+ "tool",
+ completeMessage,
+ undefined,
+ false,
+ )
this.consecutiveAutoApprovedRequestsCount++
// we need an artificial delay to let the diagnostics catch up to the changes
@@ -1284,19 +2098,31 @@ export class Cline {
showNotificationForApprovalIfAutoApprovalEnabled(
`Cline wants to ${fileExists ? "edit" : "create"} ${path.basename(relPath)}`,
)
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
// const didApprove = await askApproval("tool", completeMessage)
// Need a more customized tool response for file edits to highlight the fact that the file was not updated (particularly important for deepseek)
let didApprove = true
- const { response, text, images } = await this.ask("tool", completeMessage, false)
+ const { response, text, images } =
+ await this.ask(
+ "tool",
+ completeMessage,
+ false,
+ )
if (response !== "yesButtonClicked") {
// TODO: add similar context for other tool denial responses, to emphasize ie that a command was not run
const fileDeniedNote = fileExists
? "The file was not updated, and maintains its original contents."
: "The file was not created."
if (response === "messageResponse") {
- await this.say("user_feedback", text, images)
+ await this.say(
+ "user_feedback",
+ text,
+ images,
+ )
pushToolResult(
formatResponse.toolResult(
`The user denied this operation. ${fileDeniedNote}\nThe user provided the following feedback:\n\n${text}\n`,
@@ -1306,7 +2132,9 @@ export class Cline {
this.didRejectTool = true
didApprove = false
} else {
- pushToolResult(`The user denied this operation. ${fileDeniedNote}`)
+ pushToolResult(
+ `The user denied this operation. ${fileDeniedNote}`,
+ )
this.didRejectTool = true
didApprove = false
}
@@ -1314,18 +2142,25 @@ export class Cline {
if (!didApprove) {
await this.diffViewProvider.revertChanges()
+ await this.saveCheckpoint()
break
}
}
- const { newProblemsMessage, userEdits, autoFormattingEdits, finalContent } =
- await this.diffViewProvider.saveChanges()
+ const {
+ newProblemsMessage,
+ userEdits,
+ autoFormattingEdits,
+ finalContent,
+ } = await this.diffViewProvider.saveChanges()
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
if (userEdits) {
await this.say(
"user_feedback_diff",
JSON.stringify({
- tool: fileExists ? "editedExistingFile" : "newFileCreated",
+ tool: fileExists
+ ? "editedExistingFile"
+ : "newFileCreated",
path: getReadablePath(cwd, relPath),
diff: userEdits,
} satisfies ClineSayTool),
@@ -1357,12 +2192,14 @@ export class Cline {
)
}
await this.diffViewProvider.reset()
+ await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("writing file", error)
await this.diffViewProvider.revertChanges()
await this.diffViewProvider.reset()
+ await this.saveCheckpoint()
break
}
}
@@ -1370,7 +2207,10 @@ export class Cline {
const relPath: string | undefined = block.params.path
const sharedMessageProps: ClineSayTool = {
tool: "readFile",
- path: getReadablePath(cwd, removeClosingTag("path", relPath)),
+ path: getReadablePath(
+ cwd,
+ removeClosingTag("path", relPath),
+ ),
}
try {
if (block.partial) {
@@ -1379,17 +2219,38 @@ export class Cline {
content: undefined,
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool")
- await this.say("tool", partialMessage, undefined, block.partial)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ )
+ await this.say(
+ "tool",
+ partialMessage,
+ undefined,
+ block.partial,
+ )
} else {
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
- await this.ask("tool", partialMessage, block.partial).catch(() => {})
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
+ await this.ask(
+ "tool",
+ partialMessage,
+ block.partial,
+ ).catch(() => {})
}
break
} else {
if (!relPath) {
this.consecutiveMistakeCount++
- pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
+ pushToolResult(
+ await this.sayAndCreateMissingParamError(
+ "read_file",
+ "path",
+ ),
+ )
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -1399,36 +2260,60 @@ export class Cline {
content: absolutePath,
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool")
- await this.say("tool", completeMessage, undefined, false) // need to be sending partialValue bool, since undefined has its own purpose in that the message is treated neither as a partial or completion of a partial, but as a single complete message
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ )
+ await this.say(
+ "tool",
+ completeMessage,
+ undefined,
+ false,
+ ) // need to be sending partialValue bool, since undefined has its own purpose in that the message is treated neither as a partial or completion of a partial, but as a single complete message
this.consecutiveAutoApprovedRequestsCount++
} else {
showNotificationForApprovalIfAutoApprovalEnabled(
`Cline wants to read ${path.basename(absolutePath)}`,
)
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
- const didApprove = await askApproval("tool", completeMessage)
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
+ const didApprove = await askApproval(
+ "tool",
+ completeMessage,
+ )
if (!didApprove) {
+ await this.saveCheckpoint()
break
}
}
// now execute the tool like normal
- const content = await extractTextFromFile(absolutePath)
+ const content =
+ await extractTextFromFile(absolutePath)
pushToolResult(content)
+ await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("reading file", error)
+ await this.saveCheckpoint()
break
}
}
case "list_files": {
const relDirPath: string | undefined = block.params.path
- const recursiveRaw: string | undefined = block.params.recursive
+ const recursiveRaw: string | undefined =
+ block.params.recursive
const recursive = recursiveRaw?.toLowerCase() === "true"
const sharedMessageProps: ClineSayTool = {
- tool: !recursive ? "listFilesTopLevel" : "listFilesRecursive",
- path: getReadablePath(cwd, removeClosingTag("path", relDirPath)),
+ tool: !recursive
+ ? "listFilesTopLevel"
+ : "listFilesRecursive",
+ path: getReadablePath(
+ cwd,
+ removeClosingTag("path", relDirPath),
+ ),
}
try {
if (block.partial) {
@@ -1437,46 +2322,95 @@ export class Cline {
content: "",
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool")
- await this.say("tool", partialMessage, undefined, block.partial)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ )
+ await this.say(
+ "tool",
+ partialMessage,
+ undefined,
+ block.partial,
+ )
} else {
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
- await this.ask("tool", partialMessage, block.partial).catch(() => {})
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
+ await this.ask(
+ "tool",
+ partialMessage,
+ block.partial,
+ ).catch(() => {})
}
break
} else {
if (!relDirPath) {
this.consecutiveMistakeCount++
- pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
+ pushToolResult(
+ await this.sayAndCreateMissingParamError(
+ "list_files",
+ "path",
+ ),
+ )
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
- const absolutePath = path.resolve(cwd, relDirPath)
- const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
- const result = formatResponse.formatFilesList(absolutePath, files, didHitLimit)
+ const absolutePath = path.resolve(
+ cwd,
+ relDirPath,
+ )
+ const [files, didHitLimit] = await listFiles(
+ absolutePath,
+ recursive,
+ 200,
+ )
+ const result = formatResponse.formatFilesList(
+ absolutePath,
+ files,
+ didHitLimit,
+ )
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool")
- await this.say("tool", completeMessage, undefined, false)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ )
+ await this.say(
+ "tool",
+ completeMessage,
+ undefined,
+ false,
+ )
this.consecutiveAutoApprovedRequestsCount++
} else {
showNotificationForApprovalIfAutoApprovalEnabled(
`Cline wants to view directory ${path.basename(absolutePath)}/`,
)
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
- const didApprove = await askApproval("tool", completeMessage)
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
+ const didApprove = await askApproval(
+ "tool",
+ completeMessage,
+ )
if (!didApprove) {
+ await this.saveCheckpoint()
break
}
}
pushToolResult(result)
+ await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("listing files", error)
+ await this.saveCheckpoint()
break
}
}
@@ -1484,7 +2418,10 @@ export class Cline {
const relDirPath: string | undefined = block.params.path
const sharedMessageProps: ClineSayTool = {
tool: "listCodeDefinitionNames",
- path: getReadablePath(cwd, removeClosingTag("path", relDirPath)),
+ path: getReadablePath(
+ cwd,
+ removeClosingTag("path", relDirPath),
+ ),
}
try {
if (block.partial) {
@@ -1493,59 +2430,111 @@ export class Cline {
content: "",
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool")
- await this.say("tool", partialMessage, undefined, block.partial)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ )
+ await this.say(
+ "tool",
+ partialMessage,
+ undefined,
+ block.partial,
+ )
} else {
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
- await this.ask("tool", partialMessage, block.partial).catch(() => {})
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
+ await this.ask(
+ "tool",
+ partialMessage,
+ block.partial,
+ ).catch(() => {})
}
break
} else {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("list_code_definition_names", "path"),
+ await this.sayAndCreateMissingParamError(
+ "list_code_definition_names",
+ "path",
+ ),
)
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
- const absolutePath = path.resolve(cwd, relDirPath)
- const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath)
+ const absolutePath = path.resolve(
+ cwd,
+ relDirPath,
+ )
+ const result =
+ await parseSourceCodeForDefinitionsTopLevel(
+ absolutePath,
+ )
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool")
- await this.say("tool", completeMessage, undefined, false)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ )
+ await this.say(
+ "tool",
+ completeMessage,
+ undefined,
+ false,
+ )
this.consecutiveAutoApprovedRequestsCount++
} else {
showNotificationForApprovalIfAutoApprovalEnabled(
`Cline wants to view source code definitions in ${path.basename(absolutePath)}/`,
)
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
- const didApprove = await askApproval("tool", completeMessage)
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
+ const didApprove = await askApproval(
+ "tool",
+ completeMessage,
+ )
if (!didApprove) {
+ await this.saveCheckpoint()
break
}
}
pushToolResult(result)
+ await this.saveCheckpoint()
break
}
} catch (error) {
- await handleError("parsing source code definitions", error)
+ await handleError(
+ "parsing source code definitions",
+ error,
+ )
+ await this.saveCheckpoint()
break
}
}
case "search_files": {
const relDirPath: string | undefined = block.params.path
const regex: string | undefined = block.params.regex
- const filePattern: string | undefined = block.params.file_pattern
+ const filePattern: string | undefined =
+ block.params.file_pattern
const sharedMessageProps: ClineSayTool = {
tool: "searchFiles",
- path: getReadablePath(cwd, removeClosingTag("path", relDirPath)),
+ path: getReadablePath(
+ cwd,
+ removeClosingTag("path", relDirPath),
+ ),
regex: removeClosingTag("regex", regex),
- filePattern: removeClosingTag("file_pattern", filePattern),
+ filePattern: removeClosingTag(
+ "file_pattern",
+ filePattern,
+ ),
}
try {
if (block.partial) {
@@ -1554,64 +2543,123 @@ export class Cline {
content: "",
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool")
- await this.say("tool", partialMessage, undefined, block.partial)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ )
+ await this.say(
+ "tool",
+ partialMessage,
+ undefined,
+ block.partial,
+ )
} else {
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
- await this.ask("tool", partialMessage, block.partial).catch(() => {})
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
+ await this.ask(
+ "tool",
+ partialMessage,
+ block.partial,
+ ).catch(() => {})
}
break
} else {
if (!relDirPath) {
this.consecutiveMistakeCount++
- pushToolResult(await this.sayAndCreateMissingParamError("search_files", "path"))
+ pushToolResult(
+ await this.sayAndCreateMissingParamError(
+ "search_files",
+ "path",
+ ),
+ )
+ await this.saveCheckpoint()
break
}
if (!regex) {
this.consecutiveMistakeCount++
- pushToolResult(await this.sayAndCreateMissingParamError("search_files", "regex"))
+ pushToolResult(
+ await this.sayAndCreateMissingParamError(
+ "search_files",
+ "regex",
+ ),
+ )
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
- const absolutePath = path.resolve(cwd, relDirPath)
- const results = await regexSearchFiles(cwd, absolutePath, regex, filePattern)
+ const absolutePath = path.resolve(
+ cwd,
+ relDirPath,
+ )
+ const results = await regexSearchFiles(
+ cwd,
+ absolutePath,
+ regex,
+ filePattern,
+ )
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: results,
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "tool")
- await this.say("tool", completeMessage, undefined, false)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "tool",
+ )
+ await this.say(
+ "tool",
+ completeMessage,
+ undefined,
+ false,
+ )
this.consecutiveAutoApprovedRequestsCount++
} else {
showNotificationForApprovalIfAutoApprovalEnabled(
`Cline wants to search files in ${path.basename(absolutePath)}/`,
)
- this.removeLastPartialMessageIfExistsWithType("say", "tool")
- const didApprove = await askApproval("tool", completeMessage)
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "tool",
+ )
+ const didApprove = await askApproval(
+ "tool",
+ completeMessage,
+ )
if (!didApprove) {
+ await this.saveCheckpoint()
break
}
}
pushToolResult(results)
+ await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("searching files", error)
+ await this.saveCheckpoint()
break
}
}
case "browser_action": {
- const action: BrowserAction | undefined = block.params.action as BrowserAction
+ const action: BrowserAction | undefined = block.params
+ .action as BrowserAction
const url: string | undefined = block.params.url
- const coordinate: string | undefined = block.params.coordinate
+ const coordinate: string | undefined =
+ block.params.coordinate
const text: string | undefined = block.params.text
if (!action || !browserActions.includes(action)) {
// checking for action to ensure it is complete and valid
if (!block.partial) {
// if the block is complete and we don't have a valid action this is a mistake
this.consecutiveMistakeCount++
- pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "action"))
+ pushToolResult(
+ await this.sayAndCreateMissingParamError(
+ "browser_action",
+ "action",
+ ),
+ )
await this.browserSession.closeBrowser()
}
break
@@ -1620,8 +2668,13 @@ export class Cline {
try {
if (block.partial) {
if (action === "launch") {
- if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "browser_action_launch")
+ if (
+ this.shouldAutoApproveTool(block.name)
+ ) {
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "browser_action_launch",
+ )
await this.say(
"browser_action_launch",
removeClosingTag("url", url),
@@ -1629,7 +2682,10 @@ export class Cline {
block.partial,
)
} else {
- this.removeLastPartialMessageIfExistsWithType("say", "browser_action_launch")
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "browser_action_launch",
+ )
await this.ask(
"browser_action_launch",
removeClosingTag("url", url),
@@ -1641,8 +2697,14 @@ export class Cline {
"browser_action",
JSON.stringify({
action: action as BrowserAction,
- coordinate: removeClosingTag("coordinate", coordinate),
- text: removeClosingTag("text", text),
+ coordinate: removeClosingTag(
+ "coordinate",
+ coordinate,
+ ),
+ text: removeClosingTag(
+ "text",
+ text,
+ ),
} satisfies ClineSayBrowserAction),
undefined,
block.partial,
@@ -1655,24 +2717,46 @@ export class Cline {
if (!url) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("browser_action", "url"),
+ await this.sayAndCreateMissingParamError(
+ "browser_action",
+ "url",
+ ),
)
await this.browserSession.closeBrowser()
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
- if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "browser_action_launch")
- await this.say("browser_action_launch", url, undefined, false)
- this.consecutiveAutoApprovedRequestsCount++
+ if (
+ this.shouldAutoApproveTool(block.name)
+ ) {
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "browser_action_launch",
+ )
+ await this.say(
+ "browser_action_launch",
+ url,
+ undefined,
+ false,
+ )
+ this
+ .consecutiveAutoApprovedRequestsCount++
} else {
showNotificationForApprovalIfAutoApprovalEnabled(
`Cline wants to use a browser and launch ${url}`,
)
- this.removeLastPartialMessageIfExistsWithType("say", "browser_action_launch")
- const didApprove = await askApproval("browser_action_launch", url)
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "browser_action_launch",
+ )
+ const didApprove = await askApproval(
+ "browser_action_launch",
+ url,
+ )
if (!didApprove) {
+ await this.saveCheckpoint()
break
}
}
@@ -1682,7 +2766,10 @@ export class Cline {
await this.say("browser_action_result", "") // starts loading spinner
await this.browserSession.launchBrowser()
- browserActionResult = await this.browserSession.navigateToUrl(url)
+ browserActionResult =
+ await this.browserSession.navigateToUrl(
+ url,
+ )
} else {
if (action === "click") {
if (!coordinate) {
@@ -1694,6 +2781,7 @@ export class Cline {
),
)
await this.browserSession.closeBrowser()
+ await this.saveCheckpoint()
break // can't be within an inner switch
}
}
@@ -1701,9 +2789,13 @@ export class Cline {
if (!text) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("browser_action", "text"),
+ await this.sayAndCreateMissingParamError(
+ "browser_action",
+ "text",
+ ),
)
await this.browserSession.closeBrowser()
+ await this.saveCheckpoint()
break
}
}
@@ -1720,19 +2812,28 @@ export class Cline {
)
switch (action) {
case "click":
- browserActionResult = await this.browserSession.click(coordinate!)
+ browserActionResult =
+ await this.browserSession.click(
+ coordinate!,
+ )
break
case "type":
- browserActionResult = await this.browserSession.type(text!)
+ browserActionResult =
+ await this.browserSession.type(
+ text!,
+ )
break
case "scroll_down":
- browserActionResult = await this.browserSession.scrollDown()
+ browserActionResult =
+ await this.browserSession.scrollDown()
break
case "scroll_up":
- browserActionResult = await this.browserSession.scrollUp()
+ browserActionResult =
+ await this.browserSession.scrollUp()
break
case "close":
- browserActionResult = await this.browserSession.closeBrowser()
+ browserActionResult =
+ await this.browserSession.closeBrowser()
break
}
}
@@ -1743,15 +2844,24 @@ export class Cline {
case "type":
case "scroll_down":
case "scroll_up":
- await this.say("browser_action_result", JSON.stringify(browserActionResult))
+ await this.say(
+ "browser_action_result",
+ JSON.stringify(browserActionResult),
+ )
pushToolResult(
formatResponse.toolResult(
`The browser action has been executed. The console logs and screenshot have been captured for your analysis.\n\nConsole logs:\n${
- browserActionResult.logs || "(No new logs)"
+ browserActionResult.logs ||
+ "(No new logs)"
}\n\n(REMEMBER: if you need to proceed to using non-\`browser_action\` tools or launch a new browser, you MUST first close this browser. For example, if after analyzing the logs and screenshot you need to edit a file, you must first close the browser before you can use the write_to_file tool.)`,
- browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
+ browserActionResult.screenshot
+ ? [
+ browserActionResult.screenshot,
+ ]
+ : [],
),
)
+ await this.saveCheckpoint()
break
case "close":
pushToolResult(
@@ -1759,20 +2869,26 @@ export class Cline {
`The browser has been closed. You may now proceed to using other tools.`,
),
)
+ await this.saveCheckpoint()
break
}
+
+ await this.saveCheckpoint()
break
}
} catch (error) {
await this.browserSession.closeBrowser() // if any error occurs, the browser session is terminated
await handleError("executing browser action", error)
+ await this.saveCheckpoint()
break
}
}
case "execute_command": {
const command: string | undefined = block.params.command
- const requiresApprovalRaw: string | undefined = block.params.requires_approval
- const requiresApproval = requiresApprovalRaw?.toLowerCase() === "true"
+ const requiresApprovalRaw: string | undefined =
+ block.params.requires_approval
+ const requiresApproval =
+ requiresApprovalRaw?.toLowerCase() === "true"
try {
if (block.partial) {
@@ -1797,8 +2913,12 @@ export class Cline {
if (!command) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("execute_command", "command"),
+ await this.sayAndCreateMissingParamError(
+ "execute_command",
+ "command",
+ ),
)
+ await this.saveCheckpoint()
break
}
if (!requiresApprovalRaw) {
@@ -1809,15 +2929,27 @@ export class Cline {
"requires_approval",
),
)
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
let didAutoApprove = false
- if (!requiresApproval && this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "command")
- await this.say("command", command, undefined, false)
+ if (
+ !requiresApproval &&
+ this.shouldAutoApproveTool(block.name)
+ ) {
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "command",
+ )
+ await this.say(
+ "command",
+ command,
+ undefined,
+ false,
+ )
this.consecutiveAutoApprovedRequestsCount++
didAutoApprove = true
} else {
@@ -1831,23 +2963,30 @@ export class Cline {
`${this.shouldAutoApproveTool(block.name) && requiresApproval ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
)
if (!didApprove) {
+ await this.saveCheckpoint()
break
}
}
let timeoutId: NodeJS.Timeout | undefined
- if (didAutoApprove && this.autoApprovalSettings.enableNotifications) {
+ if (
+ didAutoApprove &&
+ this.autoApprovalSettings
+ .enableNotifications
+ ) {
// if the command was auto-approved, and it's long running we need to notify the user after some time has passed without proceeding
timeoutId = setTimeout(() => {
showSystemNotification({
- subtitle: "Command is still running",
+ subtitle:
+ "Command is still running",
message:
"An auto-approved command has been running for 30s, and may need your attention.",
})
}, 30_000)
}
- const [userRejected, result] = await this.executeCommandTool(command)
+ const [userRejected, result] =
+ await this.executeCommandTool(command)
if (timeoutId) {
clearTimeout(timeoutId)
}
@@ -1855,32 +2994,61 @@ export class Cline {
this.didRejectTool = true
}
pushToolResult(result)
+ await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("executing command", error)
+ await this.saveCheckpoint()
break
}
}
case "use_mcp_tool": {
- const server_name: string | undefined = block.params.server_name
- const tool_name: string | undefined = block.params.tool_name
- const mcp_arguments: string | undefined = block.params.arguments
+ const server_name: string | undefined =
+ block.params.server_name
+ const tool_name: string | undefined =
+ block.params.tool_name
+ const mcp_arguments: string | undefined =
+ block.params.arguments
try {
if (block.partial) {
const partialMessage = JSON.stringify({
type: "use_mcp_tool",
- serverName: removeClosingTag("server_name", server_name),
- toolName: removeClosingTag("tool_name", tool_name),
- arguments: removeClosingTag("arguments", mcp_arguments),
+ serverName: removeClosingTag(
+ "server_name",
+ server_name,
+ ),
+ toolName: removeClosingTag(
+ "tool_name",
+ tool_name,
+ ),
+ arguments: removeClosingTag(
+ "arguments",
+ mcp_arguments,
+ ),
} satisfies ClineAskUseMcpServer)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "use_mcp_server")
- await this.say("use_mcp_server", partialMessage, undefined, block.partial)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "use_mcp_server",
+ )
+ await this.say(
+ "use_mcp_server",
+ partialMessage,
+ undefined,
+ block.partial,
+ )
} else {
- this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
- await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "use_mcp_server",
+ )
+ await this.ask(
+ "use_mcp_server",
+ partialMessage,
+ block.partial,
+ ).catch(() => {})
}
break
@@ -1888,15 +3056,23 @@ export class Cline {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"),
+ await this.sayAndCreateMissingParamError(
+ "use_mcp_tool",
+ "server_name",
+ ),
)
+ await this.saveCheckpoint()
break
}
if (!tool_name) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"),
+ await this.sayAndCreateMissingParamError(
+ "use_mcp_tool",
+ "tool_name",
+ ),
)
+ await this.saveCheckpoint()
break
}
// arguments are optional, but if they are provided they must be valid JSON
@@ -1905,10 +3081,13 @@ export class Cline {
// pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "arguments"))
// break
// }
- let parsedArguments: Record | undefined
+ let parsedArguments:
+ | Record
+ | undefined
if (mcp_arguments) {
try {
- parsedArguments = JSON.parse(mcp_arguments)
+ parsedArguments =
+ JSON.parse(mcp_arguments)
} catch (error) {
this.consecutiveMistakeCount++
await this.say(
@@ -1917,9 +3096,13 @@ export class Cline {
)
pushToolResult(
formatResponse.toolError(
- formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
+ formatResponse.invalidMcpToolArgumentError(
+ server_name,
+ tool_name,
+ ),
),
)
+ await this.saveCheckpoint()
break
}
}
@@ -1932,16 +3115,31 @@ export class Cline {
} satisfies ClineAskUseMcpServer)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "use_mcp_server")
- await this.say("use_mcp_server", completeMessage, undefined, false)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "use_mcp_server",
+ )
+ await this.say(
+ "use_mcp_server",
+ completeMessage,
+ undefined,
+ false,
+ )
this.consecutiveAutoApprovedRequestsCount++
} else {
showNotificationForApprovalIfAutoApprovalEnabled(
`Cline wants to use ${tool_name} on ${server_name}`,
)
- this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
- const didApprove = await askApproval("use_mcp_server", completeMessage)
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "use_mcp_server",
+ )
+ const didApprove = await askApproval(
+ "use_mcp_server",
+ completeMessage,
+ )
if (!didApprove) {
+ await this.saveCheckpoint()
break
}
}
@@ -1950,7 +3148,11 @@ export class Cline {
await this.say("mcp_server_request_started") // same as browser_action_result
const toolResult = await this.providerRef
.deref()
- ?.mcpHub?.callTool(server_name, tool_name, parsedArguments)
+ ?.mcpHub?.callTool(
+ server_name,
+ tool_name,
+ parsedArguments,
+ )
// TODO: add progress indicator and ability to parse images and non-text responses
const toolResultPretty =
@@ -1961,39 +3163,70 @@ export class Cline {
return item.text
}
if (item.type === "resource") {
- const { blob, ...rest } = item.resource
- return JSON.stringify(rest, null, 2)
+ const { blob, ...rest } =
+ item.resource
+ return JSON.stringify(
+ rest,
+ null,
+ 2,
+ )
}
return ""
})
.filter(Boolean)
.join("\n\n") || "(No response)"
- await this.say("mcp_server_response", toolResultPretty)
- pushToolResult(formatResponse.toolResult(toolResultPretty))
+ await this.say(
+ "mcp_server_response",
+ toolResultPretty,
+ )
+ pushToolResult(
+ formatResponse.toolResult(toolResultPretty),
+ )
+ await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("executing MCP tool", error)
+ await this.saveCheckpoint()
break
}
}
case "access_mcp_resource": {
- const server_name: string | undefined = block.params.server_name
+ const server_name: string | undefined =
+ block.params.server_name
const uri: string | undefined = block.params.uri
try {
if (block.partial) {
const partialMessage = JSON.stringify({
type: "access_mcp_resource",
- serverName: removeClosingTag("server_name", server_name),
+ serverName: removeClosingTag(
+ "server_name",
+ server_name,
+ ),
uri: removeClosingTag("uri", uri),
} satisfies ClineAskUseMcpServer)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "use_mcp_server")
- await this.say("use_mcp_server", partialMessage, undefined, block.partial)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "use_mcp_server",
+ )
+ await this.say(
+ "use_mcp_server",
+ partialMessage,
+ undefined,
+ block.partial,
+ )
} else {
- this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
- await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "use_mcp_server",
+ )
+ await this.ask(
+ "use_mcp_server",
+ partialMessage,
+ block.partial,
+ ).catch(() => {})
}
break
@@ -2001,15 +3234,23 @@ export class Cline {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"),
+ await this.sayAndCreateMissingParamError(
+ "access_mcp_resource",
+ "server_name",
+ ),
)
+ await this.saveCheckpoint()
break
}
if (!uri) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"),
+ await this.sayAndCreateMissingParamError(
+ "access_mcp_resource",
+ "uri",
+ ),
)
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
@@ -2020,16 +3261,31 @@ export class Cline {
} satisfies ClineAskUseMcpServer)
if (this.shouldAutoApproveTool(block.name)) {
- this.removeLastPartialMessageIfExistsWithType("ask", "use_mcp_server")
- await this.say("use_mcp_server", completeMessage, undefined, false)
+ this.removeLastPartialMessageIfExistsWithType(
+ "ask",
+ "use_mcp_server",
+ )
+ await this.say(
+ "use_mcp_server",
+ completeMessage,
+ undefined,
+ false,
+ )
this.consecutiveAutoApprovedRequestsCount++
} else {
showNotificationForApprovalIfAutoApprovalEnabled(
`Cline wants to access ${uri} on ${server_name}`,
)
- this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
- const didApprove = await askApproval("use_mcp_server", completeMessage)
+ this.removeLastPartialMessageIfExistsWithType(
+ "say",
+ "use_mcp_server",
+ )
+ const didApprove = await askApproval(
+ "use_mcp_server",
+ completeMessage,
+ )
if (!didApprove) {
+ await this.saveCheckpoint()
break
}
}
@@ -2049,36 +3305,53 @@ export class Cline {
})
.filter(Boolean)
.join("\n\n") || "(Empty response)"
- await this.say("mcp_server_response", resourceResultPretty)
- pushToolResult(formatResponse.toolResult(resourceResultPretty))
+ await this.say(
+ "mcp_server_response",
+ resourceResultPretty,
+ )
+ pushToolResult(
+ formatResponse.toolResult(
+ resourceResultPretty,
+ ),
+ )
+ await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("accessing MCP resource", error)
+ await this.saveCheckpoint()
break
}
}
case "ask_followup_question": {
- const question: string | undefined = block.params.question
+ const question: string | undefined =
+ block.params.question
try {
if (block.partial) {
- await this.ask("followup", removeClosingTag("question", question), block.partial).catch(
- () => {},
- )
+ await this.ask(
+ "followup",
+ removeClosingTag("question", question),
+ block.partial,
+ ).catch(() => {})
break
} else {
if (!question) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("ask_followup_question", "question"),
+ await this.sayAndCreateMissingParamError(
+ "ask_followup_question",
+ "question",
+ ),
)
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
if (
this.autoApprovalSettings.enabled &&
- this.autoApprovalSettings.enableNotifications
+ this.autoApprovalSettings
+ .enableNotifications
) {
showSystemNotification({
subtitle: "Cline has a question...",
@@ -2086,13 +3359,28 @@ export class Cline {
})
}
- const { text, images } = await this.ask("followup", question, false)
- await this.say("user_feedback", text ?? "", images)
- pushToolResult(formatResponse.toolResult(`\n${text}\n`, images))
+ const { text, images } = await this.ask(
+ "followup",
+ question,
+ false,
+ )
+ await this.say(
+ "user_feedback",
+ text ?? "",
+ images,
+ )
+ pushToolResult(
+ formatResponse.toolResult(
+ `\n${text}\n`,
+ images,
+ ),
+ )
+ await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("asking question", error)
+ await this.saveCheckpoint()
break
}
}
@@ -2119,6 +3407,30 @@ export class Cline {
*/
const result: string | undefined = block.params.result
const command: string | undefined = block.params.command
+
+ const addNewChangesFlagToLastCompletionResultMessage =
+ async () => {
+ // Add newchanges flag if there are new changes to the workspace
+
+ const hasNewChanges =
+ await this.doesLatestTaskCompletionHaveNewChanges()
+ const lastCompletionResultMessage = findLast(
+ this.clineMessages,
+ (m) => m.say === "completion_result",
+ )
+ if (
+ lastCompletionResultMessage &&
+ hasNewChanges &&
+ !lastCompletionResultMessage.text?.endsWith(
+ COMPLETION_RESULT_CHANGES_FLAG,
+ )
+ ) {
+ lastCompletionResultMessage.text +=
+ COMPLETION_RESULT_CHANGES_FLAG
+ }
+ await this.saveClineMessages()
+ }
+
try {
const lastMessage = this.clineMessages.at(-1)
if (block.partial) {
@@ -2128,11 +3440,17 @@ export class Cline {
// const secondLastMessage = this.clineMessages.at(-2)
// NOTE: we do not want to auto approve a command run as part of the attempt_completion tool
- if (lastMessage && lastMessage.ask === "command") {
+ if (
+ lastMessage &&
+ lastMessage.ask === "command"
+ ) {
// update command
await this.ask(
"command",
- removeClosingTag("command", command),
+ removeClosingTag(
+ "command",
+ command,
+ ),
block.partial,
).catch(() => {})
} else {
@@ -2144,9 +3462,14 @@ export class Cline {
undefined,
false,
)
+ await this.saveCheckpoint()
+ await addNewChangesFlagToLastCompletionResultMessage()
await this.ask(
"command",
- removeClosingTag("command", command),
+ removeClosingTag(
+ "command",
+ command,
+ ),
block.partial,
).catch(() => {})
}
@@ -2164,15 +3487,20 @@ export class Cline {
if (!result) {
this.consecutiveMistakeCount++
pushToolResult(
- await this.sayAndCreateMissingParamError("attempt_completion", "result"),
+ await this.sayAndCreateMissingParamError(
+ "attempt_completion",
+ "result",
+ ),
)
+ await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
if (
this.autoApprovalSettings.enabled &&
- this.autoApprovalSettings.enableNotifications
+ this.autoApprovalSettings
+ .enableNotifications
) {
showSystemNotification({
subtitle: "Task Completed",
@@ -2182,40 +3510,81 @@ export class Cline {
let commandResult: ToolResponse | undefined
if (command) {
- if (lastMessage && lastMessage.ask !== "command") {
+ if (
+ lastMessage &&
+ lastMessage.ask !== "command"
+ ) {
// havent sent a command message yet so first send completion_result then command
- await this.say("completion_result", result, undefined, false)
+ await this.say(
+ "completion_result",
+ result,
+ undefined,
+ false,
+ )
+ await this.saveCheckpoint()
+ await addNewChangesFlagToLastCompletionResultMessage()
+ } else {
+ // we already sent a command message, meaning the complete completion message has also been sent
+ await this.saveCheckpoint()
}
// complete command message
- const didApprove = await askApproval("command", command)
+ const didApprove = await askApproval(
+ "command",
+ command,
+ )
if (!didApprove) {
+ await this.saveCheckpoint()
break
}
- const [userRejected, execCommandResult] = await this.executeCommandTool(command!)
+ const [userRejected, execCommandResult] =
+ await this.executeCommandTool(command!)
if (userRejected) {
this.didRejectTool = true
pushToolResult(execCommandResult)
+ await this.saveCheckpoint()
break
}
// user didn't reject, but the command may have output
commandResult = execCommandResult
} else {
- await this.say("completion_result", result, undefined, false)
+ await this.say(
+ "completion_result",
+ result,
+ undefined,
+ false,
+ )
+ await this.saveCheckpoint()
+ await addNewChangesFlagToLastCompletionResultMessage()
}
// we already sent completion_result says, an empty string asks relinquishes control over button and field
- const { response, text, images } = await this.ask("completion_result", "", false)
+ const { response, text, images } =
+ await this.ask(
+ "completion_result",
+ "",
+ false,
+ )
if (response === "yesButtonClicked") {
pushToolResult("") // signals to recursive loop to stop (for now this never happens since yesButtonClicked will trigger a new task)
break
}
- await this.say("user_feedback", text ?? "", images)
+ await this.say(
+ "user_feedback",
+ text ?? "",
+ images,
+ )
- const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
+ const toolResults: (
+ | Anthropic.TextBlockParam
+ | Anthropic.ImageBlockParam
+ )[] = []
if (commandResult) {
if (typeof commandResult === "string") {
- toolResults.push({ type: "text", text: commandResult })
+ toolResults.push({
+ type: "text",
+ text: commandResult,
+ })
} else if (Array.isArray(commandResult)) {
toolResults.push(...commandResult)
}
@@ -2224,17 +3593,21 @@ export class Cline {
type: "text",
text: `The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n\n${text}\n`,
})
- toolResults.push(...formatResponse.imageBlocks(images))
+ toolResults.push(
+ ...formatResponse.imageBlocks(images),
+ )
this.userMessageContent.push({
type: "text",
text: `${toolDescription()} Result:`,
})
this.userMessageContent.push(...toolResults)
+ // await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("attempting completion", error)
+ await this.saveCheckpoint()
break
}
}
@@ -2250,7 +3623,10 @@ export class Cline {
// NOTE: when tool is rejected, iterator stream is interrupted and it waits for userMessageContentReady to be true. Future calls to present will skip execution since didRejectTool and iterate until contentIndex is set to message length and it sets userMessageContentReady to true itself (instead of preemptively doing it in iterator)
if (!block.partial || this.didRejectTool || this.didAlreadyUseTool) {
// block is finished streaming and executing
- if (this.currentStreamingContentIndex === this.assistantMessageContent.length - 1) {
+ if (
+ this.currentStreamingContentIndex ===
+ this.assistantMessageContent.length - 1
+ ) {
// its okay that we increment if !didCompleteReadingStream, it'll just return bc out of bounds and as streaming continues it will call presentAssitantMessage if a new block is ready. if streaming is finished then we set userMessageContentReady to true when out of bounds. This gracefully allows the stream to continue on and all potential content blocks be presented.
// last block is complete and it is finished executing
this.userMessageContentReady = true // will allow pwaitfor to continue
@@ -2259,7 +3635,10 @@ export class Cline {
// call next block if it exists (if not then read stream will call it when its ready)
this.currentStreamingContentIndex++ // need to increment regardless, so when read stream calls this function again it will be streaming the next block
- if (this.currentStreamingContentIndex < this.assistantMessageContent.length) {
+ if (
+ this.currentStreamingContentIndex <
+ this.assistantMessageContent.length
+ ) {
// there are already more content blocks to stream, so we'll call this function ourselves
// await this.presentAssistantContent()
@@ -2276,16 +3655,21 @@ export class Cline {
async recursivelyMakeClineRequests(
userContent: UserContent,
includeFileDetails: boolean = false,
+ isNewTask: boolean = false,
): Promise {
if (this.abort) {
throw new Error("Cline instance aborted")
}
if (this.consecutiveMistakeCount >= 3) {
- if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
+ if (
+ this.autoApprovalSettings.enabled &&
+ this.autoApprovalSettings.enableNotifications
+ ) {
showSystemNotification({
subtitle: "Error",
- message: "Cline is having trouble. Would you like to continue the task?",
+ message:
+ "Cline is having trouble. Would you like to continue the task?",
})
}
const { response, text, images } = await this.ask(
@@ -2310,7 +3694,8 @@ export class Cline {
if (
this.autoApprovalSettings.enabled &&
- this.consecutiveAutoApprovedRequestsCount >= this.autoApprovalSettings.maxRequests
+ this.consecutiveAutoApprovedRequestsCount >=
+ this.autoApprovalSettings.maxRequests
) {
if (this.autoApprovalSettings.enableNotifications) {
showSystemNotification({
@@ -2327,7 +3712,10 @@ export class Cline {
}
// get previous api req's index to check token usage and determine if we need to truncate conversation history
- const previousApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
+ const previousApiReqIndex = findLastIndex(
+ this.clineMessages,
+ (m) => m.say === "api_req_started",
+ )
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
@@ -2335,21 +3723,55 @@ export class Cline {
"api_req_started",
JSON.stringify({
request:
- userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n") + "\n\nLoading...",
+ userContent
+ .map((block) => formatContentBlockToMarkdown(block))
+ .join("\n\n") + "\n\nLoading...",
}),
)
- const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
+ // 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) {
+ try {
+ this.checkpointTracker = await CheckpointTracker.create(
+ this.taskId,
+ this.providerRef.deref(),
+ )
+ this.checkpointTrackerErrorMessage = undefined
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error"
+ console.error(
+ "Failed to initialize checkpoint tracker:",
+ errorMessage,
+ )
+ this.checkpointTrackerErrorMessage = errorMessage // will be displayed right away since we saveClineMessages next which posts state to webview
+ }
+ }
+
+ const [parsedUserContent, environmentDetails] = await this.loadContext(
+ userContent,
+ includeFileDetails,
+ )
userContent = parsedUserContent
// add environment details as its own text block, separate from tool results
userContent.push({ type: "text", text: environmentDetails })
- await this.addToApiConversationHistory({ role: "user", content: userContent })
+ await this.addToApiConversationHistory({
+ role: "user",
+ content: userContent,
+ })
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
- const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
+ const lastApiReqIndex = findLastIndex(
+ this.clineMessages,
+ (m) => m.say === "api_req_started",
+ )
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
- request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
+ request: userContent
+ .map((block) => formatContentBlockToMarkdown(block))
+ .join("\n\n"),
} satisfies ClineApiReqInfo)
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
@@ -2364,9 +3786,14 @@ export class Cline {
// update api_req_started. we can't use api_req_finished anymore since it's a unique case where it could come after a streaming message (ie in the middle of being updated or executed)
// 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 updateApiReqMsg = (
+ cancelReason?: ClineApiReqCancelReason,
+ streamingFailedMessage?: string,
+ ) => {
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
- ...JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}"),
+ ...JSON.parse(
+ this.clineMessages[lastApiReqIndex].text || "{}",
+ ),
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWrites: cacheWriteTokens,
@@ -2385,7 +3812,10 @@ export class Cline {
} satisfies ClineApiReqInfo)
}
- const abortStream = async (cancelReason: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
+ const abortStream = async (
+ cancelReason: ClineApiReqCancelReason,
+ streamingFailedMessage?: string,
+ ) => {
if (this.diffViewProvider.isEditing) {
await this.diffViewProvider.revertChanges() // closes diff view
}
@@ -2422,7 +3852,7 @@ export class Cline {
await this.saveClineMessages()
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
- this.didFinishAborting = true
+ this.didFinishAbortingStream = true
}
// reset streaming state
@@ -2439,6 +3869,7 @@ export class Cline {
const stream = this.attemptApiRequest(previousApiReqIndex) // yields only if the first chunk is successful, otherwise will allow the user to retry the request (most likely due to rate limit error, which gets thrown on the first chunk)
let assistantMessage = ""
+ this.isStreaming = true
try {
for await (const chunk of stream) {
switch (chunk.type) {
@@ -2452,9 +3883,13 @@ export class Cline {
case "text":
assistantMessage += chunk.text
// parse raw assistant message into content blocks
- const prevLength = this.assistantMessageContent.length
- this.assistantMessageContent = parseAssistantMessage(assistantMessage)
- if (this.assistantMessageContent.length > prevLength) {
+ const prevLength =
+ this.assistantMessageContent.length
+ this.assistantMessageContent =
+ parseAssistantMessage(assistantMessage)
+ if (
+ this.assistantMessageContent.length > prevLength
+ ) {
this.userMessageContentReady = false // new content we need to present, reset to false in case previous content set this to true
}
// present content to user
@@ -2473,7 +3908,8 @@ export class Cline {
if (this.didRejectTool) {
// userContent has a tool rejection, so interrupt the assistant's response to present the user's feedback
- assistantMessage += "\n\n[Response interrupted by user feedback]"
+ assistantMessage +=
+ "\n\n[Response interrupted by user feedback]"
// this.userMessageContentReady = true // instead of setting this premptively, we allow the present iterator to finish and set userMessageContentReady when its ready
break
}
@@ -2492,14 +3928,21 @@ export class Cline {
this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
await abortStream(
"streaming_failed",
- error.message ?? JSON.stringify(serializeError(error), null, 2),
+ error.message ??
+ JSON.stringify(serializeError(error), null, 2),
)
- const history = await this.providerRef.deref()?.getTaskWithId(this.taskId)
+ const history = await this.providerRef
+ .deref()
+ ?.getTaskWithId(this.taskId)
if (history) {
- await this.providerRef.deref()?.initClineWithHistoryItem(history.historyItem)
+ await this.providerRef
+ .deref()
+ ?.initClineWithHistoryItem(history.historyItem)
// await this.providerRef.deref()?.postStateToWebview()
}
}
+ } finally {
+ this.isStreaming = false
}
// need to call here in case the stream was aborted
@@ -2511,7 +3954,9 @@ export class Cline {
// set any blocks to be complete to allow presentAssistantMessage to finish and set userMessageContentReady to true
// (could be a text block that had no subsequent tool uses, or a text block at the very end, or an invalid tool use, etc. whatever the case, presentAssistantMessage relies on these blocks either to be completed or the user to reject a block in order to proceed and eventually set userMessageContentReady to true)
- const partialBlocks = this.assistantMessageContent.filter((block) => block.partial)
+ const partialBlocks = this.assistantMessageContent.filter(
+ (block) => block.partial,
+ )
partialBlocks.forEach((block) => {
block.partial = false
})
@@ -2544,7 +3989,9 @@ export class Cline {
await pWaitFor(() => this.userMessageContentReady)
// if the model did not tool use, then we need to tell it to either use a tool or attempt_completion
- const didToolUse = this.assistantMessageContent.some((block) => block.type === "tool_use")
+ const didToolUse = this.assistantMessageContent.some(
+ (block) => block.type === "tool_use",
+ )
if (!didToolUse) {
this.userMessageContent.push({
type: "text",
@@ -2553,7 +4000,9 @@ export class Cline {
this.consecutiveMistakeCount++
}
- const recDidEndLoop = await this.recursivelyMakeClineRequests(this.userMessageContent)
+ const recDidEndLoop = await this.recursivelyMakeClineRequests(
+ this.userMessageContent,
+ )
didEndLoop = recDidEndLoop
} else {
// if there's no assistant_responses, that means we got no text or tool_use content blocks from API which we should assume is an error
@@ -2563,7 +4012,12 @@ export class Cline {
)
await this.addToApiConversationHistory({
role: "assistant",
- content: [{ type: "text", text: "Failure: I did not provide a response." }],
+ content: [
+ {
+ type: "text",
+ text: "Failure: I did not provide a response.",
+ },
+ ],
})
}
@@ -2574,7 +4028,10 @@ export class Cline {
}
}
- async loadContext(userContent: UserContent, includeFileDetails: boolean = false) {
+ async loadContext(
+ userContent: UserContent,
+ includeFileDetails: boolean = false,
+ ) {
return await Promise.all([
// Process userContent array, which contains various block types:
// TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam.
@@ -2586,22 +4043,42 @@ export class Cline {
if (block.type === "text") {
return {
...block,
- text: await parseMentions(block.text, cwd, this.urlContentFetcher),
+ text: await parseMentions(
+ block.text,
+ cwd,
+ this.urlContentFetcher,
+ ),
}
} else if (block.type === "tool_result") {
- const isUserMessage = (text: string) => text.includes("") || text.includes("")
- if (typeof block.content === "string" && isUserMessage(block.content)) {
+ const isUserMessage = (text: string) =>
+ text.includes("") ||
+ text.includes("")
+ if (
+ typeof block.content === "string" &&
+ isUserMessage(block.content)
+ ) {
return {
...block,
- content: await parseMentions(block.content, cwd, this.urlContentFetcher),
+ content: await parseMentions(
+ block.content,
+ cwd,
+ this.urlContentFetcher,
+ ),
}
} else if (Array.isArray(block.content)) {
const parsedContent = await Promise.all(
block.content.map(async (contentBlock) => {
- if (contentBlock.type === "text" && isUserMessage(contentBlock.text)) {
+ if (
+ contentBlock.type === "text" &&
+ isUserMessage(contentBlock.text)
+ ) {
return {
...contentBlock,
- text: await parseMentions(contentBlock.text, cwd, this.urlContentFetcher),
+ text: await parseMentions(
+ contentBlock.text,
+ cwd,
+ this.urlContentFetcher,
+ ),
}
}
return contentBlock
@@ -2662,10 +4139,16 @@ export class Cline {
if (busyTerminals.length > 0) {
// wait for terminals to cool down
// terminalWasBusy = allTerminals.some((t) => this.terminalManager.isProcessHot(t.id))
- await pWaitFor(() => busyTerminals.every((t) => !this.terminalManager.isProcessHot(t.id)), {
- interval: 100,
- timeout: 15_000,
- }).catch(() => {})
+ await pWaitFor(
+ () =>
+ busyTerminals.every(
+ (t) => !this.terminalManager.isProcessHot(t.id),
+ ),
+ {
+ interval: 100,
+ timeout: 15_000,
+ },
+ ).catch(() => {})
}
// we want to get diagnostics AFTER terminal cools down for a few reasons: terminal could be scaffolding a project, dev servers (compilers like webpack) will first re-compile and then send diagnostics, etc
@@ -2694,7 +4177,9 @@ export class Cline {
terminalDetails += "\n\n# Actively Running Terminals"
for (const busyTerminal of busyTerminals) {
terminalDetails += `\n## Original command: \`${busyTerminal.lastCommand}\``
- const newOutput = this.terminalManager.getUnretrievedOutput(busyTerminal.id)
+ const newOutput = this.terminalManager.getUnretrievedOutput(
+ busyTerminal.id,
+ )
if (newOutput) {
terminalDetails += `\n### New Output\n${newOutput}`
} else {
@@ -2706,7 +4191,9 @@ export class Cline {
if (inactiveTerminals.length > 0) {
const inactiveTerminalOutputs = new Map()
for (const inactiveTerminal of inactiveTerminals) {
- const newOutput = this.terminalManager.getUnretrievedOutput(inactiveTerminal.id)
+ const newOutput = this.terminalManager.getUnretrievedOutput(
+ inactiveTerminal.id,
+ )
if (newOutput) {
inactiveTerminalOutputs.set(inactiveTerminal.id, newOutput)
}
@@ -2714,7 +4201,9 @@ export class Cline {
if (inactiveTerminalOutputs.size > 0) {
terminalDetails += "\n\n# Inactive Terminals"
for (const [terminalId, newOutput] of inactiveTerminalOutputs) {
- const inactiveTerminal = inactiveTerminals.find((t) => t.id === terminalId)
+ const inactiveTerminal = inactiveTerminals.find(
+ (t) => t.id === terminalId,
+ )
if (inactiveTerminal) {
terminalDetails += `\n## ${inactiveTerminal.lastCommand}`
terminalDetails += `\n### New Output\n${newOutput}`
@@ -2736,13 +4225,21 @@ export class Cline {
if (includeFileDetails) {
details += `\n\n# Current Working Directory (${cwd.toPosix()}) Files\n`
- const isDesktop = arePathsEqual(cwd, path.join(os.homedir(), "Desktop"))
+ const isDesktop = arePathsEqual(
+ cwd,
+ path.join(os.homedir(), "Desktop"),
+ )
if (isDesktop) {
// don't want to immediately access desktop since it would show permission popup
- details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
+ details +=
+ "(Desktop files not shown automatically. Use list_files to explore if needed.)"
} else {
const [files, didHitLimit] = await listFiles(cwd, true, 200)
- const result = formatResponse.formatFilesList(cwd, files, didHitLimit)
+ const result = formatResponse.formatFilesList(
+ cwd,
+ files,
+ didHitLimit,
+ )
details += result
}
}
diff --git a/src/core/assistant-message/diff.ts b/src/core/assistant-message/diff.ts
index 6d7b68395e..cd4a3b04ca 100644
--- a/src/core/assistant-message/diff.ts
+++ b/src/core/assistant-message/diff.ts
@@ -29,7 +29,11 @@ function lineTrimmedFallbackMatch(
}
// For each possible starting position in original content
- for (let i = startLineNum; i <= originalLines.length - searchLines.length; i++) {
+ for (
+ let i = startLineNum;
+ i <= originalLines.length - searchLines.length;
+ i++
+ ) {
let matches = true
// Try to match all search lines from this position
@@ -122,7 +126,11 @@ function blockAnchorFallbackMatch(
}
// Look for matching start and end anchors
- for (let i = startLineNum; i <= originalLines.length - searchBlockSize; i++) {
+ for (
+ let i = startLineNum;
+ i <= originalLines.length - searchBlockSize;
+ i++
+ ) {
// Check if first line matches
if (originalLines[i].trim() !== firstLineSearch) {
continue
@@ -231,7 +239,9 @@ export async function constructNewFileContent(
const lastLine = lines[lines.length - 1]
if (
lines.length > 0 &&
- (lastLine.startsWith("<") || lastLine.startsWith("=") || lastLine.startsWith(">")) &&
+ (lastLine.startsWith("<") ||
+ lastLine.startsWith("=") ||
+ lastLine.startsWith(">")) &&
lastLine !== "<<<<<<< SEARCH" &&
lastLine !== "=======" &&
lastLine !== ">>>>>>> REPLACE"
@@ -280,7 +290,10 @@ export async function constructNewFileContent(
// }
// Exact search match scenario
- const exactIndex = originalContent.indexOf(currentSearchContent, lastProcessedIndex)
+ const exactIndex = originalContent.indexOf(
+ currentSearchContent,
+ lastProcessedIndex,
+ )
if (exactIndex !== -1) {
searchMatchIndex = exactIndex
searchEndIndex = exactIndex + currentSearchContent.length
@@ -312,7 +325,10 @@ export async function constructNewFileContent(
}
// Output everything up to the match location
- result += originalContent.slice(lastProcessedIndex, searchMatchIndex)
+ result += originalContent.slice(
+ lastProcessedIndex,
+ searchMatchIndex,
+ )
continue
}
diff --git a/src/core/assistant-message/index.ts b/src/core/assistant-message/index.ts
index 7ad2c27d7b..ed03120a32 100644
--- a/src/core/assistant-message/index.ts
+++ b/src/core/assistant-message/index.ts
@@ -60,7 +60,9 @@ export interface ToolUse {
export interface ExecuteCommandToolUse extends ToolUse {
name: "execute_command"
// Pick, "command"> makes "command" required, but Partial<> makes it optional
- params: Partial, "command" | "requires_approval">>
+ params: Partial<
+ Pick, "command" | "requires_approval">
+ >
}
export interface ReadFileToolUse extends ToolUse {
@@ -80,7 +82,9 @@ export interface ReplaceInFileToolUse extends ToolUse {
export interface SearchFilesToolUse extends ToolUse {
name: "search_files"
- params: Partial, "path" | "regex" | "file_pattern">>
+ params: Partial<
+ Pick, "path" | "regex" | "file_pattern">
+ >
}
export interface ListFilesToolUse extends ToolUse {
@@ -95,12 +99,22 @@ export interface ListCodeDefinitionNamesToolUse extends ToolUse {
export interface BrowserActionToolUse extends ToolUse {
name: "browser_action"
- params: Partial, "action" | "url" | "coordinate" | "text">>
+ params: Partial<
+ Pick<
+ Record,
+ "action" | "url" | "coordinate" | "text"
+ >
+ >
}
export interface UseMcpToolToolUse extends ToolUse {
name: "use_mcp_tool"
- params: Partial, "server_name" | "tool_name" | "arguments">>
+ params: Partial<
+ Pick<
+ Record,
+ "server_name" | "tool_name" | "arguments"
+ >
+ >
}
export interface AccessMcpResourceToolUse extends ToolUse {
diff --git a/src/core/assistant-message/parse-assistant-message.ts b/src/core/assistant-message/parse-assistant-message.ts
index e38e8f6458..9c6a2308c8 100644
--- a/src/core/assistant-message/parse-assistant-message.ts
+++ b/src/core/assistant-message/parse-assistant-message.ts
@@ -24,11 +24,15 @@ export function parseAssistantMessage(assistantMessage: string) {
// there should not be a param without a tool use
if (currentToolUse && currentParamName) {
- const currentParamValue = accumulator.slice(currentParamValueStartIndex)
+ const currentParamValue = accumulator.slice(
+ currentParamValueStartIndex,
+ )
const paramClosingTag = `${currentParamName}>`
if (currentParamValue.endsWith(paramClosingTag)) {
// end of param value
- currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
+ currentToolUse.params[currentParamName] = currentParamValue
+ .slice(0, -paramClosingTag.length)
+ .trim()
currentParamName = undefined
continue
} else {
@@ -49,11 +53,16 @@ export function parseAssistantMessage(assistantMessage: string) {
currentToolUse = undefined
continue
} else {
- const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
+ const possibleParamOpeningTags = toolParamNames.map(
+ (name) => `<${name}>`,
+ )
for (const paramOpeningTag of possibleParamOpeningTags) {
if (accumulator.endsWith(paramOpeningTag)) {
// start of a new parameter
- currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
+ currentParamName = paramOpeningTag.slice(
+ 1,
+ -1,
+ ) as ToolParamName
currentParamValueStartIndex = accumulator.length
break
}
@@ -63,13 +72,25 @@ export function parseAssistantMessage(assistantMessage: string) {
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
const contentParamName: ToolParamName = "content"
- if (currentToolUse.name === "write_to_file" && accumulator.endsWith(`${contentParamName}>`)) {
- const toolContent = accumulator.slice(currentToolUseStartIndex)
+ if (
+ currentToolUse.name === "write_to_file" &&
+ accumulator.endsWith(`${contentParamName}>`)
+ ) {
+ const toolContent = accumulator.slice(
+ currentToolUseStartIndex,
+ )
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `${contentParamName}>`
- const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
- const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
- if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
+ const contentStartIndex =
+ toolContent.indexOf(contentStartTag) +
+ contentStartTag.length
+ const contentEndIndex =
+ toolContent.lastIndexOf(contentEndTag)
+ if (
+ contentStartIndex !== -1 &&
+ contentEndIndex !== -1 &&
+ contentEndIndex > contentStartIndex
+ ) {
currentToolUse.params[contentParamName] = toolContent
.slice(contentStartIndex, contentEndIndex)
.trim()
@@ -84,7 +105,9 @@ export function parseAssistantMessage(assistantMessage: string) {
// no currentToolUse
let didStartToolUse = false
- const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
+ const possibleToolUseOpeningTags = toolUseNames.map(
+ (name) => `<${name}>`,
+ )
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
if (accumulator.endsWith(toolUseOpeningTag)) {
// start of a new tool use
@@ -128,7 +151,9 @@ export function parseAssistantMessage(assistantMessage: string) {
// stream did not complete tool call, add it as partial
if (currentParamName) {
// tool call has a parameter that was not completed
- currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
+ currentToolUse.params[currentParamName] = accumulator
+ .slice(currentParamValueStartIndex)
+ .trim()
}
contentBlocks.push(currentToolUse)
}
diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts
index 1c9c122d1d..aad682e94e 100644
--- a/src/core/mentions/index.ts
+++ b/src/core/mentions/index.ts
@@ -15,13 +15,18 @@ export function openMention(mention?: string): void {
if (mention.startsWith("/")) {
const relPath = mention.slice(1)
- const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
+ const cwd = vscode.workspace.workspaceFolders
+ ?.map((folder) => folder.uri.fsPath)
+ .at(0)
if (!cwd) {
return
}
const absPath = path.resolve(cwd, relPath)
if (mention.endsWith("/")) {
- vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
+ vscode.commands.executeCommand(
+ "revealInExplorer",
+ vscode.Uri.file(absPath),
+ )
// vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window
} else {
openFile(absPath)
@@ -33,7 +38,11 @@ export function openMention(mention?: string): void {
}
}
-export async function parseMentions(text: string, cwd: string, urlContentFetcher: UrlContentFetcher): Promise {
+export async function parseMentions(
+ text: string,
+ cwd: string,
+ urlContentFetcher: UrlContentFetcher,
+): Promise {
const mentions: Set = new Set()
let parsedText = text.replace(mentionRegexGlobal, (match, mention) => {
mentions.add(mention)
@@ -50,14 +59,18 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
return match
})
- const urlMention = Array.from(mentions).find((mention) => mention.startsWith("http"))
+ const urlMention = Array.from(mentions).find((mention) =>
+ mention.startsWith("http"),
+ )
let launchBrowserError: Error | undefined
if (urlMention) {
try {
await urlContentFetcher.launchBrowser()
} catch (error) {
launchBrowserError = error
- vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${error.message}`)
+ vscode.window.showErrorMessage(
+ `Error fetching content for ${urlMention}: ${error.message}`,
+ )
}
}
@@ -68,10 +81,13 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
result = `Error fetching content: ${launchBrowserError.message}`
} else {
try {
- const markdown = await urlContentFetcher.urlToMarkdown(mention)
+ const markdown =
+ await urlContentFetcher.urlToMarkdown(mention)
result = markdown
} catch (error) {
- vscode.window.showErrorMessage(`Error fetching content for ${mention}: ${error.message}`)
+ vscode.window.showErrorMessage(
+ `Error fetching content for ${mention}: ${error.message}`,
+ )
result = `Error fetching content: ${error.message}`
}
}
@@ -113,7 +129,10 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
return parsedText
}
-async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise {
+async function getFileOrFolderContent(
+ mentionPath: string,
+ cwd: string,
+): Promise {
const absPath = path.resolve(cwd, mentionPath)
try {
@@ -141,11 +160,14 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
fileContentPromises.push(
(async () => {
try {
- const isBinary = await isBinaryFile(absoluteFilePath).catch(() => false)
+ const isBinary = await isBinaryFile(
+ absoluteFilePath,
+ ).catch(() => false)
if (isBinary) {
return undefined
}
- const content = await extractTextFromFile(absoluteFilePath)
+ const content =
+ await extractTextFromFile(absoluteFilePath)
return `\n${content}\n`
} catch (error) {
return undefined
@@ -159,13 +181,17 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
folderContent += `${linePrefix}${entry.name}\n`
}
})
- const fileContents = (await Promise.all(fileContentPromises)).filter((content) => content)
+ const fileContents = (
+ await Promise.all(fileContentPromises)
+ ).filter((content) => content)
return `${folderContent}\n${fileContents.join("\n\n")}`.trim()
} else {
return `(Failed to read contents of ${mentionPath})`
}
} catch (error) {
- throw new Error(`Failed to access path "${mentionPath}": ${error.message}`)
+ throw new Error(
+ `Failed to access path "${mentionPath}": ${error.message}`,
+ )
}
}
diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts
index 05f33ba71a..15e0ced3b5 100644
--- a/src/core/prompts/responses.ts
+++ b/src/core/prompts/responses.ts
@@ -8,7 +8,8 @@ export const formatResponse = {
toolDeniedWithFeedback: (feedback?: string) =>
`The user denied this operation and provided the following feedback:\n\n${feedback}\n`,
- toolError: (error?: string) => `The tool execution failed with the following error:\n\n${error}\n`,
+ toolError: (error?: string) =>
+ `The tool execution failed with the following error:\n\n${error}\n`,
noToolsUsed: () =>
`[ERROR] You did not use a tool in your previous response! Please retry with a tool use.
@@ -37,7 +38,8 @@ Otherwise, if you have not completed the task and do not need additional informa
): string | Array => {
if (images && images.length > 0) {
const textBlock: Anthropic.TextBlockParam = { type: "text", text }
- const imageBlocks: Anthropic.ImageBlockParam[] = formatImagesIntoBlocks(images)
+ const imageBlocks: Anthropic.ImageBlockParam[] =
+ formatImagesIntoBlocks(images)
// Placing images after text leads to better results
return [textBlock, ...imageBlocks]
} else {
@@ -49,7 +51,11 @@ Otherwise, if you have not completed the task and do not need additional informa
return formatImagesIntoBlocks(images)
},
- formatFilesList: (absolutePath: string, files: string[], didHitLimit: boolean): string => {
+ formatFilesList: (
+ absolutePath: string,
+ files: string[],
+ didHitLimit: boolean,
+ ): string => {
const sorted = files
.map((file) => {
// convert absolute path to relative path
@@ -60,7 +66,11 @@ Otherwise, if you have not completed the task and do not need additional informa
.sort((a, b) => {
const aParts = a.split("/") // only works if we use toPosix first
const bParts = b.split("/")
- for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
+ for (
+ let i = 0;
+ i < Math.min(aParts.length, bParts.length);
+ i++
+ ) {
if (aParts[i] !== bParts[i]) {
// If one is a directory and the other isn't at this level, sort the directory first
if (i + 1 === aParts.length && i + 1 < bParts.length) {
@@ -70,7 +80,10 @@ Otherwise, if you have not completed the task and do not need additional informa
return 1
}
// Otherwise, sort alphabetically
- return aParts[i].localeCompare(bParts[i], undefined, { numeric: true, sensitivity: "base" })
+ return aParts[i].localeCompare(bParts[i], undefined, {
+ numeric: true,
+ sensitivity: "base",
+ })
}
}
// If all parts are the same up to the length of the shorter path,
@@ -81,16 +94,27 @@ Otherwise, if you have not completed the task and do not need additional informa
return `${sorted.join(
"\n",
)}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)`
- } else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) {
+ } else if (
+ sorted.length === 0 ||
+ (sorted.length === 1 && sorted[0] === "")
+ ) {
return "No files found."
} else {
return sorted.join("\n")
}
},
- createPrettyPatch: (filename = "file", oldStr?: string, newStr?: string) => {
+ createPrettyPatch: (
+ filename = "file",
+ oldStr?: string,
+ newStr?: string,
+ ) => {
// strings cannot be undefined or diff throws exception
- const patch = diff.createPatch(filename.toPosix(), oldStr || "", newStr || "")
+ const patch = diff.createPatch(
+ filename.toPosix(),
+ oldStr || "",
+ newStr || "",
+ )
const lines = patch.split("\n")
const prettyPatchLines = lines.slice(4)
return prettyPatchLines.join("\n")
@@ -98,7 +122,9 @@ Otherwise, if you have not completed the task and do not need additional informa
}
// to avoid circular dependency
-const formatImagesIntoBlocks = (images?: string[]): Anthropic.ImageBlockParam[] => {
+const formatImagesIntoBlocks = (
+ images?: string[],
+): Anthropic.ImageBlockParam[] => {
return images
? images.map((dataUrl) => {
// data:image/png;base64,base64string
@@ -106,7 +132,11 @@ const formatImagesIntoBlocks = (images?: string[]): Anthropic.ImageBlockParam[]
const mimeType = rest.split(":")[1].split(";")[0]
return {
type: "image",
- source: { type: "base64", media_type: mimeType, data: base64 },
+ source: {
+ type: "base64",
+ media_type: mimeType,
+ data: base64,
+ },
} as Anthropic.ImageBlockParam
})
: []
diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts
index 1e39799303..e9722b20f8 100644
--- a/src/core/prompts/system.ts
+++ b/src/core/prompts/system.ts
@@ -362,11 +362,17 @@ ${
.join("\n\n")
const templates = server.resourceTemplates
- ?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
+ ?.map(
+ (template) =>
+ `- ${template.uriTemplate} (${template.name}): ${template.description}`,
+ )
.join("\n")
const resources = server.resources
- ?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
+ ?.map(
+ (resource) =>
+ `- ${resource.uri} (${resource.name}): ${resource.description}`,
+ )
.join("\n")
const config = JSON.parse(server.config)
@@ -374,8 +380,12 @@ ${
return (
`## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` +
(tools ? `\n\n### Available Tools\n${tools}` : "") +
- (templates ? `\n\n### Resource Templates\n${templates}` : "") +
- (resources ? `\n\n### Direct Resources\n${resources}` : "")
+ (templates
+ ? `\n\n### Resource Templates\n${templates}`
+ : "") +
+ (resources
+ ? `\n\n### Direct Resources\n${resources}`
+ : "")
)
})
.join("\n\n")}`
@@ -889,7 +899,10 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
-export function addUserInstructions(settingsCustomInstructions?: string, clineRulesFileInstructions?: string) {
+export function addUserInstructions(
+ settingsCustomInstructions?: string,
+ clineRulesFileInstructions?: string,
+) {
let customInstructions = ""
if (settingsCustomInstructions) {
customInstructions += settingsCustomInstructions + "\n\n"
diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts
index caa604bc57..83b91eb381 100644
--- a/src/core/sliding-window/index.ts
+++ b/src/core/sliding-window/index.ts
@@ -8,19 +8,82 @@ a 200k context, we can assume that the first half is likely irrelevant to their
Therefore, this function should only be called when absolutely necessary to fit within
context limits, not as a continuous process.
*/
-export function truncateHalfConversation(
- messages: Anthropic.Messages.MessageParam[],
-): Anthropic.Messages.MessageParam[] {
- // API expects messages to be in user-assistant order, and tool use messages must be followed by tool results. We need to maintain this structure while truncating.
+// export function truncateHalfConversation(
+// messages: Anthropic.Messages.MessageParam[],
+// ): Anthropic.Messages.MessageParam[] {
+// // API expects messages to be in user-assistant order, and tool use messages must be followed by tool results. We need to maintain this structure while truncating.
+
+// // Always keep the first Task message (this includes the project's file structure in environment_details)
+// const truncatedMessages = [messages[0]]
+
+// // Remove half of user-assistant pairs
+// const messagesToRemove = Math.floor(messages.length / 4) * 2 // has to be even number
+
+// const remainingMessages = messages.slice(messagesToRemove + 1) // has to start with assistant message since tool result cannot follow assistant message with no tool use
+// truncatedMessages.push(...remainingMessages)
+
+// return truncatedMessages
+// }
+
+/*
+getNextTruncationRange: Calculates the next range of messages to be "deleted"
+- Takes the full messages array and optional current deleted range
+- Always preserves the first message (task message)
+- Removes 1/2 of remaining messages (rounded down to even number) after current deleted range
+- Returns [startIndex, endIndex] representing inclusive range to delete
+
+getTruncatedMessages: Constructs the truncated array using the deleted range
+- Takes full messages array and optional deleted range
+- Returns new array with messages in deleted range removed
+- Preserves order and structure of remaining messages
+
+The range is represented as [startIndex, endIndex] where both indices are inclusive
+The functions maintain the original array integrity while allowing progressive truncation
+through the deletedRange parameter
- // Always keep the first Task message (this includes the project's file structure in environment_details)
- const truncatedMessages = [messages[0]]
+Usage example:
+const messages = [user1, assistant1, user2, assistant2, user3, assistant3];
+let deletedRange = getNextTruncationRange(messages); // [1,2] (assistant1,user2)
+let truncated = getTruncatedMessages(messages, deletedRange);
+// [user1, assistant2, user3, assistant3]
+
+deletedRange = getNextTruncationRange(messages, deletedRange); // [2,3] (assistant2,user3)
+truncated = getTruncatedMessages(messages, deletedRange);
+// [user1, assistant3]
+*/
+
+export function getNextTruncationRange(
+ messages: Anthropic.Messages.MessageParam[],
+ currentDeletedRange: [number, number] | undefined = undefined,
+): [number, number] {
+ // Since we always keep the first message, currentDeletedRange[0] will always be 1 (for now until we have a smarter truncation algorithm)
+ const rangeStartIndex = 1
+ const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 1
// Remove half of user-assistant pairs
- const messagesToRemove = Math.floor(messages.length / 4) * 2 // has to be even number
+ const messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number
+ let rangeEndIndex = startOfRest + messagesToRemove - 1
+
+ // Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure.
+ // NOTE: anthropic format messages are always user-assitant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
+ if (messages[rangeEndIndex].role !== "user") {
+ rangeEndIndex -= 1
+ }
- const remainingMessages = messages.slice(messagesToRemove + 1) // has to start with assistant message since tool result cannot follow assistant message with no tool use
- truncatedMessages.push(...remainingMessages)
+ // this is an inclusive range that will be removed from the conversation history
+ return [rangeStartIndex, rangeEndIndex]
+}
+
+export function getTruncatedMessages(
+ messages: Anthropic.Messages.MessageParam[],
+ deletedRange: [number, number] | undefined,
+): Anthropic.Messages.MessageParam[] {
+ if (!deletedRange) {
+ return messages
+ }
- return truncatedMessages
+ const [start, end] = deletedRange
+ // the range is inclusive - both start and end indices and everything in between will be removed from the final result.
+ // NOTE: if you try to console log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
+ return [...messages.slice(0, start), ...messages.slice(end + 1)]
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 62ec46ee2d..69847a1b29 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -14,15 +14,21 @@ import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
-import { ExtensionMessage } from "../../shared/ExtensionMessage"
+import { ExtensionMessage, ExtensionState } from "../../shared/ExtensionMessage"
import { HistoryItem } from "../../shared/HistoryItem"
-import { WebviewMessage } from "../../shared/WebviewMessage"
+import {
+ ClineCheckpointRestore,
+ WebviewMessage,
+} from "../../shared/WebviewMessage"
import { fileExistsAtPath } from "../../utils/fs"
import { Cline } from "../Cline"
import { openMention } from "../mentions"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
-import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
+import {
+ AutoApprovalSettings,
+ DEFAULT_AUTO_APPROVAL_SETTINGS,
+} from "../../shared/AutoApprovalSettings"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -79,7 +85,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private cline?: Cline
private workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
- private latestAnnouncementId = "dec-17-2024" // update to some unique identifier when we add a new announcement
+ private latestAnnouncementId = "jan-5-2025" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
@@ -119,7 +125,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
public static getVisibleInstance(): ClineProvider | undefined {
- return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
+ return findLast(
+ Array.from(this.activeInstances),
+ (instance) => instance.view?.visible === true,
+ )
}
resolveWebviewView(
@@ -152,7 +161,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
webviewView.onDidChangeViewState(
() => {
if (this.view?.visible) {
- this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
+ this.postMessageToWebview({
+ type: "action",
+ action: "didBecomeVisible",
+ })
}
},
null,
@@ -163,7 +175,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
webviewView.onDidChangeVisibility(
() => {
if (this.view?.visible) {
- this.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
+ this.postMessageToWebview({
+ type: "action",
+ action: "didBecomeVisible",
+ })
}
},
null,
@@ -186,7 +201,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
// Sends latest theme name to webview
- await this.postMessageToWebview({ type: "theme", text: JSON.stringify(await getTheme()) })
+ await this.postMessageToWebview({
+ type: "theme",
+ text: JSON.stringify(await getTheme()),
+ })
}
},
null,
@@ -201,13 +219,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async initClineWithTask(task?: string, images?: string[]) {
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
- const { apiConfiguration, customInstructions, autoApprovalSettings } = await this.getState()
- this.cline = new Cline(this, apiConfiguration, autoApprovalSettings, customInstructions, task, images)
+ const { apiConfiguration, customInstructions, autoApprovalSettings } =
+ await this.getState()
+ this.cline = new Cline(
+ this,
+ apiConfiguration,
+ autoApprovalSettings,
+ customInstructions,
+ task,
+ images,
+ )
}
async initClineWithHistoryItem(historyItem: HistoryItem) {
await this.clearTask()
- const { apiConfiguration, customInstructions, autoApprovalSettings } = await this.getState()
+ const { apiConfiguration, customInstructions, autoApprovalSettings } =
+ await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
@@ -248,7 +275,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
"main.css",
])
// The JS file from the React build output
- const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "static", "js", "main.js"])
+ const scriptUri = getUri(webview, this.context.extensionUri, [
+ "webview-ui",
+ "build",
+ "static",
+ "js",
+ "main.js",
+ ])
// The codicon font from the React build output
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
@@ -319,30 +352,42 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.postStateToWebview()
this.workspaceTracker?.initializeFilePaths() // don't await
getTheme().then((theme) =>
- this.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }),
+ this.postMessageToWebview({
+ type: "theme",
+ text: JSON.stringify(theme),
+ }),
)
// post last cached models in case the call to endpoint fails
this.readOpenRouterModels().then((openRouterModels) => {
if (openRouterModels) {
- this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
+ this.postMessageToWebview({
+ type: "openRouterModels",
+ openRouterModels,
+ })
}
})
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
- this.refreshOpenRouterModels().then(async (openRouterModels) => {
- if (openRouterModels) {
- // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
- const { apiConfiguration } = await this.getState()
- if (apiConfiguration.openRouterModelId) {
- await this.updateGlobalState(
- "openRouterModelInfo",
- openRouterModels[apiConfiguration.openRouterModelId],
- )
- await this.postStateToWebview()
+ this.refreshOpenRouterModels().then(
+ async (openRouterModels) => {
+ if (openRouterModels) {
+ // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
+ const { apiConfiguration } =
+ await this.getState()
+ if (apiConfiguration.openRouterModelId) {
+ await this.updateGlobalState(
+ "openRouterModelInfo",
+ openRouterModels[
+ apiConfiguration
+ .openRouterModelId
+ ],
+ )
+ await this.postStateToWebview()
+ }
}
- }
- })
+ },
+ )
break
case "newTask":
// Code that should run in response to the hello message command
@@ -353,7 +398,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Could also do this in extension .ts
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
- await this.initClineWithTask(message.text, message.images)
+ await this.initClineWithTask(
+ message.text,
+ message.images,
+ )
break
case "apiConfiguration":
if (message.apiConfiguration) {
@@ -384,33 +432,92 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterModelId,
openRouterModelInfo,
} = message.apiConfiguration
- await this.updateGlobalState("apiProvider", apiProvider)
- await this.updateGlobalState("apiModelId", apiModelId)
+ await this.updateGlobalState(
+ "apiProvider",
+ apiProvider,
+ )
+ await this.updateGlobalState(
+ "apiModelId",
+ apiModelId,
+ )
await this.storeSecret("apiKey", apiKey)
- await this.storeSecret("openRouterApiKey", openRouterApiKey)
+ await this.storeSecret(
+ "openRouterApiKey",
+ openRouterApiKey,
+ )
await this.storeSecret("awsAccessKey", awsAccessKey)
await this.storeSecret("awsSecretKey", awsSecretKey)
- await this.storeSecret("awsSessionToken", awsSessionToken)
+ await this.storeSecret(
+ "awsSessionToken",
+ awsSessionToken,
+ )
await this.updateGlobalState("awsRegion", awsRegion)
- await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
- await this.updateGlobalState("vertexProjectId", vertexProjectId)
- await this.updateGlobalState("vertexRegion", vertexRegion)
- await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
+ await this.updateGlobalState(
+ "awsUseCrossRegionInference",
+ awsUseCrossRegionInference,
+ )
+ await this.updateGlobalState(
+ "vertexProjectId",
+ vertexProjectId,
+ )
+ await this.updateGlobalState(
+ "vertexRegion",
+ vertexRegion,
+ )
+ await this.updateGlobalState(
+ "openAiBaseUrl",
+ openAiBaseUrl,
+ )
await this.storeSecret("openAiApiKey", openAiApiKey)
- await this.updateGlobalState("openAiModelId", openAiModelId)
- await this.updateGlobalState("ollamaModelId", ollamaModelId)
- await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
- await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
- await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
- await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
+ await this.updateGlobalState(
+ "openAiModelId",
+ openAiModelId,
+ )
+ await this.updateGlobalState(
+ "ollamaModelId",
+ ollamaModelId,
+ )
+ await this.updateGlobalState(
+ "ollamaBaseUrl",
+ ollamaBaseUrl,
+ )
+ await this.updateGlobalState(
+ "lmStudioModelId",
+ lmStudioModelId,
+ )
+ await this.updateGlobalState(
+ "lmStudioBaseUrl",
+ lmStudioBaseUrl,
+ )
+ await this.updateGlobalState(
+ "anthropicBaseUrl",
+ anthropicBaseUrl,
+ )
await this.storeSecret("geminiApiKey", geminiApiKey)
- await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
- await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
- await this.updateGlobalState("azureApiVersion", azureApiVersion)
- await this.updateGlobalState("openRouterModelId", openRouterModelId)
- await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
+ await this.storeSecret(
+ "openAiNativeApiKey",
+ openAiNativeApiKey,
+ )
+ await this.storeSecret(
+ "deepSeekApiKey",
+ deepSeekApiKey,
+ )
+ await this.updateGlobalState(
+ "azureApiVersion",
+ azureApiVersion,
+ )
+ await this.updateGlobalState(
+ "openRouterModelId",
+ openRouterModelId,
+ )
+ await this.updateGlobalState(
+ "openRouterModelInfo",
+ openRouterModelInfo,
+ )
if (this.cline) {
- this.cline.api = buildApiHandler(message.apiConfiguration)
+ this.cline.api = buildApiHandler(
+ message.apiConfiguration,
+ )
}
}
await this.postStateToWebview()
@@ -420,15 +527,23 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "autoApprovalSettings":
if (message.autoApprovalSettings) {
- await this.updateGlobalState("autoApprovalSettings", message.autoApprovalSettings)
+ await this.updateGlobalState(
+ "autoApprovalSettings",
+ message.autoApprovalSettings,
+ )
if (this.cline) {
- this.cline.autoApprovalSettings = message.autoApprovalSettings
+ this.cline.autoApprovalSettings =
+ message.autoApprovalSettings
}
await this.postStateToWebview()
}
break
case "askResponse":
- this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
+ this.cline?.handleWebviewAskResponse(
+ message.askResponse!,
+ message.text,
+ message.images,
+ )
break
case "clearTask":
// newTask will start a new task with a given task text, while clear task resets the current session and allows for a new task to be started
@@ -436,12 +551,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
break
case "didShowAnnouncement":
- await this.updateGlobalState("lastShownAnnouncementId", this.latestAnnouncementId)
+ await this.updateGlobalState(
+ "lastShownAnnouncementId",
+ this.latestAnnouncementId,
+ )
await this.postStateToWebview()
break
case "selectImages":
const images = await selectImages()
- await this.postMessageToWebview({ type: "selectedImages", images })
+ await this.postMessageToWebview({
+ type: "selectedImages",
+ images,
+ })
break
case "exportCurrentTask":
const currentTaskId = this.cline?.taskId
@@ -462,12 +583,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.resetState()
break
case "requestOllamaModels":
- const ollamaModels = await this.getOllamaModels(message.text)
- this.postMessageToWebview({ type: "ollamaModels", ollamaModels })
+ const ollamaModels = await this.getOllamaModels(
+ message.text,
+ )
+ this.postMessageToWebview({
+ type: "ollamaModels",
+ ollamaModels,
+ })
break
case "requestLmStudioModels":
- const lmStudioModels = await this.getLmStudioModels(message.text)
- this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
+ const lmStudioModels = await this.getLmStudioModels(
+ message.text,
+ )
+ this.postMessageToWebview({
+ type: "lmStudioModels",
+ lmStudioModels,
+ })
break
case "refreshOpenRouterModels":
await this.refreshOpenRouterModels()
@@ -481,26 +612,53 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "openMention":
openMention(message.text)
break
- case "cancelTask":
- if (this.cline) {
- const { historyItem } = await this.getTaskWithId(this.cline.taskId)
- this.cline.abortTask()
- await pWaitFor(() => this.cline === undefined || this.cline.didFinishAborting, {
- timeout: 3_000,
- }).catch(() => {
- console.error("Failed to abort task")
+ case "checkpointDiff": {
+ if (message.number) {
+ await this.cline?.presentMultifileDiff(
+ message.number,
+ false,
+ )
+ }
+ break
+ }
+ case "checkpointRestore": {
+ await this.cancelTask() // we cannot alter message history say if the task is active, as it could be in the middle of editing a file or running a command, which expect the ask to be responded to rather than being superceded by a new message eg add deleted_api_reqs
+ // cancel task waits for any open editor to be reverted and starts a new cline instance
+ if (message.number) {
+ // wait for messages to be loaded
+ await pWaitFor(
+ () => this.cline?.isInitialized === true,
+ {
+ timeout: 3_000,
+ },
+ ).catch(() => {
+ console.error(
+ "Failed to init new cline instance",
+ )
})
- if (this.cline) {
- // 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
- this.cline.abandoned = true
- }
- await this.initClineWithHistoryItem(historyItem) // clears task again, so we need to abortTask manually above
- // await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
+ // NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
+ await this.cline?.restoreCheckpoint(
+ message.number,
+ message.text! as ClineCheckpointRestore,
+ )
+ }
+ break
+ }
+ case "taskCompletionViewChanges": {
+ if (message.number) {
+ await this.cline?.presentMultifileDiff(
+ message.number,
+ true,
+ )
}
-
+ break
+ }
+ case "cancelTask":
+ this.cancelTask()
break
case "openMcpSettings": {
- const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
+ const mcpSettingsFilePath =
+ await this.mcpHub?.getMcpSettingsFilePath()
if (mcpSettingsFilePath) {
openFile(mcpSettingsFilePath)
}
@@ -510,7 +668,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
try {
await this.mcpHub?.restartConnection(message.text!)
} catch (error) {
- console.error(`Failed to retry connection for ${message.text}:`, error)
+ console.error(
+ `Failed to retry connection for ${message.text}:`,
+ error,
+ )
}
break
}
@@ -523,9 +684,40 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
}
+ async cancelTask() {
+ if (this.cline) {
+ const { historyItem } = await this.getTaskWithId(this.cline.taskId)
+ try {
+ await this.cline.abortTask()
+ } catch (error) {
+ console.error("Failed to abort task", error)
+ }
+ await pWaitFor(
+ () =>
+ this.cline === undefined ||
+ this.cline.isStreaming === false ||
+ this.cline.didFinishAbortingStream,
+ {
+ timeout: 3_000,
+ },
+ ).catch(() => {
+ console.error("Failed to abort task")
+ })
+ if (this.cline) {
+ // 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
+ this.cline.abandoned = true
+ }
+ await this.initClineWithHistoryItem(historyItem) // clears task again, so we need to abortTask manually above
+ // await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
+ }
+ }
+
async updateCustomInstructions(instructions?: string) {
// User may be clearing the field
- await this.updateGlobalState("customInstructions", instructions || undefined)
+ await this.updateGlobalState(
+ "customInstructions",
+ instructions || undefined,
+ )
if (this.cline) {
this.cline.customInstructions = instructions || undefined
}
@@ -535,7 +727,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// MCP
async ensureMcpServersDirectoryExists(): Promise {
- const mcpServersDir = path.join(os.homedir(), "Documents", "Cline", "MCP")
+ const mcpServersDir = path.join(
+ os.homedir(),
+ "Documents",
+ "Cline",
+ "MCP",
+ )
try {
await fs.mkdir(mcpServersDir, { recursive: true })
} catch (error) {
@@ -545,7 +742,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async ensureSettingsDirectoryExists(): Promise {
- const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
+ const settingsDir = path.join(
+ this.context.globalStorageUri.fsPath,
+ "settings",
+ )
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
@@ -561,7 +761,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return []
}
const response = await axios.get(`${baseUrl}/api/tags`)
- const modelsArray = response.data?.models?.map((model: any) => model.name) || []
+ const modelsArray =
+ response.data?.models?.map((model: any) => model.name) || []
const models = [...new Set(modelsArray)]
return models
} catch (error) {
@@ -580,7 +781,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return []
}
const response = await axios.get(`${baseUrl}/v1/models`)
- const modelsArray = response.data?.data?.map((model: any) => model.id) || []
+ const modelsArray =
+ response.data?.data?.map((model: any) => model.id) || []
const models = [...new Set(modelsArray)]
return models
} catch (error) {
@@ -593,7 +795,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async handleOpenRouterCallback(code: string) {
let apiKey: string
try {
- const response = await axios.post("https://openrouter.ai/api/v1/auth/keys", { code })
+ const response = await axios.post(
+ "https://openrouter.ai/api/v1/auth/keys",
+ { code },
+ )
if (response.data && response.data.key) {
apiKey = response.data.key
} else {
@@ -609,25 +814,36 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.storeSecret("openRouterApiKey", apiKey)
await this.postStateToWebview()
if (this.cline) {
- this.cline.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
+ this.cline.api = buildApiHandler({
+ apiProvider: openrouter,
+ openRouterApiKey: apiKey,
+ })
}
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
}
private async ensureCacheDirectoryExists(): Promise {
- const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
+ const cacheDir = path.join(
+ this.context.globalStorageUri.fsPath,
+ "cache",
+ )
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
- async readOpenRouterModels(): Promise | undefined> {
+ async readOpenRouterModels(): Promise<
+ Record | undefined
+ > {
const openRouterModelsFilePath = path.join(
await this.ensureCacheDirectoryExists(),
GlobalFileNames.openRouterModels,
)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
- const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
+ const fileContents = await fs.readFile(
+ openRouterModelsFilePath,
+ "utf8",
+ )
return JSON.parse(fileContents)
}
return undefined
@@ -641,7 +857,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
let models: Record = {}
try {
- const response = await axios.get("https://openrouter.ai/api/v1/models")
+ const response = await axios.get(
+ "https://openrouter.ai/api/v1/models",
+ )
/*
{
"id": "anthropic/claude-3.5-sonnet",
@@ -680,7 +898,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const modelInfo: ModelInfo = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
- supportsImages: rawModel.architecture?.modality?.includes("image"),
+ supportsImages:
+ rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
@@ -746,7 +965,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
console.error("Error fetching OpenRouter models:", error)
}
- await this.postMessageToWebview({ type: "openRouterModels", openRouterModels: models })
+ await this.postMessageToWebview({
+ type: "openRouterModels",
+ openRouterModels: models,
+ })
return models
}
@@ -759,15 +981,32 @@ export class ClineProvider implements vscode.WebviewViewProvider {
uiMessagesFilePath: string
apiConversationHistory: Anthropic.MessageParam[]
}> {
- const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
+ const history =
+ ((await this.getGlobalState("taskHistory")) as
+ | HistoryItem[]
+ | undefined) || []
const historyItem = history.find((item) => item.id === id)
if (historyItem) {
- const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
- const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
- const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
- const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
+ const taskDirPath = path.join(
+ this.context.globalStorageUri.fsPath,
+ "tasks",
+ id,
+ )
+ const apiConversationHistoryFilePath = path.join(
+ taskDirPath,
+ GlobalFileNames.apiConversationHistory,
+ )
+ const uiMessagesFilePath = path.join(
+ taskDirPath,
+ GlobalFileNames.uiMessages,
+ )
+ const fileExists = await fileExistsAtPath(
+ apiConversationHistoryFilePath,
+ )
if (fileExists) {
- const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
+ const apiConversationHistory = JSON.parse(
+ await fs.readFile(apiConversationHistoryFilePath, "utf8"),
+ )
return {
historyItem,
taskDirPath,
@@ -789,11 +1028,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const { historyItem } = await this.getTaskWithId(id)
await this.initClineWithHistoryItem(historyItem) // clears existing task
}
- await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
+ await this.postMessageToWebview({
+ type: "action",
+ action: "chatButtonClicked",
+ })
}
async exportTaskWithId(id: string) {
- const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
+ const { historyItem, apiConversationHistory } =
+ await this.getTaskWithId(id)
await downloadTask(historyItem.ts, apiConversationHistory)
}
@@ -802,12 +1045,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.clearTask()
}
- const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
+ const {
+ taskDirPath,
+ apiConversationHistoryFilePath,
+ uiMessagesFilePath,
+ } = await this.getTaskWithId(id)
await this.deleteTaskFromState(id)
// Delete the task files
- const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
+ const apiConversationHistoryFileExists = await fileExistsAtPath(
+ apiConversationHistoryFilePath,
+ )
if (apiConversationHistoryFileExists) {
await fs.unlink(apiConversationHistoryFilePath)
}
@@ -815,16 +1064,37 @@ export class ClineProvider implements vscode.WebviewViewProvider {
if (uiMessagesFileExists) {
await fs.unlink(uiMessagesFilePath)
}
- const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
+ const legacyMessagesFilePath = path.join(
+ taskDirPath,
+ "claude_messages.json",
+ )
if (await fileExistsAtPath(legacyMessagesFilePath)) {
await fs.unlink(legacyMessagesFilePath)
}
+
+ // Delete the checkpoints directory if it exists
+ const checkpointsDir = path.join(taskDirPath, "checkpoints")
+ if (await fileExistsAtPath(checkpointsDir)) {
+ try {
+ await fs.rm(checkpointsDir, { recursive: true, force: true })
+ } catch (error) {
+ console.error(
+ `Failed to delete checkpoints directory for task ${id}:`,
+ error,
+ )
+ // Continue with deletion of task directory - don't throw since this is a cleanup operation
+ }
+ }
+
await fs.rmdir(taskDirPath) // succeeds if the dir is empty
}
async deleteTaskFromState(id: string) {
// Remove the task from history
- const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
+ const taskHistory =
+ ((await this.getGlobalState("taskHistory")) as
+ | HistoryItem[]
+ | undefined) || []
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
await this.updateGlobalState("taskHistory", updatedTaskHistory)
@@ -837,17 +1107,32 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.postMessageToWebview({ type: "state", state })
}
- async getStateToPostToWebview() {
- const { apiConfiguration, lastShownAnnouncementId, customInstructions, taskHistory, autoApprovalSettings } =
- await this.getState()
+ async getStateToPostToWebview(): Promise {
+ const {
+ apiConfiguration,
+ lastShownAnnouncementId,
+ customInstructions,
+ taskHistory,
+ autoApprovalSettings,
+ } = await this.getState()
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
customInstructions,
uriScheme: vscode.env.uriScheme,
+ currentTaskItem: this.cline?.taskId
+ ? (taskHistory || []).find(
+ (item) => item.id === this.cline?.taskId,
+ )
+ : undefined,
+ checkpointTrackerErrorMessage:
+ this.cline?.checkpointTrackerErrorMessage,
clineMessages: this.cline?.clineMessages || [],
- taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
- shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
+ taskHistory: (taskHistory || [])
+ .filter((item) => item.ts && item.task)
+ .sort((a, b) => b.ts - a.ts),
+ shouldShowAnnouncement:
+ lastShownAnnouncementId !== this.latestAnnouncementId,
autoApprovalSettings,
}
}
@@ -935,7 +1220,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
taskHistory,
autoApprovalSettings,
] = await Promise.all([
- this.getGlobalState("apiProvider") as Promise,
+ this.getGlobalState("apiProvider") as Promise<
+ ApiProvider | undefined
+ >,
this.getGlobalState("apiModelId") as Promise,
this.getSecret("apiKey") as Promise,
this.getSecret("openRouterApiKey") as Promise,
@@ -943,27 +1230,51 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getSecret("awsSecretKey") as Promise,
this.getSecret("awsSessionToken") as Promise,
this.getGlobalState("awsRegion") as Promise,
- this.getGlobalState("awsUseCrossRegionInference") as Promise,
- this.getGlobalState("vertexProjectId") as Promise,
+ this.getGlobalState("awsUseCrossRegionInference") as Promise<
+ boolean | undefined
+ >,
+ this.getGlobalState("vertexProjectId") as Promise<
+ string | undefined
+ >,
this.getGlobalState("vertexRegion") as Promise,
this.getGlobalState("openAiBaseUrl") as Promise,
this.getSecret("openAiApiKey") as Promise,
this.getGlobalState("openAiModelId") as Promise,
this.getGlobalState("ollamaModelId") as Promise,
this.getGlobalState("ollamaBaseUrl") as Promise,
- this.getGlobalState("lmStudioModelId") as Promise,
- this.getGlobalState("lmStudioBaseUrl") as Promise,
- this.getGlobalState("anthropicBaseUrl") as Promise,
+ this.getGlobalState("lmStudioModelId") as Promise<
+ string | undefined
+ >,
+ this.getGlobalState("lmStudioBaseUrl") as Promise<
+ string | undefined
+ >,
+ this.getGlobalState("anthropicBaseUrl") as Promise<
+ string | undefined
+ >,
this.getSecret("geminiApiKey") as Promise,
this.getSecret("openAiNativeApiKey") as Promise,
this.getSecret("deepSeekApiKey") as Promise,
- this.getGlobalState("azureApiVersion") as Promise,
- this.getGlobalState("openRouterModelId") as Promise,
- this.getGlobalState("openRouterModelInfo") as Promise,
- this.getGlobalState("lastShownAnnouncementId") as Promise,
- this.getGlobalState("customInstructions") as Promise,
- this.getGlobalState("taskHistory") as Promise,
- this.getGlobalState("autoApprovalSettings") as Promise,
+ this.getGlobalState("azureApiVersion") as Promise<
+ string | undefined
+ >,
+ this.getGlobalState("openRouterModelId") as Promise<
+ string | undefined
+ >,
+ this.getGlobalState("openRouterModelInfo") as Promise<
+ ModelInfo | undefined
+ >,
+ this.getGlobalState("lastShownAnnouncementId") as Promise<
+ string | undefined
+ >,
+ this.getGlobalState("customInstructions") as Promise<
+ string | undefined
+ >,
+ this.getGlobalState("taskHistory") as Promise<
+ HistoryItem[] | undefined
+ >,
+ this.getGlobalState("autoApprovalSettings") as Promise<
+ AutoApprovalSettings | undefined
+ >,
])
let apiProvider: ApiProvider
@@ -1011,12 +1322,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lastShownAnnouncementId,
customInstructions,
taskHistory,
- autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
+ autoApprovalSettings:
+ autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
}
}
async updateTaskHistory(item: HistoryItem): Promise {
- const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
+ const history =
+ ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
const existingItemIndex = history.findIndex((h) => h.id === item.id)
if (existingItemIndex !== -1) {
history[existingItemIndex] = item
@@ -1098,6 +1411,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
vscode.window.showInformationMessage("State reset")
await this.postStateToWebview()
- await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
+ await this.postMessageToWebview({
+ type: "action",
+ action: "chatButtonClicked",
+ })
}
}
diff --git a/src/core/webview/getNonce.ts b/src/core/webview/getNonce.ts
index b92871b93d..7409b500a0 100644
--- a/src/core/webview/getNonce.ts
+++ b/src/core/webview/getNonce.ts
@@ -8,7 +8,8 @@
*/
export function getNonce() {
let text = ""
- const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+ const possible =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
diff --git a/src/core/webview/getUri.ts b/src/core/webview/getUri.ts
index 13f90af516..db065dd138 100644
--- a/src/core/webview/getUri.ts
+++ b/src/core/webview/getUri.ts
@@ -10,6 +10,10 @@ import { Uri, Webview } from "vscode"
* @param pathList An array of strings representing the path to a file/resource
* @returns A URI pointing to the file/resource
*/
-export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) {
+export function getUri(
+ webview: Webview,
+ extensionUri: Uri,
+ pathList: string[],
+) {
return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList))
}
diff --git a/src/exports/README.md b/src/exports/README.md
index 40f909a217..ed688facf9 100644
--- a/src/exports/README.md
+++ b/src/exports/README.md
@@ -7,7 +7,9 @@ The Cline extension exposes an API that can be used by other extensions. To use
3. Get access to the API with the following code:
```ts
- const clineExtension = vscode.extensions.getExtension("saoudrizwan.claude-dev")
+ const clineExtension = vscode.extensions.getExtension(
+ "saoudrizwan.claude-dev",
+ )
if (!clineExtension?.isActive) {
throw new Error("Cline extension is not activated")
@@ -29,7 +31,9 @@ The Cline extension exposes an API that can be used by other extensions. To use
await cline.startNewTask("Hello, Cline! Let's make a new project...")
// Start a new task with an initial message and images
- await cline.startNewTask("Use this design language", ["data:image/webp;base64,..."])
+ await cline.startNewTask("Use this design language", [
+ "data:image/webp;base64,...",
+ ])
// Send a message to the current task
await cline.sendMessage("Can you fix the @problems?")
diff --git a/src/exports/index.ts b/src/exports/index.ts
index 04d26d8c8b..d87bed9d6c 100644
--- a/src/exports/index.ts
+++ b/src/exports/index.ts
@@ -2,7 +2,10 @@ import * as vscode from "vscode"
import { ClineProvider } from "../core/webview/ClineProvider"
import { ClineAPI } from "./cline"
-export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvider: ClineProvider): ClineAPI {
+export function createClineAPI(
+ outputChannel: vscode.OutputChannel,
+ sidebarProvider: ClineProvider,
+): ClineAPI {
const api: ClineAPI = {
setCustomInstructions: async (value: string) => {
await sidebarProvider.updateCustomInstructions(value)
@@ -10,14 +13,19 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
},
getCustomInstructions: async () => {
- return (await sidebarProvider.getGlobalState("customInstructions")) as string | undefined
+ return (await sidebarProvider.getGlobalState(
+ "customInstructions",
+ )) as string | undefined
},
startNewTask: async (task?: string, images?: string[]) => {
outputChannel.appendLine("Starting new task")
await sidebarProvider.clearTask()
await sidebarProvider.postStateToWebview()
- await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
+ await sidebarProvider.postMessageToWebview({
+ type: "action",
+ action: "chatButtonClicked",
+ })
await sidebarProvider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
diff --git a/src/extension.ts b/src/extension.ts
index 49e8bbf970..0ebb78dd29 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -29,9 +29,13 @@ export function activate(context: vscode.ExtensionContext) {
const sidebarProvider = new ClineProvider(context, outputChannel)
context.subscriptions.push(
- vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, sidebarProvider, {
- webviewOptions: { retainContextWhenHidden: true },
- }),
+ vscode.window.registerWebviewViewProvider(
+ ClineProvider.sideBarId,
+ sidebarProvider,
+ {
+ webviewOptions: { retainContextWhenHidden: true },
+ },
+ ),
)
context.subscriptions.push(
@@ -39,13 +43,19 @@ export function activate(context: vscode.ExtensionContext) {
outputChannel.appendLine("Plus button Clicked")
await sidebarProvider.clearTask()
await sidebarProvider.postStateToWebview()
- await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
+ await sidebarProvider.postMessageToWebview({
+ type: "action",
+ action: "chatButtonClicked",
+ })
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("cline.mcpButtonClicked", () => {
- sidebarProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
+ sidebarProvider.postMessageToWebview({
+ type: "action",
+ action: "mcpButtonClicked",
+ })
}),
)
@@ -55,25 +65,48 @@ export function activate(context: vscode.ExtensionContext) {
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
const tabProvider = new ClineProvider(context, outputChannel)
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
- const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
+ const lastCol = Math.max(
+ ...vscode.window.visibleTextEditors.map(
+ (editor) => editor.viewColumn || 0,
+ ),
+ )
// Check if there are any visible text editors, otherwise open a new group to the right
const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0
if (!hasVisibleEditors) {
- await vscode.commands.executeCommand("workbench.action.newGroupRight")
+ await vscode.commands.executeCommand(
+ "workbench.action.newGroupRight",
+ )
}
- const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
-
- const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Cline", targetCol, {
- enableScripts: true,
- retainContextWhenHidden: true,
- localResourceRoots: [context.extensionUri],
- })
+ const targetCol = hasVisibleEditors
+ ? Math.max(lastCol + 1, 1)
+ : vscode.ViewColumn.Two
+
+ const panel = vscode.window.createWebviewPanel(
+ ClineProvider.tabPanelId,
+ "Cline",
+ targetCol,
+ {
+ enableScripts: true,
+ retainContextWhenHidden: true,
+ localResourceRoots: [context.extensionUri],
+ },
+ )
// TODO: use better svg icon with light and dark variants (see https://stackoverflow.com/questions/58365687/vscode-extension-iconpath)
panel.iconPath = {
- light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_light.png"),
- dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_dark.png"),
+ light: vscode.Uri.joinPath(
+ context.extensionUri,
+ "assets",
+ "icons",
+ "robot_panel_light.png",
+ ),
+ dark: vscode.Uri.joinPath(
+ context.extensionUri,
+ "assets",
+ "icons",
+ "robot_panel_dark.png",
+ ),
}
tabProvider.resolveWebviewView(panel)
@@ -82,19 +115,35 @@ export function activate(context: vscode.ExtensionContext) {
await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
}
- context.subscriptions.push(vscode.commands.registerCommand("cline.popoutButtonClicked", openClineInNewTab))
- context.subscriptions.push(vscode.commands.registerCommand("cline.openInNewTab", openClineInNewTab))
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ "cline.popoutButtonClicked",
+ openClineInNewTab,
+ ),
+ )
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ "cline.openInNewTab",
+ openClineInNewTab,
+ ),
+ )
context.subscriptions.push(
vscode.commands.registerCommand("cline.settingsButtonClicked", () => {
//vscode.window.showInformationMessage(message)
- sidebarProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
+ sidebarProvider.postMessageToWebview({
+ type: "action",
+ action: "settingsButtonClicked",
+ })
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("cline.historyButtonClicked", () => {
- sidebarProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
+ sidebarProvider.postMessageToWebview({
+ type: "action",
+ action: "historyButtonClicked",
+ })
}),
)
@@ -105,13 +154,18 @@ export function activate(context: vscode.ExtensionContext) {
- Note how the provider doesn't create uris for virtual documents - its role is to provide contents given such an uri. In return, content providers are wired into the open document logic so that providers are always considered.
https://code.visualstudio.com/api/extension-guides/virtual-documents
*/
- const diffContentProvider = new (class implements vscode.TextDocumentContentProvider {
+ const diffContentProvider = new (class
+ implements vscode.TextDocumentContentProvider
+ {
provideTextDocumentContent(uri: vscode.Uri): string {
return Buffer.from(uri.query, "base64").toString("utf-8")
}
})()
context.subscriptions.push(
- vscode.workspace.registerTextDocumentContentProvider(DIFF_VIEW_URI_SCHEME, diffContentProvider),
+ vscode.workspace.registerTextDocumentContentProvider(
+ DIFF_VIEW_URI_SCHEME,
+ diffContentProvider,
+ ),
)
// URI Handler
diff --git a/src/integrations/checkpoints/CheckpointTracker.ts b/src/integrations/checkpoints/CheckpointTracker.ts
new file mode 100644
index 0000000000..9592c9309d
--- /dev/null
+++ b/src/integrations/checkpoints/CheckpointTracker.ts
@@ -0,0 +1,435 @@
+import fs from "fs/promises"
+import os from "os"
+import * as path from "path"
+import simpleGit from "simple-git"
+import * as vscode from "vscode"
+import { ClineProvider } from "../../core/webview/ClineProvider"
+import { fileExistsAtPath } from "../../utils/fs"
+import { globby } from "globby"
+
+class CheckpointTracker {
+ private providerRef: WeakRef
+ private taskId: string
+ private disposables: vscode.Disposable[] = []
+ private cwd: string
+ private lastRetrievedShadowGitConfigWorkTree?: string
+ lastCheckpointHash?: string
+
+ private constructor(provider: ClineProvider, taskId: string, cwd: string) {
+ this.providerRef = new WeakRef(provider)
+ this.taskId = taskId
+ this.cwd = cwd
+ }
+
+ public static async create(
+ taskId: string,
+ provider?: ClineProvider,
+ ): Promise {
+ try {
+ if (!provider) {
+ throw new Error(
+ "Provider is required to create a checkpoint tracker",
+ )
+ }
+
+ // Check if git is installed by attempting to get version
+ try {
+ await simpleGit().version()
+ } catch (error) {
+ throw new Error("Git must be installed to use checkpoints.") // FIXME: must match what we check for in TaskHeader to show link
+ }
+
+ const cwd = await CheckpointTracker.getWorkingDirectory()
+ const newTracker = new CheckpointTracker(provider, taskId, cwd)
+ await newTracker.initShadowGit()
+ return newTracker
+ } catch (error) {
+ console.error("Failed to create CheckpointTracker:", error)
+ throw error
+ }
+ }
+
+ private static async getWorkingDirectory(): Promise {
+ const cwd = vscode.workspace.workspaceFolders
+ ?.map((folder) => folder.uri.fsPath)
+ .at(0)
+ if (!cwd) {
+ throw new Error(
+ "No workspace detected. Please open Cline in a workspace to use checkpoints.",
+ )
+ }
+ const homedir = os.homedir()
+ const desktopPath = path.join(homedir, "Desktop")
+ const documentsPath = path.join(homedir, "Documents")
+ const downloadsPath = path.join(homedir, "Downloads")
+
+ switch (cwd) {
+ case homedir:
+ throw new Error("Cannot use checkpoints in home directory")
+ case desktopPath:
+ throw new Error("Cannot use checkpoints in Desktop directory")
+ case documentsPath:
+ throw new Error("Cannot use checkpoints in Documents directory")
+ case downloadsPath:
+ throw new Error("Cannot use checkpoints in Downloads directory")
+ default:
+ return cwd
+ }
+ }
+
+ private async getShadowGitPath(): Promise {
+ const globalStoragePath =
+ this.providerRef.deref()?.context.globalStorageUri.fsPath
+ if (!globalStoragePath) {
+ throw new Error("Global storage uri is invalid")
+ }
+ const checkpointsDir = path.join(
+ globalStoragePath,
+ "tasks",
+ this.taskId,
+ "checkpoints",
+ )
+ await fs.mkdir(checkpointsDir, { recursive: true })
+ const gitPath = path.join(checkpointsDir, ".git")
+ return gitPath
+ }
+
+ public static async doesShadowGitExist(
+ taskId: string,
+ provider?: ClineProvider,
+ ): Promise {
+ const globalStoragePath = provider?.context.globalStorageUri.fsPath
+ if (!globalStoragePath) {
+ return false
+ }
+ const gitPath = path.join(
+ globalStoragePath,
+ "tasks",
+ taskId,
+ "checkpoints",
+ ".git",
+ )
+ return await fileExistsAtPath(gitPath)
+ }
+
+ public async initShadowGit(): Promise {
+ const gitPath = await this.getShadowGitPath()
+ if (await fileExistsAtPath(gitPath)) {
+ // Make sure it's the same cwd as the configured worktree
+ const worktree = await this.getShadowGitConfigWorkTree()
+ if (worktree !== this.cwd) {
+ throw new Error(
+ "Checkpoints can only be used in the original workspace: " +
+ worktree,
+ )
+ }
+
+ return gitPath
+ } else {
+ const checkpointsDir = path.dirname(gitPath)
+ const git = simpleGit(checkpointsDir)
+ await git.init()
+
+ await git.addConfig("core.worktree", this.cwd) // sets the working tree to the current workspace
+
+ // Add basic excludes directly in git config, while respecting any .gitignore in the workspace
+ // .git/info/exclude is local to the shadow git repo, so it's not shared with the main repo - and won't conflict with user's .gitignore
+ // TODO: let user customize these
+ const excludesPath = path.join(gitPath, "info", "exclude")
+ await fs.mkdir(path.join(gitPath, "info"), { recursive: true })
+ await fs.writeFile(
+ excludesPath,
+ [
+ ".git/", // ignore the user's .git
+ `.git${GIT_DISABLED_SUFFIX}/`, // ignore the disabled nested git repos
+ ".DS_Store",
+ "*.log",
+ "node_modules/",
+ "__pycache__/",
+ "env/",
+ "venv/",
+ "target/dependency/",
+ "build/dependencies/",
+ "dist/",
+ "out/",
+ "bundle/",
+ "vendor/",
+ "tmp/",
+ "temp/",
+ "deps/",
+ "pkg/",
+ "Pods/",
+ // Media files
+ "*.jpg",
+ "*.jpeg",
+ "*.png",
+ "*.gif",
+ "*.bmp",
+ "*.ico",
+ // "*.svg",
+ "*.mp3",
+ "*.mp4",
+ "*.wav",
+ "*.avi",
+ "*.mov",
+ "*.wmv",
+ "*.webm",
+ "*.webp",
+ "*.m4a",
+ "*.flac",
+ // Build and dependency directories
+ "build/",
+ "bin/",
+ "obj/",
+ ".gradle/",
+ ".idea/",
+ ".vscode/",
+ ".vs/",
+ "coverage/",
+ ".next/",
+ ".nuxt/",
+ // Cache and temporary files
+ "*.cache",
+ "*.tmp",
+ "*.temp",
+ "*.swp",
+ "*.swo",
+ "*.pyc",
+ "*.pyo",
+ ".pytest_cache/",
+ ".eslintcache",
+ // Environment and config files
+ ".env*",
+ "*.local",
+ "*.development",
+ "*.production",
+ // Large data files
+ "*.zip",
+ "*.tar",
+ "*.gz",
+ "*.rar",
+ "*.7z",
+ "*.iso",
+ "*.bin",
+ "*.exe",
+ "*.dll",
+ "*.so",
+ "*.dylib",
+ // Database files
+ "*.sqlite",
+ "*.db",
+ "*.sql",
+ // Log files
+ "*.logs",
+ "*.error",
+ "npm-debug.log*",
+ "yarn-debug.log*",
+ "yarn-error.log*",
+ ].join("\n"),
+ )
+
+ // Set up git identity (git throws an error if user.name or user.email is not set)
+ await git.addConfig("user.name", "Cline Checkpoint")
+ await git.addConfig("user.email", "noreply@example.com")
+
+ // Initial commit (--allow-empty ensures it works even with no files)
+ await this.renameNestedGitRepos(true)
+ await git.add(".")
+ await this.renameNestedGitRepos(false)
+ await git.commit("initial commit", { "--allow-empty": null })
+
+ return gitPath
+ }
+ }
+
+ public async getShadowGitConfigWorkTree(): Promise {
+ if (this.lastRetrievedShadowGitConfigWorkTree) {
+ return this.lastRetrievedShadowGitConfigWorkTree
+ }
+ try {
+ const gitPath = await this.getShadowGitPath()
+ const git = simpleGit(path.dirname(gitPath))
+ const worktree = await git.getConfig("core.worktree")
+ this.lastRetrievedShadowGitConfigWorkTree =
+ worktree.value || undefined
+ return this.lastRetrievedShadowGitConfigWorkTree
+ } catch (error) {
+ console.error("Failed to get shadow git config worktree:", error)
+ return undefined
+ }
+ }
+
+ public async commit(): Promise {
+ try {
+ const gitPath = await this.getShadowGitPath()
+ const git = simpleGit(path.dirname(gitPath))
+ await this.renameNestedGitRepos(true)
+ await git.add(".")
+ await this.renameNestedGitRepos(false)
+ const result = await git.commit("checkpoint", {
+ "--allow-empty": null,
+ })
+ const commitHash = result.commit || ""
+ this.lastCheckpointHash = commitHash
+ return commitHash
+ } catch (error) {
+ console.error("Failed to create checkpoint:", error)
+ return undefined
+ }
+ }
+
+ public async resetHead(commitHash: string): Promise {
+ const gitPath = await this.getShadowGitPath()
+ const git = simpleGit(path.dirname(gitPath))
+
+ // Clean working directory and force reset
+ // This ensures that the operation will succeed regardless of:
+ // - Untracked files in the workspace
+ // - Staged changes
+ // - Unstaged changes
+ // - Partial commits
+ // - Merge conflicts
+ await git.clean("f", ["-d", "-f"]) // Remove untracked files and directories
+ await git.reset(["--hard", commitHash]) // Hard reset to target commit
+ }
+
+ /**
+ * Return an array describing changed files between one commit and either:
+ * - another commit, or
+ * - the current working directory (including uncommitted changes).
+ *
+ * If `rhsHash` is omitted, compares `lhsHash` to the working directory.
+ * If you want truly untracked files to appear, `git add` them first.
+ *
+ * @param lhsHash - The commit to compare from (older commit)
+ * @param rhsHash - The commit to compare to (newer commit).
+ * If omitted, we compare to the working directory.
+ * @returns Array of file changes with before/after content
+ */
+ public async getDiffSet(
+ lhsHash?: string,
+ rhsHash?: string,
+ ): Promise<
+ Array<{
+ relativePath: string
+ absolutePath: string
+ before: string
+ after: string
+ }>
+ > {
+ const gitPath = await this.getShadowGitPath()
+ const git = simpleGit(path.dirname(gitPath))
+
+ // If lhsHash is missing, use the initial commit of the repo
+ let baseHash = lhsHash
+ if (!baseHash) {
+ const rootCommit = await git.raw([
+ "rev-list",
+ "--max-parents=0",
+ "HEAD",
+ ])
+ baseHash = rootCommit.trim()
+ }
+
+ // Stage all changes so that untracked files appear in diff summary
+ await this.renameNestedGitRepos(true)
+ await git.add(".")
+ await this.renameNestedGitRepos(false)
+
+ const diffSummary = rhsHash
+ ? await git.diffSummary([`${baseHash}..${rhsHash}`])
+ : await git.diffSummary([baseHash])
+
+ // For each changed file, gather before/after content
+ const result = []
+ const cwdPath =
+ (await this.getShadowGitConfigWorkTree()) || this.cwd || ""
+
+ for (const file of diffSummary.files) {
+ const filePath = file.file
+ const absolutePath = path.join(cwdPath, filePath)
+
+ let beforeContent = ""
+ try {
+ beforeContent = await git.show([`${baseHash}:${filePath}`])
+ } catch (_) {
+ // file didn't exist in older commit => remains empty
+ }
+
+ let afterContent = ""
+ if (rhsHash) {
+ // if user provided a newer commit, use git.show at that commit
+ try {
+ afterContent = await git.show([`${rhsHash}:${filePath}`])
+ } catch (_) {
+ // file didn't exist in newer commit => remains empty
+ }
+ } else {
+ // otherwise, read from disk (includes uncommitted changes)
+ try {
+ afterContent = await fs.readFile(absolutePath, "utf8")
+ } catch (_) {
+ // file might be deleted => remains empty
+ }
+ }
+
+ result.push({
+ relativePath: filePath,
+ absolutePath,
+ before: beforeContent,
+ after: afterContent,
+ })
+ }
+
+ return result
+ }
+
+ // Since we use git to track checkpoints, we need to temporarily disable nested git repos to work around git's requirement of using submodules for nested repos.
+ async renameNestedGitRepos(disable: boolean) {
+ // Find all .git directories that are not at the root level
+ const gitPaths = await globby(
+ "**/.git" + (disable ? "" : GIT_DISABLED_SUFFIX),
+ {
+ cwd: this.cwd,
+ onlyDirectories: true,
+ ignore: [".git"], // Ignore root level .git
+ dot: true,
+ markDirectories: false,
+ },
+ )
+
+ // For each nested .git directory, rename it based on operation
+ for (const gitPath of gitPaths) {
+ const fullPath = path.join(this.cwd, gitPath)
+ let newPath: string
+ if (disable) {
+ newPath = fullPath + GIT_DISABLED_SUFFIX
+ } else {
+ newPath = fullPath.endsWith(GIT_DISABLED_SUFFIX)
+ ? fullPath.slice(0, -GIT_DISABLED_SUFFIX.length)
+ : fullPath
+ }
+
+ try {
+ await fs.rename(fullPath, newPath)
+ console.log(
+ `CheckpointTracker ${disable ? "disabled" : "enabled"} nested git repo ${gitPath}`,
+ )
+ } catch (error) {
+ console.error(
+ `CheckpointTracker failed to ${disable ? "disable" : "enable"} nested git repo ${gitPath}:`,
+ error,
+ )
+ }
+ }
+ }
+
+ public dispose() {
+ this.disposables.forEach((d) => d.dispose())
+ this.disposables = []
+ }
+}
+
+const GIT_DISABLED_SUFFIX = "_disabled"
+
+export default CheckpointTracker
diff --git a/src/integrations/diagnostics/index.ts b/src/integrations/diagnostics/index.ts
index ad4ee7755c..037529301c 100644
--- a/src/integrations/diagnostics/index.ts
+++ b/src/integrations/diagnostics/index.ts
@@ -11,7 +11,10 @@ export function getNewDiagnostics(
for (const [uri, newDiags] of newDiagnostics) {
const oldDiags = oldMap.get(uri) || []
- const newProblemsForUri = newDiags.filter((newDiag) => !oldDiags.some((oldDiag) => deepEqual(oldDiag, newDiag)))
+ const newProblemsForUri = newDiags.filter(
+ (newDiag) =>
+ !oldDiags.some((oldDiag) => deepEqual(oldDiag, newDiag)),
+ )
if (newProblemsForUri.length > 0) {
newProblems.push([uri, newProblemsForUri])
@@ -77,7 +80,9 @@ export function diagnosticsToProblemsString(
): string {
let result = ""
for (const [uri, fileDiagnostics] of diagnostics) {
- const problems = fileDiagnostics.filter((d) => severities.includes(d.severity))
+ const problems = fileDiagnostics.filter((d) =>
+ severities.includes(d.severity),
+ )
if (problems.length > 0) {
result += `\n\n${path.relative(cwd, uri.fsPath).toPosix()}`
for (const diagnostic of problems) {
diff --git a/src/integrations/editor/DecorationController.ts b/src/integrations/editor/DecorationController.ts
index 8f475408d4..f99646ea46 100644
--- a/src/integrations/editor/DecorationController.ts
+++ b/src/integrations/editor/DecorationController.ts
@@ -1,10 +1,12 @@
import * as vscode from "vscode"
-const fadedOverlayDecorationType = vscode.window.createTextEditorDecorationType({
- backgroundColor: "rgba(255, 255, 0, 0.1)",
- opacity: "0.4",
- isWholeLine: true,
-})
+const fadedOverlayDecorationType = vscode.window.createTextEditorDecorationType(
+ {
+ backgroundColor: "rgba(255, 255, 0, 0.1)",
+ opacity: "0.4",
+ isWholeLine: true,
+ },
+)
const activeLineDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: "rgba(255, 255, 0, 0.3)",
@@ -42,10 +44,20 @@ export class DecorationController {
const lastRange = this.ranges[this.ranges.length - 1]
if (lastRange && lastRange.end.line === startIndex - 1) {
- this.ranges[this.ranges.length - 1] = lastRange.with(undefined, lastRange.end.translate(numLines))
+ this.ranges[this.ranges.length - 1] = lastRange.with(
+ undefined,
+ lastRange.end.translate(numLines),
+ )
} else {
const endLine = startIndex + numLines - 1
- this.ranges.push(new vscode.Range(startIndex, 0, endLine, Number.MAX_SAFE_INTEGER))
+ this.ranges.push(
+ new vscode.Range(
+ startIndex,
+ 0,
+ endLine,
+ Number.MAX_SAFE_INTEGER,
+ ),
+ )
}
this.editor.setDecorations(this.getDecoration(), this.ranges)
@@ -65,7 +77,10 @@ export class DecorationController {
this.ranges.push(
new vscode.Range(
new vscode.Position(line + 1, 0),
- new vscode.Position(totalLines - 1, Number.MAX_SAFE_INTEGER),
+ new vscode.Position(
+ totalLines - 1,
+ Number.MAX_SAFE_INTEGER,
+ ),
),
)
}
diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts
index 4cc9f4b9d0..1f5fe56cc8 100644
--- a/src/integrations/editor/DiffViewProvider.ts
+++ b/src/integrations/editor/DiffViewProvider.ts
@@ -33,8 +33,8 @@ export class DiffViewProvider {
this.isEditing = true
// if the file is already open, ensure it's not dirty before getting its contents
if (fileExists) {
- const existingDocument = vscode.workspace.textDocuments.find((doc) =>
- arePathsEqual(doc.uri.fsPath, absolutePath),
+ const existingDocument = vscode.workspace.textDocuments.find(
+ (doc) => arePathsEqual(doc.uri.fsPath, absolutePath),
)
if (existingDocument && existingDocument.isDirty) {
await existingDocument.save()
@@ -62,7 +62,9 @@ export class DiffViewProvider {
.map((tg) => tg.tabs)
.flat()
.filter(
- (tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath),
+ (tab) =>
+ tab.input instanceof vscode.TabInputText &&
+ arePathsEqual(tab.input.uri.fsPath, absolutePath),
)
for (const tab of tabs) {
if (!tab.isDirty) {
@@ -71,16 +73,29 @@ export class DiffViewProvider {
this.documentWasOpen = true
}
this.activeDiffEditor = await this.openDiffEditor()
- this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
- this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
+ this.fadedOverlayController = new DecorationController(
+ "fadedOverlay",
+ this.activeDiffEditor,
+ )
+ this.activeLineController = new DecorationController(
+ "activeLine",
+ this.activeDiffEditor,
+ )
// Apply faded overlay to all lines initially
- this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
+ this.fadedOverlayController.addLines(
+ 0,
+ this.activeDiffEditor.document.lineCount,
+ )
this.scrollEditorToLine(0) // will this crash for new files?
this.streamedLines = []
}
async update(accumulatedContent: string, isFinal: boolean) {
- if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
+ if (
+ !this.relPath ||
+ !this.activeLineController ||
+ !this.fadedOverlayController
+ ) {
throw new Error("Required values not set")
}
this.newContent = accumulatedContent
@@ -98,7 +113,10 @@ export class DiffViewProvider {
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
const beginningOfDocument = new vscode.Position(0, 0)
- diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
+ diffEditor.selection = new vscode.Selection(
+ beginningOfDocument,
+ beginningOfDocument,
+ )
for (let i = 0; i < diffLines.length; i++) {
const currentLine = this.streamedLines.length + i
@@ -106,12 +124,16 @@ export class DiffViewProvider {
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags on previous lines are auto closed for example
const edit = new vscode.WorkspaceEdit()
const rangeToReplace = new vscode.Range(0, 0, currentLine + 1, 0)
- const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
+ const contentToReplace =
+ accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
edit.replace(document.uri, rangeToReplace, contentToReplace)
await vscode.workspace.applyEdit(edit)
// Update decorations
this.activeLineController.setActiveLine(currentLine)
- this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
+ this.fadedOverlayController.updateOverlayAfterLine(
+ currentLine,
+ document.lineCount,
+ )
// Scroll to the current line
this.scrollEditorToLine(currentLine)
}
@@ -121,7 +143,15 @@ export class DiffViewProvider {
// Handle any remaining lines if the new content is shorter than the original
if (this.streamedLines.length < document.lineCount) {
const edit = new vscode.WorkspaceEdit()
- edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
+ edit.delete(
+ document.uri,
+ new vscode.Range(
+ this.streamedLines.length,
+ 0,
+ document.lineCount,
+ 0,
+ ),
+ )
await vscode.workspace.applyEdit(edit)
}
// Add empty last line if original content had one
@@ -166,7 +196,9 @@ export class DiffViewProvider {
// get text after save in case there is any auto-formatting done by the editor
const postSaveContent = updatedDocument.getText()
- await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
+ await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
+ preview: false,
+ })
await this.closeAllDiffViews()
/*
@@ -195,14 +227,22 @@ export class DiffViewProvider {
this.cwd,
) // will be empty string if no errors
const newProblemsMessage =
- newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
+ newProblems.length > 0
+ ? `\n\nNew problems detected after saving the file:\n${newProblems}`
+ : ""
// If the edited content has different EOL characters, we don't want to show a diff with all the EOL differences.
const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n"
- const normalizedPreSaveContent = preSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // trimEnd to fix issue where editor adds in extra new line automatically
- const normalizedPostSaveContent = postSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // this is the final content we return to the model to use as the new baseline for future edits
+ const normalizedPreSaveContent =
+ preSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() +
+ newContentEOL // trimEnd to fix issue where editor adds in extra new line automatically
+ const normalizedPostSaveContent =
+ postSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() +
+ newContentEOL // this is the final content we return to the model to use as the new baseline for future edits
// just in case the new content has a mix of varying EOL characters
- const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
+ const normalizedNewContent =
+ this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() +
+ newContentEOL
let userEdits: string | undefined
if (normalizedPreSaveContent !== normalizedNewContent) {
@@ -228,7 +268,12 @@ export class DiffViewProvider {
)
}
- return { newProblemsMessage, userEdits, autoFormattingEdits, finalContent: normalizedPostSaveContent }
+ return {
+ newProblemsMessage,
+ userEdits,
+ autoFormattingEdits,
+ finalContent: normalizedPostSaveContent,
+ }
}
async revertChanges(): Promise {
@@ -247,7 +292,9 @@ export class DiffViewProvider {
// Remove only the directories we created, in reverse order
for (let i = this.createdDirs.length - 1; i >= 0; i--) {
await fs.rmdir(this.createdDirs[i])
- console.log(`Directory ${this.createdDirs[i]} has been deleted.`)
+ console.log(
+ `Directory ${this.createdDirs[i]} has been deleted.`,
+ )
}
console.log(`File ${absolutePath} has been deleted.`)
} else {
@@ -257,15 +304,24 @@ export class DiffViewProvider {
updatedDocument.positionAt(0),
updatedDocument.positionAt(updatedDocument.getText().length),
)
- edit.replace(updatedDocument.uri, fullRange, this.originalContent ?? "")
+ edit.replace(
+ updatedDocument.uri,
+ fullRange,
+ this.originalContent ?? "",
+ )
// Apply the edit and save, since contents shouldnt have changed this wont show in local history unless of course the user made changes and saved during the edit
await vscode.workspace.applyEdit(edit)
await updatedDocument.save()
- console.log(`File ${absolutePath} has been reverted to its original content.`)
+ console.log(
+ `File ${absolutePath} has been reverted to its original content.`,
+ )
if (this.documentWasOpen) {
- await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
- preview: false,
- })
+ await vscode.window.showTextDocument(
+ vscode.Uri.file(absolutePath),
+ {
+ preview: false,
+ },
+ )
}
await this.closeAllDiffViews()
}
@@ -305,23 +361,32 @@ export class DiffViewProvider {
arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
)
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
- const editor = await vscode.window.showTextDocument(diffTab.input.modified)
+ const editor = await vscode.window.showTextDocument(
+ diffTab.input.modified,
+ )
return editor
}
// Open new diff editor
return new Promise((resolve, reject) => {
const fileName = path.basename(uri.fsPath)
const fileExists = this.editType === "modify"
- const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
- if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
- disposable.dispose()
- resolve(editor)
- }
- })
+ const disposable = vscode.window.onDidChangeActiveTextEditor(
+ (editor) => {
+ if (
+ editor &&
+ arePathsEqual(editor.document.uri.fsPath, uri.fsPath)
+ ) {
+ disposable.dispose()
+ resolve(editor)
+ }
+ },
+ )
vscode.commands.executeCommand(
"vscode.diff",
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
- query: Buffer.from(this.originalContent ?? "").toString("base64"),
+ query: Buffer.from(this.originalContent ?? "").toString(
+ "base64",
+ ),
}),
uri,
`${fileName}: ${fileExists ? "Original ↔ Cline's Changes" : "New File"} (Editable)`,
@@ -329,7 +394,11 @@ export class DiffViewProvider {
// This may happen on very slow machines ie project idx
setTimeout(() => {
disposable.dispose()
- reject(new Error("Failed to open diff editor, please try again..."))
+ reject(
+ new Error(
+ "Failed to open diff editor, please try again...",
+ ),
+ )
}, 10_000)
})
}
diff --git a/src/integrations/editor/detect-omission.ts b/src/integrations/editor/detect-omission.ts
index 32de0aac72..7655131c1c 100644
--- a/src/integrations/editor/detect-omission.ts
+++ b/src/integrations/editor/detect-omission.ts
@@ -6,10 +6,21 @@ import * as vscode from "vscode"
* @param newFileContent The new content of the file to check.
* @returns True if a potential omission is detected, false otherwise.
*/
-function detectCodeOmission(originalFileContent: string, newFileContent: string): boolean {
+function detectCodeOmission(
+ originalFileContent: string,
+ newFileContent: string,
+): boolean {
const originalLines = originalFileContent.split("\n")
const newLines = newFileContent.split("\n")
- const omissionKeywords = ["remain", "remains", "unchanged", "rest", "previous", "existing", "..."]
+ const omissionKeywords = [
+ "remain",
+ "remains",
+ "unchanged",
+ "rest",
+ "previous",
+ "existing",
+ "...",
+ ]
const commentPatterns = [
/^\s*\/\//, // Single-line comment for most languages
@@ -38,7 +49,10 @@ function detectCodeOmission(originalFileContent: string, newFileContent: string)
* @param originalFileContent The original content of the file.
* @param newFileContent The new content of the file to check.
*/
-export function showOmissionWarning(originalFileContent: string, newFileContent: string): void {
+export function showOmissionWarning(
+ originalFileContent: string,
+ newFileContent: string,
+): void {
if (detectCodeOmission(originalFileContent, newFileContent)) {
vscode.window
.showWarningMessage(
diff --git a/src/integrations/misc/export-markdown.ts b/src/integrations/misc/export-markdown.ts
index 2aa9d7b6ed..8dca7b759c 100644
--- a/src/integrations/misc/export-markdown.ts
+++ b/src/integrations/misc/export-markdown.ts
@@ -3,7 +3,10 @@ import os from "os"
import * as path from "path"
import * as vscode from "vscode"
-export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) {
+export async function downloadTask(
+ dateTs: number,
+ conversationHistory: Anthropic.MessageParam[],
+) {
// File name
const date = new Date(dateTs)
const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase()
@@ -20,9 +23,12 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
// Generate markdown
const markdownContent = conversationHistory
.map((message) => {
- const role = message.role === "user" ? "**User:**" : "**Assistant:**"
+ const role =
+ message.role === "user" ? "**User:**" : "**Assistant:**"
const content = Array.isArray(message.content)
- ? message.content.map((block) => formatContentBlockToMarkdown(block)).join("\n")
+ ? message.content
+ .map((block) => formatContentBlockToMarkdown(block))
+ .join("\n")
: message.content
return `${role}\n\n${content}\n\n`
})
@@ -31,12 +37,17 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
// Prompt user for save location
const saveUri = await vscode.window.showSaveDialog({
filters: { Markdown: ["md"] },
- defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", fileName)),
+ defaultUri: vscode.Uri.file(
+ path.join(os.homedir(), "Downloads", fileName),
+ ),
})
if (saveUri) {
// Write content to the selected location
- await vscode.workspace.fs.writeFile(saveUri, Buffer.from(markdownContent))
+ await vscode.workspace.fs.writeFile(
+ saveUri,
+ Buffer.from(markdownContent),
+ )
vscode.window.showTextDocument(saveUri, { preview: true })
}
}
@@ -58,7 +69,10 @@ export function formatContentBlockToMarkdown(
let input: string
if (typeof block.input === "object" && block.input !== null) {
input = Object.entries(block.input)
- .map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`)
+ .map(
+ ([key, value]) =>
+ `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`,
+ )
.join("\n")
} else {
input = String(block.input)
@@ -72,7 +86,9 @@ export function formatContentBlockToMarkdown(
return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content}`
} else if (Array.isArray(block.content)) {
return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content
- .map((contentBlock) => formatContentBlockToMarkdown(contentBlock))
+ .map((contentBlock) =>
+ formatContentBlockToMarkdown(contentBlock),
+ )
.join("\n")}`
} else {
return `[${toolName}${block.is_error ? " (Error)" : ""}]`
@@ -82,7 +98,10 @@ export function formatContentBlockToMarkdown(
}
}
-export function findToolName(toolCallId: string, messages: Anthropic.MessageParam[]): string {
+export function findToolName(
+ toolCallId: string,
+ messages: Anthropic.MessageParam[],
+): string {
for (const message of messages) {
if (Array.isArray(message.content)) {
for (const block of message.content) {
diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts
index 67a580af9b..83644d04b9 100644
--- a/src/integrations/misc/extract-text.ts
+++ b/src/integrations/misc/extract-text.ts
@@ -24,7 +24,9 @@ export async function extractTextFromFile(filePath: string): Promise {
if (!isBinary) {
return await fs.readFile(filePath, "utf8")
} else {
- throw new Error(`Cannot read text for file type: ${fileExtension}`)
+ throw new Error(
+ `Cannot read text for file type: ${fileExtension}`,
+ )
}
}
}
@@ -46,7 +48,10 @@ async function extractTextFromIPYNB(filePath: string): Promise {
let extractedText = ""
for (const cell of notebook.cells) {
- if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
+ if (
+ (cell.cell_type === "markdown" || cell.cell_type === "code") &&
+ cell.source
+ ) {
extractedText += cell.source.join("\n") + "\n"
}
}
diff --git a/src/integrations/misc/open-file.ts b/src/integrations/misc/open-file.ts
index 8dc3029947..45eafe00ab 100644
--- a/src/integrations/misc/open-file.ts
+++ b/src/integrations/misc/open-file.ts
@@ -11,10 +11,19 @@ export async function openImage(dataUri: string) {
}
const [, format, base64Data] = matches
const imageBuffer = Buffer.from(base64Data, "base64")
- const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`)
+ const tempFilePath = path.join(
+ os.tmpdir(),
+ `temp_image_${Date.now()}.${format}`,
+ )
try {
- await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer)
- await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
+ await vscode.workspace.fs.writeFile(
+ vscode.Uri.file(tempFilePath),
+ imageBuffer,
+ )
+ await vscode.commands.executeCommand(
+ "vscode.open",
+ vscode.Uri.file(tempFilePath),
+ )
} catch (error) {
vscode.window.showErrorMessage(`Error opening image: ${error}`)
}
@@ -29,14 +38,20 @@ export async function openFile(absolutePath: string) {
for (const group of vscode.window.tabGroups.all) {
const existingTab = group.tabs.find(
(tab) =>
- tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, uri.fsPath),
+ tab.input instanceof vscode.TabInputText &&
+ arePathsEqual(tab.input.uri.fsPath, uri.fsPath),
)
if (existingTab) {
- const activeColumn = vscode.window.activeTextEditor?.viewColumn
- const tabColumn = vscode.window.tabGroups.all.find((group) =>
- group.tabs.includes(existingTab),
+ const activeColumn =
+ vscode.window.activeTextEditor?.viewColumn
+ const tabColumn = vscode.window.tabGroups.all.find(
+ (group) => group.tabs.includes(existingTab),
)?.viewColumn
- if (activeColumn && activeColumn !== tabColumn && !existingTab.isDirty) {
+ if (
+ activeColumn &&
+ activeColumn !== tabColumn &&
+ !existingTab.isDirty
+ ) {
await vscode.window.tabGroups.close(existingTab)
}
break
diff --git a/src/integrations/notifications/index.ts b/src/integrations/notifications/index.ts
index 722df87e32..7faf2d7359 100644
--- a/src/integrations/notifications/index.ts
+++ b/src/integrations/notifications/index.ts
@@ -7,7 +7,9 @@ interface NotificationOptions {
message: string
}
-async function showMacOSNotification(options: NotificationOptions): Promise {
+async function showMacOSNotification(
+ options: NotificationOptions,
+): Promise {
const { title, subtitle = "", message } = options
const script = `display notification "${message}" with title "${title}" subtitle "${subtitle}" sound name "Tink"`
@@ -19,7 +21,9 @@ async function showMacOSNotification(options: NotificationOptions): Promise {
+async function showWindowsNotification(
+ options: NotificationOptions,
+): Promise {
const { subtitle, message } = options
const script = `
@@ -50,7 +54,9 @@ async function showWindowsNotification(options: NotificationOptions): Promise {
+async function showLinuxNotification(
+ options: NotificationOptions,
+): Promise {
const { title = "", subtitle = "", message } = options
// Combine subtitle and message if subtitle exists
@@ -63,7 +69,9 @@ async function showLinuxNotification(options: NotificationOptions): Promise {
+export async function showSystemNotification(
+ options: NotificationOptions,
+): Promise {
try {
const { title = "Cline", message } = options
diff --git a/src/integrations/terminal/TerminalManager.ts b/src/integrations/terminal/TerminalManager.ts
index 81e91ab6b8..8ae4605c9c 100644
--- a/src/integrations/terminal/TerminalManager.ts
+++ b/src/integrations/terminal/TerminalManager.ts
@@ -1,7 +1,11 @@
import pWaitFor from "p-wait-for"
import * as vscode from "vscode"
import { arePathsEqual } from "../../utils/path"
-import { mergePromise, TerminalProcess, TerminalProcessResultPromise } from "./TerminalProcess"
+import {
+ mergePromise,
+ TerminalProcess,
+ TerminalProcessResultPromise,
+} from "./TerminalProcess"
import { TerminalInfo, TerminalRegistry } from "./TerminalRegistry"
/*
@@ -97,7 +101,9 @@ export class TerminalManager {
constructor() {
let disposable: vscode.Disposable | undefined
try {
- disposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => {
+ disposable = (
+ vscode.window as vscode.Window
+ ).onDidStartTerminalShellExecution?.(async (e) => {
// Creating a read stream here results in a more consistent output. This is most obvious when running the `date` command.
e?.execution?.read()
})
@@ -109,7 +115,10 @@ export class TerminalManager {
}
}
- runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise {
+ runCommand(
+ terminalInfo: TerminalInfo,
+ command: string,
+ ): TerminalProcessResultPromise {
terminalInfo.busy = true
terminalInfo.lastCommand = command
const process = new TerminalProcess()
@@ -121,7 +130,9 @@ export class TerminalManager {
// if shell integration is not available, remove terminal so it does not get reused as it may be running a long-running process
process.once("no_shell_integration", () => {
- console.log(`no_shell_integration received for terminal ${terminalInfo.id}`)
+ console.log(
+ `no_shell_integration received for terminal ${terminalInfo.id}`,
+ )
// Remove the terminal so we can't reuse it (in case it's running a long-running process)
TerminalRegistry.removeTerminal(terminalInfo.id)
this.terminalIds.delete(terminalInfo.id)
@@ -144,9 +155,15 @@ export class TerminalManager {
process.run(terminalInfo.terminal, command)
} else {
// docs recommend waiting 3s for shell integration to activate
- pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
+ pWaitFor(
+ () => terminalInfo.terminal.shellIntegration !== undefined,
+ { timeout: 4000 },
+ ).finally(() => {
const existingProcess = this.processes.get(terminalInfo.id)
- if (existingProcess && existingProcess.waitForShellIntegration) {
+ if (
+ existingProcess &&
+ existingProcess.waitForShellIntegration
+ ) {
existingProcess.waitForShellIntegration = false
existingProcess.run(terminalInfo.terminal, command)
}
@@ -158,16 +175,21 @@ export class TerminalManager {
async getOrCreateTerminal(cwd: string): Promise {
// Find available terminal from our pool first (created for this task)
- const availableTerminal = TerminalRegistry.getAllTerminals().find((t) => {
- if (t.busy) {
- return false
- }
- const terminalCwd = t.terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
- if (!terminalCwd) {
- return false
- }
- return arePathsEqual(vscode.Uri.file(cwd).fsPath, terminalCwd.fsPath)
- })
+ const availableTerminal = TerminalRegistry.getAllTerminals().find(
+ (t) => {
+ if (t.busy) {
+ return false
+ }
+ const terminalCwd = t.terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
+ if (!terminalCwd) {
+ return false
+ }
+ return arePathsEqual(
+ vscode.Uri.file(cwd).fsPath,
+ terminalCwd.fsPath,
+ )
+ },
+ )
if (availableTerminal) {
this.terminalIds.add(availableTerminal.id)
return availableTerminal
@@ -181,7 +203,9 @@ export class TerminalManager {
getTerminals(busy: boolean): { id: number; lastCommand: string }[] {
return Array.from(this.terminalIds)
.map((id) => TerminalRegistry.getTerminal(id))
- .filter((t): t is TerminalInfo => t !== undefined && t.busy === busy)
+ .filter(
+ (t): t is TerminalInfo => t !== undefined && t.busy === busy,
+ )
.map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
}
diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts
index 5597350db3..30554ce722 100644
--- a/src/integrations/terminal/TerminalProcess.ts
+++ b/src/integrations/terminal/TerminalProcess.ts
@@ -27,7 +27,10 @@ export class TerminalProcess extends EventEmitter {
// super()
async run(terminal: vscode.Terminal, command: string) {
- if (terminal.shellIntegration && terminal.shellIntegration.executeCommand) {
+ if (
+ terminal.shellIntegration &&
+ terminal.shellIntegration.executeCommand
+ ) {
const execution = terminal.shellIntegration.executeCommand(command)
const stream = execution.read()
// todo: need to handle errors
@@ -60,7 +63,9 @@ export class TerminalProcess extends EventEmitter {
// Once we've retrieved any potential output between sequences, we can remove everything up to end of the last sequence
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const vscodeSequenceRegex = /\x1b\]633;.[^\x07]*\x07/g
- const lastMatch = [...data.matchAll(vscodeSequenceRegex)].pop()
+ const lastMatch = [
+ ...data.matchAll(vscodeSequenceRegex),
+ ].pop()
if (lastMatch && lastMatch.index !== undefined) {
data = data.slice(lastMatch.index + lastMatch[0].length)
}
@@ -77,7 +82,11 @@ export class TerminalProcess extends EventEmitter {
lines[0] = lines[0].replace(/[^\x20-\x7E]/g, "")
}
// Check if first two characters are the same, if so remove the first character
- if (lines.length > 0 && lines[0].length >= 2 && lines[0][0] === lines[0][1]) {
+ if (
+ lines.length > 0 &&
+ lines[0].length >= 2 &&
+ lines[0][0] === lines[0][1]
+ ) {
lines[0] = lines[0].slice(1)
}
// Remove everything up to the first alphanumeric character for first two lines
@@ -120,7 +129,14 @@ export class TerminalProcess extends EventEmitter {
clearTimeout(this.hotTimer)
}
// these markers indicate the command is some kind of local dev server recompiling the app, which we want to wait for output of before sending request to cline
- const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
+ const compilingMarkers = [
+ "compiling",
+ "building",
+ "bundling",
+ "transpiling",
+ "generating",
+ "starting",
+ ]
const markerNullifiers = [
"compiled",
"success",
@@ -136,13 +152,19 @@ export class TerminalProcess extends EventEmitter {
"fail",
]
const isCompiling =
- compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
- !markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
+ compilingMarkers.some((marker) =>
+ data.toLowerCase().includes(marker.toLowerCase()),
+ ) &&
+ !markerNullifiers.some((nullifier) =>
+ data.toLowerCase().includes(nullifier.toLowerCase()),
+ )
this.hotTimer = setTimeout(
() => {
this.isHot = false
},
- isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL,
+ isCompiling
+ ? PROCESS_HOT_TIMEOUT_COMPILING
+ : PROCESS_HOT_TIMEOUT_NORMAL,
)
// For non-immediately returning commands we want to show loading spinner right away but this wouldnt happen until it emits a line break, so as soon as we get any output we emit "" to let webview know to show spinner
@@ -154,7 +176,8 @@ export class TerminalProcess extends EventEmitter {
this.fullOutput += data
if (this.isListening) {
this.emitIfEol(data)
- this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
+ this.lastRetrievedIndex =
+ this.fullOutput.length - this.buffer.length
}
}
@@ -237,10 +260,20 @@ export class TerminalProcess extends EventEmitter {
export type TerminalProcessResultPromise = TerminalProcess & Promise
// Similar to execa's ResultPromise, this lets us create a mixin of both a TerminalProcess and a Promise: https://github.com/sindresorhus/execa/blob/main/lib/methods/promise.js
-export function mergePromise(process: TerminalProcess, promise: Promise): TerminalProcessResultPromise {
+export function mergePromise(
+ process: TerminalProcess,
+ promise: Promise,
+): TerminalProcessResultPromise {
const nativePromisePrototype = (async () => {})().constructor.prototype
const descriptors = ["then", "catch", "finally"].map(
- (property) => [property, Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property)] as const,
+ (property) =>
+ [
+ property,
+ Reflect.getOwnPropertyDescriptor(
+ nativePromisePrototype,
+ property,
+ ),
+ ] as const,
)
for (const [property, descriptor] of descriptors) {
if (descriptor) {
diff --git a/src/integrations/terminal/TerminalRegistry.ts b/src/integrations/terminal/TerminalRegistry.ts
index ac0ed30f0d..f7edf6d507 100644
--- a/src/integrations/terminal/TerminalRegistry.ts
+++ b/src/integrations/terminal/TerminalRegistry.ts
@@ -50,7 +50,9 @@ export class TerminalRegistry {
}
static getAllTerminals(): TerminalInfo[] {
- this.terminals = this.terminals.filter((t) => !this.isTerminalClosed(t.terminal))
+ this.terminals = this.terminals.filter(
+ (t) => !this.isTerminalClosed(t.terminal),
+ )
return this.terminals
}
diff --git a/src/integrations/theme/default-themes/dark_plus.json b/src/integrations/theme/default-themes/dark_plus.json
index 3a45b1eb83..df757b7170 100644
--- a/src/integrations/theme/default-themes/dark_plus.json
+++ b/src/integrations/theme/default-themes/dark_plus.json
@@ -154,7 +154,10 @@
}
},
{
- "scope": ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
+ "scope": [
+ "keyword.operator.or.regexp",
+ "keyword.control.anchor.regexp"
+ ],
"settings": {
"foreground": "#DCDCAA"
}
diff --git a/src/integrations/theme/default-themes/dark_vs.json b/src/integrations/theme/default-themes/dark_vs.json
index e2f078182d..2d6daa713c 100644
--- a/src/integrations/theme/default-themes/dark_vs.json
+++ b/src/integrations/theme/default-themes/dark_vs.json
@@ -345,7 +345,10 @@
}
},
{
- "scope": ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
+ "scope": [
+ "punctuation.section.embedded.begin.php",
+ "punctuation.section.embedded.end.php"
+ ],
"settings": {
"foreground": "#569cd6"
}
diff --git a/src/integrations/theme/default-themes/hc_black.json b/src/integrations/theme/default-themes/hc_black.json
index b446ebc4c4..6acbdb5894 100644
--- a/src/integrations/theme/default-themes/hc_black.json
+++ b/src/integrations/theme/default-themes/hc_black.json
@@ -410,7 +410,11 @@
},
{
"name": "Variable and parameter name",
- "scope": ["variable", "meta.definition.variable.name", "support.variable"],
+ "scope": [
+ "variable",
+ "meta.definition.variable.name",
+ "support.variable"
+ ],
"settings": {
"foreground": "#9CDCFE"
}
diff --git a/src/integrations/theme/default-themes/hc_light.json b/src/integrations/theme/default-themes/hc_light.json
index 1abecd39d7..35f36229cb 100644
--- a/src/integrations/theme/default-themes/hc_light.json
+++ b/src/integrations/theme/default-themes/hc_light.json
@@ -3,7 +3,11 @@
"name": "Light High Contrast",
"tokenColors": [
{
- "scope": ["meta.embedded", "source.groovy.embedded", "variable.legacy.builtin.python"],
+ "scope": [
+ "meta.embedded",
+ "source.groovy.embedded",
+ "variable.legacy.builtin.python"
+ ],
"settings": {
"foreground": "#292929"
}
@@ -146,7 +150,10 @@
}
},
{
- "scope": ["punctuation.definition.quote.begin.markdown", "punctuation.definition.list.begin.markdown"],
+ "scope": [
+ "punctuation.definition.quote.begin.markdown",
+ "punctuation.definition.list.begin.markdown"
+ ],
"settings": {
"foreground": "#0451A5"
}
@@ -328,7 +335,10 @@
}
},
{
- "scope": ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
+ "scope": [
+ "punctuation.section.embedded.begin.php",
+ "punctuation.section.embedded.end.php"
+ ],
"settings": {
"foreground": "#0F4A85"
}
@@ -509,7 +519,10 @@
}
},
{
- "scope": ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
+ "scope": [
+ "keyword.operator.or.regexp",
+ "keyword.control.anchor.regexp"
+ ],
"settings": {
"foreground": "#EE0000"
}
diff --git a/src/integrations/theme/default-themes/light_plus.json b/src/integrations/theme/default-themes/light_plus.json
index e103f48349..51fa251202 100644
--- a/src/integrations/theme/default-themes/light_plus.json
+++ b/src/integrations/theme/default-themes/light_plus.json
@@ -160,7 +160,10 @@
}
},
{
- "scope": ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
+ "scope": [
+ "keyword.operator.or.regexp",
+ "keyword.control.anchor.regexp"
+ ],
"settings": {
"foreground": "#EE0000"
}
diff --git a/src/integrations/theme/default-themes/light_vs.json b/src/integrations/theme/default-themes/light_vs.json
index eb098e393f..678116e59b 100644
--- a/src/integrations/theme/default-themes/light_vs.json
+++ b/src/integrations/theme/default-themes/light_vs.json
@@ -185,7 +185,10 @@
}
},
{
- "scope": ["punctuation.definition.quote.begin.markdown", "punctuation.definition.list.begin.markdown"],
+ "scope": [
+ "punctuation.definition.quote.begin.markdown",
+ "punctuation.definition.list.begin.markdown"
+ ],
"settings": {
"foreground": "#0451a5"
}
@@ -370,7 +373,10 @@
}
},
{
- "scope": ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
+ "scope": [
+ "punctuation.section.embedded.begin.php",
+ "punctuation.section.embedded.end.php"
+ ],
"settings": {
"foreground": "#800000"
}
diff --git a/src/integrations/theme/getTheme.ts b/src/integrations/theme/getTheme.ts
index ffed26e462..1e9b8d487e 100644
--- a/src/integrations/theme/getTheme.ts
+++ b/src/integrations/theme/getTheme.ts
@@ -32,7 +32,10 @@ function parseThemeString(themeString: string | undefined): any {
export async function getTheme() {
let currentTheme = undefined
- const colorTheme = vscode.workspace.getConfiguration("workbench").get("colorTheme") || "Default Dark Modern"
+ const colorTheme =
+ vscode.workspace
+ .getConfiguration("workbench")
+ .get("colorTheme") || "Default Dark Modern"
try {
for (let i = vscode.extensions.all.length - 1; i >= 0; i--) {
@@ -43,7 +46,10 @@ export async function getTheme() {
if (extension.packageJSON?.contributes?.themes?.length > 0) {
for (const theme of extension.packageJSON.contributes.themes) {
if (theme.label === colorTheme) {
- const themePath = path.join(extension.extensionPath, theme.path)
+ const themePath = path.join(
+ extension.extensionPath,
+ theme.path,
+ )
currentTheme = await fs.readFile(themePath, "utf-8")
break
}
@@ -54,7 +60,14 @@ export async function getTheme() {
if (currentTheme === undefined && defaultThemes[colorTheme]) {
const filename = `${defaultThemes[colorTheme]}.json`
currentTheme = await fs.readFile(
- path.join(getExtensionUri().fsPath, "src", "integrations", "theme", "default-themes", filename),
+ path.join(
+ getExtensionUri().fsPath,
+ "src",
+ "integrations",
+ "theme",
+ "default-themes",
+ filename,
+ ),
"utf-8",
)
}
@@ -64,7 +77,14 @@ export async function getTheme() {
if (parsed.include) {
const includeThemeString = await fs.readFile(
- path.join(getExtensionUri().fsPath, "src", "integrations", "theme", "default-themes", parsed.include),
+ path.join(
+ getExtensionUri().fsPath,
+ "src",
+ "integrations",
+ "theme",
+ "default-themes",
+ parsed.include,
+ ),
"utf-8",
)
const includeTheme = parseThemeString(includeThemeString)
@@ -114,7 +134,11 @@ export function mergeJson(
// Merge keys are used to determine whether an item form the second object should override one from the first
const keptFromFirst: any[] = []
firstValue.forEach((item: any) => {
- if (!secondValue.some((item2: any) => mergeKeys[key](item, item2))) {
+ if (
+ !secondValue.some((item2: any) =>
+ mergeKeys[key](item, item2),
+ )
+ ) {
keptFromFirst.push(item)
}
})
@@ -122,9 +146,16 @@ export function mergeJson(
} else {
copyOfFirst[key] = [...firstValue, ...secondValue]
}
- } else if (typeof secondValue === "object" && typeof firstValue === "object") {
+ } else if (
+ typeof secondValue === "object" &&
+ typeof firstValue === "object"
+ ) {
// Object
- copyOfFirst[key] = mergeJson(firstValue, secondValue, mergeBehavior)
+ copyOfFirst[key] = mergeJson(
+ firstValue,
+ secondValue,
+ mergeBehavior,
+ )
} else {
// Other (boolean, number, string)
copyOfFirst[key] = secondValue
@@ -141,5 +172,6 @@ export function mergeJson(
}
function getExtensionUri(): vscode.Uri {
- return vscode.extensions.getExtension("saoudrizwan.claude-dev")!.extensionUri
+ return vscode.extensions.getExtension("saoudrizwan.claude-dev")!
+ .extensionUri
}
diff --git a/src/integrations/workspace/WorkspaceTracker.ts b/src/integrations/workspace/WorkspaceTracker.ts
index 10dfac8f9e..1d390fe0dc 100644
--- a/src/integrations/workspace/WorkspaceTracker.ts
+++ b/src/integrations/workspace/WorkspaceTracker.ts
@@ -3,7 +3,9 @@ import * as path from "path"
import { listFiles } from "../../services/glob/list-files"
import { ClineProvider } from "../../core/webview/ClineProvider"
-const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
+const cwd = vscode.workspace.workspaceFolders
+ ?.map((folder) => folder.uri.fsPath)
+ .at(0)
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
class WorkspaceTracker {
@@ -22,20 +24,28 @@ class WorkspaceTracker {
return
}
const [files, _] = await listFiles(cwd, true, 1_000)
- files.forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
+ files.forEach((file) =>
+ this.filePaths.add(this.normalizeFilePath(file)),
+ )
this.workspaceDidUpdate()
}
private registerListeners() {
// Listen for file creation
// .bind(this) ensures the callback refers to class instance when using this, not necessary when using arrow function
- this.disposables.push(vscode.workspace.onDidCreateFiles(this.onFilesCreated.bind(this)))
+ this.disposables.push(
+ vscode.workspace.onDidCreateFiles(this.onFilesCreated.bind(this)),
+ )
// Listen for file deletion
- this.disposables.push(vscode.workspace.onDidDeleteFiles(this.onFilesDeleted.bind(this)))
+ this.disposables.push(
+ vscode.workspace.onDidDeleteFiles(this.onFilesDeleted.bind(this)),
+ )
// Listen for file renaming
- this.disposables.push(vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)))
+ this.disposables.push(
+ vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)),
+ )
/*
An event that is emitted when a workspace folder is added or removed.
@@ -95,16 +105,23 @@ class WorkspaceTracker {
}
private normalizeFilePath(filePath: string): string {
- const resolvedPath = cwd ? path.resolve(cwd, filePath) : path.resolve(filePath)
+ const resolvedPath = cwd
+ ? path.resolve(cwd, filePath)
+ : path.resolve(filePath)
return filePath.endsWith("/") ? resolvedPath + "/" : resolvedPath
}
private async addFilePath(filePath: string): Promise {
const normalizedPath = this.normalizeFilePath(filePath)
try {
- const stat = await vscode.workspace.fs.stat(vscode.Uri.file(normalizedPath))
+ const stat = await vscode.workspace.fs.stat(
+ vscode.Uri.file(normalizedPath),
+ )
const isDirectory = (stat.type & vscode.FileType.Directory) !== 0
- const pathWithSlash = isDirectory && !normalizedPath.endsWith("/") ? normalizedPath + "/" : normalizedPath
+ const pathWithSlash =
+ isDirectory && !normalizedPath.endsWith("/")
+ ? normalizedPath + "/"
+ : normalizedPath
this.filePaths.add(pathWithSlash)
return pathWithSlash
} catch {
@@ -116,7 +133,10 @@ class WorkspaceTracker {
private async removeFilePath(filePath: string): Promise {
const normalizedPath = this.normalizeFilePath(filePath)
- return this.filePaths.delete(normalizedPath) || this.filePaths.delete(normalizedPath + "/")
+ return (
+ this.filePaths.delete(normalizedPath) ||
+ this.filePaths.delete(normalizedPath + "/")
+ )
}
public dispose() {
diff --git a/src/integrations/workspace/get-python-env.ts b/src/integrations/workspace/get-python-env.ts
index 92575b408a..f24dc8140c 100644
--- a/src/integrations/workspace/get-python-env.ts
+++ b/src/integrations/workspace/get-python-env.ts
@@ -33,7 +33,9 @@ export async function getPythonEnvPath(): Promise {
return undefined
}
// Get the active python environment path for the current workspace
- const pythonEnv = await pythonApi?.environments?.getActiveEnvironmentPath(workspaceFolder.uri)
+ const pythonEnv = await pythonApi?.environments?.getActiveEnvironmentPath(
+ workspaceFolder.uri,
+ )
if (pythonEnv && pythonEnv.path) {
return pythonEnv.path
} else {
diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts
index b45265c77b..e70103f7e5 100644
--- a/src/services/browser/BrowserSession.ts
+++ b/src/services/browser/BrowserSession.ts
@@ -1,7 +1,13 @@
import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as path from "path"
-import { Browser, Page, ScreenshotOptions, TimeoutError, launch } from "puppeteer-core"
+import {
+ Browser,
+ Page,
+ ScreenshotOptions,
+ TimeoutError,
+ launch,
+} from "puppeteer-core"
// @ts-ignore
import PCR from "puppeteer-chromium-resolver"
import pWaitFor from "p-wait-for"
@@ -79,7 +85,9 @@ export class BrowserSession {
return {}
}
- async doAction(action: (page: Page) => Promise): Promise {
+ async doAction(
+ action: (page: Page) => Promise,
+ ): Promise {
if (!this.page) {
throw new Error(
"Browser is not launched. This may occur if the browser was automatically closed by a non-`browser_action` tool.",
@@ -166,7 +174,10 @@ export class BrowserSession {
async navigateToUrl(url: string): Promise {
return this.doAction(async (page) => {
// networkidle2 isn't good enough since page may take some time to load. we can assume locally running dev sites will reach networkidle0 in a reasonable amount of time
- await page.goto(url, { timeout: 7_000, waitUntil: ["domcontentloaded", "networkidle2"] })
+ await page.goto(url, {
+ timeout: 7_000,
+ waitUntil: ["domcontentloaded", "networkidle2"],
+ })
// await page.goto(url, { timeout: 10_000, waitUntil: "load" })
await this.waitTillHTMLStable(page) // in case the page is loading more resources
})
diff --git a/src/services/browser/UrlContentFetcher.ts b/src/services/browser/UrlContentFetcher.ts
index caf19ee83b..614338503e 100644
--- a/src/services/browser/UrlContentFetcher.ts
+++ b/src/services/browser/UrlContentFetcher.ts
@@ -71,7 +71,10 @@ export class UrlContentFetcher {
- domcontentloaded is when the basic DOM is loaded
this should be sufficient for most doc sites
*/
- await this.page.goto(url, { timeout: 10_000, waitUntil: ["domcontentloaded", "networkidle2"] })
+ await this.page.goto(url, {
+ timeout: 10_000,
+ waitUntil: ["domcontentloaded", "networkidle2"],
+ })
const content = await this.page.content()
// use cheerio to parse and clean up the HTML
diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts
index 8578b914d7..b5c9d3ad6c 100644
--- a/src/services/glob/list-files.ts
+++ b/src/services/glob/list-files.ts
@@ -3,10 +3,15 @@ import os from "os"
import * as path from "path"
import { arePathsEqual } from "../../utils/path"
-export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
+export async function listFiles(
+ dirPath: string,
+ recursive: boolean,
+ limit: number,
+): Promise<[string[], boolean]> {
const absolutePath = path.resolve(dirPath)
// Do not allow listing files in root or home directory, which cline tends to want to do when the user's prompt is vague.
- const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
+ const root =
+ process.platform === "win32" ? path.parse(absolutePath).root : "/"
const isRoot = arePathsEqual(absolutePath, root)
if (isRoot) {
return [[root], false]
@@ -46,7 +51,9 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
onlyFiles: false, // true by default, false means it will list directories on their own too
}
// * globs all files in one dir, ** globs files in nested directories
- const files = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)
+ const files = recursive
+ ? await globbyLevelByLevel(limit, options)
+ : (await globby("*", options)).slice(0, limit)
return [files, files.length >= limit]
}
diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts
index 18a4685a9d..4734941cce 100644
--- a/src/services/mcp/McpHub.ts
+++ b/src/services/mcp/McpHub.ts
@@ -1,5 +1,8 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
-import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
+import {
+ StdioClientTransport,
+ StdioServerParameters,
+} from "@modelcontextprotocol/sdk/client/stdio.js"
import {
CallToolResultSchema,
ListResourcesResultSchema,
@@ -14,7 +17,10 @@ import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
-import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
+import {
+ ClineProvider,
+ GlobalFileNames,
+} from "../../core/webview/ClineProvider"
import {
McpResource,
McpResourceResponse,
@@ -114,11 +120,20 @@ export class McpHub {
return
}
try {
- vscode.window.showInformationMessage("Updating MCP servers...")
- await this.updateServerConnections(result.data.mcpServers || {})
- vscode.window.showInformationMessage("MCP servers updated")
+ vscode.window.showInformationMessage(
+ "Updating MCP servers...",
+ )
+ await this.updateServerConnections(
+ result.data.mcpServers || {},
+ )
+ vscode.window.showInformationMessage(
+ "MCP servers updated",
+ )
} catch (error) {
- console.error("Failed to process MCP settings change:", error)
+ console.error(
+ "Failed to process MCP settings change:",
+ error,
+ )
}
}
}),
@@ -136,16 +151,23 @@ export class McpHub {
}
}
- private async connectToServer(name: string, config: StdioServerParameters): Promise {
+ private async connectToServer(
+ name: string,
+ config: StdioServerParameters,
+ ): Promise {
// 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)
+ 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.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
+ version:
+ this.providerRef.deref()?.context.extension?.packageJSON
+ ?.version ?? "1.0.0",
},
{
capabilities: {},
@@ -165,7 +187,9 @@ export class McpHub {
transport.onerror = async (error) => {
console.error(`Transport error for "${name}":`, error)
- const connection = this.connections.find((conn) => conn.server.name === name)
+ const connection = this.connections.find(
+ (conn) => conn.server.name === name,
+ )
if (connection) {
connection.server.status = "disconnected"
this.appendErrorMessage(connection, error.message)
@@ -174,7 +198,9 @@ export class McpHub {
}
transport.onclose = async () => {
- const connection = this.connections.find((conn) => conn.server.name === name)
+ const connection = this.connections.find(
+ (conn) => conn.server.name === name,
+ )
if (connection) {
connection.server.status = "disconnected"
}
@@ -183,7 +209,9 @@ export class McpHub {
// If the config is invalid, show an error
if (!StdioConfigSchema.safeParse(config).success) {
- console.error(`Invalid config for "${name}": missing or invalid parameters`)
+ console.error(
+ `Invalid config for "${name}": missing or invalid parameters`,
+ )
const connection: McpConnection = {
server: {
name,
@@ -218,7 +246,9 @@ export class McpHub {
stderrStream.on("data", async (data: Buffer) => {
const errorOutput = data.toString()
console.error(`Server "${name}" stderr:`, errorOutput)
- const connection = this.connections.find((conn) => conn.server.name === name)
+ const connection = this.connections.find(
+ (conn) => conn.server.name === name,
+ )
if (connection) {
// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs " server running on stdio" to stderr.
this.appendErrorMessage(connection, errorOutput)
@@ -263,20 +293,28 @@ export class McpHub {
// 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)
+ connection.server.resourceTemplates =
+ await this.fetchResourceTemplatesList(name)
} catch (error) {
// Update status with error
- const connection = this.connections.find((conn) => conn.server.name === name)
+ 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))
+ this.appendErrorMessage(
+ connection,
+ error instanceof Error ? error.message : String(error),
+ )
}
throw error
}
}
private appendErrorMessage(connection: McpConnection, error: string) {
- const newError = connection.server.error ? `${connection.server.error}\n${error}` : error
+ const newError = connection.server.error
+ ? `${connection.server.error}\n${error}`
+ : error
connection.server.error = newError //.slice(0, 800)
}
@@ -284,7 +322,10 @@ export class McpHub {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
- ?.client.request({ method: "tools/list" }, ListToolsResultSchema)
+ ?.client.request(
+ { method: "tools/list" },
+ ListToolsResultSchema,
+ )
return response?.tools || []
} catch (error) {
// console.error(`Failed to fetch tools for ${serverName}:`, error)
@@ -292,11 +333,16 @@ export class McpHub {
}
}
- private async fetchResourcesList(serverName: string): Promise {
+ private async fetchResourcesList(
+ serverName: string,
+ ): Promise {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
- ?.client.request({ method: "resources/list" }, ListResourcesResultSchema)
+ ?.client.request(
+ { method: "resources/list" },
+ ListResourcesResultSchema,
+ )
return response?.resources || []
} catch (error) {
// console.error(`Failed to fetch resources for ${serverName}:`, error)
@@ -304,11 +350,16 @@ export class McpHub {
}
}
- private async fetchResourceTemplatesList(serverName: string): Promise {
+ private async fetchResourceTemplatesList(
+ serverName: string,
+ ): Promise {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
- ?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema)
+ ?.client.request(
+ { method: "resources/templates/list" },
+ ListResourceTemplatesResultSchema,
+ )
return response?.resourceTemplates || []
} catch (error) {
// console.error(`Failed to fetch resource templates for ${serverName}:`, error)
@@ -317,7 +368,9 @@ export class McpHub {
}
async deleteConnection(name: string): Promise {
- const connection = this.connections.find((conn) => conn.server.name === name)
+ const connection = this.connections.find(
+ (conn) => conn.server.name === name,
+ )
if (connection) {
try {
// connection.client.removeNotificationHandler("notifications/tools/list_changed")
@@ -329,14 +382,20 @@ export class McpHub {
} catch (error) {
console.error(`Failed to close transport for ${name}:`, error)
}
- this.connections = this.connections.filter((conn) => conn.server.name !== name)
+ this.connections = this.connections.filter(
+ (conn) => conn.server.name !== name,
+ )
}
}
- async updateServerConnections(newServers: Record): Promise {
+ async updateServerConnections(
+ newServers: Record,
+ ): Promise {
this.isConnecting = true
this.removeAllFileWatchers()
- const currentNames = new Set(this.connections.map((conn) => conn.server.name))
+ const currentNames = new Set(
+ this.connections.map((conn) => conn.server.name),
+ )
const newNames = new Set(Object.keys(newServers))
// Delete removed servers
@@ -349,7 +408,9 @@ export class McpHub {
// Update or add servers
for (const [name, config] of Object.entries(newServers)) {
- const currentConnection = this.connections.find((conn) => conn.server.name === name)
+ const currentConnection = this.connections.find(
+ (conn) => conn.server.name === name,
+ )
if (!currentConnection) {
// New server
@@ -357,17 +418,27 @@ export class McpHub {
this.setupFileWatcher(name, config)
await this.connectToServer(name, config)
} catch (error) {
- console.error(`Failed to connect to new MCP server ${name}:`, error)
+ console.error(
+ `Failed to connect to new MCP server ${name}:`,
+ error,
+ )
}
- } else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
+ } else if (
+ !deepEqual(JSON.parse(currentConnection.server.config), config)
+ ) {
// Existing server with changed config
try {
this.setupFileWatcher(name, config)
await this.deleteConnection(name)
await this.connectToServer(name, config)
- console.log(`Reconnected MCP server with updated config: ${name}`)
+ console.log(
+ `Reconnected MCP server with updated config: ${name}`,
+ )
} catch (error) {
- console.error(`Failed to reconnect MCP server ${name}:`, error)
+ console.error(
+ `Failed to reconnect MCP server ${name}:`,
+ error,
+ )
}
}
// If server exists with same config, do nothing
@@ -377,7 +448,9 @@ export class McpHub {
}
private setupFileWatcher(name: string, config: any) {
- const filePath = config.args?.find((arg: string) => arg.includes("build/index.js"))
+ const filePath = config.args?.find((arg: string) =>
+ arg.includes("build/index.js"),
+ )
if (filePath) {
// we use chokidar instead of onDidSaveTextDocument because it doesn't require the file to be open in the editor. The settings config is better suited for onDidSave since that will be manually updated by the user or Cline (and we want to detect save events, not every file change)
const watcher = chokidar.watch(filePath, {
@@ -387,7 +460,9 @@ export class McpHub {
})
watcher.on("change", () => {
- console.log(`Detected change in ${filePath}. Restarting server ${name}...`)
+ console.log(
+ `Detected change in ${filePath}. Restarting server ${name}...`,
+ )
this.restartConnection(name)
})
@@ -408,10 +483,14 @@ export class McpHub {
}
// Get existing connection and update its status
- const connection = this.connections.find((conn) => conn.server.name === serverName)
+ const connection = this.connections.find(
+ (conn) => conn.server.name === serverName,
+ )
const config = connection?.server.config
if (config) {
- vscode.window.showInformationMessage(`Restarting ${serverName} MCP server...`)
+ vscode.window.showInformationMessage(
+ `Restarting ${serverName} MCP server...`,
+ )
connection.server.status = "connecting"
connection.server.error = ""
await this.notifyWebviewOfServerChanges()
@@ -420,10 +499,17 @@ export class McpHub {
await this.deleteConnection(serverName)
// Try to connect again using existing config
await this.connectToServer(serverName, JSON.parse(config))
- vscode.window.showInformationMessage(`${serverName} MCP server connected`)
+ vscode.window.showInformationMessage(
+ `${serverName} MCP server connected`,
+ )
} catch (error) {
- console.error(`Failed to restart connection for ${serverName}:`, error)
- vscode.window.showErrorMessage(`Failed to connect to ${serverName} MCP server`)
+ console.error(
+ `Failed to restart connection for ${serverName}:`,
+ error,
+ )
+ vscode.window.showErrorMessage(
+ `Failed to connect to ${serverName} MCP server`,
+ )
}
}
@@ -451,8 +537,13 @@ export class McpHub {
// Using server
- async readResource(serverName: string, uri: string): Promise {
- const connection = this.connections.find((conn) => conn.server.name === serverName)
+ async readResource(
+ serverName: string,
+ uri: string,
+ ): Promise {
+ const connection = this.connections.find(
+ (conn) => conn.server.name === serverName,
+ )
if (!connection) {
throw new Error(`No connection found for server: ${serverName}`)
}
@@ -472,7 +563,9 @@ export class McpHub {
toolName: string,
toolArguments?: Record,
): Promise {
- const connection = this.connections.find((conn) => conn.server.name === serverName)
+ const connection = this.connections.find(
+ (conn) => conn.server.name === serverName,
+ )
if (!connection) {
throw new Error(
`No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
@@ -496,7 +589,10 @@ export class McpHub {
try {
await this.deleteConnection(connection.server.name)
} catch (error) {
- console.error(`Failed to close connection for ${connection.server.name}:`, error)
+ console.error(
+ `Failed to close connection for ${connection.server.name}:`,
+ error,
+ )
}
}
this.connections = []
diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts
index b48c60b5b2..7e02a57478 100644
--- a/src/services/ripgrep/index.ts
+++ b/src/services/ripgrep/index.ts
@@ -135,7 +135,16 @@ export async function regexSearchFiles(
throw new Error("Could not find ripgrep binary")
}
- const args = ["--json", "-e", regex, "--glob", filePattern || "*", "--context", "1", directoryPath]
+ const args = [
+ "--json",
+ "-e",
+ regex,
+ "--glob",
+ filePattern || "*",
+ "--context",
+ "1",
+ directoryPath,
+ ]
let output: string
try {
@@ -164,7 +173,9 @@ export async function regexSearchFiles(
}
} else if (parsed.type === "context" && currentResult) {
if (parsed.data.line_number < currentResult.line!) {
- currentResult.beforeContext!.push(parsed.data.lines.text)
+ currentResult.beforeContext!.push(
+ parsed.data.lines.text,
+ )
} else {
currentResult.afterContext!.push(parsed.data.lines.text)
}
@@ -205,7 +216,11 @@ function formatResults(results: SearchResult[], cwd: string): string {
output += `${filePath.toPosix()}\n│----\n`
fileResults.forEach((result, index) => {
- const allLines = [...result.beforeContext, result.match, ...result.afterContext]
+ const allLines = [
+ ...result.beforeContext,
+ result.match,
+ ...result.afterContext,
+ ]
allLines.forEach((line) => {
output += `│${line?.trimEnd() ?? ""}\n`
})
diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts
index 83e02ac615..cc5275c641 100644
--- a/src/services/tree-sitter/index.ts
+++ b/src/services/tree-sitter/index.ts
@@ -5,7 +5,9 @@ import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
import { fileExistsAtPath } from "../../utils/fs"
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
-export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise {
+export async function parseSourceCodeForDefinitionsTopLevel(
+ dirPath: string,
+): Promise {
// check if the path exists
const dirExists = await fileExistsAtPath(path.resolve(dirPath))
if (!dirExists) {
@@ -50,7 +52,10 @@ export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Pr
return result ? result : "No source code definitions found."
}
-function separateFiles(allFiles: string[]): { filesToParse: string[]; remainingFiles: string[] } {
+function separateFiles(allFiles: string[]): {
+ filesToParse: string[]
+ remainingFiles: string[]
+} {
const extensions = [
"js",
"jsx",
@@ -74,8 +79,12 @@ function separateFiles(allFiles: string[]): { filesToParse: string[]; remainingF
"php",
"swift",
].map((e) => `.${e}`)
- const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max
- const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file))
+ const filesToParse = allFiles
+ .filter((file) => extensions.includes(path.extname(file)))
+ .slice(0, 50) // 50 files max
+ const remainingFiles = allFiles.filter(
+ (file) => !filesToParse.includes(file),
+ )
return { filesToParse, remainingFiles }
}
@@ -95,7 +104,10 @@ This approach allows us to focus on the most relevant parts of the code (defined
- https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/test/helper.js
- https://tree-sitter.github.io/tree-sitter/code-navigation-systems
*/
-async function parseFile(filePath: string, languageParsers: LanguageParser): Promise {
+async function parseFile(
+ filePath: string,
+ languageParsers: LanguageParser,
+): Promise {
const fileContent = await fs.readFile(filePath, "utf8")
const ext = path.extname(filePath).toLowerCase().slice(1)
@@ -115,7 +127,9 @@ async function parseFile(filePath: string, languageParsers: LanguageParser): Pro
const captures = query.captures(tree.rootNode)
// Sort captures by their start position
- captures.sort((a, b) => a.node.startPosition.row - b.node.startPosition.row)
+ captures.sort(
+ (a, b) => a.node.startPosition.row - b.node.startPosition.row,
+ )
// Split the file content into individual lines
const lines = fileContent.split("\n")
diff --git a/src/services/tree-sitter/languageParser.ts b/src/services/tree-sitter/languageParser.ts
index 2d791b39a8..b7a5f26817 100644
--- a/src/services/tree-sitter/languageParser.ts
+++ b/src/services/tree-sitter/languageParser.ts
@@ -23,7 +23,9 @@ export interface LanguageParser {
}
async function loadLanguage(langName: string) {
- return await Parser.Language.load(path.join(__dirname, `tree-sitter-${langName}.wasm`))
+ return await Parser.Language.load(
+ path.join(__dirname, `tree-sitter-${langName}.wasm`),
+ )
}
let isParserInitialized = false
@@ -57,9 +59,13 @@ Sources:
- https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/README.md
- https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/test/query-test.js
*/
-export async function loadRequiredLanguageParsers(filesToParse: string[]): Promise {
+export async function loadRequiredLanguageParsers(
+ filesToParse: string[],
+): Promise {
await initializeParser()
- const extensionsToLoad = new Set(filesToParse.map((file) => path.extname(file).toLowerCase().slice(1)))
+ const extensionsToLoad = new Set(
+ filesToParse.map((file) => path.extname(file).toLowerCase().slice(1)),
+ )
const parsers: LanguageParser = {}
for (const ext of extensionsToLoad) {
let language: Parser.Language
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index b3e58bc4a6..04ab46686d 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -19,6 +19,7 @@ export interface ExtensionMessage {
| "partialMessage"
| "openRouterModels"
| "mcpServers"
+ | "relinquishControl"
text?: string
action?:
| "chatButtonClicked"
@@ -42,6 +43,8 @@ export interface ExtensionState {
apiConfiguration?: ApiConfiguration
customInstructions?: string
uriScheme?: string
+ currentTaskItem?: HistoryItem
+ checkpointTrackerErrorMessage?: string
clineMessages: ClineMessage[]
taskHistory: HistoryItem[]
shouldShowAnnouncement: boolean
@@ -56,6 +59,9 @@ export interface ClineMessage {
text?: string
images?: string[]
partial?: boolean
+ lastCheckpointHash?: string
+ conversationHistoryIndex?: number
+ conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
}
export type ClineAsk =
@@ -93,6 +99,7 @@ export type ClineSay =
| "mcp_server_response"
| "use_mcp_server"
| "diff_error"
+ | "deleted_api_reqs"
export interface ClineSayTool {
tool:
@@ -111,7 +118,14 @@ export interface ClineSayTool {
}
// must keep in sync with system prompt
-export const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const
+export const browserActions = [
+ "launch",
+ "click",
+ "type",
+ "scroll_down",
+ "scroll_up",
+ "close",
+] as const
export type BrowserAction = (typeof browserActions)[number]
export interface ClineSayBrowserAction {
@@ -147,3 +161,5 @@ export interface ClineApiReqInfo {
}
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"
+
+export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
diff --git a/src/shared/HistoryItem.ts b/src/shared/HistoryItem.ts
index d4539f6441..790c35cef6 100644
--- a/src/shared/HistoryItem.ts
+++ b/src/shared/HistoryItem.ts
@@ -7,4 +7,8 @@ export type HistoryItem = {
cacheWrites?: number
cacheReads?: number
totalCost: number
+
+ size?: number
+ shadowGitConfigWorkTree?: string
+ conversationHistoryDeletedRange?: [number, number]
}
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 82ad22d95e..ff6a991b91 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -26,12 +26,21 @@ export interface WebviewMessage {
| "openMcpSettings"
| "restartMcpServer"
| "autoApprovalSettings"
+ | "checkpointDiff"
+ | "checkpointRestore"
+ | "taskCompletionViewChanges"
text?: string
askResponse?: ClineAskResponse
apiConfiguration?: ApiConfiguration
images?: string[]
bool?: boolean
+ number?: number
autoApprovalSettings?: AutoApprovalSettings
}
-export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+export type ClineAskResponse =
+ | "yesButtonClicked"
+ | "noButtonClicked"
+ | "messageResponse"
+
+export type ClineCheckpointRestore = "task" | "workspace" | "taskAndWorkspace"
diff --git a/src/shared/api.ts b/src/shared/api.ts
index d87d13d272..3e1681c7d5 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -59,7 +59,8 @@ export interface ModelInfo {
// Anthropic
// https://docs.anthropic.com/en/docs/about-claude/models
export type AnthropicModelId = keyof typeof anthropicModels
-export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20241022"
+export const anthropicDefaultModelId: AnthropicModelId =
+ "claude-3-5-sonnet-20241022"
export const anthropicModels = {
"claude-3-5-sonnet-20241022": {
maxTokens: 8192,
@@ -107,7 +108,8 @@ export const anthropicModels = {
// AWS Bedrock
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
export type BedrockModelId = keyof typeof bedrockModels
-export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0"
+export const bedrockDefaultModelId: BedrockModelId =
+ "anthropic.claude-3-5-sonnet-20241022-v2:0"
export const bedrockModels = {
"anthropic.claude-3-5-sonnet-20241022-v2:0": {
maxTokens: 8192,
@@ -180,7 +182,8 @@ export const openRouterDefaultModelInfo: ModelInfo = {
// Vertex AI
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude
export type VertexModelId = keyof typeof vertexModels
-export const vertexDefaultModelId: VertexModelId = "claude-3-5-sonnet-v2@20241022"
+export const vertexDefaultModelId: VertexModelId =
+ "claude-3-5-sonnet-v2@20241022"
export const vertexModels = {
"claude-3-5-sonnet-v2@20241022": {
maxTokens: 8192,
@@ -237,7 +240,8 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
// Gemini
// https://ai.google.dev/gemini-api/docs/models/gemini
export type GeminiModelId = keyof typeof geminiModels
-export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219"
+export const geminiDefaultModelId: GeminiModelId =
+ "gemini-2.0-flash-thinking-exp-1219"
export const geminiModels = {
"gemini-2.0-flash-thinking-exp-1219": {
maxTokens: 8192,
diff --git a/src/shared/array.ts b/src/shared/array.ts
index b87c458fd3..9a847a7570 100644
--- a/src/shared/array.ts
+++ b/src/shared/array.ts
@@ -6,7 +6,10 @@
* order, until it finds one where predicate returns true. If such an element is found,
* findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1.
*/
-export function findLastIndex(array: Array, predicate: (value: T, index: number, obj: T[]) => boolean): number {
+export function findLastIndex(
+ array: Array,
+ predicate: (value: T, index: number, obj: T[]) => boolean,
+): number {
let l = array.length
while (l--) {
if (predicate(array[l], l, array)) {
@@ -16,7 +19,10 @@ export function findLastIndex(array: Array, predicate: (value: T, index: n
return -1
}
-export function findLast(array: Array, predicate: (value: T, index: number, obj: T[]) => boolean): T | undefined {
+export function findLast(
+ array: Array,
+ predicate: (value: T, index: number, obj: T[]) => boolean,
+): T | undefined {
const index = findLastIndex(array, predicate)
return index === -1 ? undefined : array[index]
}
diff --git a/src/shared/combineApiRequests.ts b/src/shared/combineApiRequests.ts
index be8721b99d..5bac684521 100644
--- a/src/shared/combineApiRequests.ts
+++ b/src/shared/combineApiRequests.ts
@@ -22,14 +22,23 @@ export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] {
const combinedApiRequests: ClineMessage[] = []
for (let i = 0; i < messages.length; i++) {
- if (messages[i].type === "say" && messages[i].say === "api_req_started") {
+ if (
+ messages[i].type === "say" &&
+ messages[i].say === "api_req_started"
+ ) {
let startedRequest = JSON.parse(messages[i].text || "{}")
let j = i + 1
while (j < messages.length) {
- if (messages[j].type === "say" && messages[j].say === "api_req_finished") {
+ if (
+ messages[j].type === "say" &&
+ messages[j].say === "api_req_finished"
+ ) {
let finishedRequest = JSON.parse(messages[j].text || "{}")
- let combinedRequest = { ...startedRequest, ...finishedRequest }
+ let combinedRequest = {
+ ...startedRequest,
+ ...finishedRequest,
+ }
combinedApiRequests.push({
...messages[i],
@@ -51,10 +60,14 @@ export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] {
// Replace original api_req_started and remove api_req_finished
return messages
- .filter((msg) => !(msg.type === "say" && msg.say === "api_req_finished"))
+ .filter(
+ (msg) => !(msg.type === "say" && msg.say === "api_req_finished"),
+ )
.map((msg) => {
if (msg.type === "say" && msg.say === "api_req_started") {
- const combinedRequest = combinedApiRequests.find((req) => req.ts === msg.ts)
+ const combinedRequest = combinedApiRequests.find(
+ (req) => req.ts === msg.ts,
+ )
return combinedRequest || msg
}
return msg
diff --git a/src/shared/combineCommandSequences.ts b/src/shared/combineCommandSequences.ts
index 3e41cd2df9..03ee5499ac 100644
--- a/src/shared/combineCommandSequences.ts
+++ b/src/shared/combineCommandSequences.ts
@@ -20,22 +20,34 @@ import { ClineMessage } from "./ExtensionMessage"
* const result = simpleCombineCommandSequences(messages);
* // Result: [{ type: 'ask', ask: 'command', text: 'ls\nfile1.txt\nfile2.txt', ts: 1625097600000 }]
*/
-export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[] {
+export function combineCommandSequences(
+ messages: ClineMessage[],
+): ClineMessage[] {
const combinedCommands: ClineMessage[] = []
// First pass: combine commands with their outputs
for (let i = 0; i < messages.length; i++) {
- if (messages[i].type === "ask" && (messages[i].ask === "command" || messages[i].say === "command")) {
+ if (
+ messages[i].type === "ask" &&
+ (messages[i].ask === "command" || messages[i].say === "command")
+ ) {
let combinedText = messages[i].text || ""
let didAddOutput = false
let j = i + 1
while (j < messages.length) {
- if (messages[j].type === "ask" && (messages[j].ask === "command" || messages[j].say === "command")) {
+ if (
+ messages[j].type === "ask" &&
+ (messages[j].ask === "command" ||
+ messages[j].say === "command")
+ ) {
// Stop if we encounter the next command
break
}
- if (messages[j].ask === "command_output" || messages[j].say === "command_output") {
+ if (
+ messages[j].ask === "command_output" ||
+ messages[j].say === "command_output"
+ ) {
if (!didAddOutput) {
// Add a newline before the first output
combinedText += `\n${COMMAND_OUTPUT_STRING}`
@@ -61,10 +73,18 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[
// Second pass: remove command_outputs and replace original commands with combined ones
return messages
- .filter((msg) => !(msg.ask === "command_output" || msg.say === "command_output"))
+ .filter(
+ (msg) =>
+ !(msg.ask === "command_output" || msg.say === "command_output"),
+ )
.map((msg) => {
- if (msg.type === "ask" && (msg.ask === "command" || msg.say === "command")) {
- const combinedCommand = combinedCommands.find((cmd) => cmd.ts === msg.ts)
+ if (
+ msg.type === "ask" &&
+ (msg.ask === "command" || msg.say === "command")
+ ) {
+ const combinedCommand = combinedCommands.find(
+ (cmd) => cmd.ts === msg.ts,
+ )
return combinedCommand || msg
}
return msg
diff --git a/src/shared/context-mentions.ts b/src/shared/context-mentions.ts
index 3912868b10..1c1c86488e 100644
--- a/src/shared/context-mentions.ts
+++ b/src/shared/context-mentions.ts
@@ -44,5 +44,6 @@ Mention regex:
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
*/
-export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
+export const mentionRegex =
+ /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
diff --git a/src/shared/getApiMetrics.ts b/src/shared/getApiMetrics.ts
index bd7b1bbce0..ee481cb19d 100644
--- a/src/shared/getApiMetrics.ts
+++ b/src/shared/getApiMetrics.ts
@@ -12,7 +12,7 @@ interface ApiMetrics {
* Calculates API metrics from an array of ClineMessages.
*
* This function processes 'api_req_started' messages that have been combined with their
- * corresponding 'api_req_finished' messages by the combineApiRequests function.
+ * corresponding 'api_req_finished' messages by the combineApiRequests function. It also takes into account 'deleted_api_reqs' messages, which are aggregated from deleted messages.
* It extracts and sums up the tokensIn, tokensOut, cacheWrites, cacheReads, and cost from these messages.
*
* @param messages - An array of ClineMessage objects to process.
@@ -35,10 +35,16 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
}
messages.forEach((message) => {
- if (message.type === "say" && message.say === "api_req_started" && message.text) {
+ if (
+ message.type === "say" &&
+ (message.say === "api_req_started" ||
+ message.say === "deleted_api_reqs") &&
+ message.text
+ ) {
try {
const parsedData = JSON.parse(message.text)
- const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = parsedData
+ const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } =
+ parsedData
if (typeof tokensIn === "number") {
result.totalTokensIn += tokensIn
@@ -47,10 +53,12 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
result.totalTokensOut += tokensOut
}
if (typeof cacheWrites === "number") {
- result.totalCacheWrites = (result.totalCacheWrites ?? 0) + cacheWrites
+ result.totalCacheWrites =
+ (result.totalCacheWrites ?? 0) + cacheWrites
}
if (typeof cacheReads === "number") {
- result.totalCacheReads = (result.totalCacheReads ?? 0) + cacheReads
+ result.totalCacheReads =
+ (result.totalCacheReads ?? 0) + cacheReads
}
if (typeof cost === "number") {
result.totalCost += cost
diff --git a/src/utils/cost.ts b/src/utils/cost.ts
index f8f5f2b125..04caf5a586 100644
--- a/src/utils/cost.ts
+++ b/src/utils/cost.ts
@@ -10,15 +10,19 @@ export function calculateApiCost(
const modelCacheWritesPrice = modelInfo.cacheWritesPrice
let cacheWritesCost = 0
if (cacheCreationInputTokens && modelCacheWritesPrice) {
- cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
+ cacheWritesCost =
+ (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
}
const modelCacheReadsPrice = modelInfo.cacheReadsPrice
let cacheReadsCost = 0
if (cacheReadInputTokens && modelCacheReadsPrice) {
- cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
+ cacheReadsCost =
+ (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
}
- const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
+ const baseInputCost =
+ ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
- const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
+ const totalCost =
+ cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
return totalCost
}
diff --git a/src/utils/fs.test.ts b/src/utils/fs.test.ts
index ea9f132d5b..32c16ac712 100644
--- a/src/utils/fs.test.ts
+++ b/src/utils/fs.test.ts
@@ -6,7 +6,10 @@ import "should"
import { createDirectoriesForFile, fileExistsAtPath } from "./fs"
describe("Filesystem Utilities", () => {
- const tmpDir = path.join(os.tmpdir(), "cline-test-" + Math.random().toString(36).slice(2))
+ const tmpDir = path.join(
+ os.tmpdir(),
+ "cline-test-" + Math.random().toString(36).slice(2),
+ )
// Clean up after tests
after(async () => {
@@ -36,7 +39,13 @@ describe("Filesystem Utilities", () => {
describe("createDirectoriesForFile", () => {
it("should create all necessary directories", async () => {
- const deepPath = path.join(tmpDir, "deep", "nested", "dir", "file.txt")
+ const deepPath = path.join(
+ tmpDir,
+ "deep",
+ "nested",
+ "dir",
+ "file.txt",
+ )
const createdDirs = await createDirectoriesForFile(deepPath)
// Verify directories were created
@@ -59,7 +68,14 @@ describe("Filesystem Utilities", () => {
})
it("should normalize paths", async () => {
- const unnormalizedPath = path.join(tmpDir, "a", "..", "b", ".", "file.txt")
+ const unnormalizedPath = path.join(
+ tmpDir,
+ "a",
+ "..",
+ "b",
+ ".",
+ "file.txt",
+ )
const createdDirs = await createDirectoriesForFile(unnormalizedPath)
// Should create only the necessary directory
diff --git a/src/utils/fs.ts b/src/utils/fs.ts
index 9f7af84e4a..38c084a1b5 100644
--- a/src/utils/fs.ts
+++ b/src/utils/fs.ts
@@ -8,7 +8,9 @@ import * as path from "path"
* @param filePath - The full path to a file.
* @returns A promise that resolves to an array of newly created directories.
*/
-export async function createDirectoriesForFile(filePath: string): Promise {
+export async function createDirectoriesForFile(
+ filePath: string,
+): Promise {
const newDirectories: string[] = []
const normalizedFilePath = path.normalize(filePath) // Normalize path for cross-platform compatibility
const directoryPath = path.dirname(normalizedFilePath)
diff --git a/src/utils/path.test.ts b/src/utils/path.test.ts
index 8efa6e59e2..60626ed69c 100644
--- a/src/utils/path.test.ts
+++ b/src/utils/path.test.ts
@@ -30,7 +30,9 @@ describe("Path Utilities", () => {
it("should handle desktop path", () => {
const desktop = path.join(os.homedir(), "Desktop")
const testPath = path.join(desktop, "test.txt")
- getReadablePath(desktop, "test.txt").should.equal(testPath.replace(/\\/g, "/"))
+ getReadablePath(desktop, "test.txt").should.equal(
+ testPath.replace(/\\/g, "/"),
+ )
})
it("should show relative paths within cwd", () => {
diff --git a/src/utils/path.ts b/src/utils/path.ts
index b61eb38bed..5253126cda 100644
--- a/src/utils/path.ts
+++ b/src/utils/path.ts
@@ -72,7 +72,10 @@ function normalizePath(p: string): string {
let normalized = path.normalize(p)
// however it doesn't remove trailing slashes
// remove trailing slash, except for root paths
- if (normalized.length > 1 && (normalized.endsWith("/") || normalized.endsWith("\\"))) {
+ if (
+ normalized.length > 1 &&
+ (normalized.endsWith("/") || normalized.endsWith("\\"))
+ ) {
normalized = normalized.slice(0, -1)
}
return normalized
diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json
index 4412b1f711..114e269a8f 100644
--- a/webview-ui/package-lock.json
+++ b/webview-ui/package-lock.json
@@ -19,6 +19,7 @@
"debounce": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^7.0.0",
+ "pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-remark": "^2.1.0",
@@ -16061,12 +16062,12 @@
}
},
"node_modules/pretty-bytes": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
- "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
+ "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
"license": "MIT",
"engines": {
- "node": ">=6"
+ "node": "^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -20557,6 +20558,18 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
+ "node_modules/workbox-build/node_modules/pretty-bytes": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/workbox-build/node_modules/source-map": {
"version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
@@ -20730,6 +20743,18 @@
"webpack": "^4.4.0 || ^5.9.0"
}
},
+ "node_modules/workbox-webpack-plugin/node_modules/pretty-bytes": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/workbox-webpack-plugin/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
diff --git a/webview-ui/package.json b/webview-ui/package.json
index cc5beb6397..5f9cfdb767 100644
--- a/webview-ui/package.json
+++ b/webview-ui/package.json
@@ -14,6 +14,7 @@
"debounce": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^7.0.0",
+ "pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-remark": "^2.1.0",
diff --git a/webview-ui/public/index.html b/webview-ui/public/index.html
index bd3562a687..202d93d3ef 100644
--- a/webview-ui/public/index.html
+++ b/webview-ui/public/index.html
@@ -5,7 +5,9 @@
-
+