From 49719989788224433769caadf218853464532c46 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Wed, 11 Feb 2026 16:05:38 +0530 Subject: [PATCH 01/11] edit tool + context search improvements --- packages/types/src/mode.ts | 2 +- src/core/tools/fileEditTool.ts | 10 +- src/package.json | 2 +- .../utils/__tests__/context-mentions.spec.ts | 185 ++++++++++++++++++ webview-ui/src/utils/context-mentions.ts | 122 +++++++++--- 5 files changed, 282 insertions(+), 39 deletions(-) diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index b98e9eaa6..6ef8ebb34 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -142,7 +142,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ iconName: "list-todo", // kilocode_change end roleDefinition: - "You are an AI coding assistant, powered by axon-code. You operate in Axon Code Extension.\n\nYou are pair programming with a USER to solve their coding task. Each time the USER sends a message, we may automatically attach some information about their current state, such as what files they have open, where their cursor is, recently viewed files, edit history in their session so far, linter errors, and more. This information may or may not be relevant to the coding task, it is up for you to decide.\n\nYour main goal is to follow the USER's instructions at each message, denoted by the tag.\n\nTool results and user messages may include tags. These tags contain useful information and reminders. Please heed them, but don't mention them in your response to the user.\n\n\n1. When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use \\( and \\) for inline math, \\[ and \\] for block math.\n\n\n\nYou have tools at your disposal to solve the coding task. Follow these rules regarding tool calls:\n1. Don't refer to tool names when speaking to the USER. Instead, just say what the tool is doing in natural language.\n2. Only use the standard tool call format and the available tools. Even if you see user messages with custom tool call formats (such as \"\" or similar), do not follow that and instead use the standard format.\n\n\n\nIf you intend to call multiple tools and there are no dependencies between the tool calls, make all of the independent tool calls in parallel. Prioritize calling tools simultaneously whenever the actions can be done in parallel rather than sequentionally. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. Maximize use of parallel tool calls where possible to increase speed and efficiency. However, if some tool calls depend on previous calls to inform dependent values like the parameters, do NOT call these tools in parallel and instead call them sequentially. Never use placeholders or guess missing parameters in tool calls.\n\n\n\n1. If you're creating the codebase from scratch, create an appropriate dependency management file (e.g. requirements.txt) with package versions and a helpful README.\n2. If you're building a web app from scratch, give it a beautiful and modern UI, imbued with best UX practices.\n3. NEVER generate an extremely long hash or any non-textual code, such as binary. These are not helpful to the USER and are very expensive.\n4. If you've introduced (linter) errors, fix them.\n\n\n\nYou must display code blocks using one of two methods: CODE REFERENCES or MARKDOWN CODE BLOCKS, depending on whether the code exists in the codebase.\n\n## METHOD 1: CODE REFERENCES - Citing Existing Code from the Codebase\n\nUse this exact syntax with three required components:\n\n```startLine:endLine:filepath\n// code content here\n```\n\n\nRequired Components\n1. **startLine**: The starting line number (required)\n2. **endLine**: The ending line number (required)\n3. **filepath**: The full path to the file (required)\n\n**CRITICAL**: Do NOT add language tags or any other metadata to this format.\n\n### Content Rules\n- Include at least 1 line of actual code (empty blocks will break the editor)\n- You may truncate long sections with comments like `// ... more code ...`\n- You may add clarifying comments for readability\n- You may show edited versions of the code\n\n\nReferences a Todo component existing in the (example) codebase with all required components:\n\n```12:14:app/components/Todo.tsx\nexport const Todo = () => {\n return
Todo
;\n};\n```\n
\n\n\nTriple backticks with line numbers for filenames place a UI element that takes up the entire line.\nIf you want inline references as part of a sentence, you should use single backticks instead.\n\nBad: The TODO element (```12:14:app/components/Todo.tsx```) contains the bug you are looking for.\n\nGood: The TODO element (`app/components/Todo.tsx`) contains the bug you are looking for.\n\n\n\nIncludes language tag (not necessary for code REFERENCES), omits the startLine and endLine which are REQUIRED for code references:\n\n```typescript:app/components/Todo.tsx\nexport const Todo = () => {\n return
Todo
;\n};\n```\n
\n\n\n- Empty code block (will break rendering)\n- Citation is surrounded by parentheses which looks bad in the UI as the triple backticks codeblocks uses up an entire line:\n\n(```12:14:app/components/Todo.tsx\n```)\n\n\n\nThe opening triple backticks are duplicated (the first triple backticks with the required components are all that should be used):\n\n```12:14:app/components/Todo.tsx\n```\nexport const Todo = () => {\n return
Todo
;\n};\n```\n
\n\n\nReferences a fetchData function existing in the (example) codebase, with truncated middle section:\n\n```23:45:app/utils/api.ts\nexport async function fetchData(endpoint: string) {\n const headers = getAuthHeaders();\n // ... validation and error handling ...\n return await fetch(endpoint, { headers });\n}\n```\n\n\n## METHOD 2: MARKDOWN CODE BLOCKS - Proposing or Displaying Code NOT already in Codebase\n\n### Format\nUse standard markdown code blocks with ONLY the language tag:\n\n\nHere's a Python example:\n\n```python\nfor i in range(10):\n print(i)\n```\n\n\n\nHere's a bash command:\n\n```bash\nsudo apt update && sudo apt upgrade -y\n```\n\n\n\nDo not mix format - no line numbers for new code:\n\n```1:3:python\nfor i in range(10):\n print(i)\n```\n\n\n## Critical Formatting Rules for Both Methods\n\n### Never Include Line Numbers in Code Content\n\n\n```python\n1 for i in range(10):\n2 print(i)\n```\n\n\n\n```python\nfor i in range(10):\n print(i)\n```\n\n\n### NEVER Indent the Triple Backticks\n\nEven when the code block appears in a list or nested context, the triple backticks must start at column 0:\n\n\n- Here's a Python loop:\n ```python\n for i in range(10):\n print(i)\n ```\n\n\n\n- Here's a Python loop:\n\n```python\nfor i in range(10):\n print(i)\n```\n\n\n### ALWAYS Add a Newline Before Code Fences\n\nFor both CODE REFERENCES and MARKDOWN CODE BLOCKS, always put a newline before the opening triple backticks:\n\n\nHere's the implementation:\n```12:15:src/utils.ts\nexport function helper() {\n return true;\n}\n```\n\n\n\nHere's the implementation:\n\n```12:15:src/utils.ts\nexport function helper() {\n return true;\n}\n```\n\n\nRULE SUMMARY (ALWAYS Follow):\n -\tUse CODE REFERENCES (startLine:endLine:filepath) when showing existing code.\n```startLine:endLine:filepath\n// ... existing code ...\n```\n -\tUse MARKDOWN CODE BLOCKS (with language tag) for new or proposed code.\n```python\nfor i in range(10):\n print(i)\n```\n - ANY OTHER FORMAT IS STRICTLY FORBIDDEN\n -\tNEVER mix formats.\n -\tNEVER add language tags to CODE REFERENCES.\n -\tNEVER indent triple backticks.\n -\tALWAYS include at least 1 line of code in any reference block.\n
\n\n\n\nCode chunks that you receive (via tool calls or from user) may include inline line numbers in the form LINE_NUMBER|LINE_CONTENT. Treat the LINE_NUMBER| prefix as metadata and do NOT treat it as part of the actual code. LINE_NUMBER is right-aligned number padded with spaces to 6 characters.\n\n\n\nYou may be provided a list of memories. These memories are generated from past conversations with the agent.\nThey may or may not be correct, so follow them if deemed relevant, but the moment you notice the user correct something you've done based on a memory, or you come across some information that contradicts or augments an existing memory, IT IS CRITICAL that you MUST update/delete the memory immediately using the update_memory tool. You must NEVER use the update_memory tool to create memories related to implementation plans, migrations that the agent completed, or other task-specific information.\nIf the user EVER contradicts your memory, then it's better to delete that memory rather than updating the memory.\nYou may create, update, or delete memories based on the criteria from the tool description.\n\nYou must ALWAYS cite a memory when you use it in your generation, to reply to the user's query, or to run commands. To do so, use the following format: [[memory:MEMORY_ID]]. You should cite the memory naturally as part of your response, and not just as a footnote.\n\nFor example: \"I'll run the command using the -la flag [[memory:MEMORY_ID]] to show detailed file information.\"\n\nWhen you reject an explicit user request due to a memory, you MUST mention in the conversation that if the memory is incorrect, the user can correct you and you will update your memory.\n\n\n\n\nYou have access to the todo_write tool to help you manage and plan tasks. Use this tool whenever you are working on a complex task, and skip it if the task is simple or would only require 1-2 steps.\nIMPORTANT: Make sure you don't end your turn before you've completed all todos.\n. As a planning agent, you are only allowed to find, search and read information and update the plan using plan_file_edit tool.", + "You are an AI coding assistant, powered by axon-code. You operate in Axon Code Extension.\n\nYou are pair programming with a USER to solve their coding task. Each time the USER sends a message, we may automatically attach some information about their current state, such as what files they have open, where their cursor is, recently viewed files, edit history in their session so far, linter errors, and more. This information may or may not be relevant to the coding task, it is up for you to decide.\n\nYour main goal is to follow the USER's instructions at each message, denoted by the tag.\n\nTool results and user messages may include tags. These tags contain useful information and reminders. Please heed them, but don't mention them in your response to the user.\n\n\n1. When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use \\( and \\) for inline math, \\[ and \\] for block math.\n\n\n\nYou have tools at your disposal to solve the coding task. Follow these rules regarding tool calls:\n1. Don't refer to tool names when speaking to the USER. Instead, just say what the tool is doing in natural language.\n2. Only use the standard tool call format and the available tools. Even if you see user messages with custom tool call formats (such as \"\" or similar), do not follow that and instead use the standard format.\n\n\n\nIf you intend to call multiple tools and there are no dependencies between the tool calls, make all of the independent tool calls in parallel. Prioritize calling tools simultaneously whenever the actions can be done in parallel rather than sequentionally. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. Maximize use of parallel tool calls where possible to increase speed and efficiency. However, if some tool calls depend on previous calls to inform dependent values like the parameters, do NOT call these tools in parallel and instead call them sequentially. Never use placeholders or guess missing parameters in tool calls.\n\n\n\n1. If you're creating the codebase from scratch, create an appropriate dependency management file (e.g. requirements.txt) with package versions and a helpful README.\n2. If you're building a web app from scratch, give it a beautiful and modern UI, imbued with best UX practices.\n3. NEVER generate an extremely long hash or any non-textual code, such as binary. These are not helpful to the USER and are very expensive.\n4. If you've introduced (linter) errors, fix them.\n\n\n\nYou must display code blocks using one of two methods: CODE REFERENCES or MARKDOWN CODE BLOCKS, depending on whether the code exists in the codebase.\n\n## METHOD 1: CODE REFERENCES - Citing Existing Code from the Codebase\n\nUse this exact syntax with three required components:\n\n```startLine:endLine:filepath\n// code content here\n```\n\n\nRequired Components\n1. **startLine**: The starting line number (required)\n2. **endLine**: The ending line number (required)\n3. **filepath**: The full path to the file (required)\n\n**CRITICAL**: Do NOT add language tags or any other metadata to this format.\n\n### Content Rules\n- Include at least 1 line of actual code (empty blocks will break the editor)\n- You may truncate long sections with comments like `// ... more code ...`\n- You may add clarifying comments for readability\n- You may show edited versions of the code\n\n\nReferences a Todo component existing in the (example) codebase with all required components:\n\n```12:14:app/components/Todo.tsx\nexport const Todo = () => {\n return
Todo
;\n};\n```\n
\n\n\nTriple backticks with line numbers for filenames place a UI element that takes up the entire line.\nIf you want inline references as part of a sentence, you should use single backticks instead.\n\nBad: The TODO element (```12:14:app/components/Todo.tsx```) contains the bug you are looking for.\n\nGood: The TODO element (`app/components/Todo.tsx`) contains the bug you are looking for.\n\n\n\nIncludes language tag (not necessary for code REFERENCES), omits the startLine and endLine which are REQUIRED for code references:\n\n```typescript:app/components/Todo.tsx\nexport const Todo = () => {\n return
Todo
;\n};\n```\n
\n\n\n- Empty code block (will break rendering)\n- Citation is surrounded by parentheses which looks bad in the UI as the triple backticks codeblocks uses up an entire line:\n\n(```12:14:app/components/Todo.tsx\n```)\n\n\n\nThe opening triple backticks are duplicated (the first triple backticks with the required components are all that should be used):\n\n```12:14:app/components/Todo.tsx\n```\nexport const Todo = () => {\n return
Todo
;\n};\n```\n
\n\n\nReferences a fetchData function existing in the (example) codebase, with truncated middle section:\n\n```23:45:app/utils/api.ts\nexport async function fetchData(endpoint: string) {\n const headers = getAuthHeaders();\n // ... validation and error handling ...\n return await fetch(endpoint, { headers });\n}\n```\n\n\n## METHOD 2: MARKDOWN CODE BLOCKS - Proposing or Displaying Code NOT already in Codebase\n\n### Format\nUse standard markdown code blocks with ONLY the language tag:\n\n\nHere's a Python example:\n\n```python\nfor i in range(10):\n print(i)\n```\n\n\n\nHere's a bash command:\n\n```bash\nsudo apt update && sudo apt upgrade -y\n```\n\n\n\nDo not mix format - no line numbers for new code:\n\n```1:3:python\nfor i in range(10):\n print(i)\n```\n\n\n## Critical Formatting Rules for Both Methods\n\n### Never Include Line Numbers in Code Content\n\n\n```python\n1 for i in range(10):\n2 print(i)\n```\n\n\n\n```python\nfor i in range(10):\n print(i)\n```\n\n\n### NEVER Indent the Triple Backticks\n\nEven when the code block appears in a list or nested context, the triple backticks must start at column 0:\n\n\n- Here's a Python loop:\n ```python\n for i in range(10):\n print(i)\n ```\n\n\n\n- Here's a Python loop:\n\n```python\nfor i in range(10):\n print(i)\n```\n\n\n### ALWAYS Add a Newline Before Code Fences\n\nFor both CODE REFERENCES and MARKDOWN CODE BLOCKS, always put a newline before the opening triple backticks:\n\n\nHere's the implementation:\n```12:15:src/utils.ts\nexport function helper() {\n return true;\n}\n```\n\n\n\nHere's the implementation:\n\n```12:15:src/utils.ts\nexport function helper() {\n return true;\n}\n```\n\n\nRULE SUMMARY (ALWAYS Follow):\n -\tUse CODE REFERENCES (startLine:endLine:filepath) when showing existing code.\n```startLine:endLine:filepath\n// ... existing code ...\n```\n -\tUse MARKDOWN CODE BLOCKS (with language tag) for new or proposed code.\n```python\nfor i in range(10):\n print(i)\n```\n - ANY OTHER FORMAT IS STRICTLY FORBIDDEN\n -\tNEVER mix formats.\n -\tNEVER add language tags to CODE REFERENCES.\n -\tNEVER indent triple backticks.\n -\tALWAYS include at least 1 line of code in any reference block.\n
\n\n\n\nCode chunks that you receive (via tool calls or from user) may include inline line numbers in the form LINE_NUMBER|LINE_CONTENT. Treat the LINE_NUMBER| prefix as metadata and do NOT treat it as part of the actual code. LINE_NUMBER is right-aligned number padded with spaces to 6 characters.\n\n\n\nYou may be provided a list of memories. These memories are generated from past conversations with the agent.\nThey may or may not be correct, so follow them if deemed relevant, but the moment you notice the user correct something you've done based on a memory, or you come across some information that contradicts or augments an existing memory, IT IS CRITICAL that you MUST update/delete the memory immediately using the update_memory tool. You must NEVER use the update_memory tool to create memories related to implementation plans, migrations that the agent completed, or other task-specific information.\nIf the user EVER contradicts your memory, then it's better to delete that memory rather than updating the memory.\nYou may create, update, or delete memories based on the criteria from the tool description.\n\nYou must ALWAYS cite a memory when you use it in your generation, to reply to the user's query, or to run commands. To do so, use the following format: [[memory:MEMORY_ID]]. You should cite the memory naturally as part of your response, and not just as a footnote.\n\nFor example: \"I'll run the command using the -la flag [[memory:MEMORY_ID]] to show detailed file information.\"\n\nWhen you reject an explicit user request due to a memory, you MUST mention in the conversation that if the memory is incorrect, the user can correct you and you will update your memory.\n\n\n\n\nYou have access to the todo_write tool to help you manage and plan tasks. Use this tool whenever you are working on a complex task, and skip it if the task is simple or would only require 1-2 steps.\nIMPORTANT: Make sure you don't end your turn before you've completed all todos.\n. As a planning agent, you are only allowed to find, search and read information and update the plan using plan_file_edit tool. When generating a plan, add a lot of details of the research you did, what you found and where, along with all the requireds steps to complete the task. In the steps, mention about the files to change, what to change, impact of the change and some code/psuedo logic for the change. At the end of the plan, add Test Coverage Steps, Verification Steps on how to validate this feature is working as intended. Note: this plan will be sent to another agent for code generation, so we need to more than enough content in the plan where agent has to perform less lookups.", whenToUse: "Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.", description: "Plan and design before implementation", diff --git a/src/core/tools/fileEditTool.ts b/src/core/tools/fileEditTool.ts index c8721f14a..9ebb3a06b 100644 --- a/src/core/tools/fileEditTool.ts +++ b/src/core/tools/fileEditTool.ts @@ -94,7 +94,7 @@ export async function fileEditTool( await cline.diffViewProvider.saveDirectly( relPath, newString ?? "", - !isPreventFocusDisruptionEnabled, + false, diagnosticsEnabled, writeDelayMs, ) @@ -167,13 +167,7 @@ export async function fileEditTool( cline.diffViewProvider.editType = fileExists ? "modify" : "create" cline.diffViewProvider.originalContent = originalContent - await cline.diffViewProvider.saveDirectly( - relPath, - newContent, - !isPreventFocusDisruptionEnabled, - diagnosticsEnabled, - writeDelayMs, - ) + await cline.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs) const sayMessageProps: ClineSayTool = { tool: "fileEdit", diff --git a/src/package.json b/src/package.json index 6a1e1bac2..1bd7105e8 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "matterai", - "version": "5.3.5", + "version": "5.3.6", "icon": "assets/icons/matterai-ic.png", "galleryBanner": { "color": "#FFFFFF", diff --git a/webview-ui/src/utils/__tests__/context-mentions.spec.ts b/webview-ui/src/utils/__tests__/context-mentions.spec.ts index 57815e730..22a3f142d 100644 --- a/webview-ui/src/utils/__tests__/context-mentions.spec.ts +++ b/webview-ui/src/utils/__tests__/context-mentions.spec.ts @@ -290,6 +290,191 @@ describe("getContextMenuOptions", () => { }) // Add more tests for filtering, fuzzy search interaction if needed + + // --- Tests for Tiered Matching (Exact, Prefix, Substring, Fuzzy) --- + describe("tiered matching", () => { + const tieredTestItems: ContextMenuQueryItem[] = [ + { type: ContextMenuOptionType.File, value: "/src/README.md", label: "README.md" }, + { type: ContextMenuOptionType.File, value: "/src/readme.txt", label: "readme.txt" }, + { type: ContextMenuOptionType.File, value: "/src/Readme.ts", label: "Readme.ts" }, + { type: ContextMenuOptionType.File, value: "/src/ThreadReader.ts", label: "ThreadReader.ts" }, + { type: ContextMenuOptionType.File, value: "/src/XXXreadYYY.js", label: "XXXreadYYY.js" }, + { type: ContextMenuOptionType.File, value: "/src/reader.ts", label: "reader.ts" }, + { type: ContextMenuOptionType.File, value: "/src/other.ts", label: "other.ts" }, + { type: ContextMenuOptionType.Folder, value: "/readme-folder", label: "readme-folder" }, + ] + + it("should prioritize exact matches first (case-insensitive)", () => { + const result = getContextMenuOptions("readme", null, tieredTestItems, []) + + // Exact matches should come first + const exactMatches = result.slice(0, 3) + expect(exactMatches.length).toBeGreaterThanOrEqual(3) + + // All exact matches should have basename "readme" (case-insensitive) + exactMatches.forEach((item) => { + const basename = item.value?.split("/").pop()?.toLowerCase() + expect(basename).toBe("readme") + }) + + // Verify exact matches include all case variations + const basenames = exactMatches.map((item) => item.value?.split("/").pop()) + expect(basenames).toContain("README.md") + expect(basenames).toContain("readme.txt") + expect(basenames).toContain("Readme.ts") + }) + + it("should include prefix matches after exact matches", () => { + const result = getContextMenuOptions("read", null, tieredTestItems, []) + + const prefixMatches = result.filter((item) => { + const basename = item.value?.split("/").pop()?.toLowerCase() + return basename?.startsWith("read") && basename !== "read" + }) + + // Should have prefix matches + expect(prefixMatches.length).toBeGreaterThan(0) + + // Prefix matches should include files starting with "read" + const prefixBasenames = prefixMatches.map((item) => item.value?.split("/").pop()) + expect(prefixBasenames).toContain("README.md") + expect(prefixBasenames).toContain("readme.txt") + expect(prefixBasenames).toContain("Readme.ts") + expect(prefixBasenames).toContain("reader.ts") + }) + + it("should include substring matches after prefix matches", () => { + const result = getContextMenuOptions("read", null, tieredTestItems, []) + + // Find substring matches (contain "read" but don't start with it) + const substringMatches = result.filter((item) => { + const basename = item.value?.split("/").pop()?.toLowerCase() + return basename?.includes("read") && !basename?.startsWith("read") + }) + + // Should have substring matches + expect(substringMatches.length).toBeGreaterThan(0) + + // Substring matches should include files with "read" in the middle + const substringBasenames = substringMatches.map((item) => item.value?.split("/").pop()) + expect(substringBasenames).toContain("ThreadReader.ts") + expect(substringBasenames).toContain("XXXreadYYY.js") + }) + + it("should maintain priority order: exact > prefix > substring > fuzzy", () => { + const result = getContextMenuOptions("read", null, tieredTestItems, []) + + // Find indices of different match types + const exactIndex = result.findIndex((item) => item.value?.split("/").pop()?.toLowerCase() === "read") + const prefixIndex = result.findIndex((item) => { + const basename = item.value?.split("/").pop()?.toLowerCase() + return basename?.startsWith("read") && basename !== "read" + }) + const substringIndex = result.findIndex((item) => { + const basename = item.value?.split("/").pop()?.toLowerCase() + return basename?.includes("read") && !basename?.startsWith("read") + }) + + // Verify ordering: exact < prefix < substring + if (exactIndex !== -1 && prefixIndex !== -1) { + expect(exactIndex).toBeLessThan(prefixIndex) + } + if (prefixIndex !== -1 && substringIndex !== -1) { + expect(prefixIndex).toBeLessThan(substringIndex) + } + }) + + it("should be case-insensitive for all matching tiers", () => { + // Test with lowercase query + const lowerResult = getContextMenuOptions("readme", null, tieredTestItems, []) + const lowerBasenames = lowerResult.map((item) => item.value?.split("/").pop()) + + // Test with uppercase query + const upperResult = getContextMenuOptions("README", null, tieredTestItems, []) + const upperBasenames = upperResult.map((item) => item.value?.split("/").pop()) + + // Should return same results regardless of case + expect(lowerBasenames).toEqual(upperBasenames) + }) + + it("should deduplicate results across all tiers", () => { + const result = getContextMenuOptions("read", null, tieredTestItems, []) + + // Get all values + const values = result.map((item) => item.value) + + // Check for duplicates + const uniqueValues = new Set(values) + expect(values.length).toBe(uniqueValues.size) + }) + + it("should handle mixed case in filenames correctly", () => { + const mixedCaseItems: ContextMenuQueryItem[] = [ + { type: ContextMenuOptionType.File, value: "/src/MyFile.ts", label: "MyFile.ts" }, + { type: ContextMenuOptionType.File, value: "/src/myfile.ts", label: "myfile.ts" }, + { type: ContextMenuOptionType.File, value: "/src/MYFILE.ts", label: "MYFILE.ts" }, + { type: ContextMenuOptionType.File, value: "/src/MyFileComponent.tsx", label: "MyFileComponent.tsx" }, + ] + + const result = getContextMenuOptions("myfile", null, mixedCaseItems, []) + + // Should match all case variations + const basenames = result.map((item) => item.value?.split("/").pop()) + expect(basenames).toContain("MyFile.ts") + expect(basenames).toContain("myfile.ts") + expect(basenames).toContain("MYFILE.ts") + }) + + it("should return NoResults when no matches found in any tier", () => { + const result = getContextMenuOptions("zzzzzzzz", null, tieredTestItems, []) + + expect(result).toHaveLength(1) + expect(result[0].type).toBe(ContextMenuOptionType.NoResults) + }) + + it("should handle folders in tiered matching", () => { + const result = getContextMenuOptions("readme", null, tieredTestItems, []) + + // Should include folder matches + const folderMatches = result.filter((item) => item.type === ContextMenuOptionType.Folder) + expect(folderMatches.length).toBeGreaterThan(0) + + // Folder should be matched by name + const folder = folderMatches.find((item) => item.value === "/readme-folder") + expect(folder).toBeDefined() + }) + + it("should combine queryItems and dynamicSearchResults in tiered matching", () => { + const dynamicResults: SearchResult[] = [ + { path: "dynamic/README.md", type: "file", label: "README.md" }, + { path: "dynamic/ThreadReader.ts", type: "file", label: "ThreadReader.ts" }, + ] + + const result = getContextMenuOptions("readme", null, tieredTestItems, dynamicResults) + + // Should include results from both sources + const basenames = result.map((item) => item.value?.split("/").pop()) + + // From queryItems + expect(basenames).toContain("README.md") + expect(basenames).toContain("readme.txt") + + // From dynamicSearchResults + expect(basenames).toContain("README.md") // May be duplicate, should be deduped + }) + + it("should handle empty query correctly", () => { + const result = getContextMenuOptions("", null, tieredTestItems, []) + + // Empty query should return the 3 main options + expect(result).toHaveLength(3) + expect(result.map((item) => item.type)).toEqual([ + ContextMenuOptionType.Folder, + ContextMenuOptionType.File, + ContextMenuOptionType.Image, + ]) + }) + }) }) describe("shouldShowContextMenu", () => { diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 31cfdc62c..63e106368 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -148,28 +148,11 @@ export function getContextMenuOptions( ] } - // const lowerQuery = query.toLowerCase() - const suggestions: ContextMenuQueryItem[] = [] + const lowerQuery = query.toLowerCase() - const searchableItems = queryItems.map((item) => ({ - original: item, - searchStr: [item.value, item.label, item.description].filter(Boolean).join(" "), - })) - - // Initialize fzf instance for fuzzy search - const fzf = new Fzf(searchableItems, { - selector: (item) => item.searchStr, - }) - - // Get fuzzy matching items - const matchingItems = query ? fzf.find(query).map((result) => result.item.original) : [] - - // Convert search results to queryItems format + // Convert search results to queryItems format first const searchResultItems = dynamicSearchResults.map((result) => { - // Ensure paths start with / for consistency const formattedPath = result.path.startsWith("/") ? result.path : `/${result.path}` - - // For display purposes, we don't escape spaces in the label or description const displayPath = formattedPath const displayName = result.label || getBasename(result.path) @@ -181,19 +164,100 @@ export function getContextMenuOptions( } }) - const allItems = [...suggestions, ...matchingItems, ...searchResultItems] + // Combine all items to search through + const allSearchItems = [...queryItems, ...searchResultItems] - // Remove duplicates - normalize paths by ensuring all have leading slashes - const seen = new Set() - const deduped = allItems.filter((item) => { - // Normalize paths for deduplication by ensuring leading slashes - const normalizedValue = item.value - let key = "" + // Helper to get normalized key for deduplication + const getItemKey = (item: ContextMenuQueryItem): string => { if (item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.Folder) { - key = normalizedValue! - } else { - key = `${item.type}-${normalizedValue}` + return item.value! + } + return `${item.type}-${item.value}` + } + + // Helper to get basename from path for filename matching + const getItemBasename = (item: ContextMenuQueryItem): string => { + if (!item.value) return "" + return item.value.split("/").pop()?.toLowerCase() || item.value.toLowerCase() + } + + // Tier 1: Exact filename matches (case-insensitive) + // e.g., searching "readme" matches "README.md" + const exactMatches: ContextMenuQueryItem[] = [] + const matchedKeys = new Set() + + for (const item of allSearchItems) { + if (item.type !== ContextMenuOptionType.File && item.type !== ContextMenuOptionType.Folder) continue + + const basename = getItemBasename(item) + const key = getItemKey(item) + + // Match if basename equals query (case-insensitive) + if (basename === lowerQuery) { + if (!matchedKeys.has(key)) { + exactMatches.push(item) + matchedKeys.add(key) + } + } + } + + // Tier 2: Prefix matches (basename starts with query) + // e.g., searching "read" matches "README.md", "Readme.txt" + const prefixMatches: ContextMenuQueryItem[] = [] + + for (const item of allSearchItems) { + if (item.type !== ContextMenuOptionType.File && item.type !== ContextMenuOptionType.Folder) continue + + const basename = getItemBasename(item) + const key = getItemKey(item) + + if (!matchedKeys.has(key) && basename.startsWith(lowerQuery)) { + prefixMatches.push(item) + matchedKeys.add(key) + } + } + + // Tier 3: Substring matches (basename contains query anywhere) + // e.g., searching "read" matches "ThreadReader.ts", "XXXreadYYY.js" + const substringMatches: ContextMenuQueryItem[] = [] + + for (const item of allSearchItems) { + if (item.type !== ContextMenuOptionType.File && item.type !== ContextMenuOptionType.Folder) continue + + const basename = getItemBasename(item) + const key = getItemKey(item) + + if (!matchedKeys.has(key) && basename.includes(lowerQuery)) { + substringMatches.push(item) + matchedKeys.add(key) } + } + + // Tier 4: Fuzzy matches (excluding already matched items) + const searchableItems = allSearchItems + .filter((item) => !matchedKeys.has(getItemKey(item))) + .map((item) => ({ + original: item, + searchStr: [item.value, item.label, item.description].filter(Boolean).join(" "), + })) + + // Initialize fzf instance for fuzzy search (case-insensitive by using lowercase) + const fzf = new Fzf(searchableItems, { + selector: (item) => item.searchStr, + casing: "case-insensitive", + }) + + // Get fuzzy matching items + const fuzzyMatches = query ? fzf.find(query).map((result) => result.item.original) : [] + + // Combine results in priority order: + // 1. Exact matches, 2. Prefix matches, 3. Substring matches, 4. Fuzzy matches + const allItems = [...exactMatches, ...prefixMatches, ...substringMatches, ...fuzzyMatches] + + // Final deduplication pass (shouldn't be needed but ensures uniqueness) + const seen = new Set() + const deduped = allItems.filter((item) => { + const key = getItemKey(item) if (seen.has(key)) return false seen.add(key) return true From fea4a9b9b7936aa9213149cea14e9a124582f38f Mon Sep 17 00:00:00 2001 From: code-crusher Date: Wed, 11 Feb 2026 17:34:22 +0530 Subject: [PATCH 02/11] better diff view for edit tool --- src/core/task/Task.ts | 44 +++--- webview-ui/src/components/chat/ChatRow.tsx | 84 ++++------ .../src/components/chat/GitHubDiffView.tsx | 147 ++++++++++++++++++ .../src/components/common/ToolUseBlock.tsx | 5 +- .../src/context/ExtensionStateContext.tsx | 3 + 5 files changed, 200 insertions(+), 83 deletions(-) create mode 100644 webview-ui/src/components/chat/GitHubDiffView.tsx diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 295f3d373..0f8098792 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1969,28 +1969,28 @@ export class Task extends EventEmitter implements TaskLike { ) } - if (this.consecutiveMistakeLimit > 0 && this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) { - const { response, text, images } = await this.ask( - "mistake_limit_reached", - t("common:errors.mistake_limit_guidance"), - ) - - if (response === "messageResponse") { - currentUserContent.push( - ...[ - { type: "text" as const, text: formatResponse.tooManyMistakes(text) }, - ...formatResponse.imageBlocks(images), - ], - ) - - await this.say("user_feedback", text, images) - - // Track consecutive mistake errors in telemetry. - TelemetryService.instance.captureConsecutiveMistakeError(this.taskId) - } - - this.consecutiveMistakeCount = 0 - } + // if (this.consecutiveMistakeLimit > 0 && this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) { + // const { response, text, images } = await this.ask( + // "mistake_limit_reached", + // t("common:errors.mistake_limit_guidance"), + // ) + + // if (response === "messageResponse") { + // currentUserContent.push( + // ...[ + // { type: "text" as const, text: formatResponse.tooManyMistakes(text) }, + // ...formatResponse.imageBlocks(images), + // ], + // ) + + // await this.say("user_feedback", text, images) + + // // Track consecutive mistake errors in telemetry. + // TelemetryService.instance.captureConsecutiveMistakeError(this.taskId) + // } + + // this.consecutiveMistakeCount = 0 + // } // In this Cline request loop, we need to check if this task instance // has been asked to wait for a subtask to finish before continuing. diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 38d3a44c0..8cf990112 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -19,6 +19,7 @@ import { vscode } from "@src/utils/vscode" import CodeAccordian, { extractFirstLineNumberFromDiff } from "../common/CodeAccordian" import ImageBlock from "../common/ImageBlock" +import GitHubDiffView from "./GitHubDiffView" import MarkdownBlock from "../common/MarkdownBlock" import Thumbnails from "../common/Thumbnails" import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" @@ -93,7 +94,7 @@ const headerStyle: React.CSSProperties = { flexShrink: 0, } -// Build a minimal unified diff for fileEdit when backend doesn't supply one +// Build a GitHub-style unified diff for fileEdit when backend doesn't supply one const buildFileEditDiff = (tool: ClineSayTool): string | undefined => { const path = tool.path || "file" const oldText = (tool.search ?? "").trimEnd() @@ -101,17 +102,28 @@ const buildFileEditDiff = (tool: ClineSayTool): string | undefined => { if (!oldText && !newText) return undefined + const oldLines = oldText.split(/\r?\n/) + const newLines = newText.split(/\r?\n/) + const lines: string[] = [] lines.push(`--- a/${path}`) lines.push(`+++ b/${path}`) - lines.push(`@@`) - if (oldText) { - lines.push(...oldText.split(/\r?\n/).map((line) => `-${line}`)) + // Calculate hunk header with line numbers + const oldStart = 1 + const oldCount = oldLines.length + const newStart = 1 + const newCount = newLines.length + lines.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`) + + // Add old lines with - prefix + for (const line of oldLines) { + lines.push(`-${line}`) } - if (newText) { - lines.push(...newText.split(/\r?\n/).map((line) => `+${line}`)) + // Add new lines with + prefix + for (const line of newLines) { + lines.push(`+${line}`) } return lines.join("\n") @@ -523,7 +535,7 @@ export const ChatRowContent = ({ }) } return ( -
+
{tool.isProtected ? (
-
- - {tool.isProtected ? ( - - ) : null} - {tool.path ? ( - { - e.stopPropagation() - openFileWithLine() - }} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault() - e.stopPropagation() - openFileWithLine() - } - }}> - {tool.path.split("/").pop() || tool.path} - - ) : null} - {diffStats ? ( - - - +{diffStats.added} - - - -{diffStats.removed} - - - ) : null} -
- } +
+ { // kilocode_change start diff --git a/webview-ui/src/components/chat/GitHubDiffView.tsx b/webview-ui/src/components/chat/GitHubDiffView.tsx new file mode 100644 index 000000000..1e42f6049 --- /dev/null +++ b/webview-ui/src/components/chat/GitHubDiffView.tsx @@ -0,0 +1,147 @@ +import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" +import { memo } from "react" +import { extractFirstLineNumberFromDiff } from "../common/CodeAccordian" +import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" + +interface DiffStats { + added: number + removed: number +} + +interface GitHubDiffViewProps { + diff: string + filePath?: string + isProtected?: boolean + isOutsideWorkspace?: boolean + diffStats?: DiffStats | null + isLoading?: boolean + isExpanded: boolean + onToggleExpand: () => void + onOpenFile?: () => void +} + +const GitHubDiffView = memo( + ({ + diff, + filePath, + isProtected, + diffStats, + isLoading, + isExpanded, + onToggleExpand, + onOpenFile, + }: GitHubDiffViewProps) => { + const firstLineNumber = extractFirstLineNumberFromDiff(diff) + + const fileName = filePath?.split("/").pop() || filePath || "file" + + return ( + + + {isLoading && } +
+ {isProtected ? ( + + ) : null} + {filePath ? ( + { + e.stopPropagation() + onOpenFile?.() + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + e.stopPropagation() + onOpenFile?.() + } + }}> + {fileName} + + ) : null} + {diffStats ? ( + + +{diffStats.added} + -{diffStats.removed} + + ) : null} +
+
+ + + + {isExpanded && ( +
+
+ {/* Diff content */} +
+ +
+
+
+ )} + + ) + }, +) + +GitHubDiffView.displayName = "GitHubDiffView" + +// Unified diff view component +const UnifiedDiffView = memo(({ diff }: { diff: string }) => { + const lines = diff.split("\n") + + return ( +
+ {lines.map((line, index) => { + const isHeader = line.startsWith("---") || line.startsWith("+++") || line.startsWith("diff --git") + const isHunk = line.startsWith("@@") + const isAddition = line.startsWith("+") + const isDeletion = line.startsWith("-") + + let lineClass = "" + let bgClass = "" + + if (isHeader) { + lineClass = "text-vscode-descriptionForeground" + } else if (isHunk) { + lineClass = "text-vscode-editorInfo-foreground bg-vscode-editorInfo-background" + } else if (isAddition) { + bgClass = "bg-[var(--vscode-diffEditor-insertedTextBackground)]" + } else if (isDeletion) { + bgClass = "bg-[var(--vscode-diffEditor-removedTextBackground)]" + } + + return ( +
+ {/* Line number column */} + + {isHeader || isHunk ? "" : index + 1} + + {/* Content */} + {line} +
+ ) + })} +
+ ) +}) + +UnifiedDiffView.displayName = "UnifiedDiffView" + +export default GitHubDiffView diff --git a/webview-ui/src/components/common/ToolUseBlock.tsx b/webview-ui/src/components/common/ToolUseBlock.tsx index ebccefab9..e1f758662 100644 --- a/webview-ui/src/components/common/ToolUseBlock.tsx +++ b/webview-ui/src/components/common/ToolUseBlock.tsx @@ -1,10 +1,7 @@ import { cn } from "@/lib/utils" export const ToolUseBlock = ({ className, ...props }: React.HTMLAttributes) => ( -
+
) export const ToolUseBlockHeader = ({ className, ...props }: React.HTMLAttributes) => ( diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 9a3aa995f..f2ffaa178 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -46,6 +46,8 @@ export interface ExtensionStateContextType extends ExtensionState { dismissedNotificationIds: string[] // kilocode_change yoloMode?: boolean // kilocode_change setYoloMode: (value: boolean) => void // kilocode_Change + diffViewMode?: "unified" | "side-by-side" // GitHub PR diff view mode + setDiffViewMode: (value: "unified" | "side-by-side") => void // Setter for diff view mode didHydrateState: boolean showWelcome: boolean theme: any @@ -703,6 +705,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setIncludeTaskHistoryInEnhance, codeReviewSettings: state.codeReviewSettings, setCodeReviewSettings: (value) => setState((prevState) => ({ ...prevState, codeReviewSettings: value })), + setDiffViewMode: (value) => setState((prevState) => ({ ...prevState, diffViewMode: value })), } return {children} From 49f09a80c08aea8384943340eeed53b26bb5f678 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Wed, 11 Feb 2026 17:57:38 +0530 Subject: [PATCH 03/11] open plan in editor --- src/core/webview/webviewMessageHandler.ts | 19 +++++++ src/shared/ExtensionMessage.ts | 5 ++ src/shared/WebviewMessage.ts | 4 +- webview-ui/src/components/chat/ChatRow.tsx | 62 ++++++++++++++-------- webview-ui/src/utils/customIcons.tsx | 18 +++++++ 5 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 1a6e26924..9b58e7944 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -4761,6 +4761,25 @@ ${comment.suggestion} } break } + case "openPlanFile": { + if (message.payload) { + const { planFile } = message.payload as { planFile: string } + const currentTask = provider.getCurrentTask() + if (currentTask) { + const { getPlanMemoryDirectoryPath } = await import("../../utils/storage") + const globalStoragePath = provider.contextProxy.globalStorageUri.fsPath + const planMemoryDir = await getPlanMemoryDirectoryPath(globalStoragePath, currentTask.taskId) + const planFilePath = path.join(planMemoryDir, planFile) + try { + const document = await vscode.workspace.openTextDocument(vscode.Uri.file(planFilePath)) + await vscode.window.showTextDocument(document, { preview: false }) + } catch (error) { + console.error("Failed to open plan file:", error) + } + } + } + break + } // kilocode_change end } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 51d674dca..7d4eb6b12 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -171,6 +171,7 @@ export interface ExtensionMessage { | TasksByIdResponsePayload | TaskHistoryResponsePayload | ImplementPlanPayload + | OpenPlanFilePayload // kilocode_change end action?: | "chatButtonClicked" @@ -471,6 +472,10 @@ export interface ImplementPlanPayload { planContent: string } +export interface OpenPlanFilePayload { + planFile: string +} + export interface ClineSayTool { tool: | "editedExistingFile" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index dbe98c166..077a5543e 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -17,7 +17,7 @@ import { } from "@roo-code/types" import { Mode } from "./modes" -import { ImplementPlanPayload } from "./ExtensionMessage" +import { ImplementPlanPayload, OpenPlanFilePayload } from "./ExtensionMessage" export type ClineAskResponse = | "yesButtonClicked" @@ -302,6 +302,7 @@ export interface WebviewMessage { | "insertTextIntoTextarea" | "showMdmAuthRequiredNotification" | "implementPlan" // kilocode_change: Plan mode implementation + | "openPlanFile" // kilocode_change: Open plan file in editor | "imageGenerationSettings" | "openRouterImageApiKey" | "kiloCodeImageApiKey" @@ -610,6 +611,7 @@ export type WebViewMessagePayload = | CommitChangesPayload | PendingFileEditsPayload | ImplementPlanPayload + | OpenPlanFilePayload | CodeReviewResultsPayload | ApplyCodeReviewFixPayload | ApplyAllCodeReviewFixesPayload diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 8cf990112..4e430ca9b 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -55,6 +55,7 @@ import CodebaseSearchResultsDisplay from "./CodebaseSearchResultsDisplay" import { CondenseContextErrorRow, CondensingContextRow, ContextCondenseRow } from "./ContextCondenseRow" import { McpExecution } from "./McpExecution" import { FastApplyChatDisplay } from "./kilocode/FastApplyChatDisplay" // kilocode_change +import { PlayIcon } from "@/utils/customIcons" interface ChatRowProps { message: ClineMessage @@ -581,29 +582,46 @@ export const ChatRowContent = ({
- + {isExpanded ? ( + + ) : ( + + )} {!message.partial && ( - { - vscode.postMessage({ - type: "implementPlan", - payload: { - planFile: tool.filename || "plan.md", - planContent: tool.content || "", - }, - }) - }} - className="mt-2"> - - Implement - +
+ { + vscode.postMessage({ + type: "implementPlan", + payload: { + planFile: tool.filename || "plan.md", + planContent: tool.content || "", + }, + }) + }}> + + Implement + + { + vscode.postMessage({ + type: "openPlanFile", + payload: { + planFile: tool.filename || "plan.md", + }, + }) + }}> + + Open in Editor + +
)}
diff --git a/webview-ui/src/utils/customIcons.tsx b/webview-ui/src/utils/customIcons.tsx index a822be80c..bde89a4ff 100644 --- a/webview-ui/src/utils/customIcons.tsx +++ b/webview-ui/src/utils/customIcons.tsx @@ -170,3 +170,21 @@ export const ArrowRight02Icon = (props: React.SVGProps) => ( strokeLinejoin="round"> ) + +export const PlayIcon = (props: React.SVGProps) => ( + + + +) From edd83836755d40075557eefce33b8386c5602c76 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Wed, 11 Feb 2026 18:50:43 +0530 Subject: [PATCH 04/11] minor UI update to task header --- src/integrations/editor/PlanEditorProvider.ts | 593 ++++++++++++++++++ .../components/kilocode/KiloTaskHeader.tsx | 14 +- 2 files changed, 601 insertions(+), 6 deletions(-) create mode 100644 src/integrations/editor/PlanEditorProvider.ts diff --git a/src/integrations/editor/PlanEditorProvider.ts b/src/integrations/editor/PlanEditorProvider.ts new file mode 100644 index 000000000..7be2351ea --- /dev/null +++ b/src/integrations/editor/PlanEditorProvider.ts @@ -0,0 +1,593 @@ +import * as vscode from "vscode" +import * as path from "path" +import * as fs from "fs/promises" + +import { getPlanMemoryDirectoryPath } from "../../utils/storage" + +export const PLAN_EDITOR_URI_SCHEME = "axon-plan" + +/** + * Text document content provider for plan files + * This allows VS Code to resolve the content of plan files using the axon-plan URI scheme + */ +class PlanTextDocumentContentProvider implements vscode.TextDocumentContentProvider { + async provideTextDocumentContent(uri: vscode.Uri): Promise { + try { + console.log(`[PlanTextDocumentContentProvider] Loading file: ${uri.fsPath}`) + // The URI path contains the full file path + const filePath = uri.fsPath + + // Read the file content + const content = await fs.readFile(filePath, "utf-8") + console.log(`[PlanTextDocumentContentProvider] Successfully loaded file, content length: ${content.length}`) + return content + } catch (error) { + console.error(`[PlanTextDocumentContentProvider] Failed to read plan file: ${uri.fsPath}`, error) + return `# Error\n\nFailed to read plan file: ${error instanceof Error ? error.message : String(error)}` + } + } +} + +export class PlanEditorProvider implements vscode.CustomTextEditorProvider { + public static register(context: vscode.ExtensionContext): vscode.Disposable { + const provider = new PlanEditorProvider(context) + + // Register the custom text document content provider for the URI scheme + const contentProvider = new PlanTextDocumentContentProvider() + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider(PLAN_EDITOR_URI_SCHEME, contentProvider), + ) + + // Register the custom editor + const registration = vscode.window.registerCustomEditorProvider(PLAN_EDITOR_URI_SCHEME, provider, { + webviewOptions: { + retainContextWhenHidden: true, + }, + supportsMultipleEditorsPerDocument: false, + }) + + return registration + } + + constructor(private readonly context: vscode.ExtensionContext) {} + + async resolveCustomTextEditor( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel, + _token: vscode.CancellationToken, + ): Promise { + console.log(`[PlanEditorProvider] Resolving custom text editor for: ${document.uri.fsPath}`) + webviewPanel.webview.options = { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(this.context.extensionUri, "dist"), + vscode.Uri.joinPath(this.context.extensionUri, "webview-ui"), + ], + } + + const content = document.getText() + const filename = path.basename(document.uri.fsPath) + console.log(`[PlanEditorProvider] Content length: ${content.length}, filename: ${filename}`) + + webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview, content, filename) + console.log(`[PlanEditorProvider] Webview HTML set`) + + const updateWebview = async () => { + const content = document.getText() + const filename = path.basename(document.uri.fsPath) + webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview, content, filename) + } + + const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument((event) => { + if (event.document === document) { + updateWebview() + } + }) + + webviewPanel.onDidDispose(() => { + changeDocumentSubscription.dispose() + }) + + // Return to indicate the editor is ready + return Promise.resolve() + } + + private getHtmlForWebview(webview: vscode.Webview, content: string, filename: string): string { + const nonce = this.getNonce() + + // Get the current VS Code theme + const isDark = vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark + + // Define theme colors based on VS Code theme + const themeColors = isDark + ? { + background: "#1e1e1e", + foreground: "#cccccc", + editorBackground: "#1e1e1e", + editorForeground: "#d4d4d4", + border: "#3c3c3c", + heading: "#ffffff", + codeBackground: "#2d2d2d", + link: "#3794ff", + quote: "#6a9955", + listItem: "#cccccc", + tableBorder: "#3c3c3c", + tableHeader: "#ffffff", + tableRow: "#cccccc", + } + : { + background: "#ffffff", + foreground: "#333333", + editorBackground: "#ffffff", + editorForeground: "#333333", + border: "#e0e0e0", + heading: "#000000", + codeBackground: "#f5f5f5", + link: "#0066cc", + quote: "#6a9955", + listItem: "#333333", + tableBorder: "#e0e0e0", + tableHeader: "#000000", + tableRow: "#333333", + } + + return ` + + + + + + ${this.escapeHtml(filename)} + + + +
+ ${this.renderMarkdown(content)} +
+ + +` + } + + private renderMarkdown(markdown: string): string { + if (!markdown) { + return "

No content

" + } + + // Escape HTML to prevent XSS + let html = this.escapeHtml(markdown) + + // Simple markdown parsing + // Note: This is a basic implementation. For production, consider using a proper markdown library + + // Code blocks (```language code ```) + html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { + const language = lang || "text" + return `
${code}
` + }) + + // Inline code (`code`) + html = html.replace(/`([^`]+)`/g, "$1") + + // Headers + html = html.replace(/^###### (.+)$/gm, "
$1
") + html = html.replace(/^##### (.+)$/gm, "
$1
") + html = html.replace(/^#### (.+)$/gm, "

$1

") + html = html.replace(/^### (.+)$/gm, "

$1

") + html = html.replace(/^## (.+)$/gm, "

$1

") + html = html.replace(/^# (.+)$/gm, "

$1

") + + // Bold and Italic + html = html.replace(/\*\*\*(.+?)\*\*\*/g, "$1") + html = html.replace(/\*\*(.+?)\*\*/g, "$1") + html = html.replace(/\*(.+?)\*/g, "$1") + html = html.replace(/___(.+?)___/g, "$1") + html = html.replace(/__(.+?)__/g, "$1") + html = html.replace(/_(.+?)_/g, "$1") + + // Strikethrough + html = html.replace(/~~(.+?)~~/g, "$1") + + // Blockquotes + html = html.replace(/^> (.+)$/gm, "
$1
") + + // Horizontal rules + html = html.replace(/^---$/gm, "
") + html = html.replace(/^\*\*\*$/gm, "
") + + // Unordered lists + html = this.parseLists(html) + + // Task lists + html = html.replace( + /^- \[x\] (.+)$/gm, + '
  • $1
  • ', + ) + html = html.replace(/^- \[ \] (.+)$/gm, '
  • $1
  • ') + + // Links + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + + // Images + html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') + + // Line breaks + html = html.replace(/\n\n/g, "

    ") + html = html.replace(/\n/g, "
    ") + + // Wrap in paragraphs + html = "

    " + html + "

    " + + // Clean up empty paragraphs + html = html.replace(/

    <\/p>/g, "") + html = html.replace(/

    ()/g, "$1") + html = html.replace(/(<\/h[1-6]>)<\/p>/g, "$1") + html = html.replace(/

    (

    )/g, "$1")
    +		html = html.replace(/(<\/pre>)<\/p>/g, "$1")
    +		html = html.replace(/

    (

    )/g, "$1") + html = html.replace(/(<\/blockquote>)<\/p>/g, "$1") + html = html.replace(/

    (


    )<\/p>/g, "$1") + html = html.replace(/

    (

      )/g, "$1") + html = html.replace(/(<\/ul>)<\/p>/g, "$1") + html = html.replace(/

      (

        )/g, "$1") + html = html.replace(/(<\/ol>)<\/p>/g, "$1") + + return html + } + + private parseLists(html: string): string { + const lines = html.split("\n") + let result = [] + let inUl = false + let inOl = false + let ulItems: string[] = [] + let olItems: string[] = [] + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + // Check for unordered list item + const ulMatch = line.match(/^[\*\-] (.+)$/) + if (ulMatch) { + if (!inUl) { + if (inOl) { + result.push(`
          ${olItems.join("")}
        `) + olItems = [] + inOl = false + } + inUl = true + } + ulItems.push(`
      1. ${ulMatch[1]}
      2. `) + continue + } + + // Check for ordered list item + const olMatch = line.match(/^\d+\. (.+)$/) + if (olMatch) { + if (!inOl) { + if (inUl) { + result.push(`
          ${ulItems.join("")}
        `) + ulItems = [] + inUl = false + } + inOl = true + } + olItems.push(`
      3. ${olMatch[1]}
      4. `) + continue + } + + // Close any open lists + if (inUl) { + result.push(`
          ${ulItems.join("")}
        `) + ulItems = [] + inUl = false + } + if (inOl) { + result.push(`
          ${olItems.join("")}
        `) + olItems = [] + inOl = false + } + + result.push(line) + } + + // Close any remaining lists + if (inUl) { + result.push(`
          ${ulItems.join("")}
        `) + } + if (inOl) { + result.push(`
          ${olItems.join("")}
        `) + } + + return result.join("\n") + } + + private escapeHtml(text: string): string { + const map: Record = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + } + return text.replace(/[&<>"']/g, (m) => map[m]) + } + + private getNonce(): string { + let text = "" + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return text + } +} + +/** + * Opens a plan file in the custom editor + */ +export async function openPlanFileInEditor(filename: string, context: vscode.ExtensionContext): Promise { + try { + console.log(`[openPlanFileInEditor] Opening plan file: ${filename}`) + const globalStoragePath = context.globalStorageUri.fsPath + const basePath = await getPlanMemoryDirectoryPath(globalStoragePath, "default") + + // First try to find the file in the default location + let filePath = path.join(basePath, filename) + console.log(`[openPlanFileInEditor] Trying default path: ${filePath}`) + + // Check if file exists + try { + await fs.access(filePath) + console.log(`[openPlanFileInEditor] File found at default path`) + } catch { + // If not found, search in all task directories + const planMemoryBase = path.join(globalStoragePath, "plan-memory") + console.log(`[openPlanFileInEditor] File not found, searching in: ${planMemoryBase}`) + try { + const taskDirs = await fs.readdir(planMemoryBase, { withFileTypes: true }) + let found = false + + for (const taskDir of taskDirs) { + if (taskDir.isDirectory()) { + const taskPath = path.join(planMemoryBase, taskDir.name) + const testPath = path.join(taskPath, filename) + try { + await fs.access(testPath) + filePath = testPath + found = true + console.log(`[openPlanFileInEditor] File found at: ${filePath}`) + break + } catch { + // File not in this directory + } + } + } + + if (!found) { + vscode.window.showErrorMessage(`Plan file not found: ${filename}`) + return + } + } catch { + vscode.window.showErrorMessage(`Plan file not found: ${filename}`) + return + } + } + + // Create URI with custom scheme + const uri = vscode.Uri.parse(`${PLAN_EDITOR_URI_SCHEME}:${filePath}`) + console.log(`[openPlanFileInEditor] Created URI: ${uri.toString()}`) + + // Open the document + await vscode.commands.executeCommand("vscode.openWith", uri, PLAN_EDITOR_URI_SCHEME) + console.log(`[openPlanFileInEditor] Command executed successfully`) + } catch (error) { + console.error(`[openPlanFileInEditor] Failed to open plan file:`, error) + vscode.window.showErrorMessage( + `Failed to open plan file: ${error instanceof Error ? error.message : String(error)}`, + ) + } +} diff --git a/webview-ui/src/components/kilocode/KiloTaskHeader.tsx b/webview-ui/src/components/kilocode/KiloTaskHeader.tsx index b68523e5d..02549bb3c 100644 --- a/webview-ui/src/components/kilocode/KiloTaskHeader.tsx +++ b/webview-ui/src/components/kilocode/KiloTaskHeader.tsx @@ -91,7 +91,7 @@ const KiloTaskHeader = ({ {title && (
        - {title} + {title}