From 01382b5a09cd6a9c562610c3244cff59045512f7 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Sun, 9 Nov 2025 15:56:59 +0530 Subject: [PATCH 1/3] update follow ups and ui --- src/api/providers/openrouter.ts | 6 - .../native-tools/ask_followup_question.ts | 2 +- src/core/task/Task.ts | 120 +++++++++++++++++- webview-ui/src/components/chat/ChatRow.tsx | 5 +- webview-ui/src/components/chat/ChatView.tsx | 39 +++--- .../src/components/chat/FollowUpSuggest.tsx | 2 +- 6 files changed, 145 insertions(+), 29 deletions(-) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 7a369a551c..7924b746ec 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -146,9 +146,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH const baseURL = this.options.openRouterBaseUrl || "https://api.matterai.so/v1/web" const apiKey = this.options.openRouterApiKey ?? "not-provided" - console.log("baseURL", baseURL) - console.log("apiKey", apiKey) - this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS }) } @@ -226,8 +223,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH // const transforms = (this.options.openRouterUseMiddleOutTransform ?? true) ? ["middle-out"] : undefined - console.log("convertedMessages", convertedMessages) - // https://openrouter.ai/docs/transforms // const completionParams: OpenRouterChatCompletionParams = { // model: modelId, @@ -256,7 +251,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH let stream try { console.log("requestOptions", requestOptions) - // console.log("customRequestOptions", this.customRequestOptions(metadata)) stream = await this.client.chat.completions.create( requestOptions, this.customRequestOptions(metadata), // kilocode_change diff --git a/src/core/prompts/tools/native-tools/ask_followup_question.ts b/src/core/prompts/tools/native-tools/ask_followup_question.ts index 81846d762c..d2ef7df9d6 100644 --- a/src/core/prompts/tools/native-tools/ask_followup_question.ts +++ b/src/core/prompts/tools/native-tools/ask_followup_question.ts @@ -28,7 +28,7 @@ export default { mode: { type: ["string", "null"], description: - "Optional mode slug to switch to if this suggestion is chosen (e.g., code, architect)", + "Optional mode slug to switch to if this suggestion is chosen (e.g., agent, plan)", }, }, required: ["text", "mode"], diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 23479a6274..8c00a379c4 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -272,7 +272,10 @@ export class Task extends EventEmitter implements TaskLike { private askResponse?: ClineAskResponse private askResponseText?: string private askResponseImages?: string[] + private isWaitingForAskResponse = false public lastMessageTs?: number + private manualMessageQueue: Array<{ text: string; images?: string[] }> = [] + private isProcessingManualMessages = false // Tool Use consecutiveMistakeCount: number = 0 @@ -943,7 +946,12 @@ export class Task extends EventEmitter implements TaskLike { } // Wait for askResponse to be set. - await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 }) + this.isWaitingForAskResponse = true + try { + await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 }) + } finally { + this.isWaitingForAskResponse = false + } if (this.lastMessageTs !== askTs) { // Could happen if we send multiple asks in a row i.e. with @@ -969,6 +977,7 @@ export class Task extends EventEmitter implements TaskLike { } this.emit(RooCodeEventName.TaskAskResponded) + void this.processManualMessageQueue() return result } @@ -977,6 +986,11 @@ export class Task extends EventEmitter implements TaskLike { } handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) { + if (!this.isWaitingForAskResponse && askResponse === "messageResponse") { + void this.enqueueManualUserMessage(text, images) + return + } + // this.askResponse = askResponse kilocode_change this.askResponseText = text this.askResponseImages = images @@ -1021,6 +1035,109 @@ export class Task extends EventEmitter implements TaskLike { this.handleWebviewAskResponse("noButtonClicked", text, images) } + private async enqueueManualUserMessage(text?: string, images?: string[]): Promise { + const trimmedText = text?.trim() ?? "" + const hasImages = Array.isArray(images) && images.length > 0 + + if (!trimmedText && !hasImages) { + return + } + + this.manualMessageQueue.push({ + text: trimmedText, + images: hasImages ? [...(images as string[])] : undefined, + }) + + try { + await this.processManualMessageQueue() + } catch (error) { + console.error("Failed to process manual user message queue:", error) + } + } + + private async processManualMessageQueue(): Promise { + if (this.isProcessingManualMessages) { + return + } + + if (this.manualMessageQueue.length === 0) { + return + } + + if (this.isStreaming || this.isWaitingForAskResponse) { + return + } + + this.isProcessingManualMessages = true + + try { + while (this.manualMessageQueue.length > 0) { + if (this.isStreaming || this.isWaitingForAskResponse) { + break + } + + const nextMessage = this.manualMessageQueue.shift() + + if (!nextMessage) { + break + } + + await this.handleManualUserMessage(nextMessage) + } + } finally { + this.isProcessingManualMessages = false + } + + if (!this.isStreaming && !this.isWaitingForAskResponse && this.manualMessageQueue.length > 0) { + void this.processManualMessageQueue() + } + } + + private async handleManualUserMessage(message: { text: string; images?: string[] }): Promise { + const { text, images } = message + + try { + await this.checkpointSave(false, true) + } catch (error) { + console.error("Failed to checkpoint before manual user message:", error) + } + + try { + await this.say("user_feedback", text, images) + } catch (error) { + console.error("Failed to append manual user message to conversation:", error) + } + + this.emit(RooCodeEventName.TaskUserMessage, this.taskId) + + const userContent: Anthropic.Messages.ContentBlockParam[] = [] + + if (text.length > 0) { + userContent.push({ + type: "text", + text: `\n${text}\n`, + }) + } else if (images && images.length > 0) { + userContent.push({ + type: "text", + text: "[User provided images]", + }) + } + + if (images && images.length > 0) { + userContent.push(...formatResponse.imageBlocks(images)) + } + + this.userMessageContent = [] + this.userMessageContentReady = false + + try { + await this.recursivelyMakeClineRequests(userContent, false) + } catch (error) { + console.error("Failed to process manual user follow-up message:", error) + } + } + public async submitUserMessage( text: string, images?: string[], @@ -2365,6 +2482,7 @@ export class Task extends EventEmitter implements TaskLike { } } finally { this.isStreaming = false + void this.processManualMessageQueue() } // Need to call here in case the stream was aborted. diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 8ba34c1864..081b1547d7 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1150,10 +1150,10 @@ export const ChatRowContent = ({ {t("chat:text.rooSaid")} */} -
+
{message.images && message.images.length > 0 && ( -
+
{message.images.map((image, index) => ( ))} @@ -1171,6 +1171,7 @@ export const ChatRowContent = ({
*/}
{ text = text.trim() - if (text || images.length > 0) { if (sendingDisabled) { try { - console.log("queueMessage", text, images) vscode.postMessage({ type: "queueMessage", text, images }) setInputValue("") setSelectedImages([]) diff --git a/webview-ui/src/components/chat/FollowUpSuggest.tsx b/webview-ui/src/components/chat/FollowUpSuggest.tsx index d18ccc2517..6516b10530 100644 --- a/webview-ui/src/components/chat/FollowUpSuggest.tsx +++ b/webview-ui/src/components/chat/FollowUpSuggest.tsx @@ -126,7 +126,7 @@ export const FollowUpSuggest = ({ )} {suggestion.mode && ( -
+
{suggestion.mode}
From e78bd069c8c021ad4efdc194f91a3fc31e844d5b Mon Sep 17 00:00:00 2001 From: code-crusher Date: Tue, 11 Nov 2025 16:55:47 +0530 Subject: [PATCH 2/3] new file edit tool --- .../docs/features/tools/file-edit.md | 53 ++ .../docs/features/tools/tool-use-overview.md | 3 +- packages/types/src/tool.ts | 1 + .../kilocode/nativeToolCallHelpers.ts | 6 +- src/api/providers/openrouter.ts | 44 +- .../presentAssistantMessage.ts | 6 + .../system-prompt/with-diff-enabled-true.snap | 4 +- src/core/prompts/__tests__/sections.spec.ts | 7 +- src/core/prompts/sections/capabilities.ts | 8 +- src/core/prompts/sections/rules.ts | 4 +- src/core/prompts/system.ts | 117 +++- src/core/prompts/tools/file-edit.ts | 21 + src/core/prompts/tools/index.ts | 11 +- .../prompts/tools/native-tools/apply_diff.ts | 2 + .../prompts/tools/native-tools/file_edit.ts | 37 + .../getAllowedJSONToolsForMode.ts | 8 +- src/core/prompts/tools/native-tools/index.ts | 14 +- .../tools/native-tools/search_files.ts | 2 +- src/core/tools/fileEditTool.ts | 661 ++++++++++++++++++ src/shared/ExtensionMessage.ts | 2 + src/shared/experiments.ts | 2 +- src/shared/tools.ts | 11 + webview-ui/src/components/chat/ChatRow.tsx | 40 +- .../src/components/common/MarkdownBlock.tsx | 2 +- 24 files changed, 1007 insertions(+), 59 deletions(-) create mode 100644 apps/kilocode-docs/docs/features/tools/file-edit.md create mode 100644 src/core/prompts/tools/file-edit.ts create mode 100644 src/core/prompts/tools/native-tools/file_edit.ts create mode 100644 src/core/tools/fileEditTool.ts diff --git a/apps/kilocode-docs/docs/features/tools/file-edit.md b/apps/kilocode-docs/docs/features/tools/file-edit.md new file mode 100644 index 0000000000..20bb9ba2c3 --- /dev/null +++ b/apps/kilocode-docs/docs/features/tools/file-edit.md @@ -0,0 +1,53 @@ +--- +title: file_edit +--- + +# file_edit + +The `file_edit` tool performs targeted string replacements inside an existing file without requiring full diff blocks or a Fast Apply model. It combines deterministic matching with fuzzy fallbacks so you can provide an `old_string` and `new_string`, and the tool will locate and replace the intended section while still showing a diff for review. + +## Parameters + +- `target_file` (required): Path to the file to modify, relative to the workspace root. +- `old_string` (required): The text you expect to replace. Provide enough context for a unique match. Use an empty string to replace the entire file. +- `new_string` (required): Replacement text. This can be empty when you want to delete the matched block. +- `replace_all` (optional, default `false`): When `true`, every occurrence of the matched text is replaced. When `false`, the tool refuses to apply the change if the match is ambiguous. + +## How It Works + +1. **Validation** – Ensures required parameters are provided and that `old_string` differs from `new_string`. +2. **Access Checks** – Respects `.rooignore` and write-protection rules before modifying files. +3. **Content Matching** – Searches for `old_string` using multiple strategies: + - Exact substring matches + - Trimmed and indentation-insensitive comparisons + - Context-aware block matching with anchor lines + - Escaped character normalization and whitespace normalization +4. **Replacement** – Applies the update (single occurrence by default, or all occurrences when `replace_all` is `true`). +5. **Review** – Opens a diff preview for approval before writing changes to disk. + +Because the tool still previews differences, you maintain full control over the edit before it is applied. + +## When to Use + +- You want a precise, deterministic edit without crafting manual `apply_diff` blocks. +- The change is localized and can be described as “replace this text with that text.” +- You need to remove or rewrite a block of code using string-based matching. +- Fast Apply is disabled or unavailable, and you want an alternative to `apply_diff`. + +## Tips + +- Prefer multi-line `old_string` values for more reliable matching. +- Include surrounding context (such as function signatures and closing braces) when multiple similar blocks exist. +- Set `replace_all` to `true` only when you intentionally want to update each occurrence of the match. +- Use the tool in combination with `read_file` or `search_files` to confirm the exact text you need to replace. + +## Comparison to Other Editing Tools + +| Tool | Best For | Notes | +| --------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| `apply_diff` | Structured changes with explicit SEARCH/REPLACE blocks | Supports multi-file edits and precise line control via `:start_line:` metadata. | +| `file_edit` | String-based replacements with fuzzy matching | Great when you know the before/after text but want deterministic, model-free edits. | +| `edit_file` | Morph Fast Apply powered edits | Delegates the change to an external model; ideal for large or semantic refactors. | +| `write_to_file` | Creating or completely replacing files | Overwrites entire files or creates new files from scratch. | + +Choose the tool that best matches your workflow and the level of control you need over the edit. diff --git a/apps/kilocode-docs/docs/features/tools/tool-use-overview.md b/apps/kilocode-docs/docs/features/tools/tool-use-overview.md index 7ea9d78edb..172f4fae01 100644 --- a/apps/kilocode-docs/docs/features/tools/tool-use-overview.md +++ b/apps/kilocode-docs/docs/features/tools/tool-use-overview.md @@ -11,7 +11,7 @@ Tools are organized into logical groups based on their functionality: | Category | Purpose | Tools | Common Use | | ------------------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | | **Read Group** | File system reading and searching | [read_file](/features/tools/read-file), [search_files](/features/tools/search-files), [list_files](/features/tools/list-files), [list_code_definition_names](/features/tools/list-code-definition-names) | Code exploration and analysis | -| **Edit Group** | File system modifications | [apply_diff](/features/tools/apply-diff), [write_to_file](/features/tools/write-to-file) | Code changes and file manipulation | +| **Edit Group** | File system modifications | [apply_diff](/features/tools/apply-diff), [file_edit](/features/tools/file-edit), [write_to_file](/features/tools/write-to-file) | Code changes and file manipulation | | **Browser Group** | Web automation | [browser_action](/features/tools/browser-action) | Web testing and interaction | | **Command Group** | System command execution | [execute_command](/features/tools/execute-command) | Running scripts, building projects | | **MCP Group** | External tool integration | [use_mcp_tool](/features/tools/use-mcp-tool), [access_mcp_resource](/features/tools/access-mcp-resource) | Specialized functionality through external servers | @@ -43,6 +43,7 @@ These tools help Axon Code understand your code and project: These tools help Axon Code make changes to your code: - [apply_diff](/features/tools/apply-diff) - Makes precise, surgical changes to your code +- [file_edit](/features/tools/file-edit) - Performs string-based replacements with fuzzy matching - [write_to_file](/features/tools/write-to-file) - Creates new files or completely rewrites existing ones ### Browser Tools diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 421f4d564a..789b4d6e46 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -19,6 +19,7 @@ export const toolNames = [ "read_file", "write_to_file", "apply_diff", + "file_edit", "insert_content", "search_and_replace", "search_files", diff --git a/src/api/providers/kilocode/nativeToolCallHelpers.ts b/src/api/providers/kilocode/nativeToolCallHelpers.ts index 4c0b4825f1..7e020c7f33 100644 --- a/src/api/providers/kilocode/nativeToolCallHelpers.ts +++ b/src/api/providers/kilocode/nativeToolCallHelpers.ts @@ -85,13 +85,13 @@ import type { ApiStreamNativeToolCallsChunk } from "../../transform/kilocode/api */ export function addNativeToolCallsToParams( params: T, - options: ProviderSettings, - metadata?: ApiHandlerCreateMessageMetadata, + _options: ProviderSettings, + _metadata?: ApiHandlerCreateMessageMetadata, ): T { // When toolStyle is "json", always add all native tools // Use allowedTools if provided, otherwise use all native tools - const tools = metadata?.allowedTools || nativeTools + const tools = nativeTools if (tools && tools.length > 0) { params.tools = tools //optimally we'd have tool_choice as 'required', but many providers, especially diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 7924b746ec..7f66b093ac 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -147,11 +147,18 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH const apiKey = this.options.openRouterApiKey ?? "not-provided" this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS }) + // this.client = new OpenAI({ baseURL: "http://localhost:4064/v1/web", apiKey, defaultHeaders: DEFAULT_HEADERS }) } // kilocode_change start - customRequestOptions(_metadata?: ApiHandlerCreateMessageMetadata): { headers: Record } | undefined { - return undefined + customRequestOptions(metadata?: ApiHandlerCreateMessageMetadata): { headers: Record } | undefined { + const headers: Record = {} + + if (metadata?.taskId) { + headers["X-AXON-TASK-ID"] = metadata.taskId + } + + return Object.keys(headers).length > 0 ? { headers } : undefined } getCustomRequestHeaders(taskId?: string) { @@ -205,38 +212,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH let { id: modelId, maxTokens, temperature, topP, reasoning } = model const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)] - // openAiMessages = openAiMessages - // .map((msg: any) => { - // let content = flattenMessageContent(msg.content) - - // // Strip thinking tokens from assistant messages to prevent confusion - // if (msg.role === "assistant") { - // content = stripThinkingTokens(content) - // } - - // return { - // role: msg.role, - // content, - // } - // }) - // .filter((msg: any) => msg.content.trim() !== "") - - // const transforms = (this.options.openRouterUseMiddleOutTransform ?? true) ? ["middle-out"] : undefined - - // https://openrouter.ai/docs/transforms - // const completionParams: OpenRouterChatCompletionParams = { - // model: modelId, - // ...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }), - // temperature, - // top_p: topP, - // messages: convertedMessages, - // stream: true, - // stream_options: { include_usage: true }, - // ...this.getProviderParams(), // kilocode_change: original expression was moved into function - // ...(transforms && { transforms }), - // ...(reasoning && { reasoning }), - // } - const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelId, temperature: 0, @@ -251,6 +226,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH let stream try { console.log("requestOptions", requestOptions) + console.log("metadata", metadata) stream = await this.client.chat.completions.create( requestOptions, this.customRequestOptions(metadata), // kilocode_change diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 75614d7f30..af3f4f83f8 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -16,6 +16,7 @@ import { writeToFileTool } from "../tools/writeToFileTool" import { applyDiffTool } from "../tools/multiApplyDiffTool" import { insertContentTool } from "../tools/insertContentTool" import { searchAndReplaceTool } from "../tools/searchAndReplaceTool" +import { fileEditTool } from "../tools/fileEditTool" import { editFileTool } from "../tools/editFileTool" // kilocode_change: Morph fast apply import { listCodeDefinitionNamesTool } from "../tools/listCodeDefinitionNamesTool" import { searchFilesTool } from "../tools/searchFilesTool" @@ -201,6 +202,8 @@ export async function presentAssistantMessage(cline: Task) { }]` case "insert_content": return `[${block.name} for '${block.params.path}']` + case "file_edit": + return `[${block.name} for '${block.params.target_file}']` case "search_and_replace": return `[${block.name} for '${block.params.path}']` // kilocode_change start: Morph fast apply @@ -508,6 +511,9 @@ export async function presentAssistantMessage(cline: Task) { // await checkpointSaveAndMark(cline) // kilocode_change await searchAndReplaceTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break + case "file_edit": + await fileEditTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + break // kilocode_change start: Morph fast apply case "edit_file": await editFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap index 9268b83591..3e4bead9d0 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap @@ -555,7 +555,7 @@ CAPABILITIES - When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop. - You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring. - You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task. - - For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the apply_diff or write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed. + - For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the apply_diff, file_edit, or write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed. - You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance. ==== @@ -573,7 +573,7 @@ RULES - You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path. - Do not use the ~ character or $HOME to refer to the home directory. - Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`. -- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using apply_diff or write_to_file to make informed changes. +- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using apply_diff, file_edit, or write_to_file to make informed changes. - When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser. - For editing files, you have access to these tools: apply_diff (for surgical edits - targeted changes to specific lines or functions), write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files), search_and_replace (for finding and replacing individual pieces of text). - The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line. diff --git a/src/core/prompts/__tests__/sections.spec.ts b/src/core/prompts/__tests__/sections.spec.ts index 68458631ea..6be9cffd4c 100644 --- a/src/core/prompts/__tests__/sections.spec.ts +++ b/src/core/prompts/__tests__/sections.spec.ts @@ -43,15 +43,14 @@ describe("getCapabilitiesSection", () => { it("includes apply_diff in capabilities when diffStrategy is provided", () => { const result = getCapabilitiesSection(cwd, false, mcpHub, mockDiffStrategy) - expect(result).toContain("apply_diff or") - expect(result).toContain("then use the apply_diff or write_to_file tool") + expect(result).toContain("apply_diff, file_edit, or write_to_file") + expect(result).toContain("then use the apply_diff, file_edit, or write_to_file tool") }) it("excludes apply_diff from capabilities when diffStrategy is undefined", () => { const result = getCapabilitiesSection(cwd, false, mcpHub, undefined) - expect(result).not.toContain("apply_diff or") + expect(result).not.toContain("apply_diff, file_edit, or write_to_file") expect(result).toContain("then use the write_to_file tool") - expect(result).not.toContain("apply_diff or write_to_file") }) }) diff --git a/src/core/prompts/sections/capabilities.ts b/src/core/prompts/sections/capabilities.ts index ed1cbf7ca0..942fd25100 100644 --- a/src/core/prompts/sections/capabilities.ts +++ b/src/core/prompts/sections/capabilities.ts @@ -34,7 +34,13 @@ CAPABILITIES } - You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring. - You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task. - - For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use ${kiloCodeUseMorph ? "the edit_file" : diffStrategy ? "the apply_diff or write_to_file" : "the write_to_file"} tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed. + - For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use ${ + kiloCodeUseMorph + ? "the edit_file" + : diffStrategy + ? "the apply_diff, file_edit, or write_to_file" + : "the write_to_file" + } tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed. - You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.${ supportsComputerUse ? "\n- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.\n - For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser." diff --git a/src/core/prompts/sections/rules.ts b/src/core/prompts/sections/rules.ts index c3125490bc..03e657851d 100644 --- a/src/core/prompts/sections/rules.ts +++ b/src/core/prompts/sections/rules.ts @@ -84,7 +84,9 @@ RULES - You cannot \`cd\` into a different directory to complete a task. You are stuck operating from '${cwd.toPosix()}', so be sure to pass in the correct 'path' parameter when using tools that require a path. - Do not use the ~ character or $HOME to refer to the home directory. - Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '${cwd.toPosix()}', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '${cwd.toPosix()}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd.toPosix()}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) && (command, in this case npm install)\`. -${codebaseSearchRule}- When using the search_files tool${isCodebaseSearchAvailable ? " (after codebase_search)" : ""}, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using ${kiloCodeUseMorph ? "edit_file" : diffStrategy ? "apply_diff or write_to_file" : "write_to_file"} to make informed changes. +${codebaseSearchRule}- When using the search_files tool${isCodebaseSearchAvailable ? " (after codebase_search)" : ""}, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using ${ + kiloCodeUseMorph ? "edit_file" : diffStrategy ? "apply_diff, file_edit, or write_to_file" : "write_to_file" + } to make informed changes. - When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the ${kiloCodeUseMorph ? "edit_file" : "write_to_file"} tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser. ${kiloCodeUseMorph ? getFastApplyEditingInstructions(getFastApplyModelType(clineProviderState)) : getEditingInstructions(diffStrategy)} - Some modes have restrictions on which files they can edit. If you attempt to edit a restricted file, the operation will be rejected with a FileRestrictionError that will specify which file patterns are allowed for the current mode. diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index e2e766b4b7..d6e008f915 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -32,6 +32,119 @@ export function getPromptComponent( return component } +const applyDiffToolDescription = ` +## apply_diff Tool Usage + +The \`apply_diff\` tool allows you to make precise, surgical edits to one or more files simultaneously. + +### CRITICAL: Single Content Field with Complete Structure + +**Each diff has ONLY TWO fields:** +- \`content\`: A single string containing the complete SEARCH/REPLACE block +- \`start_line\`: The line number where SEARCH begins + +**DO NOT create separate fields like \`search\`, \`replace\`, \`old\`, \`new\`, etc.** + +### REQUIRED Content Format + +The \`content\` field MUST contain this COMPLETE structure as a SINGLE STRING: +\`\`\` +<<<<<<< SEARCH +[exact lines from original file] +======= +[new lines to replace with] +>>>>>>> REPLACE +\`\`\` + +**All three markers must be present IN THE CONTENT STRING.** + +### Correct Example +\`\`\`json +{ + "files": [ + { + "path": "src/services/llmPricing.js", + "diffs": [ + { + "content": "<<<<<<< SEARCH\n \"accounts/fireworks/models/glm-4p5\": {\n input: 0.55,\n output: 2.19,\n },\n \"gpt-oss-120b\": {\n input: 0.25,\n output: 0.69,\n },\n=======\n \"accounts/fireworks/models/glm-4p5\": {\n input: 0.55,\n output: 2.19,\n },\n \"accounts/fireworks/models/glm-4.6\": {\n input: 0.6,\n output: 2.2,\n },\n \"gpt-oss-120b\": {\n input: 0.25,\n output: 0.69,\n },\n>>>>>>> REPLACE", + "start_line": 30 + } + ] + } + ] +} +\`\`\` + +### ❌ INCORRECT Examples +\`\`\`json +// WRONG - Incomplete content (missing ======= and >>>>>>> REPLACE) +{ + "content": "<<<<<<< SEARCH\n old code\n", + "start_line": 30 +} + +// WRONG - Creating separate fields +{ + "content": "<<<<<<< SEARCH\n old code\n", + "replace": "new code\n>>>>>>> REPLACE", + "start_line": 30 +} + +// WRONG - Using search/replace fields +{ + "search": "old code", + "replace": "new code", + "start_line": 30 +} +\`\`\` + +### Step-by-Step Process + +When creating a diff: + +1. **Identify the exact lines** to change from the original file +2. **Write the SEARCH block**: Include 2-3 lines of context before and after +3. **Add the separator**: \`=======\` on its own line +4. **Write the REPLACE block**: The new content (can include the context lines) +5. **Close with marker**: \`>>>>>>> REPLACE\` on its own line +6. **Combine into single string**: Put all of this into the \`content\` field +7. **Add start_line**: The line number where your SEARCH block begins + +### JSON Schema Reminder +\`\`\`typescript +{ + path: string, // File path + diffs: [ + { + content: string, // COMPLETE "<<<<<<< SEARCH\n...\n=======\n...\n>>>>>>> REPLACE" block + start_line: number // Line where SEARCH begins + } + ] +} +\`\`\` + +**Only these two fields exist in each diff object. Do not invent additional fields.** + +### Common Errors to Avoid + +- ❌ **Stopping the content string before \`=======\` and \`>>>>>>> REPLACE\`** (most common) +- ❌ Creating \`replace\`, \`search\`, \`old\`, or \`new\` fields +- ❌ Missing the \`=======\` separator line +- ❌ Missing the \`>>>>>>> REPLACE\` closing marker +- ❌ Not including enough context in SEARCH block +- ❌ Whitespace mismatches between SEARCH and original file + +### Verification Checklist + +Before submitting, verify each diff has: +- ✅ Single \`content\` field (not multiple fields) +- ✅ Starts with \`<<<<<<< SEARCH\n\` +- ✅ Contains \`=======\n\` in the middle +- ✅ Ends with \`>>>>>>> REPLACE\` +- ✅ SEARCH block matches original file exactly +- ✅ Correct \`start_line\` number +` + async function generatePrompt( context: vscode.ExtensionContext, cwd: string, @@ -60,7 +173,7 @@ async function generatePrompt( } // If diff is disabled, don't pass the diffStrategy - const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined + const effectiveDiffStrategy = diffStrategy // Get the full mode config to ensure we have the role definition (used for groups, etc.) const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0] @@ -103,6 +216,8 @@ ${ : "" } +${applyDiffToolDescription} + ${mcpServersSection} ${getSystemInfoSection(cwd)} diff --git a/src/core/prompts/tools/file-edit.ts b/src/core/prompts/tools/file-edit.ts new file mode 100644 index 0000000000..42cd07e5d8 --- /dev/null +++ b/src/core/prompts/tools/file-edit.ts @@ -0,0 +1,21 @@ +export function getFileEditDescription(): string { + return `## file_edit + +**Description**: Perform targeted text replacements within a single file without constructing manual diff blocks. + +**When to use**: +- You know the exact text that should be replaced and its updated form. +- You want a deterministic edit without invoking Fast Apply models. +- You need to delete or rewrite a block of code but don't want to craft search/replace diff markers manually. + +**Parameters**: +1. \`target_file\` — Relative path to the file you want to modify. +2. \`old_string\` — The current text you expect to replace. Provide enough context for a unique match; this can be empty to replace the entire file. +3. \`new_string\` — The text that should replace the match. Use an empty string to delete the matched content. +4. \`replace_all\` (optional, default false) — Set to true to replace every occurrence of the matched text. Leave false to replace only a single uniquely identified match. + +**Guidance**: +- Prefer multi-line snippets for \`old_string\` to help the tool locate the correct section. +- If multiple matches exist, either refine \`old_string\` or set \`replace_all\` to true when you intend to change every occurrence. +- The tool shows a diff before applying changes so you can confirm the result.` +} diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index acc1b15500..f68d9609e8 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -19,6 +19,7 @@ import { getSearchFilesDescription } from "./search-files" import { getListFilesDescription } from "./list-files" import { getInsertContentDescription } from "./insert-content" import { getSearchAndReplaceDescription } from "./search-and-replace" +import { getFileEditDescription } from "./file-edit" import { getListCodeDefinitionNamesDescription } from "./list-code-definition-names" import { getBrowserActionDescription } from "./browser-action" import { getAskFollowupQuestionDescription } from "./ask-followup-question" @@ -65,6 +66,7 @@ const toolDescriptionMap: Record string | undefined> new_task: (args) => getNewTaskDescription(args), insert_content: (args) => getInsertContentDescription(args), search_and_replace: (args) => getSearchAndReplaceDescription(args), + file_edit: () => getFileEditDescription(), edit_file: () => getEditFileDescription(), // kilocode_change: Morph fast apply apply_diff: (args) => args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "", @@ -143,7 +145,13 @@ export function getToolDescriptionsForMode( // kilocode_change start: Morph fast apply if (isFastApplyAvailable(clineProviderState)) { // When Morph is enabled, disable traditional editing tools - const traditionalEditingTools = ["apply_diff", "write_to_file", "insert_content", "search_and_replace"] + const traditionalEditingTools = [ + "apply_diff", + "file_edit", + "write_to_file", + "insert_content", + "search_and_replace", + ] traditionalEditingTools.forEach((tool) => tools.delete(tool)) } else { tools.delete("edit_file") @@ -199,6 +207,7 @@ export { getSwitchModeDescription, getInsertContentDescription, getSearchAndReplaceDescription, + getFileEditDescription, getEditFileDescription, // kilocode_change: Morph fast apply getCodebaseSearchDescription, getRunSlashCommandDescription, diff --git a/src/core/prompts/tools/native-tools/apply_diff.ts b/src/core/prompts/tools/native-tools/apply_diff.ts index 2c7351d4cf..412578413e 100644 --- a/src/core/prompts/tools/native-tools/apply_diff.ts +++ b/src/core/prompts/tools/native-tools/apply_diff.ts @@ -7,6 +7,7 @@ export const apply_diff_single_file = { description: ` Apply precise, targeted modifications to an existing file using one or more search/replace blocks. This tool is for surgical edits only; the 'SEARCH' block must exactly match the existing content, including whitespace and indentation. To make multiple targeted changes, provide multiple SEARCH/REPLACE blocks in the 'diff' parameter. Use the 'read_file' tool first if you are not confident in the exact content to search for. `, + strict: true, parameters: { type: "object", properties: { @@ -41,6 +42,7 @@ export const apply_diff_multi_file = { name: "apply_diff", description: "Apply precise, targeted modifications to one or more files by searching for specific sections of content and replacing them. This tool is for surgical edits only and supports making changes across multiple files in a single request. The 'SEARCH' block must exactly match the existing content, including whitespace and indentation. You must use this tool to edit multiple files in a single operation whenever possible.", + strict: true, parameters: { type: "object", properties: { diff --git a/src/core/prompts/tools/native-tools/file_edit.ts b/src/core/prompts/tools/native-tools/file_edit.ts new file mode 100644 index 0000000000..56d6274042 --- /dev/null +++ b/src/core/prompts/tools/native-tools/file_edit.ts @@ -0,0 +1,37 @@ +import type OpenAI from "openai" + +export default { + type: "function", + function: { + name: "file_edit", + description: + "Replace existing text within a single file without constructing manual diff blocks. Provide the current text (`old_string`) and the desired text (`new_string`). By default only a single uniquely matched occurrence is replaced; set `replace_all` to true to update every matching occurrence.", + strict: true, + parameters: { + type: "object", + properties: { + target_file: { + type: "string", + description: "Path to the file to modify, relative to the workspace root.", + }, + old_string: { + type: "string", + description: + "Exact text to replace. Provide enough context for a unique match. Use an empty string to replace the entire file.", + }, + new_string: { + type: "string", + description: + "Replacement text. This will be inserted in place of the matched section. Can be an empty string to delete the match.", + }, + replace_all: { + type: "boolean", + description: + "Set to true to replace every occurrence of the matched text. Defaults to false (replace a single uniquely identified occurrence).", + }, + }, + required: ["target_file", "old_string", "new_string"], + additionalProperties: false, + }, + }, +} satisfies OpenAI.Chat.ChatCompletionTool diff --git a/src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts b/src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts index d0bd9823d7..c7b0af417b 100644 --- a/src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts +++ b/src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts @@ -55,7 +55,13 @@ export function getAllowedJSONToolsForMode( if (isFastApplyAvailable(clineProviderState)) { // When Morph is enabled, disable traditional editing tools - const traditionalEditingTools = ["apply_diff", "write_to_file", "insert_content", "search_and_replace"] + const traditionalEditingTools = [ + "apply_diff", + "file_edit", + "write_to_file", + "insert_content", + "search_and_replace", + ] traditionalEditingTools.forEach((tool) => tools.delete(tool)) } else { tools.delete("edit_file") diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 521780f045..c25c0d4aca 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -3,7 +3,6 @@ import askFollowupQuestion from "./ask_followup_question" import attemptCompletion from "./attempt_completion" import browserAction from "./browser_action" import codebaseSearch from "./codebase_search" -// import editFile from "./edit_file" import executeCommand from "./execute_command" import fetchInstructions from "./fetch_instructions" import generateImage from "./generate_image" @@ -19,18 +18,21 @@ import searchFiles from "./search_files" import updateTodoList from "./update_todo_list" import writeToFile from "./write_to_file" import { apply_diff_single_file, apply_diff_multi_file } from "./apply_diff" +import editFile from "./edit_file" +import fileEdit from "./file_edit" export const nativeTools = [ - apply_diff_single_file, - apply_diff_multi_file, + // apply_diff_single_file, + // apply_diff_multi_file, + fileEdit, askFollowupQuestion, attemptCompletion, - browserAction, - codebaseSearch, + // browserAction, + // codebaseSearch, // editFile, executeCommand, fetchInstructions, - generateImage, + // generateImage, insertContent, listCodeDefinitionNames, listFiles, diff --git a/src/core/prompts/tools/native-tools/search_files.ts b/src/core/prompts/tools/native-tools/search_files.ts index c4ab0306ae..95f60db0ae 100644 --- a/src/core/prompts/tools/native-tools/search_files.ts +++ b/src/core/prompts/tools/native-tools/search_files.ts @@ -19,7 +19,7 @@ export default { }, file_pattern: { type: ["string", "null"], - description: "Optional glob to limit which files are searched (e.g., *.ts)", + description: "Optional string glob to limit which files are searched (e.g., '*.ts')", }, }, required: ["path", "regex", "file_pattern"], diff --git a/src/core/tools/fileEditTool.ts b/src/core/tools/fileEditTool.ts new file mode 100644 index 0000000000..34914afc32 --- /dev/null +++ b/src/core/tools/fileEditTool.ts @@ -0,0 +1,661 @@ +import path from "path" +import { promises as fs } from "fs" + +import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" + +import { Task } from "../task/Task" +import { formatResponse } from "../prompts/responses" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolUse } from "../../shared/tools" +import { fileExistsAtPath } from "../../utils/fs" +import { getReadablePath } from "../../utils/path" +import { ClineSayTool } from "../../shared/ExtensionMessage" +import { experiments, EXPERIMENT_IDS } from "../../shared/experiments" +import { RecordSource } from "../context-tracking/FileContextTrackerTypes" + +type ReplacementResult = { + content: string + replacements: number +} + +type Replacer = (content: string, find: string) => Generator + +const PREVIEW_LIMIT = 500 + +export async function fileEditTool( + cline: Task, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, +): Promise { + const targetFile = block.params.target_file + const oldString = block.params.old_string + const newString = block.params.new_string + const replaceAllFlag = block.params.replace_all + const replaceAll = replaceAllFlag === "true" || replaceAllFlag === "1" + + try { + if (block.partial) { + const partialMessageProps: ClineSayTool = { + tool: "fileEdit", + path: getReadablePath(cline.cwd, removeClosingTag("target_file", targetFile)), + search: removeClosingTag("old_string", oldString), + replace: removeClosingTag("new_string", newString), + useRegex: false, + ignoreCase: false, + replaceAll, + startLine: undefined, + endLine: undefined, + } + + await cline.ask("tool", JSON.stringify(partialMessageProps), block.partial).catch(() => {}) + return + } + + if (!(await validateParams(cline, targetFile, oldString, newString, pushToolResult))) { + return + } + + const relPath = targetFile as string + const readablePath = getReadablePath(cline.cwd, relPath) + const absolutePath = path.resolve(cline.cwd, relPath) + + const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath) + if (!accessAllowed) { + await cline.say("rooignore_error", relPath) + pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath))) + return + } + + const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false + const fileExists = await fileExistsAtPath(absolutePath) + + if (!fileExists && oldString) { + const trimmedOld = oldString.trim() + if (trimmedOld.length > 0) { + cline.consecutiveMistakeCount++ + cline.recordToolError("file_edit") + const errorMessage = `File does not exist at path: ${absolutePath}\nCannot replace non-empty old_string in a missing file.` + const formattedError = formatResponse.toolError(errorMessage) + await cline.say("error", formattedError) + pushToolResult(formattedError) + return + } + } + + const originalContent = fileExists ? await fs.readFile(absolutePath, "utf-8") : "" + let replacement: ReplacementResult + + try { + replacement = performReplacement(originalContent, oldString ?? "", newString ?? "", replaceAll) + } catch (error) { + cline.consecutiveMistakeCount++ + cline.recordToolError("file_edit") + const message = error instanceof Error ? error.message : String(error) + const formattedError = formatResponse.toolError(message) + await cline.say("error", formattedError) + pushToolResult(formattedError) + return + } + + const newContent = replacement.content + + if (newContent === originalContent) { + pushToolResult(`No changes needed for '${relPath}'.`) + return + } + + const diff = formatResponse.createPrettyPatch(relPath, originalContent, newContent) + + if (!diff) { + pushToolResult(`No changes needed for '${relPath}'.`) + return + } + + const provider = cline.providerRef.deref() + const state = await provider?.getState() + const diagnosticsEnabled = state?.diagnosticsEnabled ?? true + const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS + const isPreventFocusDisruptionEnabled = experiments.isEnabled( + state?.experiments ?? {}, + EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION, + ) + + cline.diffViewProvider.editType = fileExists ? "modify" : "create" + cline.diffViewProvider.originalContent = originalContent + + if (!isPreventFocusDisruptionEnabled) { + await cline.diffViewProvider.open(relPath) + await cline.diffViewProvider.update(newContent, true) + cline.diffViewProvider.scrollToFirstDiff() + } + + const approvalMessage = JSON.stringify({ + tool: "fileEdit", + path: readablePath, + diff, + isProtected: isWriteProtected, + search: truncatePreview(oldString ?? "", PREVIEW_LIMIT), + replace: truncatePreview(newString ?? "", PREVIEW_LIMIT), + useRegex: false, + ignoreCase: false, + replaceAll, + } satisfies ClineSayTool) + + const approved = await askApproval("tool", approvalMessage, undefined, isWriteProtected) + + if (!approved) { + if (!isPreventFocusDisruptionEnabled) { + await cline.diffViewProvider.revertChanges() + } + pushToolResult("Changes were rejected by the user.") + await cline.diffViewProvider.reset() + return + } + + if (isPreventFocusDisruptionEnabled) { + await cline.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs) + } else { + await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + } + + await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource) + cline.didEditFile = true + cline.consecutiveMistakeCount = 0 + cline.recordToolUsage("file_edit") + + const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) + pushToolResult(message) + + await cline.diffViewProvider.reset() + cline.processQueuedMessages() + } catch (error) { + await handleError("editing file content", error as Error) + await cline.diffViewProvider.reset() + } +} + +async function validateParams( + cline: Task, + targetFile: string | undefined, + oldString: string | undefined, + newString: string | undefined, + pushToolResult: PushToolResult, +): Promise { + if (!targetFile) { + cline.consecutiveMistakeCount++ + cline.recordToolError("file_edit") + pushToolResult(await cline.sayAndCreateMissingParamError("file_edit", "target_file")) + return false + } + + if (oldString === undefined) { + cline.consecutiveMistakeCount++ + cline.recordToolError("file_edit") + pushToolResult(await cline.sayAndCreateMissingParamError("file_edit", "old_string")) + return false + } + + if (newString === undefined) { + cline.consecutiveMistakeCount++ + cline.recordToolError("file_edit") + pushToolResult(await cline.sayAndCreateMissingParamError("file_edit", "new_string")) + return false + } + + if (oldString === newString) { + cline.consecutiveMistakeCount++ + cline.recordToolError("file_edit") + const errorMessage = formatResponse.toolError( + "`old_string` and `new_string` must be different to perform a replacement.", + ) + await cline.say("error", errorMessage) + pushToolResult(errorMessage) + return false + } + + return true +} + +function performReplacement( + content: string, + oldString: string, + newString: string, + replaceAll: boolean, +): ReplacementResult { + if (oldString === "") { + return { content: newString, replacements: newString === content ? 0 : 1 } + } + + let foundMatch = false + let sawAmbiguousMatch = false + + for (const replacer of REPLACERS) { + const candidates = Array.from(new Set(replacer(content, oldString))) + for (const candidate of candidates) { + if (!candidate) continue + const firstIndex = content.indexOf(candidate) + if (firstIndex === -1) continue + + foundMatch = true + + if (replaceAll) { + const occurrences = countOccurrences(content, candidate) + if (occurrences === 0) continue + return { + content: content.split(candidate).join(newString), + replacements: occurrences, + } + } + + const lastIndex = content.lastIndexOf(candidate) + if (firstIndex === lastIndex) { + return { + content: content.slice(0, firstIndex) + newString + content.slice(firstIndex + candidate.length), + replacements: 1, + } + } + + sawAmbiguousMatch = true + } + } + + if (!foundMatch) { + throw new Error("old_string not found in file content.") + } + + if (sawAmbiguousMatch && !replaceAll) { + throw new Error("old_string matched multiple locations. Provide more context or set replace_all to true.") + } + + throw new Error("Unable to apply replacement. Provide additional context for old_string.") +} + +function countOccurrences(haystack: string, needle: string): number { + if (!needle) { + return 0 + } + + let count = 0 + let index = 0 + while ((index = haystack.indexOf(needle, index)) !== -1) { + count++ + index += needle.length + } + return count +} + +function truncatePreview(value: string, limit: number): string { + if (value.length <= limit) { + return value + } + return value.slice(0, limit) + "\n...(truncated)" +} + +function* simpleReplacer(content: string, find: string): Generator { + if (find.length === 0) return + if (content.includes(find)) { + yield find + } +} + +function* lineTrimmedReplacer(content: string, find: string): Generator { + const originalLines = content.split("\n") + const searchLines = find.split("\n") + + if (searchLines[searchLines.length - 1] === "") { + searchLines.pop() + } + + for (let i = 0; i <= originalLines.length - searchLines.length; i++) { + let matches = true + + for (let j = 0; j < searchLines.length; j++) { + if (originalLines[i + j].trim() !== searchLines[j].trim()) { + matches = false + break + } + } + + if (!matches) continue + + let startIndex = 0 + for (let k = 0; k < i; k++) { + startIndex += originalLines[k].length + 1 + } + + let endIndex = startIndex + for (let k = 0; k < searchLines.length; k++) { + endIndex += originalLines[i + k].length + if (k < searchLines.length - 1) { + endIndex += 1 + } + } + + yield content.substring(startIndex, endIndex) + } +} + +function* blockAnchorReplacer(content: string, find: string): Generator { + const originalLines = content.split("\n") + const searchLines = find.split("\n") + + if (searchLines.length < 3) return + if (searchLines[searchLines.length - 1] === "") { + searchLines.pop() + } + + const firstLineSearch = searchLines[0].trim() + const lastLineSearch = searchLines[searchLines.length - 1].trim() + const searchBlockSize = searchLines.length + + const candidates: Array<{ start: number; end: number }> = [] + + for (let i = 0; i < originalLines.length; i++) { + if (originalLines[i].trim() !== firstLineSearch) continue + + for (let j = i + 2; j < originalLines.length; j++) { + if (originalLines[j].trim() === lastLineSearch) { + candidates.push({ start: i, end: j }) + break + } + } + } + + if (candidates.length === 0) return + + if (candidates.length === 1) { + const { start, end } = candidates[0] + let similarity = 0 + const actualSize = end - start + 1 + const linesToCheck = Math.min(searchBlockSize - 2, actualSize - 2) + + if (linesToCheck > 0) { + for (let j = 1; j < searchBlockSize - 1 && j < actualSize - 1; j++) { + const originalLine = originalLines[start + j].trim() + const searchLine = searchLines[j].trim() + const maxLen = Math.max(originalLine.length, searchLine.length) + if (maxLen === 0) continue + const distance = levenshtein(originalLine, searchLine) + similarity += (1 - distance / maxLen) / linesToCheck + if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) { + break + } + } + } else { + similarity = 1 + } + + if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) { + yield extractBlock(content, originalLines, start, end) + } + return + } + + let bestMatch: { start: number; end: number } | null = null + let maxSimilarity = -1 + + for (const candidate of candidates) { + const { start, end } = candidate + let similarity = 0 + const actualSize = end - start + 1 + const linesToCheck = Math.min(searchBlockSize - 2, actualSize - 2) + + if (linesToCheck > 0) { + for (let j = 1; j < searchBlockSize - 1 && j < actualSize - 1; j++) { + const originalLine = originalLines[start + j].trim() + const searchLine = searchLines[j].trim() + const maxLen = Math.max(originalLine.length, searchLine.length) + if (maxLen === 0) continue + const distance = levenshtein(originalLine, searchLine) + similarity += 1 - distance / maxLen + } + similarity /= linesToCheck + } else { + similarity = 1 + } + + if (similarity > maxSimilarity) { + maxSimilarity = similarity + bestMatch = candidate + } + } + + if (bestMatch && maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD) { + yield extractBlock(content, originalLines, bestMatch.start, bestMatch.end) + } +} + +function* whitespaceNormalizedReplacer(content: string, find: string): Generator { + const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim() + const normalizedFind = normalizeWhitespace(find) + + const lines = content.split("\n") + + for (const line of lines) { + const normalizedLine = normalizeWhitespace(line) + if (normalizedLine === normalizedFind) { + yield line + } else if (normalizedLine.includes(normalizedFind)) { + const words = find.trim().split(/\s+/) + if (words.length === 0) continue + const pattern = words.map((word) => escapeRegExp(word)).join("\\s+") + try { + const regex = new RegExp(pattern) + const match = line.match(regex) + if (match) { + yield match[0] + } + } catch { + // ignore invalid pattern + } + } + } + + const findLines = find.split("\n") + if (findLines.length > 1) { + for (let i = 0; i <= lines.length - findLines.length; i++) { + const block = lines.slice(i, i + findLines.length).join("\n") + if (normalizeWhitespace(block) === normalizedFind) { + yield block + } + } + } +} + +function* indentationFlexibleReplacer(content: string, find: string): Generator { + const removeIndentation = (text: string) => { + const lines = text.split("\n") + const nonEmpty = lines.filter((line) => line.trim().length > 0) + if (nonEmpty.length === 0) return text + const minIndent = Math.min( + ...nonEmpty.map((line) => { + const match = line.match(/^(\s*)/) + return match ? match[1].length : 0 + }), + ) + return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join("\n") + } + + const normalizedFind = removeIndentation(find) + const contentLines = content.split("\n") + const findLines = find.split("\n") + + for (let i = 0; i <= contentLines.length - findLines.length; i++) { + const block = contentLines.slice(i, i + findLines.length).join("\n") + if (removeIndentation(block) === normalizedFind) { + yield block + } + } +} + +function* escapeNormalizedReplacer(content: string, find: string): Generator { + const unescapeString = (str: string): string => + str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, captured) => { + switch (captured) { + case "n": + return "\n" + case "t": + return "\t" + case "r": + return "\r" + case "'": + return "'" + case '"': + return '"' + case "`": + return "`" + case "\\": + return "\\" + case "\n": + return "\n" + case "$": + return "$" + default: + return match + } + }) + + const unescapedFind = unescapeString(find) + + if (content.includes(unescapedFind)) { + yield unescapedFind + } + + const lines = content.split("\n") + const findLines = unescapedFind.split("\n") + + for (let i = 0; i <= lines.length - findLines.length; i++) { + const block = lines.slice(i, i + findLines.length).join("\n") + const unescapedBlock = unescapeString(block) + if (unescapedBlock === unescapedFind) { + yield block + } + } +} + +function* multiOccurrenceReplacer(content: string, find: string): Generator { + if (find.length === 0) return + let startIndex = 0 + while (true) { + const index = content.indexOf(find, startIndex) + if (index === -1) break + yield find + startIndex = index + find.length + } +} + +function* trimmedBoundaryReplacer(content: string, find: string): Generator { + const trimmed = find.trim() + if (trimmed === find) return + + if (content.includes(trimmed)) { + yield trimmed + } + + const lines = content.split("\n") + const findLines = find.split("\n") + + for (let i = 0; i <= lines.length - findLines.length; i++) { + const block = lines.slice(i, i + findLines.length).join("\n") + if (block.trim() === trimmed) { + yield block + } + } +} + +function* contextAwareReplacer(content: string, find: string): Generator { + const findLines = find.split("\n") + if (findLines.length < 3) return + if (findLines[findLines.length - 1] === "") { + findLines.pop() + } + + const contentLines = content.split("\n") + const firstLine = findLines[0].trim() + const lastLine = findLines[findLines.length - 1].trim() + + for (let i = 0; i < contentLines.length; i++) { + if (contentLines[i].trim() !== firstLine) continue + + for (let j = i + 2; j < contentLines.length; j++) { + if (contentLines[j].trim() !== lastLine) continue + const blockLines = contentLines.slice(i, j + 1) + if (blockLines.length !== findLines.length) continue + + let matchingLines = 0 + let totalNonEmpty = 0 + for (let k = 1; k < blockLines.length - 1; k++) { + const blockLine = blockLines[k].trim() + const findLine = findLines[k].trim() + if (blockLine.length > 0 || findLine.length > 0) { + totalNonEmpty++ + if (blockLine === findLine) { + matchingLines++ + } + } + } + + if (totalNonEmpty === 0 || matchingLines / totalNonEmpty >= 0.5) { + yield blockLines.join("\n") + break + } + } + } +} + +function extractBlock(content: string, lines: string[], start: number, end: number): string { + let startIndex = 0 + for (let i = 0; i < start; i++) { + startIndex += lines[i].length + 1 + } + + let endIndex = startIndex + for (let i = start; i <= end; i++) { + endIndex += lines[i].length + if (i < end) { + endIndex += 1 + } + } + + return content.substring(startIndex, endIndex) +} + +function levenshtein(a: string, b: string): number { + if (a === "" || b === "") { + return Math.max(a.length, b.length) + } + + const matrix = Array.from({ length: a.length + 1 }, (_, i) => + Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)), + ) + + for (let i = 1; i <= a.length; i++) { + for (let j = 1; j <= b.length; j++) { + const cost = a[i - 1] === b[j - 1] ? 0 : 1 + matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost) + } + } + + return matrix[a.length][b.length] +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} + +const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0 +const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3 + +const REPLACERS: Replacer[] = [ + simpleReplacer, + lineTrimmedReplacer, + blockAnchorReplacer, + whitespaceNormalizedReplacer, + indentationFlexibleReplacer, + escapeNormalizedReplacer, + trimmedBoundaryReplacer, + contextAwareReplacer, + multiOccurrenceReplacer, +] diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 7c20c4b914..93e05dc2c3 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -462,6 +462,7 @@ export interface ClineSayTool { | "finishTask" | "searchAndReplace" | "insertContent" + | "fileEdit" | "generateImage" | "imageGenerated" | "runSlashCommand" @@ -479,6 +480,7 @@ export interface ClineSayTool { replace?: string useRegex?: boolean ignoreCase?: boolean + replaceAll?: boolean startLine?: number endLine?: number lineNumber?: number diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index bfdbb0f24b..3bc0837007 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -18,7 +18,7 @@ interface ExperimentConfig { } export const experimentConfigsMap: Record = { - MORPH_FAST_APPLY: { enabled: false }, // kilocode_change + MORPH_FAST_APPLY: { enabled: true }, // kilocode_change MULTI_FILE_APPLY_DIFF: { enabled: false }, POWER_STEERING: { enabled: false }, PREVENT_FOCUS_DISRUPTION: { enabled: false }, diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 8f994e8b25..a1b8af9cd1 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -63,6 +63,9 @@ export const toolParamNames = [ "title", "description", "target_file", + "old_string", + "new_string", + "replace_all", "instructions", "code_edit", "files", @@ -185,6 +188,12 @@ export interface SearchAndReplaceToolUse extends ToolUse { Partial, "use_regex" | "ignore_case" | "start_line" | "end_line">> } +export interface FileEditToolUse extends ToolUse { + name: "file_edit" + params: Required, "target_file" | "old_string" | "new_string">> & + Partial, "replace_all">> +} + // kilocode_change start: Morph fast apply export interface EditFileToolUse extends ToolUse { name: "edit_file" @@ -209,6 +218,7 @@ export const TOOL_DISPLAY_NAMES: Record = { fetch_instructions: "fetch instructions", write_to_file: "write files", apply_diff: "apply changes", + file_edit: "replace text in files", edit_file: "edit file", // kilocode_change: Morph fast apply search_files: "search files", list_files: "list files", @@ -246,6 +256,7 @@ export const TOOL_GROUPS: Record = { edit: { tools: [ "apply_diff", + "file_edit", "edit_file", // kilocode_change: Morph fast apply "write_to_file", "insert_content", diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 081b1547d7..50587b3233 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -265,7 +265,7 @@ export const ChatRowContent = ({ isCommandExecuting ? ( ) : ( - + ), {t("chat:commandExecution.running")}, ] @@ -444,6 +444,44 @@ export const ChatRowContent = ({
) + case "fileEdit": + return ( + <> +
+ {tool.isProtected ? ( + + ) : ( + toolIcon("edit") + )} + + {tool.isProtected + ? t("chat:fileOperations.wantsToEditProtected") + : tool.isOutsideWorkspace + ? t("chat:fileOperations.wantsToEditOutsideWorkspace") + : t("chat:fileOperations.wantsToEdit")} + +
+
+ vscode.postMessage({ type: "openFile", text: "./" + tool.path })} + /> + { + // kilocode_change start + tool.fastApplyResult && + // kilocode_change end + } +
+ + ) case "insertContent": return ( <> diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx index c98ddbde06..b4435e6e3e 100644 --- a/webview-ui/src/components/common/MarkdownBlock.tsx +++ b/webview-ui/src/components/common/MarkdownBlock.tsx @@ -136,7 +136,7 @@ const StyledMarkdown = styled.div` p { white-space: pre-wrap; - margin: 1em 0 0.25em; + margin: 0.25em 0 0.25em; } /* Prevent layout shifts during streaming */ From 8f2600ca94d27b563cb9748f90e70e19d241b6f9 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Tue, 11 Nov 2025 18:34:42 +0530 Subject: [PATCH 3/3] add exec cmd tool in prompt --- src/core/prompts/system.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index d6e008f915..d26e135f47 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -143,6 +143,17 @@ Before submitting, verify each diff has: - ✅ Ends with \`>>>>>>> REPLACE\` - ✅ SEARCH block matches original file exactly - ✅ Correct \`start_line\` number + +# execute_command + +The \`execute_command\` tool runs CLI commands on the user's system. It allows Axon Code to perform system operations, install dependencies, build projects, start servers, and execute other terminal-based tasks needed to accomplish user objectives. + +## Parameters + +The tool accepts these parameters: + +- \`command\` (required): The CLI command to execute. Must be valid for the user's operating system. +- \`cwd\` (optional): The working directory to execute the command in. If not provided, the current working directory is used. Ensure this is always an absolute path, starting with \`/\`. If you are running the command in the root directly, skip this parameter. The command executor is defaulted to run in the root directory. You already have the Current Workspace Directory in . ` async function generatePrompt(