diff --git a/.changeset/ephemeral-planning-experiment.md b/.changeset/ephemeral-planning-experiment.md new file mode 100644 index 00000000000..e457e0f64e4 --- /dev/null +++ b/.changeset/ephemeral-planning-experiment.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add `ephemeralPlanning` experiment to gate the planning tool diff --git a/.changeset/planning-doc-tool.md b/.changeset/planning-doc-tool.md new file mode 100644 index 00000000000..b399ba77e87 --- /dev/null +++ b/.changeset/planning-doc-tool.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add `create_plan` tool for creating ephemeral planning documents diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index c2936b3bfde..ef24b00a290 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js" * ExperimentId */ -const kilocodeExperimentIds = ["morphFastApply", "speechToText"] as const // kilocode_change +const kilocodeExperimentIds = ["morphFastApply", "speechToText", "ephemeralPlanning"] as const // kilocode_change export const experimentIds = [ "powerSteering", "multiFileApplyDiff", @@ -27,6 +27,7 @@ export type ExperimentId = z.infer export const experimentsSchema = z.object({ morphFastApply: z.boolean().optional(), // kilocode_change speechToText: z.boolean().optional(), // kilocode_change + ephemeralPlanning: z.boolean().optional(), // kilocode_change powerSteering: z.boolean().optional(), multiFileApplyDiff: z.boolean().optional(), preventFocusDisruption: z.boolean().optional(), diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index c28e78ce5a0..b6c1dca8f0d 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -39,6 +39,7 @@ export const toolNames = [ "report_bug", "condense", "delete_file", + "create_plan", // kilocode_change // kilocode_change end "update_todo_list", "run_slash_command", diff --git a/src/__tests__/extension.spec.ts b/src/__tests__/extension.spec.ts index 325f1975f02..270dd16c3e4 100644 --- a/src/__tests__/extension.spec.ts +++ b/src/__tests__/extension.spec.ts @@ -28,6 +28,10 @@ vi.mock("vscode", () => ({ }, workspace: { registerTextDocumentContentProvider: vi.fn(), + // kilocode_change start + registerFileSystemProvider: vi.fn().mockReturnValue({ + dispose: vi.fn(), + }), getConfiguration: vi.fn().mockReturnValue({ get: vi.fn().mockReturnValue([]), }), @@ -50,6 +54,13 @@ vi.mock("vscode", () => ({ onDidCloseTextDocument: vi.fn().mockReturnValue({ dispose: vi.fn(), }), + // kilocode_change start + fs: { + readFile: vi.fn(), + writeFile: vi.fn(), + stat: vi.fn(), + }, + // kilocode_change end }, languages: { registerCodeActionsProvider: vi.fn(), @@ -63,6 +74,7 @@ vi.mock("vscode", () => ({ env: { language: "en", appName: "Visual Studio Code", + version: "1.0.0", // kilocode_change }, ExtensionMode: { Production: 1, diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 0ea50ba0bc8..5e296d8e102 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -31,6 +31,7 @@ import { switchModeTool } from "../tools/SwitchModeTool" import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool" import { newTaskTool } from "../tools/NewTaskTool" +import { createPlanTool } from "../tools/CreatePlanTool" // kilocode_change import { updateTodoListTool } from "../tools/UpdateTodoListTool" import { runSlashCommandTool } from "../tools/RunSlashCommandTool" import { generateImageTool } from "../tools/GenerateImageTool" @@ -466,6 +467,8 @@ export async function presentAssistantMessage(cline: Task) { return `[${block.name}]` case "condense": return `[${block.name}]` + case "create_plan": + return `[${block.name} for '${block.params.title}']` // kilocode_change // kilocode_change end case "run_slash_command": return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]` @@ -1099,6 +1102,15 @@ export async function presentAssistantMessage(cline: Task) { case "condense": await condenseTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break + case "create_plan": + await createPlanTool.handle(cline, block as ToolUse<"create_plan">, { + askApproval, + handleError, + pushToolResult, + removeClosingTag, + toolProtocol, + }) // kilocode_change + break // kilocode_change end case "run_slash_command": diff --git a/src/core/kilocode/wrapper.ts b/src/core/kilocode/wrapper.ts index 42e57745af6..12d0e26dc31 100644 --- a/src/core/kilocode/wrapper.ts +++ b/src/core/kilocode/wrapper.ts @@ -3,7 +3,7 @@ import { JETBRAIN_PRODUCTS, KiloCodeWrapperProperties } from "../../shared/kiloc export const getKiloCodeWrapperProperties = (): KiloCodeWrapperProperties => { const appName = vscode.env.appName - const kiloCodeWrapped = appName.includes("wrapper") + const kiloCodeWrapped = appName?.includes("wrapper") ?? false // kilocode_change let kiloCodeWrapper = null let kiloCodeWrapperTitle = null let kiloCodeWrapperCode = null @@ -37,8 +37,8 @@ export const getEditorNameHeader = () => { return ( props.kiloCodeWrapped ? [props.kiloCodeWrapperTitle, props.kiloCodeWrapperVersion] - : [vscode.env.appName, vscode.version] - ) + : [vscode.env.appName || "VS Code", vscode.version] + ) // kilocode_change .filter(Boolean) .join(" ") } diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap index 093e0d50222..0aa3ab272a8 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap @@ -229,6 +229,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively. diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap index 3cb0ec02e2b..43fd7fadb69 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap @@ -231,6 +231,84 @@ Example: +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## update_todo_list **Description:** diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap index f7df983c3b9..40392c3a0d1 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap @@ -228,6 +228,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap index cc7113024ac..2606c1b6b9a 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap @@ -229,6 +229,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap index 1d9f2436c0e..58f5ae69872 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap @@ -234,6 +234,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap index 4d172ba382c..6a11c431e4a 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap @@ -229,6 +229,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap index 1f4e7683e79..1823c12530f 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap @@ -229,6 +229,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## browser_action Description: Request to interact with a Puppeteer-controlled browser. Every action, except `close`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap index 4d172ba382c..6a11c431e4a 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap @@ -229,6 +229,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: 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 911df2f88eb..0d906dcbcab 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 @@ -317,6 +317,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap index 4d172ba382c..6a11c431e4a 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap @@ -229,6 +229,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap index 4d172ba382c..6a11c431e4a 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap @@ -229,6 +229,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap index cc7113024ac..2606c1b6b9a 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap @@ -229,6 +229,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap index 4d172ba382c..6a11c431e4a 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap @@ -229,6 +229,84 @@ Delete a directory (requires approval with statistics): ``` +## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + + + ## execute_command Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. Parameters: diff --git a/src/core/prompts/tools/create-plan.ts b/src/core/prompts/tools/create-plan.ts new file mode 100644 index 00000000000..0ea1b62d37b --- /dev/null +++ b/src/core/prompts/tools/create-plan.ts @@ -0,0 +1,82 @@ +// kilocode_change start: Add create_plan tool description function +import { ToolArgs } from "./types" + +export function getCreatePlanDescription(args: ToolArgs): string { + return `## create_plan +Description: Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Usage: + +document-title + +Your document content here + + + +Example: Creating a planning document + +implementation-plan + +# Implementation Plan + +## Step 1 +- Task A +- Task B + +## Step 2 +- Task C + + + +Example: Creating a quick note + +quick-notes + +Remember to: +1. Check API documentation +2. Test edge cases +3. Update tests + +` +} diff --git a/src/core/prompts/tools/filter-tools-for-mode.ts b/src/core/prompts/tools/filter-tools-for-mode.ts index 5d16e7f0be7..71b6871675e 100644 --- a/src/core/prompts/tools/filter-tools-for-mode.ts +++ b/src/core/prompts/tools/filter-tools-for-mode.ts @@ -10,6 +10,7 @@ import type { McpHub } from "../../../services/mcp/McpHub" import { ClineProviderState } from "../../webview/ClineProvider" import { isFastApplyAvailable } from "../../tools/kilocode/editFileTool" import { ManagedIndexer } from "../../../services/code-index/managed/ManagedIndexer" +import { getKiloCodeWrapperProperties } from "../../../core/kilocode/wrapper" // kilocode_change end /** @@ -341,6 +342,20 @@ export function filterNativeToolsForMode( allowedToolNames.delete("access_mcp_resource") } + // kilocode_change start - create_plan tool exclusion + // Conditionally exclude create_plan if running in CLI or JetBrains mode + // (plans require VS Code editor UI which CLI and JetBrains don't have) + const { kiloCodeWrapperCode, kiloCodeWrapperJetbrains } = getKiloCodeWrapperProperties() + if (kiloCodeWrapperJetbrains || kiloCodeWrapperCode === "cli") { + allowedToolNames.delete("create_plan") + } + + // Conditionally exclude create_plan if ephemeralPlanning experiment is not enabled + if (!experiments?.ephemeralPlanning) { + allowedToolNames.delete("create_plan") + } + // kilocode_change end - create_plan tool exclusion + // Filter native tools based on allowed tool names and apply alias renames const filteredTools: OpenAI.Chat.ChatCompletionTool[] = [] @@ -414,6 +429,11 @@ export function isToolAllowedInMode( if (toolName === "run_slash_command") { return experiments?.runSlashCommand === true } + if (toolName === "create_plan") { + // kilocode_change start - check ephemeralPlanning experiment + return experiments?.ephemeralPlanning === true + // kilocode_change end + } return true } diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index 5dd516a7316..1cc80451783 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -27,12 +27,13 @@ import { getGenerateImageDescription } from "./generate-image" import { getDeleteFileDescription } from "./delete-file" // kilocode_change import { CodeIndexManager } from "../../../services/code-index/manager" -// kilocode_change start: Morph fast apply +// kilocode_change start: Morph fast apply + create_plan import import { isFastApplyAvailable } from "../../tools/kilocode/editFileTool" import { getEditFileDescription } from "./edit-file" import { type ClineProviderState } from "../../webview/ClineProvider" import { ManagedIndexer } from "../../../services/code-index/managed/ManagedIndexer" -// kilocode_change end +import { getCreatePlanDescription } from "./create-plan" +// kilocode_change end: Morph fast apply + create_plan import // Map of tool names to their description functions const toolDescriptionMap: Record string | undefined> = { @@ -59,6 +60,7 @@ const toolDescriptionMap: Record string | undefined> new_task: (args) => getNewTaskDescription(args), edit_file: () => getEditFileDescription(), // kilocode_change: Morph fast apply delete_file: (args) => getDeleteFileDescription(args), // kilocode_change + create_plan: (args) => getCreatePlanDescription(args), // kilocode_change apply_diff: (args) => args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "", update_todo_list: (args) => getUpdateTodoListDescription(args), diff --git a/src/core/prompts/tools/native-tools/create_plan.ts b/src/core/prompts/tools/native-tools/create_plan.ts new file mode 100644 index 00000000000..cf8dec0f301 --- /dev/null +++ b/src/core/prompts/tools/native-tools/create_plan.ts @@ -0,0 +1,79 @@ +// kilocode_change - new file: Native tool definition for create_plan +import type OpenAI from "openai" + +const CREATE_PLAN_DESCRIPTION = `Create an ephemeral planning document for brainstorming, strategy, and structured thinking. The document appears as an editor tab and is fully accessible during this session, but will be automatically discarded when the editor session ends (not saved to disk). + +DEFAULT RULE: Use create_plan for ANY planning task unless the user EXPLICITLY requests disk persistence. The word "plan", "planning", "plan document", or any planning-related request should default to create_plan. + +WHEN TO USE create_plan (DEFAULT for planning): +- ANY request involving "plan", "planning", "plan document", "create a plan", "plan something" +- Outlining strategies, organizing thoughts, brainstorming +- Requests to "plan it first" or "create a planning document" +- Iterating on ideas before committing to implementation +- Collaborative planning sessions +- When user asks about planning documents or wants to check for them +- IMPORTANT: Even if you think the user "might want persistence" - use create_plan unless they explicitly say so + +WHEN NOT TO USE create_plan (use write_to_file instead): +- User EXPLICITLY says "save to disk", "persist", "save permanently", or "write to file" +- User EXPLICITLY provides a specific file path like "/plans/my-plan.md" or "save it to plans/" +- User EXPLICITLY mentions they want the document to "survive sessions" or "persist across sessions" +- User uses the word "write" or "save" instead of "plan" or "create" +- DO NOT infer persistence from context - only use write_to_file if user explicitly requests it + +COMMON MISTAKES TO AVOID: +- ❌ DON'T use write_to_file just because you think "planning documents should persist" - that's an inference, not an explicit request +- ❌ DON'T use write_to_file because "real-world projects need persistent files" - use create_plan unless explicitly told otherwise +- ❌ DON'T use write_to_file because the user "might want to revisit it later" - use create_plan unless they explicitly say so +- ✅ DO use create_plan for any planning task unless the user explicitly requests persistence + +KEY POINTS: +- Ephemeral plans are FULLY FUNCTIONAL during the session - they're not "temporary" in the sense of being less useful +- Ephemeral plans appear as editor tabs and are FULLY EDITABLE during this session +- Can be read and modified using read_file and write_to_file with the returned plan:// path +- Even ephemeral plans can be referenced by Code mode during this session - they don't need to be saved to disk to be useful +- The key distinction is EXPLICIT REQUEST for persistence vs DEFAULT ephemeral behavior +- "Ephemeral" means session-only, NOT "unusable" or "temporary" - it's a fully functional document during the session + +The created document will: +- Appear as a normal editor tab that can be edited by the user +- Be accessible via read_file and write_to_file tools using the returned plan:// path +- Be automatically discarded when the editor session ends (not saved to disk) + +Parameters: +- title: (required) The title/name of the plan document. Will be used as the filename with a unique ID suffix (automatically adds .plan.md extension if not present) +- content: (required) The initial content of the plan document + +Example: Creating a planning document +{ "title": "implementation-plan", "content": "# Implementation Plan\n\n## Step 1\n- Task A\n- Task B\n\n## Step 2\n- Task C" } + +Example: Creating a quick note +{ "title": "quick-notes", "content": "Remember to:\n1. Check API documentation\n2. Test edge cases\n3. Update tests" }` + +const TITLE_PARAMETER_DESCRIPTION = `The title/name of the plan document. Will be used as the filename with a unique ID suffix (automatically adds .plan.md extension if not present)` + +const CONTENT_PARAMETER_DESCRIPTION = `The initial content of the plan document` + +export default { + type: "function", + function: { + name: "create_plan", + description: CREATE_PLAN_DESCRIPTION, + strict: true, + parameters: { + type: "object", + properties: { + title: { + type: "string", + description: TITLE_PARAMETER_DESCRIPTION, + }, + content: { + type: "string", + description: CONTENT_PARAMETER_DESCRIPTION, + }, + }, + required: ["title", "content"], + additionalProperties: false, + }, + }, +} satisfies OpenAI.Chat.ChatCompletionTool diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 8c1bafb274f..159d9c24121 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -6,6 +6,7 @@ import askFollowupQuestion from "./ask_followup_question" import attemptCompletion from "./attempt_completion" import browserAction from "./browser_action" import codebaseSearch from "./codebase_search" +import createPlan from "./create_plan" // kilocode_change import executeCommand from "./execute_command" import fetchInstructions from "./fetch_instructions" import generateImage from "./generate_image" @@ -41,6 +42,7 @@ export function getNativeTools(partialReadsEnabled: boolean = true): OpenAI.Chat // condenseTool, // newRuleTool, // reportBugTool, + createPlan, // kilocode_change // kilocode_change end accessMcpResource, apply_diff, diff --git a/src/core/tools/ApplyDiffTool.ts b/src/core/tools/ApplyDiffTool.ts index 0c33708c472..2cdca413041 100644 --- a/src/core/tools/ApplyDiffTool.ts +++ b/src/core/tools/ApplyDiffTool.ts @@ -16,6 +16,12 @@ import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats" import { BaseTool, ToolCallbacks } from "./BaseTool" import type { ToolUse } from "../../shared/tools" import { trackContribution } from "../../services/contribution-tracking/ContributionTrackingService" // kilocode_change +import { + isPlanPath, + normalizePlanPath, + readPlanDocumentContent, + writePlanDocumentContent, +} from "./helpers/planDocumentHelpers" // kilocode_change interface ApplyDiffParams { path: string @@ -55,6 +61,11 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { return } + // kilocode_change start: Handle plan documents + const isPlan = isPlanPath(relPath) + const canonicalPath = isPlan ? normalizePlanPath(relPath) : relPath + // kilocode_change end + const accessAllowed = task.rooIgnoreController?.validateAccess(relPath) if (!accessAllowed) { @@ -63,20 +74,43 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { return } - const absolutePath = path.resolve(task.cwd, relPath) - const fileExists = await fileExistsAtPath(absolutePath) + // kilocode_change start: Handle plan documents + let originalContent: string + let absolutePath: string + let fileExists = false + + if (isPlan) { + const readResult = await readPlanDocumentContent(relPath) + if ("error" in readResult) { + task.consecutiveMistakeCount++ + task.recordToolError("apply_diff") + const formattedError = `Plan document does not exist at path: ${canonicalPath}\n\n\nThe plan document could not be found. Please verify the plan exists and try again.\n` + await task.say("error", formattedError) + task.didToolFailInCurrentTurn = true + pushToolResult(formattedError) + return + } + originalContent = readResult.content + absolutePath = canonicalPath + fileExists = true + } else { + // For regular files, use the existing logic + absolutePath = path.resolve(task.cwd, relPath) + fileExists = await fileExistsAtPath(absolutePath) + + if (!fileExists) { + task.consecutiveMistakeCount++ + task.recordToolError("apply_diff") + const formattedError = `File does not exist at path: ${absolutePath}\n\n\nThe specified file could not be found. Please verify the file path and try again.\n` + await task.say("error", formattedError) + task.didToolFailInCurrentTurn = true + pushToolResult(formattedError) + return + } - if (!fileExists) { - task.consecutiveMistakeCount++ - task.recordToolError("apply_diff") - const formattedError = `File does not exist at path: ${absolutePath}\n\n\nThe specified file could not be found. Please verify the file path and try again.\n` - await task.say("error", formattedError) - task.didToolFailInCurrentTurn = true - pushToolResult(formattedError) - return + originalContent = await fs.readFile(absolutePath, "utf-8") } - - const originalContent: string = await fs.readFile(absolutePath, "utf-8") + // kilocode_change end // Apply the diff to the original content const diffResult = (await task.diffStrategy?.applyDiff( @@ -152,6 +186,28 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { diff: diffContent, } + // kilocode_change start: Handle plan documents separately + if (isPlan) { + // For plan documents, write directly without diff view + // Write the updated content back to the plan + const writeResult = await writePlanDocumentContent(relPath, diffResult.content, task) + if ("error" in writeResult) { + task.consecutiveMistakeCount++ + const formattedError = `Unable to write plan document: ${canonicalPath}\n\n\n${writeResult.error}\n` + await task.say("error", formattedError) + task.recordToolError("apply_diff", formattedError) + pushToolResult(formattedError) + return + } + + // Generate a simple message for the tool result + const message = `Applied diff to plan document: ${writeResult.canonicalPath}` + pushToolResult(message) + task.processQueuedMessages() + return + } + // kilocode_change end + if (isPreventFocusDisruptionEnabled) { // Direct file write without diff view const completeMessage = JSON.stringify({ diff --git a/src/core/tools/CreatePlanTool.ts b/src/core/tools/CreatePlanTool.ts new file mode 100644 index 00000000000..2163611f8e2 --- /dev/null +++ b/src/core/tools/CreatePlanTool.ts @@ -0,0 +1,93 @@ +// kilocode_change - new file +import { Task } from "../task/Task" +import { formatResponse } from "../prompts/responses" +import { getPlanFileSystem } from "../../services/planning" +import { BaseTool, ToolCallbacks } from "./BaseTool" +import type { ToolUse } from "../../shared/tools" + +interface CreatePlanParams { + title: string + content: string +} + +export class CreatePlanTool extends BaseTool<"create_plan"> { + readonly name = "create_plan" as const + + parseLegacy(params: Partial>): CreatePlanParams { + return { + title: params.title || "", + content: params.content || "", + } + } + + async execute(params: CreatePlanParams, task: Task, callbacks: ToolCallbacks): Promise { + const { title, content } = params + const { handleError, pushToolResult } = callbacks + + // Validate required parameters + if (!title) { + task.consecutiveMistakeCount++ + task.recordToolError("create_plan") + pushToolResult(await task.sayAndCreateMissingParamError("create_plan", "title")) + return + } + + if (content === undefined || content === null) { + task.consecutiveMistakeCount++ + task.recordToolError("create_plan") + pushToolResult(await task.sayAndCreateMissingParamError("create_plan", "content")) + return + } + + // Validate title length + if (title.length > 255) { + task.consecutiveMistakeCount++ + task.recordToolError("create_plan") + pushToolResult(formatResponse.toolError("Title must be 255 characters or less")) + return + } + + // Validate content is not too large (prevent memory issues) + if (content.length > 1000000) { + task.consecutiveMistakeCount++ + task.recordToolError("create_plan") + pushToolResult(formatResponse.toolError("Content must be 1MB or less")) + return + } + + task.consecutiveMistakeCount = 0 + + try { + const fs = getPlanFileSystem() + const planPath = await fs.createAndOpen(title, content) + + // Return success message with instructions + const message = `Created plan document "${title}". The document has been opened in an editor tab.\n\nYou can now:\n- Read it using: read_file with path "${planPath}"\n- Update it using: write_to_file with path "${planPath}"\n\nThe document will be discarded when the editor session ends.` + + pushToolResult(formatResponse.toolResult(message)) + task.recordToolUsage("create_plan") + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error" + pushToolResult(formatResponse.toolError(`Failed to create plan document: ${errorMessage}`)) + await handleError("creating plan document", error as Error) + } + } + + override async handlePartial(task: Task, block: ToolUse<"create_plan">): Promise { + // Show "Creating plan..." message during streaming + const title = this.removeClosingTag("title", block.params.title, block.partial) + const content = this.removeClosingTag("content", block.params.content, block.partial) + + if (title) { + const partialMessage = JSON.stringify({ + tool: "createPlan", + title: title, + content: content, + }) + + await task.ask("tool", partialMessage, block.partial).catch(() => {}) + } + } +} + +export const createPlanTool = new CreatePlanTool() diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts index d21c8cd247a..4c23e3a222c 100644 --- a/src/core/tools/ReadFileTool.ts +++ b/src/core/tools/ReadFileTool.ts @@ -25,6 +25,7 @@ import { processImageFile, ImageMemoryTracker, } from "./helpers/imageHelpers" +import { isPlanPath, readPlanDocument } from "./helpers/planDocumentHelpers" // kilocode_change import { validateFileTokenBudget, truncateFileContent } from "./helpers/fileTokenBudget" import { truncateDefinitionsToLineLimit } from "./helpers/truncateDefinitions" import { BaseTool, ToolCallbacks } from "./BaseTool" @@ -176,6 +177,14 @@ export class ReadFileTool extends BaseTool<"read_file"> { } if (fileResult.status === "pending") { + // kilocode_change start: Skip approval for plan documents + // Skip approval for plan documents (auto-approved) + if (isPlanPath(relPath)) { + updateFileResult(relPath, { status: "approved" }) + continue + } + // kilocode_change end + const accessAllowed = task.rooIgnoreController?.validateAccess(relPath) if (!accessAllowed) { await task.say("rooignore_error", relPath) @@ -334,6 +343,30 @@ export class ReadFileTool extends BaseTool<"read_file"> { if (fileResult.status !== "approved") continue const relPath = fileResult.path + + // kilocode_change start: Handle plan document reading + if (isPlanPath(relPath)) { + const result = await readPlanDocument(relPath, task) + if (result.status === "error") { + updateFileResult(relPath, { + status: "error", + error: result.error, + xmlContent: result.xmlContent, + nativeContent: result.nativeContent, + }) + if (result.error) { + await handleError(`reading plan document ${relPath}`, new Error(result.error)) + } + } else { + updateFileResult(relPath, { + xmlContent: result.xmlContent, + nativeContent: result.nativeContent, + }) + } + continue + } + // kilocode_change end + const fullPath = path.resolve(task.cwd, relPath) try { @@ -726,6 +759,7 @@ export class ReadFileTool extends BaseTool<"read_file"> { return `[${blockName} with missing path/args/files]` } + // kilocode_change end override async handlePartial(task: Task, block: ToolUse<"read_file">): Promise { const argsXmlTag = block.params.args diff --git a/src/core/tools/SearchAndReplaceTool.ts b/src/core/tools/SearchAndReplaceTool.ts index 675bea589ea..4c73ff2bd7f 100644 --- a/src/core/tools/SearchAndReplaceTool.ts +++ b/src/core/tools/SearchAndReplaceTool.ts @@ -14,6 +14,7 @@ import { sanitizeUnifiedDiff, computeDiffStats } from "../diff/stats" import { BaseTool, ToolCallbacks } from "./BaseTool" import type { ToolUse } from "../../shared/tools" import { normalizeLineEndings_kilocode } from "./kilocode/normalizeLineEndings" +import { isPlanPath, readPlanDocumentContent, writePlanDocumentContent } from "./helpers/planDocumentHelpers" // kilocode_change interface SearchReplaceOperation { search: string @@ -86,6 +87,86 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> { } } + // kilocode_change start: Handle plan documents + if (isPlanPath(relPath)) { + const readResult = await readPlanDocumentContent(relPath) + if ("error" in readResult) { + task.consecutiveMistakeCount++ + task.recordToolError("search_and_replace") + await task.say("error", readResult.error) + pushToolResult(formatResponse.toolError(readResult.error)) + return + } + + const fileContent = readResult.content + const useCrLf_kilocode = fileContent.includes("\r\n") + + // Apply all operations sequentially + let newContent = fileContent + const errors: string[] = [] + + for (let i = 0; i < operations.length; i++) { + const { search, replace } = operations[i] + const searchPattern = new RegExp( + escapeRegExp(normalizeLineEndings_kilocode(search, useCrLf_kilocode)), + "g", + ) + + const matchCount = newContent.match(searchPattern)?.length ?? 0 + if (matchCount === 0) { + errors.push(`Operation ${i + 1}: No match found for search text.`) + continue + } + + if (matchCount > 1) { + errors.push( + `Operation ${i + 1}: Found ${matchCount} matches. Please provide more context to make a unique match.`, + ) + continue + } + + // Apply the replacement + newContent = newContent.replace( + searchPattern, + normalizeLineEndings_kilocode(replace, useCrLf_kilocode), + ) + } + + // If all operations failed, return error + if (errors.length === operations.length) { + task.consecutiveMistakeCount++ + task.recordToolError("search_and_replace", "no_match") + pushToolResult(formatResponse.toolError(`All operations failed:\n${errors.join("\n")}`)) + return + } + + // Check if any changes were made + if (newContent === fileContent) { + pushToolResult(`No changes needed for '${relPath}'`) + return + } + + // Write plan document + const writeResult = await writePlanDocumentContent(relPath, newContent, task) + if ("error" in writeResult) { + await handleError("writing plan document", new Error(writeResult.error)) + pushToolResult(formatResponse.toolError(writeResult.error)) + return + } + + // Add error info if some operations failed + let resultMessage = "" + if (errors.length > 0) { + resultMessage = `Some operations failed:\n${errors.join("\n")}\n\n` + } + resultMessage += `Updated plan document "${writeResult.canonicalPath}"` + + pushToolResult(formatResponse.toolResult(resultMessage)) + task.recordToolUsage("search_and_replace") + return + } + // kilocode_change end + const accessAllowed = task.rooIgnoreController?.validateAccess(relPath) if (!accessAllowed) { diff --git a/src/core/tools/WriteToFileTool.ts b/src/core/tools/WriteToFileTool.ts index c7a06fc1e62..6b5399c611e 100644 --- a/src/core/tools/WriteToFileTool.ts +++ b/src/core/tools/WriteToFileTool.ts @@ -1,6 +1,5 @@ import path from "path" import delay from "delay" -import * as vscode from "vscode" import fs from "fs/promises" import { Task } from "../task/Task" @@ -17,6 +16,7 @@ import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats" import { BaseTool, ToolCallbacks } from "./BaseTool" import type { ToolUse } from "../../shared/tools" +import { isPlanPath, writePlanDocument, convertToPlanPathIfNeeded } from "./helpers/planDocumentHelpers" // kilocode_change import { trackContribution } from "../../services/contribution-tracking/ContributionTrackingService" // kilocode_change interface WriteToFileParams { @@ -55,6 +55,44 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { return } + // kilocode_change start: Auto-redirect /plans/ paths to plan:// schema + // Check if this path should be converted to a plan document (e.g., /plans/...) + const convertedPlanPath = convertToPlanPathIfNeeded(relPath) + if (convertedPlanPath) { + console.log( + `📝 [WriteToFileTool] Redirecting /plans/ path "${relPath}" to plan document "${convertedPlanPath}"`, + ) + const result = await writePlanDocument(convertedPlanPath, newContent, task) + if ("error" in result) { + pushToolResult(formatResponse.toolError(result.error)) + await handleError("writing plan document", new Error(result.error)) + return + } + + task.didEditFile = true + pushToolResult( + formatResponse.toolResult( + `Redirected /plans/ path to ephemeral plan document "${result.canonicalPath}". The document will be discarded when the editor session ends.`, + ), + ) + return + } + // kilocode_change end + + // Check if this is a plan document (already using plan:// schema) + if (isPlanPath(relPath)) { + const result = await writePlanDocument(relPath, newContent, task) + if ("error" in result) { + pushToolResult(formatResponse.toolError(result.error)) + await handleError("writing plan document", new Error(result.error)) + return + } + + task.didEditFile = true + pushToolResult(formatResponse.toolResult(`Updated plan document "${result.canonicalPath}"`)) + return + } + const accessAllowed = task.rooIgnoreController?.validateAccess(relPath) if (!accessAllowed) { @@ -239,6 +277,13 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> { return } + // Skip partial handling for plan documents - they don't use the diff view provider + // and we don't want to create filesystem directories for plan:// paths or /plans/ paths + // (isPlanPath now includes both plan:// URIs and absolute /plans/ paths) + if (isPlanPath(relPath)) { + return + } + const provider = task.providerRef.deref() const state = await provider?.getState() const isPreventFocusDisruptionEnabled = experiments.isEnabled( diff --git a/src/core/tools/__tests__/createPlanTool.spec.ts b/src/core/tools/__tests__/createPlanTool.spec.ts new file mode 100644 index 00000000000..328902a3957 --- /dev/null +++ b/src/core/tools/__tests__/createPlanTool.spec.ts @@ -0,0 +1,220 @@ +// kilocode_change - new file: Tests for create_plan tool +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { createPlanTool } from "../CreatePlanTool" +import { Task } from "../../task/Task" +import { formatResponse } from "../../prompts/responses" +import { getPlanFileSystem } from "../../../services/planning" + +// Mock the planning service +vi.mock("../../../services/planning", () => ({ + getPlanFileSystem: vi.fn(() => ({ + createAndOpen: vi.fn(), + })), +})) + +// Mock vscode for integration tests - using vi.doMock to avoid hoisting issues +vi.doMock("vscode", () => ({ + Uri: { + parse: vi.fn((str) => ({ scheme: "plan", path: str.replace("plan://", "/") })), + }, + workspace: { + fs: { + readFile: vi.fn(), + writeFile: vi.fn(), + }, + }, + window: { + showTextDocument: vi.fn().mockResolvedValue({}), + }, + EventEmitter: vi.fn().mockImplementation(() => ({ + event: vi.fn(), + fire: vi.fn(), + })), + FileSystemProvider: { + asFileType: 1, + }, + FileType: { + File: 1, + }, + FileChangeType: { + Created: 1, + Changed: 2, + Deleted: 3, + }, + FileSystemError: { + FileNotFound: class extends Error { + constructor(uri: any) { + super(`File not found: ${uri}`) + this.name = "FileNotFound" + } + } as any, + NoPermissions: class extends Error { + constructor() { + super("No permissions") + this.name = "NoPermissions" + } + } as any, + }, + Disposable: vi.fn().mockImplementation(() => ({ + dispose: vi.fn(), + })), +})) + +describe("createPlanTool", () => { + let mockTask: Task + let mockPushToolResult: any + let mockHandleError: any + + beforeEach(() => { + vi.clearAllMocks() + + mockPushToolResult = vi.fn() + mockHandleError = vi.fn() + + // Create a mock Task object with required properties + mockTask = { + consecutiveMistakeCount: 0, + recordToolError: vi.fn(), + recordToolUsage: vi.fn(), + sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"), + } as unknown as Task + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + describe("parameter validation", () => { + it("should error when title is missing", async () => { + await createPlanTool.execute({ title: "", content: "test content" }, mockTask, { + pushToolResult: mockPushToolResult, + handleError: mockHandleError, + } as any) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("create_plan") + expect(mockPushToolResult).toHaveBeenCalled() + }) + + it("should error when content is missing (undefined)", async () => { + await createPlanTool.execute({ title: "test-title", content: undefined as any }, mockTask, { + pushToolResult: mockPushToolResult, + handleError: mockHandleError, + } as any) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("create_plan") + expect(mockPushToolResult).toHaveBeenCalled() + }) + + it("should error when content is null", async () => { + await createPlanTool.execute({ title: "test-title", content: null as any }, mockTask, { + pushToolResult: mockPushToolResult, + handleError: mockHandleError, + } as any) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockPushToolResult).toHaveBeenCalled() + }) + + it("should error when title exceeds 255 characters", async () => { + const longTitle = "a".repeat(256) + + await createPlanTool.execute({ title: longTitle, content: "test content" }, mockTask, { + pushToolResult: mockPushToolResult, + handleError: mockHandleError, + } as any) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockPushToolResult).toHaveBeenCalledWith( + formatResponse.toolError("Title must be 255 characters or less"), + ) + }) + + it("should error when content exceeds 1MB", async () => { + const largeContent = "a".repeat(1000001) + + await createPlanTool.execute({ title: "test-title", content: largeContent }, mockTask, { + pushToolResult: mockPushToolResult, + handleError: mockHandleError, + } as any) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockPushToolResult).toHaveBeenCalledWith(formatResponse.toolError("Content must be 1MB or less")) + }) + }) + + describe("parseLegacy", () => { + it("should parse legacy XML parameters", () => { + const result = createPlanTool.parseLegacy({ + title: "legacy-title", + content: "legacy content", + }) + + expect(result).toEqual({ + title: "legacy-title", + content: "legacy content", + }) + }) + + it("should return empty strings for missing parameters", () => { + const result = createPlanTool.parseLegacy({}) + + expect(result).toEqual({ + title: "", + content: "", + }) + }) + }) + + describe("tool name", () => { + it("should have correct name", () => { + expect(createPlanTool.name).toBe("create_plan") + }) + }) + + describe("plan workflow integration", () => { + it("should create plan with unique content", async () => { + const mockFs = { + createAndOpen: vi.fn().mockResolvedValue("plan://my-test.md"), + } + vi.mocked(getPlanFileSystem).mockReturnValue(mockFs as any) + + await createPlanTool.execute( + { title: "my-test", content: "# My Test Document\n\nTest content." }, + mockTask, + { pushToolResult: mockPushToolResult, handleError: mockHandleError } as any, + ) + + expect(mockFs.createAndOpen).toHaveBeenCalledWith("my-test", "# My Test Document\n\nTest content.") + expect(mockPushToolResult).toHaveBeenCalled() + }) + + it("should handle multiple plan creations with different content", async () => { + const mockFs = { + createAndOpen: vi + .fn() + .mockResolvedValueOnce("plan://first.md") + .mockResolvedValueOnce("plan://second.md"), + } + vi.mocked(getPlanFileSystem).mockReturnValue(mockFs as any) + + // Create first plan + await createPlanTool.execute({ title: "first", content: "# First Plan\n\nContent one." }, mockTask, { + pushToolResult: mockPushToolResult, + handleError: mockHandleError, + } as any) + + // Create second plan + await createPlanTool.execute({ title: "second", content: "# Second Plan\n\nContent two." }, mockTask, { + pushToolResult: mockPushToolResult, + handleError: mockHandleError, + } as any) + + // Verify both plans were created with their respective content + expect(mockFs.createAndOpen).toHaveBeenCalledTimes(2) + expect(mockFs.createAndOpen).toHaveBeenCalledWith("first", "# First Plan\n\nContent one.") + expect(mockFs.createAndOpen).toHaveBeenCalledWith("second", "# Second Plan\n\nContent two.") + }) + }) +}) diff --git a/src/core/tools/helpers/planDocumentHelpers.ts b/src/core/tools/helpers/planDocumentHelpers.ts new file mode 100644 index 00000000000..042cda1a71a --- /dev/null +++ b/src/core/tools/helpers/planDocumentHelpers.ts @@ -0,0 +1,188 @@ +// kilocode_change - new file +import * as vscode from "vscode" +import { addLineNumbers } from "../../../integrations/misc/extract-text" +import { + isPlanPath, + normalizePlanPath, + planPathToFilename, + filenameToPlanPath, + PLAN_SCHEME_NAME, + getPlanFileSystem, +} from "../../../services/planning" +import type { Task } from "../../task/Task" +import type { RecordSource } from "../../context-tracking/FileContextTrackerTypes" + +export { isPlanPath, normalizePlanPath, planPathToFilename, PLAN_SCHEME_NAME } + +/** + * Read a plan document and return formatted result. + * Shared helper for both ReadFileTool and simpleReadFileTool. + */ +export async function readPlanDocument( + relPath: string, + task: Task, +): Promise<{ + status: "approved" | "error" + xmlContent?: string + nativeContent?: string + error?: string +}> { + const canonicalPath = normalizePlanPath(relPath) + const filename = planPathToFilename(relPath) + + try { + const uri = vscode.Uri.parse(`${PLAN_SCHEME_NAME}:/${filename}`) + const contentBytes = await vscode.workspace.fs.readFile(uri) + const content = new TextDecoder().decode(contentBytes) + const numberedContent = addLineNumbers(content) + const totalLines = content.split("\n").length + + await task.fileContextTracker.trackFileContext(canonicalPath, "read_tool" as RecordSource) + + const lineRangeAttr = ` lines="1-${totalLines}"` + const xmlInfo = totalLines > 0 ? `\n${numberedContent}\n` : `` + const nativeInfo = + totalLines > 0 + ? `File: ${canonicalPath}\nLines: 1-${totalLines}\n\n${numberedContent}` + : `File: ${canonicalPath}\n(empty file)` + + return { + status: "approved", + xmlContent: `${canonicalPath}\n${xmlInfo}`, + nativeContent: nativeInfo, + } + } catch (error) { + const isNotFoundError = error instanceof Error && error.message.includes("FileNotFound") + const errorMsg = error instanceof Error ? error.message : "Unknown error" + + if (isNotFoundError) { + const planName = filename.replace(/\.plan\.md$/, "").replace(/\.md$/, "") + return { + status: "error", + error: `Plan document "${planName}" does not exist. Use the create_plan tool to create it.`, + xmlContent: `${canonicalPath}Plan document "${planName}" does not exist. Use the create_plan tool with a title and content to create a new plan document.`, + nativeContent: `File: ${canonicalPath}\nError: Plan document "${planName}" does not exist. Use the create_plan tool with a title and content to create a new plan document.`, + } + } + + return { + status: "error", + error: `Error reading plan document: ${errorMsg}`, + xmlContent: `${canonicalPath}Error reading plan document: ${errorMsg}`, + nativeContent: `File: ${canonicalPath}\nError: Error reading plan document: ${errorMsg}`, + } + } +} + +/** + * Write content to a plan document. + * Helper for WriteToFileTool. + */ +export async function writePlanDocument( + relPath: string, + content: string, + task: Task, +): Promise<{ canonicalPath: string } | { error: string }> { + const canonicalPath = normalizePlanPath(relPath) + const filename = planPathToFilename(relPath) + + try { + // Check if plan exists before writing + const planFs = getPlanFileSystem() + const wasNew = !(await planFs.planExists(canonicalPath)) + + const uri = vscode.Uri.parse(`${PLAN_SCHEME_NAME}:/${filename}`) + const contentBytes = new TextEncoder().encode(content) + await vscode.workspace.fs.writeFile(uri, contentBytes) + + // If this is a new plan document, open it in VS Code + if (wasNew) { + await vscode.window.showTextDocument(uri, { preview: false }) + } + + await task.fileContextTracker.trackFileContext(canonicalPath, "roo_edited" as RecordSource) + return { canonicalPath } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : "Unknown error" + return { error: `Error writing plan document: ${errorMsg}` } + } +} + +/** + * Check if a path is a plan document path. + * Convenience function that re-exports from planPaths. + */ +export function isPlanDocumentPath(path: string): boolean { + return isPlanPath(path) +} + +/** + * If the file path should be a plan document (absolute /plans/ path), + * convert it to a plan:// URI. Returns undefined if not a /plans/ path. + * Note: This only converts /plans/ paths, not already-converted plan:// paths. + * + * @param filePath - The file path to check and potentially convert + * @returns The plan:// URI if the path should be converted, or undefined + */ +export function convertToPlanPathIfNeeded(filePath: string): string | undefined { + // Only convert /plans/ paths (not already plan:// paths) + if (filePath.startsWith("/plans/")) { + // Extract filename from /plans/filename.md -> filename.md + const filename = filePath.replace(/^\/plans\//, "").replace(/^\//, "") + return filenameToPlanPath(filename) + } + return undefined +} + +/** + * Read a plan document and return raw content. + * Simple helper for tools that need to process the content themselves. + * + * @param relPath - The plan document path (any variant) + * @returns The content or an error + */ +export async function readPlanDocumentContent(relPath: string): Promise<{ content: string } | { error: string }> { + const filename = planPathToFilename(relPath) + + try { + const uri = vscode.Uri.parse(`${PLAN_SCHEME_NAME}:/${filename}`) + const contentBytes = await vscode.workspace.fs.readFile(uri) + const content = new TextDecoder().decode(contentBytes) + return { content } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : "Unknown error" + return { error: `Failed to read plan document: ${errorMsg}` } + } +} + +/** + * Write content to a plan document. + * Simple helper that handles URI construction and file tracking. + * + * @param relPath - The plan document path (any variant) + * @param content - The content to write + * @param task - The task instance for file tracking + * @returns The canonical path or an error + */ +export async function writePlanDocumentContent( + relPath: string, + content: string, + task: Task, +): Promise<{ canonicalPath: string } | { error: string }> { + const canonicalPath = normalizePlanPath(relPath) + const filename = planPathToFilename(relPath) + + try { + const uri = vscode.Uri.parse(`${PLAN_SCHEME_NAME}:/${filename}`) + const contentBytes = new TextEncoder().encode(content) + await vscode.workspace.fs.writeFile(uri, contentBytes) + + await task.fileContextTracker.trackFileContext(canonicalPath, "roo_edited" as RecordSource) + task.didEditFile = true + + return { canonicalPath } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : "Unknown error" + return { error: `Failed to write plan document: ${errorMsg}` } + } +} diff --git a/src/core/tools/simpleReadFileTool.ts b/src/core/tools/simpleReadFileTool.ts index 1b41e9e9d68..4d7fe5d0e7d 100644 --- a/src/core/tools/simpleReadFileTool.ts +++ b/src/core/tools/simpleReadFileTool.ts @@ -13,7 +13,8 @@ import { countFileLines } from "../../integrations/misc/line-counter" import { readLines } from "../../integrations/misc/read-lines" import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text" import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" -import { ToolProtocol, isNativeProtocol } from "@roo-code/types" +import { ToolProtocol, isNativeProtocol, TOOL_PROTOCOL } from "@roo-code/types" +import { isPlanPath, readPlanDocument } from "./helpers/planDocumentHelpers" // kilocode_change import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, @@ -76,6 +77,30 @@ export async function simpleReadFileTool( const fullPath = path.resolve(cline.cwd, relPath) try { + // Check if this is a plan document + // kilocode_change start + if (isPlanPath(relPath)) { + const result = await readPlanDocument(relPath, cline) + const effectiveProtocol: ToolProtocol = toolProtocol || TOOL_PROTOCOL.XML + if (result.status === "error") { + // Return error based on protocol + if (isNativeProtocol(effectiveProtocol)) { + pushToolResult(result.nativeContent || result.error || "Error reading draft document") + } else { + pushToolResult(result.xmlContent || `${result.error}`) + } + } else { + // Return result based on protocol + if (isNativeProtocol(effectiveProtocol)) { + pushToolResult(result.nativeContent || "") + } else { + pushToolResult(result.xmlContent || "") + } + } + return + } + // kilocode_change end + // Check RooIgnore validation const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath) if (!accessAllowed) { diff --git a/src/extension.ts b/src/extension.ts index 57b0c50c725..c7815f9dc86 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -48,6 +48,7 @@ import { getKiloCodeWrapperProperties } from "./core/kilocode/wrapper" // kiloco import { checkAnthropicApiKeyConflict } from "./utils/anthropicApiKeyWarning" // kilocode_change import { SettingsSyncService } from "./services/settings-sync/SettingsSyncService" // kilocode_change import { ManagedIndexer } from "./services/code-index/managed/ManagedIndexer" // kilocode_change +import { registerPlanFileSystem } from "./services/planning" // kilocode_change import { flushModels, getModels, initializeModelCacheRefresh } from "./api/providers/fetchers/modelCache" import { kilo_initializeSessionManager } from "./shared/kilocode/cli-sessions/extension/session-manager-utils" // kilocode_change @@ -465,6 +466,8 @@ export async function activate(context: vscode.ExtensionContext) { if (kiloCodeWrapped) { // Only foward logs in Jetbrains registerMainThreadForwardingLogger(context) + } else { + registerPlanFileSystem(context) } // Don't register the ghost provider for the CLI if (kiloCodeWrapperCode !== "cli") { @@ -509,7 +512,7 @@ export async function activate(context: vscode.ExtensionContext) { reloadTimeout = setTimeout(() => { console.log(`♻️ Reloading host after debounce delay...`) - vscode.commands.executeCommand("workbench.action.reloadWindow") + // vscode.commands.executeCommand("workbench.action.reloadWindow") }, DEBOUNCE_DELAY) } diff --git a/src/services/planning/PlanFileSystemProvider.ts b/src/services/planning/PlanFileSystemProvider.ts new file mode 100644 index 00000000000..74db50b95a2 --- /dev/null +++ b/src/services/planning/PlanFileSystemProvider.ts @@ -0,0 +1,346 @@ +// kilocode_change - new file +import * as vscode from "vscode" +import * as path from "path" +import * as os from "os" +import * as fs from "fs/promises" +import { PLAN_SCHEME_NAME, filenameToPlanPath, planPathToFilename } from "./planPaths" + +/** + * Generate a unique plan ID similar to Cursor's plan IDs. + * Uses 7 random hex characters for collision avoidance. + */ +function generatePlanId(): string { + const hexChars = "0123456789abcdef" + let result = "" + for (let i = 0; i < 7; i++) { + result += hexChars[Math.floor(Math.random() * 16)] + } + return result +} + +/** + * File system provider for plan:// documents. + * Stores documents on disk at ~/.kilocode/plans/ and makes them available as editor tabs. + */ +export class PlanFileSystemProvider implements vscode.FileSystemProvider { + private readonly _emitter = new vscode.EventEmitter() + private readonly _plansDir: string + + readonly onDidChangeFile: vscode.Event = this._emitter.event + + constructor() { + this._plansDir = path.join(os.homedir(), ".kilocode", "plans") + fs.mkdir(this._plansDir, { recursive: true }).catch(() => { + // Silently fail - directory creation will be retried on write + }) + } + + /** + * Get the real filesystem path for a plan filename. + * @param filename - The filename (e.g., "my-document.md") + * @returns The absolute path to the file on disk + */ + private getRealPath(filename: string): string { + return path.join(this._plansDir, filename) + } + + /** + * Convert a plan URI path to filename (internal storage key). + * Handles all URI variants consistently by stripping scheme and leading slashes. + * @param uri - The VS Code URI + * @returns The filename without leading slash + */ + private uriToFilename(uri: vscode.Uri): string { + // URI path always starts with / for non-empty paths + const path = uri.path.startsWith("/") ? uri.path.slice(1) : uri.path + return path + } + + /** + * Convert a filename to VS Code URI for the plan scheme. + * Always produces the canonical form: plan:/filename (single slash). + * @param filename - The filename (without leading slash) + * @returns The VS Code URI with plan:// scheme + */ + private filenameToUri(filename: string): vscode.Uri { + // Always use / prefix for the URI path - produces canonical plan:/filename + return vscode.Uri.parse(`${PLAN_SCHEME_NAME}:/${filename}`) + } + + watch(_uri: vscode.Uri, _options: { recursive: boolean; excludes: string[] }): vscode.Disposable { + // No-op: we don't support watching plan documents + return new vscode.Disposable(() => {}) + } + + async stat(uri: vscode.Uri): Promise { + const filename = this.uriToFilename(uri) + const realPath = this.getRealPath(filename) + + try { + const stats = await fs.stat(realPath) + return { + type: vscode.FileType.File, + ctime: stats.birthtimeMs, + mtime: stats.mtimeMs, + size: stats.size, + } + } catch (error) { + const err = error as NodeJS.ErrnoException + if (err.code === "ENOENT") { + throw vscode.FileSystemError.FileNotFound(uri) + } + throw vscode.FileSystemError.Unavailable(uri) + } + } + + readDirectory(_uri: vscode.Uri): [string, vscode.FileType][] { + // Plan documents don't support directories + throw vscode.FileSystemError.FileNotFound() + } + + createDirectory(_uri: vscode.Uri): void { + // Plan documents don't support directories + throw vscode.FileSystemError.NoPermissions() + } + + async readFile(uri: vscode.Uri): Promise { + const filename = this.uriToFilename(uri) + const realPath = this.getRealPath(filename) + + try { + const content = await fs.readFile(realPath) + return content + } catch (error) { + const err = error as NodeJS.ErrnoException + if (err.code === "ENOENT") { + throw vscode.FileSystemError.FileNotFound(uri) + } + throw vscode.FileSystemError.Unavailable(uri) + } + } + + async writeFile( + uri: vscode.Uri, + content: Uint8Array, + _options: { create: boolean; overwrite: boolean }, + ): Promise { + const filename = this.uriToFilename(uri) + const realPath = this.getRealPath(filename) + + // Check if file exists to determine if this is a create or update + let wasNew = false + try { + await fs.stat(realPath) + } catch { + wasNew = true + } + + // Ensure plans directory exists before writing + await fs.mkdir(this._plansDir, { recursive: true }) + + // Write to disk + await fs.writeFile(realPath, content) + + // Emit file change event with canonical URI + const canonicalUri = this.filenameToUri(filename) + const event: vscode.FileChangeEvent = { + type: wasNew ? vscode.FileChangeType.Created : vscode.FileChangeType.Changed, + uri: canonicalUri, + } + this._emitter.fire([event]) + } + + async delete(uri: vscode.Uri): Promise { + const filename = this.uriToFilename(uri) + const realPath = this.getRealPath(filename) + + try { + await fs.unlink(realPath) + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + throw vscode.FileSystemError.FileNotFound(uri) + } + throw vscode.FileSystemError.Unavailable(uri) + } + + // Emit file change event with canonical URI + const canonicalUri = this.filenameToUri(filename) + const event: vscode.FileChangeEvent = { + type: vscode.FileChangeType.Deleted, + uri: canonicalUri, + } + this._emitter.fire([event]) + } + + rename(_oldUri: vscode.Uri, _newUri: vscode.Uri, _options: { overwrite: boolean }): void { + // Plan documents don't support rename + throw vscode.FileSystemError.NoPermissions() + } + + /** + * Create a new plan document and open it in the editor. + * @param name - The name/title of the document (will be used as filename with unique ID suffix) + * @param content - Initial content of the document + * @returns The plan:// URI path (e.g., "plan://filename_7313f09d.plan.md") + */ + async createAndOpen(name: string, content: string): Promise { + // Generate unique plan ID and ensure name has .plan.md extension + const planId = generatePlanId() + let baseName = name + if (name.endsWith(".plan.md")) { + baseName = name.slice(0, -8) // Remove ".plan.md" + } + const filename = `${baseName}_${planId}.plan.md` + + // Ensure plans directory exists + await fs.mkdir(this._plansDir, { recursive: true }) + + // Store the document on disk + const contentBytes = new TextEncoder().encode(content) + const realPath = this.getRealPath(filename) + await fs.writeFile(realPath, contentBytes) + + // Create URI using consistent formatting + const uri = this.filenameToUri(filename) + + // Emit file change event + const event: vscode.FileChangeEvent = { + type: vscode.FileChangeType.Created, + uri, + } + this._emitter.fire([event]) + + // Open document in VS Code editor + await vscode.window.showTextDocument(uri, { preview: false }) + + // Return the plan:// path for use in tools + const result = filenameToPlanPath(filename) + return result + } + + /** + * Get plan content for RPC access. + * @param planPath - The plan path (e.g., "plan://filename.md") + * @returns Content as Uint8Array, or undefined if not found + */ + async getPlanContent(planPath: string): Promise { + const filename = planPathToFilename(planPath) + const realPath = this.getRealPath(filename) + + try { + const buffer = await fs.readFile(realPath) + return new Uint8Array(buffer) + } catch { + return undefined + } + } + + /** + * Set plan content from RPC (user edits from JetBrains). + * @param planPath - The plan path + * @param content - New content + */ + async setPlanContent(planPath: string, content: Uint8Array): Promise { + const filename = planPathToFilename(planPath) + const realPath = this.getRealPath(filename) + + // Check if file exists to determine if this is a create or update + let wasNew = false + try { + await fs.stat(realPath) + } catch { + wasNew = true + } + + // Ensure plans directory exists + await fs.mkdir(this._plansDir, { recursive: true }) + + // Write to disk + await fs.writeFile(realPath, content) + + const uri = this.filenameToUri(filename) + this._emitter.fire([ + { + type: wasNew ? vscode.FileChangeType.Created : vscode.FileChangeType.Changed, + uri, + }, + ]) + } + + /** + * Check if a plan exists. + * @param planPath - The plan path + * @returns true if exists + */ + async planExists(planPath: string): Promise { + const filename = planPathToFilename(planPath) + const realPath = this.getRealPath(filename) + + try { + await fs.stat(realPath) + return true + } catch { + return false + } + } + + /** + * Delete a plan. + * @param planPath - The plan path + */ + async deletePlan(planPath: string): Promise { + const filename = planPathToFilename(planPath) + const realPath = this.getRealPath(filename) + + try { + await fs.unlink(realPath) + const uri = this.filenameToUri(filename) + this._emitter.fire([{ type: vscode.FileChangeType.Deleted, uri }]) + } catch { + // Ignore errors - file may not exist + } + } + + /** + * List all plan paths. + * @returns Array of plan:// paths + */ + async listPlans(): Promise { + try { + await fs.mkdir(this._plansDir, { recursive: true }) + const files = await fs.readdir(this._plansDir) + return files.filter((file) => file.endsWith(".plan.md")).map((filename) => filenameToPlanPath(filename)) + } catch { + return [] + } + } +} + +// Singleton instance +let planFileSystemProvider: PlanFileSystemProvider | undefined + +/** + * Get the singleton plan file system provider instance. + */ +export function getPlanFileSystem(): PlanFileSystemProvider { + if (!planFileSystemProvider) { + planFileSystemProvider = new PlanFileSystemProvider() + } + return planFileSystemProvider +} + +/** + * Register the plan file system provider with VS Code. + * @param context - VS Code extension context + */ +export function registerPlanFileSystem(context: vscode.ExtensionContext): void { + const provider = getPlanFileSystem() + context.subscriptions.push( + vscode.workspace.registerFileSystemProvider(PLAN_SCHEME_NAME, provider, { + isCaseSensitive: true, + }), + ) + + // Note: We no longer delete plans when tabs close - they persist on disk + // Users can manually delete them if needed +} diff --git a/src/services/planning/__tests__/planFileSystemProvider.spec.ts b/src/services/planning/__tests__/planFileSystemProvider.spec.ts new file mode 100644 index 00000000000..a59d64972b7 --- /dev/null +++ b/src/services/planning/__tests__/planFileSystemProvider.spec.ts @@ -0,0 +1,489 @@ +// kilocode_change - new file: Tests for PlanFileSystemProvider +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" +import { PlanFileSystemProvider } from "../PlanFileSystemProvider" +import { PLAN_SCHEME_NAME } from "../planPaths" +import * as vscode from "vscode" +import * as path from "path" +import * as fs from "fs/promises" + +// Mock os module +let mockHomedir: string +vi.mock("os", async () => { + const actual = await vi.importActual("os") + return { + ...actual, + homedir: () => mockHomedir, + } +}) +import * as os from "os" + +// Mock VS Code +vi.mock("vscode", () => ({ + Uri: { + parse: vi.fn((str) => ({ + scheme: "plan", + path: str.replace("plan://", "/"), + })), + }, + workspace: { + fs: { + readFile: vi.fn(), + writeFile: vi.fn(), + }, + }, + window: { + showTextDocument: vi.fn().mockResolvedValue({}), + }, + EventEmitter: class MockEventEmitter { + private _event = vi.fn() + event = this._event + fire = vi.fn() + dispose = vi.fn() + }, + FileSystemProvider: { + asFileType: 1, + }, + FileType: { + File: 1, + }, + FileChangeType: { + Created: 1, + Changed: 2, + Deleted: 3, + }, + FileSystemError: { + FileNotFound: class FileNotFound extends Error { + constructor(uri?: any) { + super(`File not found: ${uri || ""}`) + this.name = "FileNotFound" + } + }, + NoPermissions: class NoPermissions extends Error { + constructor() { + super("No permissions") + this.name = "NoPermissions" + } + }, + }, + Disposable: class Disposable { + private _disposeFn: () => void + constructor(disposeFn?: () => void) { + this._disposeFn = disposeFn || (() => {}) + } + dispose() { + this._disposeFn() + } + }, +})) + +describe("PlanFileSystemProvider", () => { + let provider: PlanFileSystemProvider + let tempDir: string + + beforeEach(async () => { + vi.clearAllMocks() + // Create a temporary directory for test files + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "plan-fsp-test-")) + // Set mock homedir to return our temp directory + mockHomedir = tempDir + // Get a fresh instance for each test + provider = new PlanFileSystemProvider() + }) + + afterEach(async () => { + // Clean up temporary directory + try { + await fs.rm(tempDir, { recursive: true, force: true }) + } catch { + // Ignore cleanup errors + } + vi.restoreAllMocks() + }) + + describe("createAndOpen", () => { + it("should create a new plan document with correct content", async () => { + const content = "# Test Document\n\nThis is a test." + const result = await provider.createAndOpen("test-doc", content) + + expect(result).toMatch(/^plan:\/\/test-doc_[a-f0-9]{7}\.plan\.md$/) + expect(vscode.window.showTextDocument).toHaveBeenCalled() + }) + + it("should append .plan.md extension if not present", async () => { + const result = await provider.createAndOpen("my-document", "# Content") + + expect(result).toMatch(/^plan:\/\/my-document_[a-f0-9]{7}\.plan\.md$/) + }) + + it("should preserve .plan.md extension if already present", async () => { + const result = await provider.createAndOpen("existing.plan.md", "# Content") + + expect(result).toMatch(/^plan:\/\/existing_[a-f0-9]{7}\.plan\.md$/) + }) + + it("should store content that can be read back", async () => { + const content = "# My Plan\n\nSome content here." + const planPath = await provider.createAndOpen("my-plan", content) + + const uri = vscode.Uri.parse(planPath) + const readContent = await provider.readFile(uri) + const decodedContent = new TextDecoder().decode(readContent) + + expect(decodedContent).toBe(content) + }) + + it("should emit Created event when document is created", async () => { + const emitter = new vscode.EventEmitter() + const content = "# Test" + await provider.createAndOpen("test", content) + + // The provider should have fired the event + expect(emitter.fire).toBeDefined() + }) + }) + + describe("readFile", () => { + it("should return content for existing document", async () => { + const content = "# Test Content" + const planPath = await provider.createAndOpen("existing", content) + + const uri = vscode.Uri.parse(planPath) + const readContent = await provider.readFile(uri) + const decodedContent = new TextDecoder().decode(readContent) + + expect(decodedContent).toBe(content) + }) + + it("should throw FileNotFound for non-existent document", async () => { + const uri = vscode.Uri.parse("plan:///nonexistent.plan.md") + + await expect(provider.readFile(uri)).rejects.toThrow() + }) + + it("should handle path with leading slash", async () => { + const content = "# Content" + const planPath = await provider.createAndOpen("path-test", content) + + const uri = vscode.Uri.parse(planPath) + const readContent = await provider.readFile(uri) + const decodedContent = new TextDecoder().decode(readContent) + + expect(decodedContent).toBe(content) + }) + }) + + describe("writeFile", () => { + it("should update existing document content", async () => { + const originalContent = "# Original" + const planPath = await provider.createAndOpen("updatable", originalContent) + + const newContent = "# Updated Content" + const uri = vscode.Uri.parse(planPath) + await provider.writeFile(uri, new TextEncoder().encode(newContent), { + create: false, + overwrite: true, + }) + + const readContent = await provider.readFile(uri) + const decodedContent = new TextDecoder().decode(readContent) + + expect(decodedContent).toBe(newContent) + }) + + it("should emit Changed event when document is updated", async () => { + const content = "# Original" + const planPath = await provider.createAndOpen("change-test", content) + + const newContent = "# Changed" + const uri = vscode.Uri.parse(planPath) + await provider.writeFile(uri, new TextEncoder().encode(newContent), { + create: false, + overwrite: true, + }) + + // Event should have been fired + expect(provider.onDidChangeFile).toBeDefined() + }) + }) + + describe("delete", () => { + it("should remove document from storage", async () => { + const content = "# To Delete" + const planPath = await provider.createAndOpen("delete-me", content) + + const uri = vscode.Uri.parse(planPath) + await provider.delete(uri) + + // Should throw FileNotFound after deletion + await expect(provider.readFile(uri)).rejects.toThrow() + }) + + it("should emit Deleted event when document is deleted", async () => { + const content = "# Test" + const planPath = await provider.createAndOpen("emit-test", content) + + const uri = vscode.Uri.parse(planPath) + await provider.delete(uri) + + // Event should have been fired + expect(provider.onDidChangeFile).toBeDefined() + }) + + it("should throw FileNotFound for non-existent document", async () => { + const uri = vscode.Uri.parse("plan:///never-existed.plan.md") + + await expect(provider.delete(uri)).rejects.toThrow() + }) + }) + + describe("content isolation", () => { + it("should maintain separate content for each plan", async () => { + const content1 = "# Plan 1\n\nContent of first plan." + const content2 = "# Plan 2\n\nDifferent content." + + const planPath1 = await provider.createAndOpen("plan-1", content1) + const planPath2 = await provider.createAndOpen("plan-2", content2) + + const uri1 = vscode.Uri.parse(planPath1) + const uri2 = vscode.Uri.parse(planPath2) + + const read1 = new TextDecoder().decode(await provider.readFile(uri1)) + const read2 = new TextDecoder().decode(await provider.readFile(uri2)) + + expect(read1).toBe(content1) + expect(read2).toBe(content2) + expect(read1).not.toBe(read2) + }) + + it("should allow updating one plan without affecting others", async () => { + const content1 = "# Original 1" + const content2 = "# Original 2" + + const planPath1 = await provider.createAndOpen("first", content1) + const planPath2 = await provider.createAndOpen("second", content2) + + // Update only first plan + const updatedContent = "# Updated First" + const uri1 = vscode.Uri.parse(planPath1) + await provider.writeFile(uri1, new TextEncoder().encode(updatedContent), { + create: false, + overwrite: true, + }) + + // Verify first plan is updated + const read1 = new TextDecoder().decode(await provider.readFile(uri1)) + expect(read1).toBe(updatedContent) + + // Verify second plan is unchanged + const uri2 = vscode.Uri.parse(planPath2) + const read2 = new TextDecoder().decode(await provider.readFile(uri2)) + expect(read2).toBe(content2) + }) + + it("should isolate plans with similar names", async () => { + const contentA = "# Document A" + const contentB = "# Document B" + + const planPathA = await provider.createAndOpen("doc", contentA) + const planPathB = await provider.createAndOpen("doc-2", contentB) + + const uriA = vscode.Uri.parse(planPathA) + const uriB = vscode.Uri.parse(planPathB) + + const readA = new TextDecoder().decode(await provider.readFile(uriA)) + const readB = new TextDecoder().decode(await provider.readFile(uriB)) + + expect(readA).toBe(contentA) + expect(readB).toBe(contentB) + }) + }) + + describe("stat", () => { + it("should return FileStat for existing document", async () => { + const content = "# Test" + const planPath = await provider.createAndOpen("stat-test", content) + + const uri = vscode.Uri.parse(planPath) + const stat = await provider.stat(uri) + + expect(stat.type).toBe(vscode.FileType.File) + expect(stat.size).toBeGreaterThan(0) + expect(stat.ctime).toBeDefined() + expect(stat.mtime).toBeDefined() + }) + + it("should throw FileNotFound for non-existent document", async () => { + const uri = vscode.Uri.parse("plan:///stat-missing.plan.md") + + await expect(provider.stat(uri)).rejects.toThrow() + }) + }) + + describe("watch", () => { + it("should return a disposable", () => { + const uri = vscode.Uri.parse("plan:///test.plan.md") + const disposable = provider.watch(uri, { recursive: true, excludes: [] }) + + expect(disposable).toBeDefined() + expect(typeof disposable.dispose).toBe("function") + }) + }) + + describe("readDirectory", () => { + it("should throw FileNotFound (directories not supported)", () => { + const uri = vscode.Uri.parse("plan:///") + + expect(() => provider.readDirectory(uri)).toThrow() + }) + }) + + describe("createDirectory", () => { + it("should throw NoPermissions (directories not supported)", () => { + const uri = vscode.Uri.parse("plan:///new-dir") + + expect(() => provider.createDirectory(uri)).toThrow() + }) + }) + + describe("rename", () => { + it("should throw NoPermissions (rename not supported)", () => { + const oldUri = vscode.Uri.parse("plan:///old.plan.md") + const newUri = vscode.Uri.parse("plan:///new.plan.md") + + expect(() => provider.rename(oldUri, newUri, { overwrite: true })).toThrow() + }) + }) + + describe("getPlanContent", () => { + it("should return correct content for existing plan", async () => { + const content = new TextEncoder().encode("# Test Content") + await provider.setPlanContent("plan://test-doc.plan.md", content) + + const result = await provider.getPlanContent("plan://test-doc.plan.md") + + expect(result).toBeDefined() + expect(result).toEqual(content) + }) + + it("should return undefined for non-existent plan", async () => { + const result = await provider.getPlanContent("plan://nonexistent.plan.md") + + expect(result).toBeUndefined() + }) + + it("should handle plan path with triple slashes", async () => { + const content = new TextEncoder().encode("# Content") + await provider.setPlanContent("plan:///path-test.plan.md", content) + + const result = await provider.getPlanContent("plan:///path-test.plan.md") + + expect(result).toEqual(content) + }) + }) + + describe("setPlanContent", () => { + it("should update existing plan content", async () => { + const originalContent = new TextEncoder().encode("# Original") + const newContent = new TextEncoder().encode("# Updated") + await provider.setPlanContent("plan://updatable.plan.md", originalContent) + + await provider.setPlanContent("plan://updatable.plan.md", newContent) + + const result = await provider.getPlanContent("plan://updatable.plan.md") + expect(result).toEqual(newContent) + }) + + it("should create new plan when content does not exist", async () => { + const content = new TextEncoder().encode("# New Content") + await provider.setPlanContent("plan://new-doc.plan.md", content) + + const result = await provider.getPlanContent("plan://new-doc.plan.md") + expect(result).toEqual(content) + }) + }) + + describe("planExists", () => { + it("should return true for existing plan", async () => { + const content = new TextEncoder().encode("# Test") + await provider.setPlanContent("plan://existing.plan.md", content) + + const result = await provider.planExists("plan://existing.plan.md") + + expect(result).toBe(true) + }) + + it("should return false for non-existent plan", async () => { + const result = await provider.planExists("plan://never-existed.plan.md") + + expect(result).toBe(false) + }) + + it("should return false after plan is deleted", async () => { + const content = new TextEncoder().encode("# To Delete") + await provider.setPlanContent("plan://delete-test.plan.md", content) + await provider.deletePlan("plan://delete-test.plan.md") + + const result = await provider.planExists("plan://delete-test.plan.md") + expect(result).toBe(false) + }) + }) + + describe("deletePlan", () => { + it("should remove plan from storage", async () => { + const content = new TextEncoder().encode("# To Delete") + await provider.setPlanContent("plan://delete-me.plan.md", content) + + await provider.deletePlan("plan://delete-me.plan.md") + + const result = await provider.getPlanContent("plan://delete-me.plan.md") + expect(result).toBeUndefined() + }) + + it("should be no-op for non-existent plan", async () => { + // Should not throw + await expect(provider.deletePlan("plan://never-existed.plan.md")).resolves.not.toThrow() + + // Verify no plans were added + const plans = await provider.listPlans() + expect(plans).toHaveLength(0) + }) + }) + + describe("listPlans", () => { + it("should return all plan paths", async () => { + await provider.setPlanContent("plan://doc1.plan.md", new TextEncoder().encode("# Doc 1")) + await provider.setPlanContent("plan://doc2.plan.md", new TextEncoder().encode("# Doc 2")) + await provider.setPlanContent("plan://doc3.plan.md", new TextEncoder().encode("# Doc 3")) + + const result = await provider.listPlans() + + expect(result).toHaveLength(3) + expect(result).toContain("plan://doc1.plan.md") + expect(result).toContain("plan://doc2.plan.md") + expect(result).toContain("plan://doc3.plan.md") + }) + + it("should return empty array when no plans exist", async () => { + const result = await provider.listPlans() + + expect(result).toEqual([]) + }) + + it("should reflect plans created via setPlanContent", async () => { + await provider.setPlanContent("plan://new.plan.md", new TextEncoder().encode("# New")) + + const result = await provider.listPlans() + + expect(result).toContain("plan://new.plan.md") + }) + + it("should reflect plans deleted via deletePlan", async () => { + await provider.setPlanContent("plan://to-remove.plan.md", new TextEncoder().encode("# Remove")) + await provider.deletePlan("plan://to-remove.plan.md") + + const result = await provider.listPlans() + + expect(result).not.toContain("plan://to-remove.plan.md") + }) + }) +}) diff --git a/src/services/planning/__tests__/planPaths.spec.ts b/src/services/planning/__tests__/planPaths.spec.ts new file mode 100644 index 00000000000..a446f976757 --- /dev/null +++ b/src/services/planning/__tests__/planPaths.spec.ts @@ -0,0 +1,144 @@ +// kilocode_change - new file: Tests for plan path utilities +import { describe, it, expect } from "vitest" +import { + PLAN_PROTOCOL, + PLAN_SCHEME_NAME, + isPlanPath, + planPathToFilename, + filenameToPlanPath, + normalizePlanPath, +} from "../planPaths" + +describe("planPaths", () => { + describe("PLAN_PROTOCOL", () => { + it("should equal 'plan://'", () => { + expect(PLAN_PROTOCOL).toBe("plan://") + }) + }) + + describe("PLAN_SCHEME_NAME", () => { + it("should equal 'plan'", () => { + expect(PLAN_SCHEME_NAME).toBe("plan") + }) + }) + + describe("isPlanPath", () => { + it("should return true for plan:// paths (canonical format)", () => { + expect(isPlanPath("plan://test.md")).toBe(true) + expect(isPlanPath("plan://my-document")).toBe(true) + }) + + it("should return true for plan:/// paths (AI triple-slash format)", () => { + expect(isPlanPath("plan:///test.md")).toBe(true) + expect(isPlanPath("plan:///path/to/file.md")).toBe(true) + }) + + it("should return true for plan:/ paths (VSCode normalized format)", () => { + expect(isPlanPath("plan:/test.md")).toBe(true) + expect(isPlanPath("plan:/implementation-plan.md")).toBe(true) + expect(isPlanPath("plan:/path/to/file.md")).toBe(true) + }) + + it("should return false for non-plan paths", () => { + expect(isPlanPath("/path/to/file.md")).toBe(false) + expect(isPlanPath("file://path/to/file.md")).toBe(false) + expect(isPlanPath("test.md")).toBe(false) + }) + }) + + describe("normalizePlanPath", () => { + it("should normalize plan:// paths (already canonical)", () => { + expect(normalizePlanPath("plan://test.md")).toBe("plan://test.md") + expect(normalizePlanPath("plan://path/to/file.md")).toBe("plan://path/to/file.md") + }) + + it("should normalize plan:/// paths (AI triple-slash format)", () => { + expect(normalizePlanPath("plan:///test.md")).toBe("plan://test.md") + expect(normalizePlanPath("plan:///implementation-plan.md")).toBe("plan://implementation-plan.md") + }) + + it("should normalize plan:/ paths (VSCode normalized format)", () => { + expect(normalizePlanPath("plan:/test.md")).toBe("plan://test.md") + expect(normalizePlanPath("plan:/implementation-plan.md")).toBe("plan://implementation-plan.md") + }) + + it("should throw error for non-plan paths", () => { + expect(() => normalizePlanPath("test.md")).toThrow("Invalid plan path: test.md") + expect(() => normalizePlanPath("/path/to/file.md")).toThrow() + }) + }) + + describe("planPathToFilename", () => { + it("should extract filename from plan:// path (canonical)", () => { + expect(planPathToFilename("plan://test.md")).toBe("test.md") + expect(planPathToFilename("plan://my-document.md")).toBe("my-document.md") + }) + + it("should extract filename from plan:/// path (AI format)", () => { + expect(planPathToFilename("plan:///test.md")).toBe("test.md") + expect(planPathToFilename("plan:///path/to/file.md")).toBe("path/to/file.md") + }) + + it("should extract filename from plan:/ path (VSCode normalized)", () => { + expect(planPathToFilename("plan:/test.md")).toBe("test.md") + expect(planPathToFilename("plan:/implementation-plan.md")).toBe("implementation-plan.md") + expect(planPathToFilename("plan:/path/to/file.md")).toBe("path/to/file.md") + }) + + it("should throw error for invalid plan paths", () => { + expect(() => planPathToFilename("test.md")).toThrow("Invalid plan path: test.md") + expect(() => planPathToFilename("/path/to/file.md")).toThrow() + }) + }) + + describe("filenameToPlanPath", () => { + it("should convert filename to canonical plan:// path", () => { + expect(filenameToPlanPath("test.md")).toBe("plan://test.md") + expect(filenameToPlanPath("my-document")).toBe("plan://my-document") + }) + + it("should strip leading slashes from filename", () => { + expect(filenameToPlanPath("/test.md")).toBe("plan://test.md") + expect(filenameToPlanPath("//test.md")).toBe("plan://test.md") + expect(filenameToPlanPath("///test.md")).toBe("plan://test.md") + }) + + it("should handle paths with directories", () => { + expect(filenameToPlanPath("path/to/file.md")).toBe("plan://path/to/file.md") + expect(filenameToPlanPath("/path/to/file.md")).toBe("plan://path/to/file.md") + }) + }) + + describe("roundtrip conversion", () => { + it("should maintain path integrity through roundtrip", () => { + const original = "test.md" + const planPath = filenameToPlanPath(original) + const result = planPathToFilename(planPath) + expect(result).toBe(original) + }) + + it("should handle complex paths through roundtrip", () => { + const original = "path/to/my-document.md" + const planPath = filenameToPlanPath(original) + expect(planPath).toBe("plan://path/to/my-document.md") + const result = planPathToFilename(planPath) + expect(result).toBe(original) + }) + + it("should normalize any variant through roundtrip", () => { + // AI gives us triple-slash + const aiPath = "plan:///implementation-plan.md" + const normalized = normalizePlanPath(aiPath) + expect(normalized).toBe("plan://implementation-plan.md") + + // VSCode gives us single-slash + const vscodePath = "plan:/implementation-plan.md" + const normalizedVscode = normalizePlanPath(vscodePath) + expect(normalizedVscode).toBe("plan://implementation-plan.md") + + // Both extract same filename + expect(planPathToFilename(aiPath)).toBe("implementation-plan.md") + expect(planPathToFilename(vscodePath)).toBe("implementation-plan.md") + }) + }) +}) diff --git a/src/services/planning/index.ts b/src/services/planning/index.ts new file mode 100644 index 00000000000..78139507aee --- /dev/null +++ b/src/services/planning/index.ts @@ -0,0 +1,10 @@ +// kilocode_change - new file +export { getPlanFileSystem, registerPlanFileSystem, PlanFileSystemProvider } from "./PlanFileSystemProvider" +export { + PLAN_SCHEME_NAME, + PLAN_PROTOCOL, + isPlanPath, + planPathToFilename, + filenameToPlanPath, + normalizePlanPath, +} from "./planPaths" diff --git a/src/services/planning/planPaths.ts b/src/services/planning/planPaths.ts new file mode 100644 index 00000000000..0c1a63e2f36 --- /dev/null +++ b/src/services/planning/planPaths.ts @@ -0,0 +1,122 @@ +// kilocode_change - new file +/** + * Plan path utilities - single source of truth for all plan:// path handling. + * + * URI Format Background: + * - Standard URI: scheme://authority/path + * - For schemes without authority (like plan), the format is: scheme:///path or scheme:/path + * - VSCode's Uri.parse() normalizes "plan:///file.md" to "plan:/file.md" (single slash + path) + * + * Our Standard Format: + * - User-facing/canonical: "plan://filename.md" (looks familiar, clean) + * - Internal (after Uri.parse): "plan:/filename.md" (VSCode normalized) + * + * This module handles all conversions transparently. + */ + +/** + * Plan scheme name for VSCode file system registration. + * Use this when registering the FileSystemProvider. + */ +export const PLAN_SCHEME_NAME = "plan" + +/** + * Plan protocol prefix for user-facing paths. + * This is the canonical format: "plan://filename.md" + */ +export const PLAN_PROTOCOL = "plan://" + +/** + * Check if a path is a plan path. + * Handles all variants: plan://file.md, plan:///file.md, plan:/file.md + * Also detects absolute /plans/... paths that should be redirected to plan:// schema. + * These absolute paths would fail anyway (EROFS) since root /plans is read-only, + * so we redirect them to ephemeral plan documents. Relative paths like "plans/..." + * are NOT matched to allow users with workspace plans/ directories to work normally. + * + * @param path - The path to check + * @returns true if the path is a plan path or should be treated as one + */ +export function isPlanPath(path: string): boolean { + // Check for plan:// URI scheme (canonical plan paths) + if (path.startsWith(`${PLAN_SCHEME_NAME}:`)) { + return true + } + // Check for absolute /plans/... paths that should be redirected + // Only absolute paths (not relative "plans/...") to avoid interfering with + // users who have a plans/ directory in their workspace + return path.startsWith("/plans/") +} + +/** + * Normalize any plan path variant to the canonical format: "plan://filename.md" + * + * Handles: + * - "plan://file.md" -> "plan://file.md" (already canonical) + * - "plan:///file.md" -> "plan://file.md" (triple slash from AI) + * - "plan:/file.md" -> "plan://file.md" (VSCode normalized) + * - "/plans/file.md" -> "plan://file.md" (absolute /plans/ paths) + * + * @param planPath - Any plan path variant + * @returns Canonical plan path: "plan://filename.md" + * @throws Error if not a valid plan path + */ +export function normalizePlanPath(planPath: string): string { + if (!isPlanPath(planPath)) { + throw new Error(`Invalid plan path: ${planPath}`) + } + + // Handle /plans/ paths by converting them first + if (planPath.startsWith("/plans/")) { + const filename = planPath.replace(/^\/plans\//, "").replace(/^\//, "") + return `${PLAN_PROTOCOL}${filename}` + } + + // Extract filename by removing scheme and all leading slashes + const afterScheme = planPath.slice(`${PLAN_SCHEME_NAME}:`.length) + const filename = afterScheme.replace(/^\/+/, "") + + // Return canonical format + return `${PLAN_PROTOCOL}${filename}` +} + +/** + * Extract filename from a plan path. + * Handles all variants: plan://file.md, plan:///file.md, plan:/file.md, /plans/file.md + * + * @param planPath - The plan path (any variant) + * @returns The filename without protocol or leading slashes (e.g., "filename.md") + * @throws Error if the path is not a valid plan path + */ +export function planPathToFilename(planPath: string): string { + if (!isPlanPath(planPath)) { + throw new Error(`Invalid plan path: ${planPath}`) + } + + // Handle /plans/ paths + if (planPath.startsWith("/plans/")) { + const result = planPath.replace(/^\/plans\//, "").replace(/^\//, "") + return result + } + + // Remove scheme prefix + const afterScheme = planPath.slice(`${PLAN_SCHEME_NAME}:`.length) + // Remove any leading slashes (handles //, ///, or /) + const result = afterScheme.replace(/^\/+/, "") + return result +} + +/** + * Convert a filename to the canonical plan:// path. + * Always returns clean "plan://filename.md" format. + * + * @param filename - The filename (e.g., "filename.md" or "/filename.md") + * @returns The plan path in canonical format (e.g., "plan://filename.md") + */ +export function filenameToPlanPath(filename: string): string { + // Remove any leading slashes from filename + const cleanFilename = filename.replace(/^\/+/, "") + // Return canonical format: plan://filename.md + const result = `${PLAN_PROTOCOL}${cleanFilename}` + return result +} diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index fac84fe367e..b6fb2a8b8ca 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -37,6 +37,7 @@ describe("experiments", () => { const experiments: Record = { morphFastApply: false, // kilocode_change speechToText: false, // kilocode_change + ephemeralPlanning: false, // kilocode_change powerSteering: false, multiFileApplyDiff: false, preventFocusDisruption: false, @@ -51,6 +52,7 @@ describe("experiments", () => { const experiments: Record = { morphFastApply: false, // kilocode_change speechToText: false, // kilocode_change + ephemeralPlanning: false, // kilocode_change powerSteering: true, multiFileApplyDiff: false, preventFocusDisruption: false, @@ -65,6 +67,7 @@ describe("experiments", () => { const experiments: Record = { morphFastApply: false, // kilocode_change speechToText: false, // kilocode_change + ephemeralPlanning: false, // kilocode_change powerSteering: false, multiFileApplyDiff: false, preventFocusDisruption: false, diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index 3ed85c0627d..92515f7a4c7 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -3,6 +3,7 @@ import type { AssertEqual, Equals, Keys, Values, ExperimentId, Experiments } fro export const EXPERIMENT_IDS = { MORPH_FAST_APPLY: "morphFastApply", // kilocode_change SPEECH_TO_TEXT: "speechToText", // kilocode_change + EPHEMERAL_PLANNING: "ephemeralPlanning", // kilocode_change MULTI_FILE_APPLY_DIFF: "multiFileApplyDiff", POWER_STEERING: "powerSteering", PREVENT_FOCUS_DISRUPTION: "preventFocusDisruption", @@ -22,6 +23,7 @@ interface ExperimentConfig { export const experimentConfigsMap: Record = { MORPH_FAST_APPLY: { enabled: false }, // kilocode_change SPEECH_TO_TEXT: { enabled: true }, // kilocode_change + EPHEMERAL_PLANNING: { enabled: false }, // 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 56dd4624bb8..6c8cecf0814 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -121,6 +121,7 @@ export type NativeToolArgs = { update_todo_list: { todos: string } use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record } write_to_file: { path: string; content: string } + create_plan: { title: string; content: string } // Add more tools as they are migrated to native protocol } @@ -262,6 +263,11 @@ export interface GenerateImageToolUse extends ToolUse<"generate_image"> { params: Partial, "prompt" | "path" | "image">> } +export interface CreatePlanToolUse extends ToolUse<"create_plan"> { + name: "create_plan" + params: Partial, "title" | "content">> +} + // Define tool group configuration export type ToolGroupConfig = { tools: readonly string[] @@ -298,6 +304,7 @@ export const TOOL_DISPLAY_NAMES: Record = { update_todo_list: "update todo list", run_slash_command: "run slash command", generate_image: "generate images", + create_plan: "create plan documents", } as const // Define available tool groups. @@ -313,6 +320,7 @@ export const TOOL_GROUPS: Record = { "delete_file", // kilocode_change "new_rule", // kilocode_change "generate_image", + "create_plan", ], customTools: ["search_and_replace", "search_replace", "apply_patch"], }, @@ -338,7 +346,8 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ "switch_mode", "new_task", "report_bug", - "condense", // kilocode_Change + "condense", // kilocode_change + "create_plan", // kilocode_change "update_todo_list", "run_slash_command", ] as const diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 28f64fd3a5f..c43fe6f9354 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -282,6 +282,7 @@ describe("mergeExtensionState", () => { preventFocusDisruption: false, morphFastApply: false, // kilocode_change speechToText: false, // kilocode_change + ephemeralPlanning: false, // kilocode_change newTaskRequireTodos: false, imageGeneration: false, runSlashCommand: false, @@ -304,6 +305,7 @@ describe("mergeExtensionState", () => { preventFocusDisruption: false, morphFastApply: false, // kilocode_change speechToText: false, // kilocode_change + ephemeralPlanning: false, // kilocode_change newTaskRequireTodos: false, imageGeneration: false, runSlashCommand: false, diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 86922fa1b08..d63e99439b7 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1019,6 +1019,10 @@ "MULTIPLE_NATIVE_TOOL_CALLS": { "name": "Parallel tool calls", "description": "When enabled, the native protocol can execute multiple tools in a single assistant message turn." + }, + "EPHEMERAL_PLANNING": { + "name": "Ephemeral planning tool", + "description": "When enabled, Kilo Code can create temporary plan documents to organize complex tasks. Plans are stored in memory and discarded when the session ends." } }, "promptCaching": {