From 19c56c6ec436f9582dcdc41f8b19e40b4ba48628 Mon Sep 17 00:00:00 2001 From: Evan <58194240+celestial-vault@users.noreply.github.com> Date: Sat, 8 Feb 2025 11:28:10 +0800 Subject: [PATCH 1/6] Attention Is Not What You Need (#1680) * initialize class * wip * wip * cleaning up * update comment * fire n forget error handling * files added * changeset * duplicate path function * cleanup * Use special error type for when cline tries to access clineignore'd file * Rename LLMFileAccessController to ClineIgnoreController * Use better changeset * Remove unnecessary file parsing * Allow access to files outside cwd * Use more efficient clineIgnoreExists * Modify clineignore prompt * Use LOCK_TEXT_SYMBOL * Block commands attempting to access clineignored files * Fix prompt responses * fix tests; make sure .clineignore is ignored * Fix clineignore prompt * Fixes --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> --- .changeset/loud-countries-draw.md | 5 + src/core/Cline.ts | 118 ++++++++--- .../ignore/ClineIgnoreController.test.ts} | 42 ++-- src/core/ignore/ClineIgnoreController.ts | 189 ++++++++++++++++++ src/core/prompts/responses.ts | 35 +++- src/core/prompts/system.ts | 11 +- src/services/glob/list-files.ts | 6 +- .../LLMFileAccessController.ts | 151 -------------- src/services/ripgrep/index.ts | 34 ++-- src/services/tree-sitter/index.ts | 27 ++- src/shared/ExtensionMessage.ts | 1 + webview-ui/src/components/chat/ChatRow.tsx | 41 ++++ 12 files changed, 426 insertions(+), 234 deletions(-) create mode 100644 .changeset/loud-countries-draw.md rename src/{services/llm-access-control/LLMFileAccessController.test.ts => core/ignore/ClineIgnoreController.test.ts} (86%) create mode 100644 src/core/ignore/ClineIgnoreController.ts delete mode 100644 src/services/llm-access-control/LLMFileAccessController.ts diff --git a/.changeset/loud-countries-draw.md b/.changeset/loud-countries-draw.md new file mode 100644 index 0000000000..3a6e55f337 --- /dev/null +++ b/.changeset/loud-countries-draw.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Add .clineignore file to block Cline from accessing specified file patterns \ No newline at end of file diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 6c1239cae3..3c6821f63c 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -9,6 +9,9 @@ import * as path from "path" import { serializeError } from "serialize-error" import * as vscode from "vscode" import { ApiHandler, buildApiHandler } from "../api" +import { OpenAiHandler } from "../api/providers/openai" +import { OpenRouterHandler } from "../api/providers/openrouter" +import { ApiStream } from "../api/transform/stream" import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker" import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider" import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown" @@ -46,20 +49,16 @@ import { HistoryItem } from "../shared/HistoryItem" import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage" import { calculateApiCost } from "../utils/cost" import { fileExistsAtPath } from "../utils/fs" -import { LLMFileAccessController } from "../services/llm-access-control/LLMFileAccessController" import { arePathsEqual, getReadablePath } from "../utils/path" import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string" import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message" import { constructNewFileContent } from "./assistant-message/diff" +import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "./ignore/ClineIgnoreController" import { parseMentions } from "./mentions" import { formatResponse } from "./prompts/responses" -import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider" -import { OpenRouterHandler } from "../api/providers/openrouter" +import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system" import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window" -import { SYSTEM_PROMPT } from "./prompts/system" -import { addUserInstructions } from "./prompts/system" -import { OpenAiHandler } from "../api/providers/openai" -import { ApiStream } from "../api/transform/stream" +import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider" 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 @@ -81,7 +80,7 @@ export class Cline { private chatSettings: ChatSettings apiConversationHistory: Anthropic.MessageParam[] = [] clineMessages: ClineMessage[] = [] - private llmAccessController: LLMFileAccessController + private clineIgnoreController: ClineIgnoreController private askResponse?: ClineAskResponse private askResponseText?: string private askResponseImages?: string[] @@ -125,9 +124,9 @@ export class Cline { images?: string[], historyItem?: HistoryItem, ) { - this.llmAccessController = new LLMFileAccessController(cwd) - this.llmAccessController.initialize().catch((error) => { - console.error("Failed to initialize LLMFileAccessController:", error) + this.clineIgnoreController = new ClineIgnoreController(cwd) + this.clineIgnoreController.initialize().catch((error) => { + console.error("Failed to initialize ClineIgnoreController:", error) }) this.providerRef = new WeakRef(provider) this.api = buildApiHandler(apiConfiguration) @@ -1057,7 +1056,7 @@ export class Cline { this.terminalManager.disposeAll() this.urlContentFetcher.closeBrowser() this.browserSession.closeBrowser() - this.llmAccessController.dispose() + this.clineIgnoreController.dispose() 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 } @@ -1242,9 +1241,15 @@ export class Cline { } } + const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent + let clineIgnoreInstructions: string | undefined + if (clineIgnoreContent) { + clineIgnoreInstructions = `# .clineignore\n\n(The following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${clineIgnoreContent}\n.clineignore` + } + 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, clineIgnoreInstructions) } // 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 @@ -1591,6 +1596,15 @@ export class Cline { // wait so we can determine if it's a new file or editing an existing file break } + + const accessAllowed = this.clineIgnoreController.validateAccess(relPath) + if (!accessAllowed) { + await this.say("clineignore_error", relPath) + pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath))) + await this.saveCheckpoint() + break + } + // Check if file exists using cached map or fs.access let fileExists: boolean if (this.diffViewProvider.editType !== undefined) { @@ -1708,6 +1722,7 @@ export class Cline { await this.saveCheckpoint() break } + this.consecutiveMistakeCount = 0 // if isEditingFile false, that means we have the full contents of the file already. @@ -1859,6 +1874,15 @@ export class Cline { await this.saveCheckpoint() break } + + const accessAllowed = this.clineIgnoreController.validateAccess(relPath) + if (!accessAllowed) { + await this.say("clineignore_error", relPath) + pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath))) + await this.saveCheckpoint() + break + } + this.consecutiveMistakeCount = 0 const absolutePath = path.resolve(cwd, relPath) const completeMessage = JSON.stringify({ @@ -1922,9 +1946,17 @@ export class Cline { 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 result = formatResponse.formatFilesList( + absolutePath, + files, + didHitLimit, + this.clineIgnoreController, + ) const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result, @@ -1981,9 +2013,15 @@ export class Cline { await this.saveCheckpoint() break } + this.consecutiveMistakeCount = 0 + const absolutePath = path.resolve(cwd, relDirPath) - const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath) + const result = await parseSourceCodeForDefinitionsTopLevel( + absolutePath, + this.clineIgnoreController, + ) + const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result, @@ -2051,8 +2089,16 @@ export class Cline { break } this.consecutiveMistakeCount = 0 + const absolutePath = path.resolve(cwd, relDirPath) - const results = await regexSearchFiles(cwd, absolutePath, regex, filePattern) + const results = await regexSearchFiles( + cwd, + absolutePath, + regex, + filePattern, + this.clineIgnoreController, + ) + const completeMessage = JSON.stringify({ ...sharedMessageProps, content: results, @@ -2289,6 +2335,16 @@ export class Cline { } this.consecutiveMistakeCount = 0 + const ignoredFileAttemptedToAccess = this.clineIgnoreController.validateCommand(command) + if (ignoredFileAttemptedToAccess) { + await this.say("clineignore_error", ignoredFileAttemptedToAccess) + pushToolResult( + formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)), + ) + await this.saveCheckpoint() + break + } + let didAutoApprove = false if (!requiresApproval && this.shouldAutoApproveTool(block.name)) { @@ -3192,26 +3248,38 @@ export class Cline { // It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context details += "\n\n# VSCode Visible Files" - const visibleFiles = vscode.window.visibleTextEditors + const visibleFilePaths = vscode.window.visibleTextEditors ?.map((editor) => editor.document?.uri?.fsPath) .filter(Boolean) - .map((absolutePath) => path.relative(cwd, absolutePath).toPosix()) + .map((absolutePath) => path.relative(cwd, absolutePath)) + + // Filter paths through clineIgnoreController + const allowedVisibleFiles = this.clineIgnoreController + .filterPaths(visibleFilePaths) + .map((p) => p.toPosix()) .join("\n") - if (visibleFiles) { - details += `\n${visibleFiles}` + + if (allowedVisibleFiles) { + details += `\n${allowedVisibleFiles}` } else { details += "\n(No visible files)" } details += "\n\n# VSCode Open Tabs" - const openTabs = vscode.window.tabGroups.all + const openTabPaths = vscode.window.tabGroups.all .flatMap((group) => group.tabs) .map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath) .filter(Boolean) - .map((absolutePath) => path.relative(cwd, absolutePath).toPosix()) + .map((absolutePath) => path.relative(cwd, absolutePath)) + + // Filter paths through clineIgnoreController + const allowedOpenTabs = this.clineIgnoreController + .filterPaths(openTabPaths) + .map((p) => p.toPosix()) .join("\n") - if (openTabs) { - details += `\n${openTabs}` + + if (allowedOpenTabs) { + details += `\n${allowedOpenTabs}` } else { details += "\n(No open tabs)" } @@ -3325,7 +3393,7 @@ export class Cline { 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, this.clineIgnoreController) details += result } } diff --git a/src/services/llm-access-control/LLMFileAccessController.test.ts b/src/core/ignore/ClineIgnoreController.test.ts similarity index 86% rename from src/services/llm-access-control/LLMFileAccessController.test.ts rename to src/core/ignore/ClineIgnoreController.test.ts index 8bf6353a48..06f3d212ad 100644 --- a/src/services/llm-access-control/LLMFileAccessController.test.ts +++ b/src/core/ignore/ClineIgnoreController.test.ts @@ -1,13 +1,13 @@ -import { LLMFileAccessController } from "./LLMFileAccessController" +import { ClineIgnoreController } from "./ClineIgnoreController" import fs from "fs/promises" import path from "path" import os from "os" import { after, beforeEach, describe, it } from "mocha" import "should" -describe("LLMFileAccessController", () => { +describe("ClineIgnoreController", () => { let tempDir: string - let controller: LLMFileAccessController + let controller: ClineIgnoreController beforeEach(async () => { // Create a temp directory for testing @@ -22,7 +22,7 @@ describe("LLMFileAccessController", () => { ), ) - controller = new LLMFileAccessController(tempDir) + controller = new ClineIgnoreController(tempDir) await controller.initialize() }) @@ -49,6 +49,11 @@ describe("LLMFileAccessController", () => { ] results.forEach((result) => result.should.be.true()) }) + + it("should block access to .clineignore file", async () => { + const result = controller.validateAccess(".clineignore") + result.should.be.false() + }) }) describe("Custom Patterns", () => { @@ -80,7 +85,7 @@ describe("LLMFileAccessController", () => { ["*.secret", "private/", "*.tmp", "data-*.json", "temp/*"].join("\n"), ) - controller = new LLMFileAccessController(tempDir) + controller = new ClineIgnoreController(tempDir) await controller.initialize() const results = [ @@ -111,7 +116,7 @@ describe("LLMFileAccessController", () => { // ].join("\n"), // ) - // controller = new LLMFileAccessController(tempDir) + // controller = new ClineIgnoreController(tempDir) // const results = [ // // Basic negation @@ -150,7 +155,7 @@ describe("LLMFileAccessController", () => { ["# Comment line", "*.secret", "private/", "temp.*"].join("\n"), ) - controller = new LLMFileAccessController(tempDir) + controller = new ClineIgnoreController(tempDir) await controller.initialize() const result = controller.validateAccess("test.secret") @@ -194,25 +199,6 @@ describe("LLMFileAccessController", () => { const result = controller.validateAccess("src\\file.ts") result.should.be.true() }) - - it("should handle paths outside cwd", async () => { - // Create a path that points to parent directory of cwd - const outsidePath = path.join(path.dirname(tempDir), "outside.txt") - const result = controller.validateAccess(outsidePath) - - // Should return false for security since path is outside cwd - result.should.be.false() - - // Test with a deeply nested path outside cwd - const deepOutsidePath = path.join(path.dirname(tempDir), "deep", "nested", "outside.secret") - const deepResult = controller.validateAccess(deepOutsidePath) - deepResult.should.be.false() - - // Test with a path that tries to escape using ../ - const escapeAttemptPath = path.join(tempDir, "..", "escape-attempt.txt") - const escapeResult = controller.validateAccess(escapeAttemptPath) - escapeResult.should.be.false() - }) }) describe("Batch Filtering", () => { @@ -237,7 +223,7 @@ describe("LLMFileAccessController", () => { await fs.mkdir(emptyDir) try { - const controller = new LLMFileAccessController(emptyDir) + const controller = new ClineIgnoreController(emptyDir) await controller.initialize() const result = controller.validateAccess("file.txt") result.should.be.true() @@ -249,7 +235,7 @@ describe("LLMFileAccessController", () => { it("should handle empty .clineignore", async () => { await fs.writeFile(path.join(tempDir, ".clineignore"), "") - controller = new LLMFileAccessController(tempDir) + controller = new ClineIgnoreController(tempDir) await controller.initialize() const result = controller.validateAccess("regular-file.txt") diff --git a/src/core/ignore/ClineIgnoreController.ts b/src/core/ignore/ClineIgnoreController.ts new file mode 100644 index 0000000000..ebd08788e7 --- /dev/null +++ b/src/core/ignore/ClineIgnoreController.ts @@ -0,0 +1,189 @@ +import path from "path" +import { fileExistsAtPath } from "../../utils/fs" +import fs from "fs/promises" +import ignore, { Ignore } from "ignore" +import * as vscode from "vscode" + +export const LOCK_TEXT_SYMBOL = "\u{1F512}" + +/** + * Controls LLM access to files by enforcing ignore patterns. + * Designed to be instantiated once in Cline.ts and passed to file manipulation services. + * Uses the 'ignore' library to support standard .gitignore syntax in .clineignore files. + */ +export class ClineIgnoreController { + private cwd: string + private ignoreInstance: Ignore + private disposables: vscode.Disposable[] = [] + clineIgnoreContent: string | undefined + + constructor(cwd: string) { + this.cwd = cwd + this.ignoreInstance = ignore() + this.clineIgnoreContent = undefined + // Set up file watcher for .clineignore + this.setupFileWatcher() + } + + /** + * Initialize the controller by loading custom patterns + * Must be called after construction and before using the controller + */ + async initialize(): Promise { + await this.loadClineIgnore() + } + + /** + * Set up the file watcher for .clineignore changes + */ + private setupFileWatcher(): void { + const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore") + const fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern) + + // Watch for changes and updates + this.disposables.push( + fileWatcher.onDidChange(() => { + this.loadClineIgnore() + }), + fileWatcher.onDidCreate(() => { + this.loadClineIgnore() + }), + fileWatcher.onDidDelete(() => { + this.loadClineIgnore() + }), + ) + + // Add fileWatcher itself to disposables + this.disposables.push(fileWatcher) + } + + /** + * Load custom patterns from .clineignore if it exists + */ + private async loadClineIgnore(): Promise { + try { + // Reset ignore instance to prevent duplicate patterns + this.ignoreInstance = ignore() + const ignorePath = path.join(this.cwd, ".clineignore") + if (await fileExistsAtPath(ignorePath)) { + const content = await fs.readFile(ignorePath, "utf8") + this.clineIgnoreContent = content + this.ignoreInstance.add(content) + this.ignoreInstance.add(".clineignore") + } else { + this.clineIgnoreContent = undefined + } + } catch (error) { + // Should never happen: reading file failed even though it exists + console.error("Unexpected error loading .clineignore:", error) + } + } + + /** + * Check if a file should be accessible to the LLM + * @param filePath - Path to check (relative to cwd) + * @returns true if file is accessible, false if ignored + */ + validateAccess(filePath: string): boolean { + // Always allow access if .clineignore does not exist + if (!this.clineIgnoreContent) { + return true + } + try { + // Normalize path to be relative to cwd and use forward slashes + const absolutePath = path.resolve(this.cwd, filePath) + const relativePath = path.relative(this.cwd, absolutePath).toPosix() + + // Ignore expects paths to be path.relative()'d + return !this.ignoreInstance.ignores(relativePath) + } catch (error) { + // console.error(`Error validating access for ${filePath}:`, error) + // Ignore is designed to work with relative file paths, so will throw error for paths outside cwd. We are allowing access to all files outside cwd. + return true + } + } + + /** + * Check if a terminal command should be allowed to execute based on file access patterns + * @param command - Terminal command to validate + * @returns path of file that is being accessed if it is being accessed, undefined if command is allowed + */ + validateCommand(command: string): string | undefined { + // Always allow if no .clineignore exists + if (!this.clineIgnoreContent) { + return undefined + } + + // Split command into parts and get the base command + const parts = command.trim().split(/\s+/) + const baseCommand = parts[0].toLowerCase() + + // Commands that read file contents + const fileReadingCommands = [ + // Unix commands + "cat", + "less", + "more", + "head", + "tail", + "grep", + "awk", + "sed", + // PowerShell commands and aliases + "get-content", + "gc", + "type", + "select-string", + "sls", + ] + + if (fileReadingCommands.includes(baseCommand)) { + // Check each argument that could be a file path + for (let i = 1; i < parts.length; i++) { + const arg = parts[i] + // Skip command flags/options (both Unix and PowerShell style) + if (arg.startsWith("-") || arg.startsWith("/")) { + continue + } + // Ignore PowerShell parameter names + if (arg.includes(":")) { + continue + } + // Validate file access + if (!this.validateAccess(arg)) { + return arg + } + } + } + + return undefined + } + + /** + * Filter an array of paths, removing those that should be ignored + * @param paths - Array of paths to filter (relative to cwd) + * @returns Array of allowed paths + */ + filterPaths(paths: string[]): string[] { + try { + return paths + .map((p) => ({ + path: p, + allowed: this.validateAccess(p), + })) + .filter((x) => x.allowed) + .map((x) => x.path) + } catch (error) { + console.error("Error filtering paths:", error) + return [] // Fail closed for security + } + } + + /** + * Clean up resources when the controller is no longer needed + */ + dispose(): void { + this.disposables.forEach((d) => d.dispose()) + this.disposables = [] + } +} diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index e932f4042c..623e3d8806 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -1,6 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" -import * as path from "path" import * as diff from "diff" +import * as path from "path" +import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController" export const formatResponse = { toolDenied: () => `The user denied this operation.`, @@ -10,6 +11,9 @@ export const formatResponse = { toolError: (error?: string) => `The tool execution failed with the following error:\n\n${error}\n`, + clineIgnoreError: (path: string) => + `Access to ${path} is blocked by the .clineignore file settings. You must try to continue in the task without using this file, or ask the user to update the .clineignore file.`, + noToolsUsed: () => `[ERROR] You did not use a tool in your previous response! Please retry with a tool use. @@ -46,7 +50,12 @@ 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, + clineIgnoreController?: ClineIgnoreController, + ): string => { const sorted = files .map((file) => { // convert absolute path to relative path @@ -77,14 +86,30 @@ Otherwise, if you have not completed the task and do not need additional informa // the shorter one comes first return aParts.length - bParts.length }) + + const clineIgnoreParsed = clineIgnoreController + ? sorted.map((filePath) => { + // path is relative to absolute path, not cwd + // validateAccess expects either path relative to cwd or absolute path + // otherwise, for validating against ignore patterns like "assets/icons", we would end up with just "icons", which would result in the path not being ignored. + const absoluteFilePath = path.resolve(absolutePath, filePath) + const isIgnored = !clineIgnoreController.validateAccess(absoluteFilePath) + if (isIgnored) { + return LOCK_TEXT_SYMBOL + " " + filePath + } + + return filePath + }) + : sorted + if (didHitLimit) { - return `${sorted.join( + return `${clineIgnoreParsed.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 (clineIgnoreParsed.length === 0 || (clineIgnoreParsed.length === 1 && clineIgnoreParsed[0] === "")) { return "No files found." } else { - return sorted.join("\n") + return clineIgnoreParsed.join("\n") } }, diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index a043189438..a9a13f1e90 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -957,13 +957,20 @@ 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, + clineIgnoreInstructions?: string, +) { let customInstructions = "" if (settingsCustomInstructions) { customInstructions += settingsCustomInstructions + "\n\n" } if (clineRulesFileInstructions) { - customInstructions += clineRulesFileInstructions + customInstructions += clineRulesFileInstructions + "\n\n" + } + if (clineIgnoreInstructions) { + customInstructions += clineIgnoreInstructions } return ` diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 8578b914d7..745ac60d4d 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -45,9 +45,11 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb ignore: recursive ? dirsToIgnore : undefined, // just in case there is no gitignore, we ignore sensible defaults 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) - return [files, files.length >= limit] + const filePaths = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit) + + return [filePaths, filePaths.length >= limit] } /* diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts deleted file mode 100644 index 40a2e46b55..0000000000 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ /dev/null @@ -1,151 +0,0 @@ -import path from "path" -import { fileExistsAtPath } from "../../utils/fs" -import fs from "fs/promises" -import ignore, { Ignore } from "ignore" -import * as vscode from "vscode" - -/** - * Controls LLM access to files by enforcing ignore patterns. - * Designed to be instantiated once in Cline.ts and passed to file manipulation services. - * Uses the 'ignore' library to support standard .gitignore syntax in .clineignore files. - */ -export class LLMFileAccessController { - private cwd: string - private ignoreInstance: Ignore - private fileWatcher: vscode.FileSystemWatcher | null - private disposables: vscode.Disposable[] = [] - - /** - * Default patterns that are always ignored for security - */ - private static readonly DEFAULT_PATTERNS = [] // empty for now - - constructor(cwd: string) { - this.cwd = cwd - this.ignoreInstance = ignore() - this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) - this.fileWatcher = null - - // Set up file watcher for .clineignore - this.setupFileWatcher() - } - - /** - * Initialize the controller by loading custom patterns - * Must be called after construction and before using the controller - */ - async initialize(): Promise { - await this.loadCustomPatterns() - } - - /** - * Set up the file watcher for .clineignore changes - */ - private setupFileWatcher(): void { - const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore") - this.fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern) - - // Watch for changes and updates - this.disposables.push( - this.fileWatcher.onDidChange(() => { - this.loadCustomPatterns().catch((error) => { - console.error("Failed to load updated .clineignore patterns:", error) - }) - }), - this.fileWatcher.onDidCreate(() => { - this.loadCustomPatterns().catch((error) => { - console.error("Failed to load new .clineignore patterns:", error) - }) - }), - this.fileWatcher.onDidDelete(() => { - this.resetToDefaultPatterns() - }), - ) - - // Add fileWatcher itself to disposables - this.disposables.push(this.fileWatcher) - } - - /** - * Load custom patterns from .clineignore if it exists - */ - private async loadCustomPatterns(): Promise { - try { - const ignorePath = path.join(this.cwd, ".clineignore") - if (await fileExistsAtPath(ignorePath)) { - // Reset ignore instance to prevent duplicate patterns - this.resetToDefaultPatterns() - const content = await fs.readFile(ignorePath, "utf8") - const customPatterns = content - .split("\n") - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#")) - - this.ignoreInstance.add(customPatterns) - } - } catch (error) { - // Continue with default patterns - } - } - - /** - * Reset ignore patterns to defaults - */ - private resetToDefaultPatterns(): void { - this.ignoreInstance = ignore() - this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) - } - - /** - * Check if a file should be accessible to the LLM - * @param filePath - Path to check (relative to cwd) - * @returns true if file is accessible, false if ignored - */ - validateAccess(filePath: string): boolean { - try { - // Normalize path to be relative to cwd and use forward slashes - const absolutePath = path.resolve(this.cwd, filePath) - const relativePath = path.relative(this.cwd, absolutePath).replace(/\\/g, "/") - - // Block access to paths outside cwd (those starting with '..') - if (relativePath.startsWith("..")) { - return false - } - - // Use ignore library to check if path should be ignored - return !this.ignoreInstance.ignores(relativePath) - } catch (error) { - console.error(`Error validating access for ${filePath}:`, error) - return false // Fail closed for security - } - } - - /** - * Filter an array of paths, removing those that should be ignored - * @param paths - Array of paths to filter (relative to cwd) - * @returns Array of allowed paths - */ - filterPaths(paths: string[]): string[] { - try { - return paths - .map((p) => ({ - path: p, - allowed: this.validateAccess(p), - })) - .filter((x) => x.allowed) - .map((x) => x.path) - } catch (error) { - console.error("Error filtering paths:", error) - return [] // Fail closed for security - } - } - - /** - * Clean up resources when the controller is no longer needed - */ - dispose(): void { - this.disposables.forEach((d) => d.dispose()) - this.disposables = [] - this.fileWatcher = null - } -} diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index b9ebe68c77..21d3d9d0df 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -1,8 +1,9 @@ import * as vscode from "vscode" import * as childProcess from "child_process" import * as path from "path" -import * as fs from "fs" import * as readline from "readline" +import { fileExistsAtPath } from "../../utils/fs" +import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController" /* This file provides functionality to perform regex searches on files using ripgrep. @@ -50,7 +51,7 @@ const isWindows = /^win/.test(process.platform) const binName = isWindows ? "rg.exe" : "rg" interface SearchResult { - file: string + filePath: string line: number column: number match: string @@ -63,7 +64,7 @@ const MAX_RESULTS = 300 async function getBinPath(vscodeAppRoot: string): Promise { const checkPath = async (pkgFolder: string) => { const fullPath = path.join(vscodeAppRoot, pkgFolder, binName) - return (await pathExists(fullPath)) ? fullPath : undefined + return (await fileExistsAtPath(fullPath)) ? fullPath : undefined } return ( @@ -74,14 +75,6 @@ async function getBinPath(vscodeAppRoot: string): Promise { ) } -async function pathExists(path: string): Promise { - return new Promise((resolve) => { - fs.access(path, (err) => { - resolve(err === null) - }) - }) -} - async function execRipgrep(bin: string, args: string[]): Promise { return new Promise((resolve, reject) => { const rgProcess = childProcess.spawn(bin, args) @@ -122,7 +115,13 @@ async function execRipgrep(bin: string, args: string[]): Promise { }) } -export async function regexSearchFiles(cwd: string, directoryPath: string, regex: string, filePattern?: string): Promise { +export async function regexSearchFiles( + cwd: string, + directoryPath: string, + regex: string, + filePattern?: string, + clineIgnoreController?: ClineIgnoreController, +): Promise { const vscodeAppRoot = vscode.env.appRoot const rgPath = await getBinPath(vscodeAppRoot) @@ -150,7 +149,7 @@ export async function regexSearchFiles(cwd: string, directoryPath: string, regex results.push(currentResult as SearchResult) } currentResult = { - file: parsed.data.path.text, + filePath: parsed.data.path.text, line: parsed.data.line_number, column: parsed.data.submatches[0].start, match: parsed.data.lines.text, @@ -174,7 +173,12 @@ export async function regexSearchFiles(cwd: string, directoryPath: string, regex results.push(currentResult as SearchResult) } - return formatResults(results, cwd) + // Filter results using ClineIgnoreController if provided + const filteredResults = clineIgnoreController + ? results.filter((result) => clineIgnoreController.validateAccess(result.filePath)) + : results + + return formatResults(filteredResults, cwd) } function formatResults(results: SearchResult[], cwd: string): string { @@ -189,7 +193,7 @@ function formatResults(results: SearchResult[], cwd: string): string { // Group results by file name results.slice(0, MAX_RESULTS).forEach((result) => { - const relativeFilePath = path.relative(cwd, result.file) + const relativeFilePath = path.relative(cwd, result.filePath) if (!groupedResults[relativeFilePath]) { groupedResults[relativeFilePath] = [] } diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index 19d0234f01..262e3cd5cc 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -3,9 +3,13 @@ import * as path from "path" import { listFiles } from "../glob/list-files" import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser" import { fileExistsAtPath } from "../../utils/fs" +import { ClineIgnoreController } from "../../core/ignore/ClineIgnoreController" // 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, + clineIgnoreController?: ClineIgnoreController, +): Promise { // check if the path exists const dirExists = await fileExistsAtPath(path.resolve(dirPath)) if (!dirExists) { @@ -24,10 +28,14 @@ export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Pr // Parse specific files we have language parsers for // const filesWithoutDefinitions: string[] = [] - for (const file of filesToParse) { - const definitions = await parseFile(file, languageParsers) + + // Filter filepaths for access if controller is provided + const allowedFilesToParse = clineIgnoreController ? clineIgnoreController.filterPaths(filesToParse) : filesToParse + + for (const filePath of allowedFilesToParse) { + const definitions = await parseFile(filePath, languageParsers, clineIgnoreController) if (definitions) { - result += `${path.relative(dirPath, file).toPosix()}\n${definitions}\n` + result += `${path.relative(dirPath, filePath).toPosix()}\n${definitions}\n` } // else { // filesWithoutDefinitions.push(file) @@ -98,7 +106,14 @@ 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, + clineIgnoreController?: ClineIgnoreController, +): Promise { + if (clineIgnoreController && !clineIgnoreController.validateAccess(filePath)) { + return null + } const fileContent = await fs.readFile(filePath, "utf8") const ext = path.extname(filePath).toLowerCase().slice(1) @@ -159,5 +174,5 @@ async function parseFile(filePath: string, languageParsers: LanguageParser): Pro if (formattedOutput.length > 0) { return `|----\n${formattedOutput}|----\n` } - return undefined + return null } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 5a93caf07b..7f03c8e230 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -121,6 +121,7 @@ export type ClineSay = | "use_mcp_server" | "diff_error" | "deleted_api_reqs" + | "clineignore_error" export interface ClineSayTool { tool: diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 59d40936f1..7137851eb5 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -989,6 +989,47 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi ) + case "clineignore_error": + return ( + <> +
+
+ + + Access Denied + +
+
+ Cline tried to access {message.text} which is blocked by the{" "} + .clineignore + file. +
+
+ + ) case "completion_result": const hasChanges = message.text?.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false const text = hasChanges ? message.text?.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text From 134020d51b58c8f28c61f313c4a2ceb60b284242 Mon Sep 17 00:00:00 2001 From: brownrw8 Date: Fri, 7 Feb 2025 17:29:54 -1000 Subject: [PATCH 2/6] Add a keyboard shortcut to switch between plan and act mode (#1626) * feat: add keyboard shortcut to Plan/Act toggle * remove Shift * fix: shortcut now Meta+Shift+a * ENG-123: Tooltip * feat: tooltip and platform detection * fix:impl suggestions * fix: add changeset * Fix style * remove platform detection - use metaKey detection and os utils * missed comma * Fixes * Fixes --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> --- .changeset/tricky-rats-drum.md | 5 ++ src/core/webview/ClineProvider.ts | 3 +- src/shared/ExtensionMessage.ts | 5 ++ .../src/components/chat/ChatTextArea.tsx | 24 ++++-- webview-ui/src/components/common/Tooltip.tsx | 60 +++++++++++++ .../src/context/ExtensionStateContext.tsx | 3 +- webview-ui/src/utils/__tests__/hooks.spec.ts | 64 ++++++++++++++ .../src/utils/__tests__/platformUtils.spec.ts | 24 ++++++ webview-ui/src/utils/hooks.ts | 86 +++++++++++++++++++ webview-ui/src/utils/platformUtils.ts | 36 ++++++++ 10 files changed, 301 insertions(+), 9 deletions(-) create mode 100644 .changeset/tricky-rats-drum.md create mode 100644 webview-ui/src/components/common/Tooltip.tsx create mode 100644 webview-ui/src/utils/__tests__/hooks.spec.ts create mode 100644 webview-ui/src/utils/__tests__/platformUtils.spec.ts create mode 100644 webview-ui/src/utils/hooks.ts create mode 100644 webview-ui/src/utils/platformUtils.ts diff --git a/.changeset/tricky-rats-drum.md b/.changeset/tricky-rats-drum.md new file mode 100644 index 0000000000..cabf8947e3 --- /dev/null +++ b/.changeset/tricky-rats-drum.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Feature: Added keyboard shortcut + tooltips for Plan/Act toggle diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 0f4758ff92..d55851d171 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -17,7 +17,7 @@ import { McpHub } from "../../services/mcp/McpHub" import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager" import { ApiProvider, ModelInfo } from "../../shared/api" import { findLast } from "../../shared/array" -import { ExtensionMessage, ExtensionState } from "../../shared/ExtensionMessage" +import { ExtensionMessage, ExtensionState, Platform } from "../../shared/ExtensionMessage" import { HistoryItem } from "../../shared/HistoryItem" import { ClineCheckpointRestore, WebviewMessage } from "../../shared/WebviewMessage" import { fileExistsAtPath } from "../../utils/fs" @@ -1306,6 +1306,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { clineMessages: this.cline?.clineMessages || [], taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts), shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, + platform: process.platform as Platform, autoApprovalSettings, browserSettings, chatSettings, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 7f03c8e230..c08d611eda 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -48,6 +48,10 @@ export interface ExtensionMessage { mcpServers?: McpServer[] } +export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown" + +export const DEFAULT_PLATFORM = "unknown" + export interface ExtensionState { version: string apiConfiguration?: ApiConfiguration @@ -62,6 +66,7 @@ export interface ExtensionState { browserSettings: BrowserSettings chatSettings: ChatSettings isLoggedIn: boolean + platform: Platform userInfo?: { displayName: string | null email: string | null diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index fa8584f21f..baae8e682f 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -19,6 +19,9 @@ import Thumbnails from "../common/Thumbnails" import ApiOptions, { normalizeApiConfiguration } from "../settings/ApiOptions" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" +import { useShortcut } from "../../utils/hooks" +import Tooltip from "../common/Tooltip" +import { useMetaKeyDetection } from "../../utils/hooks" interface ChatTextAreaProps { inputValue: string @@ -210,7 +213,7 @@ const ChatTextArea = forwardRef( }, ref, ) => { - const { filePaths, chatSettings, apiConfiguration, openRouterModels } = useExtensionState() + const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState() const [isTextAreaFocused, setIsTextAreaFocused] = useState(false) const [thumbnailsHeight, setThumbnailsHeight] = useState(0) const [textAreaBaseHeight, setTextAreaBaseHeight] = useState(undefined) @@ -232,6 +235,8 @@ const ChatTextArea = forwardRef( const [arrowPosition, setArrowPosition] = useState(0) const [menuPosition, setMenuPosition] = useState(0) + const [, metaKeyChar] = useMetaKeyDetection(platform) + // Add a ref to track previous menu state const prevShowModelSelector = useRef(showModelSelector) @@ -619,6 +624,8 @@ const ChatTextArea = forwardRef( }, changeModeDelay) }, [chatSettings.mode, showModelSelector, submitApiConfig]) + useShortcut("Meta+Shift+a", onModeToggle, { disableTextInputs: false }) // important that we don't disable the text input here + const handleContextButtonClick = useCallback(() => { if (textAreaDisabled) return @@ -1067,12 +1074,15 @@ const ChatTextArea = forwardRef( )} - - - - Plan - Act - + + + + Plan + Act + + ) diff --git a/webview-ui/src/components/common/Tooltip.tsx b/webview-ui/src/components/common/Tooltip.tsx new file mode 100644 index 0000000000..de3ee05908 --- /dev/null +++ b/webview-ui/src/components/common/Tooltip.tsx @@ -0,0 +1,60 @@ +import React, { useState } from "react" +import styled from "styled-components" +import { + getAsVar, + VSC_DESCRIPTION_FOREGROUND, + VSC_SIDEBAR_BACKGROUND, + VSC_INPUT_PLACEHOLDER_FOREGROUND, + VSC_INPUT_BORDER, +} from "../../utils/vscStyles" + +interface TooltipProps { + hintText: string + tipText: string + children: React.ReactNode +} + +// add styled component for tooltip +const TooltipBody = styled.div` + position: absolute; + background-color: ${getAsVar(VSC_SIDEBAR_BACKGROUND)}; + color: ${getAsVar(VSC_DESCRIPTION_FOREGROUND)}; + padding: 5px; + border-radius: 5px; + bottom: 100%; + left: -180%; + z-index: 10; + white-space: wrap; + max-width: 200px; + border: 1px solid ${getAsVar(VSC_INPUT_BORDER)}; + pointer-events: none; + font-size: 0.9em; +` + +const Hint = styled.div` + font-size: 0.8em; + color: ${getAsVar(VSC_INPUT_PLACEHOLDER_FOREGROUND)}; + opacity: 0.8; + margin-top: 2px; +` + +const Tooltip: React.FC = ({ tipText, hintText, children }) => { + const [visible, setVisible] = useState(false) + + const showTooltip = () => setVisible(true) + const hideTooltip = () => setVisible(false) + + return ( +
+ {children} + {visible && ( + + {tipText} + {hintText && {hintText}} + + )} +
+ ) +} + +export default Tooltip diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index a02b6f121a..deb840c1eb 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -1,7 +1,7 @@ import React, { createContext, useCallback, useContext, useEffect, useState } from "react" import { useEvent } from "react-use" import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../../src/shared/AutoApprovalSettings" -import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage" +import { ExtensionMessage, ExtensionState, DEFAULT_PLATFORM } from "../../../src/shared/ExtensionMessage" import { ApiConfiguration, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../../src/shared/api" import { findLastIndex } from "../../../src/shared/array" import { McpServer } from "../../../src/shared/mcp" @@ -37,6 +37,7 @@ export const ExtensionStateContextProvider: React.FC<{ browserSettings: DEFAULT_BROWSER_SETTINGS, chatSettings: DEFAULT_CHAT_SETTINGS, isLoggedIn: false, + platform: DEFAULT_PLATFORM, }) const [didHydrateState, setDidHydrateState] = useState(false) const [showWelcome, setShowWelcome] = useState(false) diff --git a/webview-ui/src/utils/__tests__/hooks.spec.ts b/webview-ui/src/utils/__tests__/hooks.spec.ts new file mode 100644 index 0000000000..c613db72b3 --- /dev/null +++ b/webview-ui/src/utils/__tests__/hooks.spec.ts @@ -0,0 +1,64 @@ +import { renderHook } from "@testing-library/react" +import { useShortcut, useMetaKeyDetection } from "../hooks" +import { vi } from "vitest" + +describe("useShortcut", () => { + it("should call the callback when the shortcut is pressed", () => { + const callback = vi.fn() + renderHook(() => useShortcut("Meta+Shift+a", callback)) + + const event = new KeyboardEvent("keydown", { key: "a", metaKey: true, shiftKey: true }) + window.dispatchEvent(event) + + expect(callback).toHaveBeenCalled() + }) + + it("should not call the callback when the shortcut is not pressed", () => { + const callback = vi.fn() + renderHook(() => useShortcut("Command+Shift+b", callback)) + + const event = new KeyboardEvent("keydown", { key: "a", metaKey: true, shiftKey: true }) + window.dispatchEvent(event) + + expect(callback).not.toHaveBeenCalled() + }) + + it("should not call the callback when typing in a text input when disableTextInputs is true", () => { + const callback = vi.fn() + renderHook(() => useShortcut("Meta+Shift+a", callback, { disableTextInputs: true })) + + const input = document.createElement("input") + document.body.appendChild(input) + input.focus() + + const event = new KeyboardEvent("keydown", { key: "a", metaKey: true, shiftKey: true }) + input.dispatchEvent(event) + + expect(callback).not.toHaveBeenCalled() + + document.body.removeChild(input) + }) +}) + +describe("useMetaKeyDetection", () => { + it("should detect Windows OS and metaKey from platform", () => { + // mock the detect functions + const { result } = renderHook(() => useMetaKeyDetection("win32")) + expect(result.current[0]).toBe("windows") + expect(result.current[1]).toBe("⊞ Win") + }) + + it("should detect Mac OS and metaKey from platform", () => { + // mock the detect functions + const { result } = renderHook(() => useMetaKeyDetection("darwin")) + expect(result.current[0]).toBe("mac") + expect(result.current[1]).toBe("⌘ Command") + }) + + it("should detect Linux OS and metaKey from platform", () => { + // mock the detect functions + const { result } = renderHook(() => useMetaKeyDetection("linux")) + expect(result.current[0]).toBe("linux") + expect(result.current[1]).toBe("Alt") + }) +}) diff --git a/webview-ui/src/utils/__tests__/platformUtils.spec.ts b/webview-ui/src/utils/__tests__/platformUtils.spec.ts new file mode 100644 index 0000000000..9ec19ba3d0 --- /dev/null +++ b/webview-ui/src/utils/__tests__/platformUtils.spec.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest" +import { detectMetaKeyChar } from "../platformUtils" + +describe("detectMetaKeyChar", () => { + it("should return ⌘ Command for darwin platform", () => { + const result = detectMetaKeyChar("darwin") + expect(result).toBe("⌘ Command") + }) + + it("should return ⊞ Win for win32 platform", () => { + const result = detectMetaKeyChar("win32") + expect(result).toBe("⊞ Win") + }) + + it("should return Alt for linux platform", () => { + const result = detectMetaKeyChar("linux") + expect(result).toBe("Alt") + }) + + it("should return generic CMD for unknown platform", () => { + const result = detectMetaKeyChar("somethingelse") + expect(result).toBe("CMD") + }) +}) diff --git a/webview-ui/src/utils/hooks.ts b/webview-ui/src/utils/hooks.ts new file mode 100644 index 0000000000..18100c07e3 --- /dev/null +++ b/webview-ui/src/utils/hooks.ts @@ -0,0 +1,86 @@ +import { useCallback, useRef, useLayoutEffect, useState, useEffect } from "react" +import { detectMetaKeyChar, detectOS, unknown } from "./platformUtils" + +export const useMetaKeyDetection = (platform: string) => { + const [metaKeyChar, setMetaKeyChar] = useState(unknown) + const [os, setOs] = useState(unknown) + + useEffect(() => { + const detectedMetaKeyChar = detectMetaKeyChar(platform) + const detectedOs = detectOS(platform) + setMetaKeyChar(detectedMetaKeyChar) + setOs(detectedOs) + }, [platform]) + + return [os, metaKeyChar] +} + +export const useShortcut = (shortcut: string, callback: any, options = { disableTextInputs: true }) => { + const callbackRef = useRef(callback) + const [keyCombo, setKeyCombo] = useState([]) + + useLayoutEffect(() => { + callbackRef.current = callback + }) + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + const isTextInput = + event.target instanceof HTMLTextAreaElement || + (event.target instanceof HTMLInputElement && (!event.target.type || event.target.type === "text")) || + (event.target as HTMLElement).isContentEditable + + const modifierMap: { [key: string]: boolean } = { + Control: event.ctrlKey, + Alt: event.altKey, + Meta: event.metaKey, // alias for Command + Shift: event.shiftKey, + } + + if (event.repeat) { + return null + } + + if (options.disableTextInputs && isTextInput) { + return event.stopPropagation() + } + + if (shortcut.includes("+")) { + const keyArray = shortcut.split("+") + + if (Object.keys(modifierMap).includes(keyArray[0])) { + const finalKey = keyArray.pop() + + if (keyArray.every((k) => modifierMap[k]) && finalKey === event.key) { + return callbackRef.current(event) + } + } else { + if (keyArray[keyCombo.length] === event.key) { + if (keyArray[keyArray.length - 1] === event.key && keyCombo.length === keyArray.length - 1) { + callbackRef.current(event) + return setKeyCombo([]) + } + + return setKeyCombo((prevCombo) => [...prevCombo, event.key]) + } + if (keyCombo.length > 0) { + return setKeyCombo([]) + } + } + } + + if (shortcut === event.key) { + return callbackRef.current(event) + } + }, + [keyCombo.length, options.disableTextInputs, shortcut], + ) + + useEffect(() => { + window.addEventListener("keydown", handleKeyDown) + + return () => { + window.removeEventListener("keydown", handleKeyDown) + } + }, [handleKeyDown]) +} diff --git a/webview-ui/src/utils/platformUtils.ts b/webview-ui/src/utils/platformUtils.ts new file mode 100644 index 0000000000..6016268f13 --- /dev/null +++ b/webview-ui/src/utils/platformUtils.ts @@ -0,0 +1,36 @@ +export interface NavigatorUAData { + platform: string + brands: { brand: string; version: string }[] +} + +export const unknown = "Unknown" + +const platforms = { + windows: /win32/, + mac: /darwin/, + linux: /linux/, +} + +export const detectOS = (platform: string) => { + let detectedOs = unknown + if (platform.match(platforms.windows)) { + detectedOs = "windows" + } else if (platform.match(platforms.mac)) { + detectedOs = "mac" + } else if (platform.match(platforms.linux)) { + detectedOs = "linux" + } + return detectedOs +} + +export const detectMetaKeyChar = (platform: string) => { + if (platform.match(platforms.mac)) { + return "CMD" + } else if (platform.match(platforms.windows)) { + return "Win" + } else if (platform.match(platforms.linux)) { + return "Alt" + } else { + return "CMD" + } +} From 887456ead8a7743db8cbaa9348266d6753c55448 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 20:52:51 -0800 Subject: [PATCH 3/6] Fix bug where new files won't show up in files dropdown (#1704) * Fix bug where new files won't show up in files dropdown * Create small-gifts-count.md --- .changeset/small-gifts-count.md | 5 +++++ src/core/Cline.ts | 9 +++++++++ src/core/webview/ClineProvider.ts | 4 ++-- src/integrations/workspace/WorkspaceTracker.ts | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 .changeset/small-gifts-count.md diff --git a/.changeset/small-gifts-count.md b/.changeset/small-gifts-count.md new file mode 100644 index 0000000000..e5b5e523a9 --- /dev/null +++ b/.changeset/small-gifts-count.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Fix bug where new files won't show up in files dropdown diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 3c6821f63c..9f5af06f3f 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1835,6 +1835,11 @@ export class Cline { `${newProblemsMessage}`, ) } + + if (!fileExists) { + this.providerRef.deref()?.workspaceTracker?.populateFilePaths() + } + await this.diffViewProvider.reset() await this.saveCheckpoint() break @@ -2387,6 +2392,10 @@ export class Cline { if (userRejected) { this.didRejectTool = true } + + // Re-populate file paths in case the command modified the workspace (vscode listeners do not trigger unless the user manually creates/deletes files) + this.providerRef.deref()?.workspaceTracker?.populateFilePaths() + pushToolResult(result) await this.saveCheckpoint() break diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d55851d171..397e11eb80 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -102,7 +102,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { private disposables: vscode.Disposable[] = [] private view?: vscode.WebviewView | vscode.WebviewPanel private cline?: Cline - private workspaceTracker?: WorkspaceTracker + workspaceTracker?: WorkspaceTracker mcpHub?: McpHub private authManager: FirebaseAuthManager private latestAnnouncementId = "jan-20-2025" // update to some unique identifier when we add a new announcement @@ -379,7 +379,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { switch (message.type) { case "webviewDidLaunch": this.postStateToWebview() - this.workspaceTracker?.initializeFilePaths() // don't await + this.workspaceTracker?.populateFilePaths() // don't await getTheme().then((theme) => this.postMessageToWebview({ type: "theme", diff --git a/src/integrations/workspace/WorkspaceTracker.ts b/src/integrations/workspace/WorkspaceTracker.ts index 10dfac8f9e..e148312cba 100644 --- a/src/integrations/workspace/WorkspaceTracker.ts +++ b/src/integrations/workspace/WorkspaceTracker.ts @@ -16,7 +16,7 @@ class WorkspaceTracker { this.registerListeners() } - async initializeFilePaths() { + async populateFilePaths() { // should not auto get filepaths for desktop since it would immediately show permission popup before cline ever creates a file if (!cwd) { return From c1a72768673dbfcdf7c3fe50e5798a3f36e4fad5 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:22:55 -0800 Subject: [PATCH 4/6] Prepare for release --- .changeset/big-plums-wave.md | 5 - .changeset/breezy-bobcats-change.md | 5 - .changeset/dry-socks-talk.md | 5 - .changeset/green-oranges-sit.md | 5 - .changeset/little-pianos-juggle.md | 5 - .changeset/loud-countries-draw.md | 5 - .changeset/modern-knives-tan.md | 5 - .changeset/neat-apricots-search.md | 5 - .changeset/purple-panthers-arrive.md | 5 - .changeset/small-gifts-count.md | 5 - .changeset/tasty-readers-move.md | 5 - .changeset/tricky-rats-drum.md | 5 - CHANGELOG.md | 290 ++++++++++++++------------- package.json | 2 +- 14 files changed, 152 insertions(+), 200 deletions(-) delete mode 100644 .changeset/big-plums-wave.md delete mode 100644 .changeset/breezy-bobcats-change.md delete mode 100644 .changeset/dry-socks-talk.md delete mode 100644 .changeset/green-oranges-sit.md delete mode 100644 .changeset/little-pianos-juggle.md delete mode 100644 .changeset/loud-countries-draw.md delete mode 100644 .changeset/modern-knives-tan.md delete mode 100644 .changeset/neat-apricots-search.md delete mode 100644 .changeset/purple-panthers-arrive.md delete mode 100644 .changeset/small-gifts-count.md delete mode 100644 .changeset/tasty-readers-move.md delete mode 100644 .changeset/tricky-rats-drum.md diff --git a/.changeset/big-plums-wave.md b/.changeset/big-plums-wave.md deleted file mode 100644 index a3c836e45f..0000000000 --- a/.changeset/big-plums-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Adding Requesty API Provider diff --git a/.changeset/breezy-bobcats-change.md b/.changeset/breezy-bobcats-change.md deleted file mode 100644 index 0a99f60a05..0000000000 --- a/.changeset/breezy-bobcats-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Change default OpenRouter model to anthropic/claude-3.5-sonnet diff --git a/.changeset/dry-socks-talk.md b/.changeset/dry-socks-talk.md deleted file mode 100644 index df4995488e..0000000000 --- a/.changeset/dry-socks-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Add Alibaba qwen models plus/max/coder-plus/turbo diff --git a/.changeset/green-oranges-sit.md b/.changeset/green-oranges-sit.md deleted file mode 100644 index 4d2bfebfd4..0000000000 --- a/.changeset/green-oranges-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Add Together API Provider diff --git a/.changeset/little-pianos-juggle.md b/.changeset/little-pianos-juggle.md deleted file mode 100644 index 02457339e9..0000000000 --- a/.changeset/little-pianos-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Adding reasoning_effort support for openrouter and openai-native diff --git a/.changeset/loud-countries-draw.md b/.changeset/loud-countries-draw.md deleted file mode 100644 index 3a6e55f337..0000000000 --- a/.changeset/loud-countries-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Add .clineignore file to block Cline from accessing specified file patterns \ No newline at end of file diff --git a/.changeset/modern-knives-tan.md b/.changeset/modern-knives-tan.md deleted file mode 100644 index d2aaa250b3..0000000000 --- a/.changeset/modern-knives-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Add automatic retry for rate limited requests diff --git a/.changeset/neat-apricots-search.md b/.changeset/neat-apricots-search.md deleted file mode 100644 index 46ce78fc0f..0000000000 --- a/.changeset/neat-apricots-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Update README.md to include Getting Started diff --git a/.changeset/purple-panthers-arrive.md b/.changeset/purple-panthers-arrive.md deleted file mode 100644 index c4be31f613..0000000000 --- a/.changeset/purple-panthers-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Fix a bug where we were not properly checking for changesets in check-changeset git action diff --git a/.changeset/small-gifts-count.md b/.changeset/small-gifts-count.md deleted file mode 100644 index e5b5e523a9..0000000000 --- a/.changeset/small-gifts-count.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Fix bug where new files won't show up in files dropdown diff --git a/.changeset/tasty-readers-move.md b/.changeset/tasty-readers-move.md deleted file mode 100644 index 3d0db43570..0000000000 --- a/.changeset/tasty-readers-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Added support for AWS provider profiles using the AWS CLI to make the profile. enabling long lived connections to AWS bedrock diff --git a/.changeset/tricky-rats-drum.md b/.changeset/tricky-rats-drum.md deleted file mode 100644 index cabf8947e3..0000000000 --- a/.changeset/tricky-rats-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Feature: Added keyboard shortcut + tooltips for Plan/Act toggle diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c6a59c8f..2a38d09639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,365 +1,377 @@ # Changelog +## [3.3.0] + +- Add .clineignore to block Cline from accessing specified file patterns +- Add keyboard shortcut + tooltips for Plan/Act toggle +- Fix bug where new files won't show up in files dropdown +- Add automatic retry for rate limited requests (thanks @ViezeVingertjes!) +- Adding reasoning_effort support for o3-mini in Advanced Settings +- Added support for AWS provider profiles using the AWS CLI to make the profile, enabling long lived connections to AWS bedrock +- Adding Requesty API provider +- Add Together API provider +- Add Alibaba Qwen API provider (thanks @aicccode!) + ## [3.2.13] -- Add new gemini models gemini-2.0-flash-lite-preview-02-05 and gemini-2.0-flash-001 -- Add all available Mistral API models (thanks @ViezeVingertjes!) -- Add LiteLLM API provider support (thanks @him0!) +- Add new gemini models gemini-2.0-flash-lite-preview-02-05 and gemini-2.0-flash-001 +- Add all available Mistral API models (thanks @ViezeVingertjes!) +- Add LiteLLM API provider support (thanks @him0!) ## [3.2.12] -- Fix command chaining for Windows users -- Fix reasoning_content error for OpenAI providers +- Fix command chaining for Windows users +- Fix reasoning_content error for OpenAI providers ## [3.2.11] -- Add OpenAI o3-mini model +- Add OpenAI o3-mini model ## [3.2.10] -- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct -- Show Reasoning tokens for models that support it -- Fix issues with switching models between Plan/Act modes +- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct +- Show Reasoning tokens for models that support it +- Fix issues with switching models between Plan/Act modes ## [3.2.6] -- Save last used API/model when switching between Plan and Act, for users that like to use different models for each mode -- New Context Window progress bar in the task header to understand increased cost/generation degradation as the context increases -- Localize READMEs and add language selector for English, Spanish, German, Chinese, and Japanese -- Add Advanced Settings to remove MCP prompts from requests to save tokens, enable/disable checkpoints for users that don't use git (more coming soon!) -- Add Gemini 2.0 Flash Thinking experimental model -- Allow new users to subscribe to mailing list to get notified when new Accounts option is available +- Save last used API/model when switching between Plan and Act, for users that like to use different models for each mode +- New Context Window progress bar in the task header to understand increased cost/generation degradation as the context increases +- Localize READMEs and add language selector for English, Spanish, German, Chinese, and Japanese +- Add Advanced Settings to remove MCP prompts from requests to save tokens, enable/disable checkpoints for users that don't use git (more coming soon!) +- Add Gemini 2.0 Flash Thinking experimental model +- Allow new users to subscribe to mailing list to get notified when new Accounts option is available ## [3.2.5] -- Use yellow textfield outline in Plan mode to better distinguish from Act mode +- Use yellow textfield outline in Plan mode to better distinguish from Act mode ## [3.2.3] -- Add DeepSeek-R1 (deepseek-reasoner) model support with proper parameter handling (thanks @slavakurilyak!) +- Add DeepSeek-R1 (deepseek-reasoner) model support with proper parameter handling (thanks @slavakurilyak!) ## [3.2.0] -- Add Plan/Act mode toggle to let you plan tasks with Cline before letting him get to work -- Easily switch between API providers and models using a new popup menu under the chat field -- Add VS Code LM API provider to run models provided by other VS Code extensions (e.g. GitHub Copilot). Shoutout to @julesmons, @RaySinner, and @MrUbens for putting this together! -- Add on/off toggle for MCP servers to disable them when not in use. Thanks @MrUbens! -- Add Auto-approve option for individual tools in MCP servers. Thanks @MrUbens! +- Add Plan/Act mode toggle to let you plan tasks with Cline before letting him get to work +- Easily switch between API providers and models using a new popup menu under the chat field +- Add VS Code LM API provider to run models provided by other VS Code extensions (e.g. GitHub Copilot). Shoutout to @julesmons, @RaySinner, and @MrUbens for putting this together! +- Add on/off toggle for MCP servers to disable them when not in use. Thanks @MrUbens! +- Add Auto-approve option for individual tools in MCP servers. Thanks @MrUbens! ## [3.1.10] -- New icon! +- New icon! ## [3.1.9] -- Add Mistral API provider with codestral-latest model +- Add Mistral API provider with codestral-latest model ## [3.1.7] -- Add ability to change viewport size and headless mode when Cline asks to launch the browser +- Add ability to change viewport size and headless mode when Cline asks to launch the browser ## [3.1.6] -- Fix bug where filepaths with Chinese characters would not show up in context mention menu (thanks @chi-chat!) -- Update Anthropic model prices (thanks @timoteostewart!) +- Fix bug where filepaths with Chinese characters would not show up in context mention menu (thanks @chi-chat!) +- Update Anthropic model prices (thanks @timoteostewart!) ## [3.1.5] -- Fix bug where Cline couldn't read "@/" import path aliases from tool results +- Fix bug where Cline couldn't read "@/" import path aliases from tool results ## [3.1.4] -- Fix issue where checkpoints would not work for users with git commit signing enabled globally +- Fix issue where checkpoints would not work for users with git commit signing enabled globally ## [3.1.2] -- Fix issue where LFS files would be not be ignored when creating checkpoints +- Fix issue where LFS files would be not be ignored when creating checkpoints ## [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 +- 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) +- 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) ## [3.0.11] -- Emphasize auto-formatting done by the editor in file edit responses for more reliable diff editing +- Emphasize auto-formatting done by the editor in file edit responses for more reliable diff editing ## [3.0.10] -- Add DeepSeek provider to API Provider options -- Fix context window limit errors for DeepSeek v3 +- Add DeepSeek provider to API Provider options +- Fix context window limit errors for DeepSeek v3 ## [3.0.9] -- Fix bug where DeepSeek v3 would incorrectly escape HTML entities in diff edits +- Fix bug where DeepSeek v3 would incorrectly escape HTML entities in diff edits ## [3.0.8] -- Mitigate DeepSeek v3 diff edit errors by adding 'auto-formatting considerations' to system prompt, encouraging model to use updated file contents as reference point for SEARCH blocks +- Mitigate DeepSeek v3 diff edit errors by adding 'auto-formatting considerations' to system prompt, encouraging model to use updated file contents as reference point for SEARCH blocks ## [3.0.7] -- Revert to using batched file watcher to fix crash when many files would be created at once +- Revert to using batched file watcher to fix crash when many files would be created at once ## [3.0.6] -- Fix bug where some files would be missing in the `@` context mention menu -- Add Bedrock support in additional regions -- Diff edit improvements -- Add OpenRouter's middle-out transform for models that don't use prompt caching (prevents context window limit errors, but cannot be applied to models like Claude since it would continuously break the cache) +- Fix bug where some files would be missing in the `@` context mention menu +- Add Bedrock support in additional regions +- Diff edit improvements +- Add OpenRouter's middle-out transform for models that don't use prompt caching (prevents context window limit errors, but cannot be applied to models like Claude since it would continuously break the cache) ## [3.0.4] -- Fix bug where gemini models would add code block artifacts to the end of text content -- Fix context mention menu visual issues on light themes +- Fix bug where gemini models would add code block artifacts to the end of text content +- Fix context mention menu visual issues on light themes ## [3.0.2] -- Adds block anchor matching for more reliable diff edits (if 3+ lines, first and last line are used as anchors to search for) -- Add instruction to system prompt to use complete lines in diff edits to work properly with fallback strategies -- Improves diff edit error handling -- Adds new Gemini models +- Adds block anchor matching for more reliable diff edits (if 3+ lines, first and last line are used as anchors to search for) +- Add instruction to system prompt to use complete lines in diff edits to work properly with fallback strategies +- Improves diff edit error handling +- Adds new Gemini models ## [3.0.0] -- Cline now uses a search & replace diff based approach when editing large files to prevent code deletion issues. -- Adds support for a more comprehensive auto-approve configuration, allowing you to specify which tools require approval and which don't. -- Adds ability to enable system notifications for when Cline needs approval or completes a task. -- Adds support for a root-level `.clinerules` file that can be used to specify custom instructions for the project. +- Cline now uses a search & replace diff based approach when editing large files to prevent code deletion issues. +- Adds support for a more comprehensive auto-approve configuration, allowing you to specify which tools require approval and which don't. +- Adds ability to enable system notifications for when Cline needs approval or completes a task. +- Adds support for a root-level `.clinerules` file that can be used to specify custom instructions for the project. ## [2.2.0] -- Add support for Model Context Protocol (MCP), enabling Cline to use custom tools like web-search tool or GitHub tool -- Add MCP server management tab accessible via the server icon in the menu bar -- Add ability for Cline to dynamically create new MCP servers based on user requests (e.g., "add a tool that gets the latest npm docs") +- Add support for Model Context Protocol (MCP), enabling Cline to use custom tools like web-search tool or GitHub tool +- Add MCP server management tab accessible via the server icon in the menu bar +- Add ability for Cline to dynamically create new MCP servers based on user requests (e.g., "add a tool that gets the latest npm docs") ## [2.1.6] -- Add LM Studio as an API provider option (make sure to start the LM Studio server to use it with the extension!) +- Add LM Studio as an API provider option (make sure to start the LM Studio server to use it with the extension!) ## [2.1.5] -- Add support for prompt caching for new Claude model IDs on OpenRouter (e.g. `anthropic/claude-3.5-sonnet-20240620`) +- Add support for prompt caching for new Claude model IDs on OpenRouter (e.g. `anthropic/claude-3.5-sonnet-20240620`) ## [2.1.4] -- AWS Bedrock fixes (add missing regions, support for cross-region inference, and older Sonnet model for regions where new model is not available) +- AWS Bedrock fixes (add missing regions, support for cross-region inference, and older Sonnet model for regions where new model is not available) ## [2.1.3] -- Add support for Claude 3.5 Haiku, 66% cheaper than Sonnet with similar intelligence +- Add support for Claude 3.5 Haiku, 66% cheaper than Sonnet with similar intelligence ## [2.1.2] -- Misc. bug fixes -- Update README with new browser feature +- Misc. bug fixes +- Update README with new browser feature ## [2.1.1] -- Add stricter prompt to prevent Cline from editing files during a browser session without first closing the browser +- Add stricter prompt to prevent Cline from editing files during a browser session without first closing the browser ## [2.1.0] -- Cline now uses Anthropic's new "Computer Use" feature to launch a browser, click, type, and scroll. This gives him more autonomy in runtime debugging, end-to-end testing, and even general web use. Try asking "Look up the weather in Colorado" to see it in action! (Available with Claude 3.5 Sonnet v2) +- Cline now uses Anthropic's new "Computer Use" feature to launch a browser, click, type, and scroll. This gives him more autonomy in runtime debugging, end-to-end testing, and even general web use. Try asking "Look up the weather in Colorado" to see it in action! (Available with Claude 3.5 Sonnet v2) ## [2.0.19] -- Fix model info for Claude 3.5 Sonnet v1 on OpenRouter +- Fix model info for Claude 3.5 Sonnet v1 on OpenRouter ## [2.0.18] -- Add support for both v1 and v2 of Claude 3.5 Sonnet for GCP Vertex and AWS Bedrock (for cases where the new model is not enabled yet or unavailable in your region) +- Add support for both v1 and v2 of Claude 3.5 Sonnet for GCP Vertex and AWS Bedrock (for cases where the new model is not enabled yet or unavailable in your region) ## [2.0.17] -- Update Anthropic model IDs +- Update Anthropic model IDs ## [2.0.16] -- Adjustments to system prompt +- Adjustments to system prompt ## [2.0.15] -- Fix bug where modifying Cline's edits would lead him to try to re-apply the edits -- Fix bug where weaker models would display file contents before using the write_to_file tool -- Fix o1-mini and o1-preview errors when using OpenAI native +- Fix bug where modifying Cline's edits would lead him to try to re-apply the edits +- Fix bug where weaker models would display file contents before using the write_to_file tool +- Fix o1-mini and o1-preview errors when using OpenAI native ## [2.0.14] -- Gracefully cancel requests while stream could be hanging +- Gracefully cancel requests while stream could be hanging ## [2.0.13] -- Detect code omission and show warning with troubleshooting link +- Detect code omission and show warning with troubleshooting link ## [2.0.12] -- Keep cursor out of the way during file edit streaming animation +- Keep cursor out of the way during file edit streaming animation ## [2.0.11] -- Adjust prompts around read_file to prevent re-reading files unnecessarily +- Adjust prompts around read_file to prevent re-reading files unnecessarily ## [2.0.10] -- More adjustments to system prompt to prevent lazy coding +- More adjustments to system prompt to prevent lazy coding ## [2.0.9] -- Update system prompt to try to prevent Cline from lazy coding (`// rest of code here...`) +- Update system prompt to try to prevent Cline from lazy coding (`// rest of code here...`) ## [2.0.8] -- Fix o1-mini and o1-preview for OpenAI -- Fix diff editor not opening sometimes in slow environments like project idx +- Fix o1-mini and o1-preview for OpenAI +- Fix diff editor not opening sometimes in slow environments like project idx ## [2.0.7] -- Misc. bug fixes +- Misc. bug fixes ## [2.0.6] -- Update URLs to https://github.com/cline/cline +- Update URLs to https://github.com/cline/cline ## [2.0.5] -- Fixed bug where Cline's edits would stream into the active tab when switching tabs during a write_to_file -- Added explanation in task continuation prompt that an interrupted write_to_file reverts the file to its original contents, preventing unnecessary re-reads -- Fixed non-first chunk error handling in case stream fails mid-way through +- Fixed bug where Cline's edits would stream into the active tab when switching tabs during a write_to_file +- Added explanation in task continuation prompt that an interrupted write_to_file reverts the file to its original contents, preventing unnecessary re-reads +- Fixed non-first chunk error handling in case stream fails mid-way through ## [2.0.0] -- New name! Meet Cline, an AI assistant that can use your CLI and Editor -- Responses are now streamed with a yellow text decoration animation to keep track of Cline's progress as he edits files -- New Cancel button to give Cline feedback if he goes off in the wrong direction, giving you more control over tasks -- Re-imagined tool calling prompt resulting in ~40% fewer requests to accomplish tasks + better performance with other models -- Search and use any model with OpenRouter +- New name! Meet Cline, an AI assistant that can use your CLI and Editor +- Responses are now streamed with a yellow text decoration animation to keep track of Cline's progress as he edits files +- New Cancel button to give Cline feedback if he goes off in the wrong direction, giving you more control over tasks +- Re-imagined tool calling prompt resulting in ~40% fewer requests to accomplish tasks + better performance with other models +- Search and use any model with OpenRouter ## [1.9.7] -- Only auto-include error diagnostics after file edits, removed warnings to keep Claude from getting distracted in projects with strict linting rules +- Only auto-include error diagnostics after file edits, removed warnings to keep Claude from getting distracted in projects with strict linting rules ## [1.9.6] -- Added support for new Google Gemini models `gemini-1.5-flash-002` and `gemini-1.5-pro-002` -- Updated system prompt to be more lenient when terminal output doesn't stream back properly -- Adjusted system prompt to prevent overuse of the inspect_site tool -- Increased global line height for improved readability +- Added support for new Google Gemini models `gemini-1.5-flash-002` and `gemini-1.5-pro-002` +- Updated system prompt to be more lenient when terminal output doesn't stream back properly +- Adjusted system prompt to prevent overuse of the inspect_site tool +- Increased global line height for improved readability ## [1.9.0] -- Claude can now use a browser! This update adds a new `inspect_site` tool that captures screenshots and console logs from websites (including localhost), making it easier for Claude to troubleshoot issues on his own. -- Improved automatic linter/compiler debugging by only sending Claude new errors that result from his edits, rather than reporting all workspace problems. +- Claude can now use a browser! This update adds a new `inspect_site` tool that captures screenshots and console logs from websites (including localhost), making it easier for Claude to troubleshoot issues on his own. +- Improved automatic linter/compiler debugging by only sending Claude new errors that result from his edits, rather than reporting all workspace problems. ## [1.8.0] -- You can now use '@' in the textarea to add context! - - @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs! - - @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging - - @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files) - - @folder: Adds folder's files all at once to speed up your workflow even more +- You can now use '@' in the textarea to add context! + - @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs! + - @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging + - @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files) + - @folder: Adds folder's files all at once to speed up your workflow even more ## [1.7.0] -- Adds problems monitoring to keep Claude updated on linter/compiler/build issues, letting him proactively fix errors on his own! (adding missing imports, fixing type errors, etc.) +- Adds problems monitoring to keep Claude updated on linter/compiler/build issues, letting him proactively fix errors on his own! (adding missing imports, fixing type errors, etc.) ## [1.6.5] -- Adds support for OpenAI o1, Azure OpenAI, and Google Gemini (free for up to 15 requests per minute!) -- Task header can now be collapsed to provide more space for viewing conversations -- Adds fuzzy search and sorting to Task History, making it easier to find specific tasks +- Adds support for OpenAI o1, Azure OpenAI, and Google Gemini (free for up to 15 requests per minute!) +- Task header can now be collapsed to provide more space for viewing conversations +- Adds fuzzy search and sorting to Task History, making it easier to find specific tasks ## [1.6.0] -- Commands now run directly in your terminal thanks to VSCode 1.93's new shell integration updates! Plus a new 'Proceed While Running' button to let Claude continue working while commands run, sending him new output along the way (i.e. letting him react to server errors as he edits files) +- Commands now run directly in your terminal thanks to VSCode 1.93's new shell integration updates! Plus a new 'Proceed While Running' button to let Claude continue working while commands run, sending him new output along the way (i.e. letting him react to server errors as he edits files) ## [1.5.27] -- Claude's changes now appear in your file's Timeline, allowing you to easily view a diff of each edit. This is especially helpful if you want to revert to a previous version. No need for git—everything is tracked by VSCode's local history! -- Updated system prompt to keep Claude from re-reading files unnecessarily +- Claude's changes now appear in your file's Timeline, allowing you to easily view a diff of each edit. This is especially helpful if you want to revert to a previous version. No need for git—everything is tracked by VSCode's local history! +- Updated system prompt to keep Claude from re-reading files unnecessarily ## [1.5.19] -- Adds support for OpenAI compatible API providers (e.g. Ollama!) +- Adds support for OpenAI compatible API providers (e.g. Ollama!) ## [1.5.13] -- New terminal emulator! When Claude runs commands, you can now type directly in the terminal (+ support for Python environments) -- Adds search to Task History +- New terminal emulator! When Claude runs commands, you can now type directly in the terminal (+ support for Python environments) +- Adds search to Task History ## [1.5.6] -- You can now edit Claude's changes before accepting! When he edits or creates a file, you can modify his changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in the center to undo `// rest of code here` shenanigans) +- You can now edit Claude's changes before accepting! When he edits or creates a file, you can modify his changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in the center to undo `// rest of code here` shenanigans) ## [1.5.4] -- Adds support for reading .pdf and .docx files (try "turn my business_plan.docx into a company website") +- Adds support for reading .pdf and .docx files (try "turn my business_plan.docx into a company website") ## [1.5.0] -- Adds new `search_files` tool that lets Claude perform regex searches in your project, making it easy for him to refactor code, address TODOs and FIXMEs, remove dead code, and more! +- Adds new `search_files` tool that lets Claude perform regex searches in your project, making it easy for him to refactor code, address TODOs and FIXMEs, remove dead code, and more! ## [1.4.0] -- Adds "Always allow read-only operations" setting to let Claude read files and view directories without needing approval (off by default) -- Implement sliding window context management to keep tasks going past 200k tokens -- Adds Google Cloud Vertex AI support and updates Claude 3.5 Sonnet max output to 8192 tokens for all providers. -- Improves system prompt to gaurd against lazy edits (less "//rest of code here") +- Adds "Always allow read-only operations" setting to let Claude read files and view directories without needing approval (off by default) +- Implement sliding window context management to keep tasks going past 200k tokens +- Adds Google Cloud Vertex AI support and updates Claude 3.5 Sonnet max output to 8192 tokens for all providers. +- Improves system prompt to gaurd against lazy edits (less "//rest of code here") ## [1.3.0] -- Adds task history +- Adds task history ## [1.2.0] -- Adds support for Prompt Caching to significantly reduce costs and response times (currently only available through Anthropic API for Claude 3.5 Sonnet and Claude 3.0 Haiku) +- Adds support for Prompt Caching to significantly reduce costs and response times (currently only available through Anthropic API for Claude 3.5 Sonnet and Claude 3.0 Haiku) ## [1.1.1] -- Adds option to choose other Claude models (+ GPT-4o, DeepSeek, and Mistral if you use OpenRouter) -- Adds option to add custom instructions to the end of the system prompt +- Adds option to choose other Claude models (+ GPT-4o, DeepSeek, and Mistral if you use OpenRouter) +- Adds option to add custom instructions to the end of the system prompt ## [1.1.0] -- Paste images in chat to use Claude's vision capabilities and turn mockups into fully functional applications or fix bugs with screenshots +- Paste images in chat to use Claude's vision capabilities and turn mockups into fully functional applications or fix bugs with screenshots ## [1.0.9] -- Add support for OpenRouter and AWS Bedrock +- Add support for OpenRouter and AWS Bedrock ## [1.0.8] -- Shows diff view of new or edited files right in the editor +- Shows diff view of new or edited files right in the editor ## [1.0.7] -- Replace `list_files` and `analyze_project` with more explicit `list_files_top_level`, `list_files_recursive`, and `view_source_code_definitions_top_level` to get source code definitions only for files relevant to the task +- Replace `list_files` and `analyze_project` with more explicit `list_files_top_level`, `list_files_recursive`, and `view_source_code_definitions_top_level` to get source code definitions only for files relevant to the task ## [1.0.6] -- Interact with CLI commands by sending messages to stdin and terminating long-running processes like servers -- Export tasks to markdown files (useful as context for future tasks) +- Interact with CLI commands by sending messages to stdin and terminating long-running processes like servers +- Export tasks to markdown files (useful as context for future tasks) ## [1.0.5] -- Claude now has context about vscode's visible editors and opened tabs +- Claude now has context about vscode's visible editors and opened tabs ## [1.0.4] -- Open in the editor (using menu bar or `Claude Dev: Open In New Tab` in command palette) to see how Claude updates your workspace more clearly -- New `analyze_project` tool to help Claude get a comprehensive overview of your project's source code definitions and file structure -- Provide feedback to tool use like terminal commands and file edits -- Updated max output tokens to 8192 so less lazy coding (`// rest of code here...`) -- Added ability to retry failed API requests (helpful for rate limits) -- Quality of life improvements like markdown rendering, memory optimizations, better theme support +- Open in the editor (using menu bar or `Claude Dev: Open In New Tab` in command palette) to see how Claude updates your workspace more clearly +- New `analyze_project` tool to help Claude get a comprehensive overview of your project's source code definitions and file structure +- Provide feedback to tool use like terminal commands and file edits +- Updated max output tokens to 8192 so less lazy coding (`// rest of code here...`) +- Added ability to retry failed API requests (helpful for rate limits) +- Quality of life improvements like markdown rendering, memory optimizations, better theme support ## [0.0.6] -- Initial release \ No newline at end of file +- Initial release diff --git a/package.json b/package.json index 3149914521..830d212709 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "claude-dev", "displayName": "Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", - "version": "3.2.13", + "version": "3.3.0", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", From fa05f94dfa8d2e3f664a2bd69bb503f801a817e5 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:34:47 -0800 Subject: [PATCH 5/6] Fixes --- src/api/providers/qwen.ts | 5 ++++- webview-ui/src/components/chat/ChatTextArea.tsx | 10 ++++++++++ webview-ui/src/components/settings/ApiOptions.tsx | 8 +++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/api/providers/qwen.ts b/src/api/providers/qwen.ts index 9744fc1338..aa4138d09c 100644 --- a/src/api/providers/qwen.ts +++ b/src/api/providers/qwen.ts @@ -12,7 +12,10 @@ export class QwenHandler implements ApiHandler { constructor(options: ApiHandlerOptions) { this.options = options this.client = new OpenAI({ - baseURL: this.options.qwenApiLine || "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + baseURL: + this.options.qwenApiLine === "china" + ? "https://dashscope.aliyuncs.com/compatible-mode/v1" + : "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", apiKey: this.options.qwenApiKey, }) } diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index baae8e682f..0255ef0301 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -702,6 +702,16 @@ const ChatTextArea = forwardRef( return `openai-compat:${selectedModelId}` case "vscode-lm": return `vscode-lm:${apiConfiguration.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` : unknownModel}` + case "together": + return `${selectedProvider}:${apiConfiguration.togetherModelId}` + case "lmstudio": + return `${selectedProvider}:${apiConfiguration.lmStudioModelId}` + case "ollama": + return `${selectedProvider}:${apiConfiguration.ollamaModelId}` + case "litellm": + return `${selectedProvider}:${apiConfiguration.liteLlmModelId}` + case "requesty": + return `${selectedProvider}:${apiConfiguration.requestyModelId}` default: return `${selectedProvider}:${selectedModelId}` } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index f5f69517c3..77c62127fa 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -323,16 +323,14 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is - China API - - International API - + China API + International API

Date: Fri, 7 Feb 2025 21:37:03 -0800 Subject: [PATCH 6/6] Re-arrange providers --- webview-ui/src/components/settings/ApiOptions.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 77c62127fa..5e39320092 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -179,17 +179,17 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is }}> OpenRouter Anthropic + AWS Bedrock + OpenAI Compatible + GCP Vertex AI Google Gemini DeepSeek - Qwen Mistral - GCP Vertex AI - AWS Bedrock OpenAI - OpenAI Compatible + VS Code LM API Requesty Together - VS Code LM API + Alibaba Qwen LM Studio Ollama LiteLLM