diff --git a/.changeset/large-olives-wink.md b/.changeset/large-olives-wink.md new file mode 100644 index 0000000000..a3b4486f1e --- /dev/null +++ b/.changeset/large-olives-wink.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix model search being prefilled in dropdown to prevent confusion in available models diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index 51a10f0154..0ad19d8676 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -24,12 +24,16 @@ export const codebaseIndexConfigSchema = z.object({ codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible", "gemini"]).optional(), codebaseIndexEmbedderBaseUrl: z.string().optional(), codebaseIndexEmbedderModelId: z.string().optional(), + codebaseIndexEmbedderModelDimension: z.number().optional(), codebaseIndexSearchMinScore: z.number().min(0).max(1).optional(), codebaseIndexSearchMaxResults: z .number() .min(CODEBASE_INDEX_DEFAULTS.MIN_SEARCH_RESULTS) .max(CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_RESULTS) .optional(), + // OpenAI Compatible specific fields + codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(), + codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(), }) export type CodebaseIndexConfig = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 6a0dda1cf2..79a09ff017 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -47,6 +47,7 @@ export const globalSettingsSchema = z.object({ alwaysAllowExecute: z.boolean().optional(), alwaysAllowFollowupQuestions: z.boolean().optional(), followupAutoApproveTimeoutMs: z.number().optional(), + alwaysAllowUpdateTodoList: z.boolean().optional(), allowedCommands: z.array(z.string()).optional(), allowedMaxRequests: z.number().nullish(), autoCondenseContext: z.boolean().optional(), @@ -195,6 +196,7 @@ export const EVALS_SETTINGS: RooCodeSettings = { alwaysAllowSubtasks: true, alwaysAllowExecute: true, alwaysAllowFollowupQuestions: true, + alwaysAllowUpdateTodoList: true, followupAutoApproveTimeoutMs: 0, allowedCommands: ["*"], diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 6c6db9ccd5..44937da235 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -20,3 +20,4 @@ export * from "./terminal.js" export * from "./tool.js" export * from "./type-fu.js" export * from "./vscode.js" +export * from "./todo.js" diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index 914f02ecd6..49d5203c66 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -106,6 +106,7 @@ export const clineSays = [ "condense_context", "condense_context_error", "codebase_search_result", + "user_edit_todos", ] as const export const clineSaySchema = z.enum(clineSays) diff --git a/packages/types/src/todo.ts b/packages/types/src/todo.ts new file mode 100644 index 0000000000..4e874e1750 --- /dev/null +++ b/packages/types/src/todo.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +/** + * TodoStatus + */ +export const todoStatusSchema = z.enum(["pending", "in_progress", "completed"] as const) + +export type TodoStatus = z.infer + +/** + * TodoItem + */ +export const todoItemSchema = z.object({ + id: z.string(), + content: z.string(), + status: todoStatusSchema, +}) + +export type TodoItem = z.infer diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 9e807d639d..7a3fd21199 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -33,6 +33,7 @@ export const toolNames = [ "new_task", "fetch_instructions", "codebase_search", + "update_todo_list", ] as const export const toolNamesSchema = z.enum(toolNames) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 21c973ab50..ee3fa148b4 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -26,6 +26,7 @@ import { attemptCompletionTool } from "../tools/attemptCompletionTool" import { newTaskTool } from "../tools/newTaskTool" import { checkpointSave } from "../checkpoints" +import { updateTodoListTool } from "../tools/updateTodoListTool" import { formatResponse } from "../prompts/responses" import { validateToolUse } from "../tools/validateToolUse" @@ -205,6 +206,8 @@ export async function presentAssistantMessage(cline: Task) { return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]` case "codebase_search": // Add case for the new tool return `[${block.name} for '${block.params.query}']` + case "update_todo_list": + return `[${block.name}]` case "new_task": { const mode = block.params.mode ?? defaultModeSlug const message = block.params.message ?? "(no message)" @@ -410,6 +413,9 @@ export async function presentAssistantMessage(cline: Task) { case "write_to_file": await writeToFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break + case "update_todo_list": + await updateTodoListTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + break case "apply_diff": { // Get the provider and state to check experiment settings const provider = cline.providerRef.deref() diff --git a/src/core/config/__tests__/importExport.spec.ts b/src/core/config/__tests__/importExport.spec.ts index 1f6bd5f28e..052bfc77f8 100644 --- a/src/core/config/__tests__/importExport.spec.ts +++ b/src/core/config/__tests__/importExport.spec.ts @@ -595,5 +595,1024 @@ describe("importExport", () => { expect(vscode.Uri.file).toHaveBeenCalledWith(path.join("/mock/home", "Documents", "roo-code-settings.json")) }) + + describe("codebase indexing export", () => { + it("should export correct base URL for OpenAI Compatible provider", async () => { + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/roo-code-settings.json", + }) + + const mockProviderProfiles = { + currentApiConfigName: "openai-compatible-provider", + apiConfigs: { + "openai-compatible-provider": { + apiProvider: "openai" as ProviderName, + id: "openai-compatible-id", + // Remove OpenAI Compatible settings from provider profile + }, + "ollama-provider": { + apiProvider: "ollama" as ProviderName, + id: "ollama-id", + codebaseIndexOllamaBaseUrl: "http://localhost:11434", + }, + }, + modeApiConfigs: {}, + } + + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexEmbedderBaseUrl: "http://localhost:11434", // Wrong URL from Ollama + // OpenAI Compatible settings are now stored directly in codebaseIndexConfig + codebaseIndexOpenAiCompatibleBaseUrl: "https://custom-openai-api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1536, + }, + } + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { + providerProfiles: mockProviderProfiles, + globalSettings: mockGlobalSettings, + }) + }) + + it("should export model dimension for OpenAI Compatible provider", async () => { + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/roo-code-settings.json", + }) + + const mockProviderProfiles = { + currentApiConfigName: "test-provider", + apiConfigs: { + "test-provider": { + apiProvider: "openai" as ProviderName, + id: "test-id", + // Remove OpenAI Compatible settings from provider profile + }, + }, + modeApiConfigs: {}, + } + + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "custom-embedding-model", + codebaseIndexEmbedderBaseUrl: "", + // OpenAI Compatible settings are now stored directly in codebaseIndexConfig + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 768, + }, + } + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + const exportedData = (safeWriteJson as Mock).mock.calls[0][1] + // Settings are now exported as-is from codebaseIndexConfig + expect( + exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension, + ).toBe(768) + expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl).toBe( + "https://api.example.com/v1", + ) + }) + + it("should not mix settings between different providers", async () => { + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/roo-code-settings.json", + }) + + const mockProviderProfiles = { + currentApiConfigName: "openai-compatible-provider", + apiConfigs: { + "openai-compatible-provider": { + apiProvider: "openai" as ProviderName, + id: "openai-compatible-id", + // Remove OpenAI Compatible settings from provider profile + }, + "ollama-provider": { + apiProvider: "ollama" as ProviderName, + id: "ollama-id", + codebaseIndexOllamaBaseUrl: "http://localhost:11434", + }, + "anthropic-provider": { + apiProvider: "anthropic" as ProviderName, + id: "anthropic-id", + }, + }, + modeApiConfigs: {}, + } + + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexEmbedderBaseUrl: "http://localhost:11434", // Wrong URL from Ollama + // OpenAI Compatible settings are now stored directly in codebaseIndexConfig + codebaseIndexOpenAiCompatibleBaseUrl: "https://openai-compatible.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1536, + }, + } + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + const exportedData = (safeWriteJson as Mock).mock.calls[0][1] + // Settings are now exported as-is from codebaseIndexConfig + expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl).toBe( + "https://openai-compatible.example.com/v1", + ) + expect( + exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension, + ).toBe(1536) + // The generic embedder base URL is still there + expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexEmbedderBaseUrl).toBe( + "http://localhost:11434", + ) + }) + + it("should handle missing provider-specific settings gracefully", async () => { + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/roo-code-settings.json", + }) + + const mockProviderProfiles = { + currentApiConfigName: "incomplete-provider", + apiConfigs: { + "incomplete-provider": { + apiProvider: "openai" as ProviderName, + id: "incomplete-id", + // Missing codebaseIndexOpenAiCompatibleBaseUrl and dimension + }, + }, + modeApiConfigs: {}, + } + + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexEmbedderBaseUrl: "https://fallback.example.com/v1", + }, + } + + // Mock getGlobalState to return undefined (no settings) + mockContextProxy.getGlobalState = vi.fn().mockReturnValue(undefined) + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + // Should not throw an error and should preserve original settings + expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { + providerProfiles: mockProviderProfiles, + globalSettings: mockGlobalSettings, // Should remain unchanged + }) + }) + + it("should maintain backward compatibility with existing exports", async () => { + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/roo-code-settings.json", + }) + + const mockProviderProfiles = { + currentApiConfigName: "openai-provider", + apiConfigs: { + "openai-provider": { + apiProvider: "openai" as ProviderName, + id: "openai-id", + // Regular OpenAI provider without OpenAI Compatible settings + }, + }, + modeApiConfigs: {}, + } + + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai" as const, // Not openai-compatible + codebaseIndexEmbedderModelId: "text-embedding-ada-002", + codebaseIndexEmbedderBaseUrl: "https://api.openai.com/v1", + }, + } + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + // Should not modify settings for non-openai-compatible providers + expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { + providerProfiles: mockProviderProfiles, + globalSettings: mockGlobalSettings, // Should remain unchanged + }) + }) + + it("should handle missing current provider gracefully", async () => { + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/roo-code-settings.json", + }) + + const mockProviderProfiles = { + currentApiConfigName: "nonexistent-provider", + apiConfigs: { + "other-provider": { + apiProvider: "openai" as ProviderName, + id: "other-id", + }, + }, + modeApiConfigs: {}, + } + + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexEmbedderBaseUrl: "https://fallback.example.com/v1", + }, + } + + // Mock getGlobalState to return undefined (no settings) + mockContextProxy.getGlobalState = vi.fn().mockReturnValue(undefined) + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + // Should not throw an error and should preserve original settings + expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { + providerProfiles: mockProviderProfiles, + globalSettings: mockGlobalSettings, // Should remain unchanged + }) + }) + }) + + describe("import with OpenAI Compatible codebase indexing settings", () => { + it("should properly import OpenAI Compatible settings in codebaseIndexConfig", async () => { + ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }]) + + const mockFileContent = JSON.stringify({ + providerProfiles: { + currentApiConfigName: "openai-compatible-provider", + apiConfigs: { + "openai-compatible-provider": { + apiProvider: "openai" as ProviderName, + id: "openai-compatible-id", + // Provider-specific settings remain in provider profile + codebaseIndexOpenAiCompatibleBaseUrl: "https://old-url.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 512, + }, + }, + modeApiConfigs: {}, + }, + globalSettings: { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexEmbedderBaseUrl: "https://imported-url.example.com/v1", + codebaseIndexEmbedderModelDimension: 1536, + // OpenAI Compatible settings are now stored directly here + codebaseIndexOpenAiCompatibleBaseUrl: "https://imported-url.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1536, + }, + }, + }) + + ;(fs.readFile as Mock).mockResolvedValue(mockFileContent) + + const previousProviderProfiles = { + currentApiConfigName: "default", + apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } }, + } + + mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { + name: "openai-compatible-provider", + id: "openai-compatible-id", + apiProvider: "openai" as ProviderName, + }, + { name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName }, + ]) + + const result = await importSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(result.success).toBe(true) + + // Verify that the global settings were imported correctly + expect(mockContextProxy.setValues).toHaveBeenCalledWith( + expect.objectContaining({ + codebaseIndexConfig: expect.objectContaining({ + codebaseIndexOpenAiCompatibleBaseUrl: "https://imported-url.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1536, + }), + }), + ) + + // Provider profiles are imported as-is + const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0] + const importedProvider = importedProviderProfiles.apiConfigs["openai-compatible-provider"] + + // Provider still has its own settings (not modified by import) + expect(importedProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBe("https://old-url.example.com/v1") + expect(importedProvider.codebaseIndexOpenAiCompatibleModelDimension).toBe(512) + }) + + it("should handle missing OpenAI Compatible settings gracefully during import", async () => { + ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }]) + + const mockFileContent = JSON.stringify({ + providerProfiles: { + currentApiConfigName: "openai-compatible-provider", + apiConfigs: { + "openai-compatible-provider": { + apiProvider: "openai" as ProviderName, + id: "openai-compatible-id", + }, + }, + modeApiConfigs: {}, + }, + globalSettings: { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + // Missing base URL and model dimension + }, + }, + }) + + ;(fs.readFile as Mock).mockResolvedValue(mockFileContent) + + const previousProviderProfiles = { + currentApiConfigName: "default", + apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } }, + } + + mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { + name: "openai-compatible-provider", + id: "openai-compatible-id", + apiProvider: "openai" as ProviderName, + }, + ]) + + const result = await importSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(result.success).toBe(true) + // Should not throw an error when settings are missing + }) + + it("should not modify provider settings for non-openai-compatible providers during import", async () => { + ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }]) + + const mockFileContent = JSON.stringify({ + providerProfiles: { + currentApiConfigName: "anthropic-provider", + apiConfigs: { + "anthropic-provider": { + apiProvider: "anthropic" as ProviderName, + id: "anthropic-id", + }, + }, + modeApiConfigs: {}, + }, + globalSettings: { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai" as const, // Not openai-compatible + codebaseIndexEmbedderModelId: "text-embedding-ada-002", + codebaseIndexEmbedderBaseUrl: "https://api.openai.com/v1", + codebaseIndexEmbedderModelDimension: 1536, + }, + }, + }) + + ;(fs.readFile as Mock).mockResolvedValue(mockFileContent) + + const previousProviderProfiles = { + currentApiConfigName: "default", + apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } }, + } + + mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { name: "anthropic-provider", id: "anthropic-id", apiProvider: "anthropic" as ProviderName }, + ]) + + const result = await importSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(result.success).toBe(true) + + // Verify that the provider settings were not modified with OpenAI Compatible fields + const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0] + const importedProvider = importedProviderProfiles.apiConfigs["anthropic-provider"] + + expect(importedProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined() + expect(importedProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined() + }) + }) + + it("should preserve model dimension exactly in export/import roundtrip", async () => { + // This test specifically isolates the model dimension export/import roundtrip + // to catch the exact issue the user is experiencing + + const testModelDimension = 768 + + // Step 1: Set up a provider without OpenAI Compatible settings in profile + const mockProviderProfiles = { + currentApiConfigName: "test-openai-compatible", + apiConfigs: { + "test-openai-compatible": { + apiProvider: "openai" as ProviderName, + id: "test-id", + // Remove OpenAI Compatible settings from provider profile + }, + }, + modeApiConfigs: {}, + } + + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "custom-embedding-model", + codebaseIndexEmbedderBaseUrl: "https://api.example.com/v1", + codebaseIndexEmbedderModelDimension: testModelDimension, + // OpenAI Compatible settings are now stored directly in codebaseIndexConfig + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: testModelDimension, + }, + } + + // Step 2: Mock export operation + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/test-settings.json", + }) + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + // Step 3: Export settings + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + // Step 4: Verify the exported data includes the model dimension + expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/test-settings.json", { + providerProfiles: mockProviderProfiles, + globalSettings: mockGlobalSettings, + }) + + // Step 5: Get the exported data for import test + const exportedData = (safeWriteJson as Mock).mock.calls[0][1] + const exportedFileContent = JSON.stringify(exportedData) + + // Step 6: Mock import operation + ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/test-settings.json" }]) + ;(fs.readFile as Mock).mockResolvedValue(exportedFileContent) + + // Reset mocks for import + vi.clearAllMocks() + mockProviderSettingsManager.export.mockResolvedValue({ + currentApiConfigName: "default", + apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } }, + }) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { name: "test-openai-compatible", id: "test-id", apiProvider: "openai" as ProviderName }, + ]) + + // Step 7: Import the settings back + const importResult = await importSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + // Step 8: Verify import was successful + expect(importResult.success).toBe(true) + + // Step 9: Verify that the model dimension was preserved exactly in global settings + const importedGlobalSettings = mockContextProxy.setValues.mock.calls[0][0] + expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleModelDimension).toBe( + testModelDimension, + ) + expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl).toBe( + "https://api.example.com/v1", + ) + + // Step 10: Verify that the embedder settings were imported correctly + expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexEmbedderModelDimension).toBe( + testModelDimension, + ) + }) + + it("should handle edge case model dimension values (0, null) correctly", async () => { + // Test with model dimension = 0 (which is falsy but valid) + const testModelDimension = 0 + + const mockProviderProfiles = { + currentApiConfigName: "test-openai-compatible", + apiConfigs: { + "test-openai-compatible": { + apiProvider: "openai" as ProviderName, + id: "test-id", + // Remove OpenAI Compatible settings from provider profile + }, + }, + modeApiConfigs: {}, + } + + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "custom-embedding-model", + codebaseIndexEmbedderBaseUrl: "https://api.example.com/v1", + // OpenAI Compatible settings are now stored directly in codebaseIndexConfig + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: testModelDimension, // 0 is a valid value + }, + } + + // Mock export operation + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/test-settings.json", + }) + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + // Export settings + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + // Verify the exported data includes the model dimension even when it's 0 + const exportedData = (safeWriteJson as Mock).mock.calls[0][1] + expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension).toBe(0) + + // Test import roundtrip + const exportedFileContent = JSON.stringify(exportedData) + ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/test-settings.json" }]) + ;(fs.readFile as Mock).mockResolvedValue(exportedFileContent) + + // Reset mocks for import + vi.clearAllMocks() + mockProviderSettingsManager.export.mockResolvedValue({ + currentApiConfigName: "default", + apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } }, + }) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { name: "test-openai-compatible", id: "test-id", apiProvider: "openai" as ProviderName }, + ]) + + // Import the settings back + const importResult = await importSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(importResult.success).toBe(true) + + // Verify that model dimension 0 was preserved in global settings + const setValuesCall = mockContextProxy.setValues.mock.calls[0][0] + expect(setValuesCall.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleModelDimension).toBe(0) + }) + + it("should handle missing model dimension gracefully", async () => { + // Test when model dimension is undefined in global state + const mockProviderProfiles = { + currentApiConfigName: "test-openai-compatible", + apiConfigs: { + "test-openai-compatible": { + apiProvider: "openai" as ProviderName, + id: "test-id", + // Remove OpenAI Compatible settings from provider profile + }, + }, + modeApiConfigs: {}, + } + + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "custom-embedding-model", + codebaseIndexEmbedderBaseUrl: "https://api.example.com/v1", + }, + } + + // Mock getGlobalState to return undefined for model dimension + mockContextProxy.getGlobalState = vi.fn().mockImplementation((key: string) => { + if (key === "codebaseIndexOpenAiCompatibleBaseUrl") { + return "https://api.example.com/v1" + } + if (key === "codebaseIndexOpenAiCompatibleModelDimension") { + return undefined + } + return undefined + }) + + // Mock export operation + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/test-settings.json", + }) + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + // Export settings + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + // Verify the exported data does NOT include model dimension when it's undefined + const exportedData = (safeWriteJson as Mock).mock.calls[0][1] + expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexEmbedderModelDimension).toBeUndefined() + }) + + it("should handle provider mismatch during import - BUG REPRODUCTION", async () => { + // This test reproduces the bug where model dimension is lost when importing + // settings where the current provider is different from the exported provider + + // Step 1: Create exported settings from "provider-a" with model dimension + const exportedSettings = { + providerProfiles: { + currentApiConfigName: "provider-a", + apiConfigs: { + "provider-a": { + apiProvider: "openai" as ProviderName, + id: "provider-a-id", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api-a.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1536, + }, + "provider-b": { + apiProvider: "anthropic" as ProviderName, + id: "provider-b-id", + }, + }, + modeApiConfigs: {}, + }, + globalSettings: { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexEmbedderBaseUrl: "https://api-a.example.com/v1", + codebaseIndexEmbedderModelDimension: 1536, + }, + }, + } + + // Step 2: Set up import environment where current provider is "provider-b" (different!) + const currentProviderProfiles = { + currentApiConfigName: "provider-b", // Different from exported settings! + apiConfigs: { + "provider-b": { + apiProvider: "anthropic" as ProviderName, + id: "provider-b-id", + }, + }, + } + + // Step 3: Mock import operation + ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }]) + ;(fs.readFile as Mock).mockResolvedValue(JSON.stringify(exportedSettings)) + + mockProviderSettingsManager.export.mockResolvedValue(currentProviderProfiles) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { name: "provider-a", id: "provider-a-id", apiProvider: "openai" as ProviderName }, + { name: "provider-b", id: "provider-b-id", apiProvider: "anthropic" as ProviderName }, + ]) + + // Step 4: Import the settings + const importResult = await importSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(importResult.success).toBe(true) + + // Step 5: Check what was imported + const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0] + + // The bug: provider-a should have its model dimension preserved, but it might be lost + // because the import logic only updates the CURRENT provider (provider-b) + const providerA = importedProviderProfiles.apiConfigs["provider-a"] + const providerB = importedProviderProfiles.apiConfigs["provider-b"] + + // This should pass but might fail due to the bug + expect(providerA.codebaseIndexOpenAiCompatibleModelDimension).toBe(1536) + expect(providerA.codebaseIndexOpenAiCompatibleBaseUrl).toBe("https://api-a.example.com/v1") + + // Provider B should not have OpenAI Compatible settings + expect(providerB.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined() + expect(providerB.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined() + }) + + it("should NOT copy OpenAI Compatible settings to provider profiles - FIXED BEHAVIOR", async () => { + // This test verifies the FIXED behavior: OpenAI Compatible settings stay in global settings only + + const exportedSettings = { + providerProfiles: { + currentApiConfigName: "openai-compatible-provider", + apiConfigs: { + "openai-compatible-provider": { + apiProvider: "openai" as ProviderName, + id: "openai-compatible-id", + // NO OpenAI Compatible settings here in the fixed version + }, + "anthropic-provider": { + apiProvider: "anthropic" as ProviderName, + id: "anthropic-id", + }, + }, + modeApiConfigs: {}, + }, + globalSettings: { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexEmbedderBaseUrl: "https://new-url.example.com/v1", + codebaseIndexEmbedderModelDimension: 1536, + // OpenAI Compatible settings are stored here + codebaseIndexOpenAiCompatibleBaseUrl: "https://new-url.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1536, + }, + }, + } + + const currentProviderProfiles = { + currentApiConfigName: "anthropic-provider", + apiConfigs: { + "anthropic-provider": { + apiProvider: "anthropic" as ProviderName, + id: "anthropic-id", + }, + }, + } + + ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }]) + ;(fs.readFile as Mock).mockResolvedValue(JSON.stringify(exportedSettings)) + + mockProviderSettingsManager.export.mockResolvedValue(currentProviderProfiles) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { + name: "openai-compatible-provider", + id: "openai-compatible-id", + apiProvider: "openai" as ProviderName, + }, + { name: "anthropic-provider", id: "anthropic-id", apiProvider: "anthropic" as ProviderName }, + ]) + + const importResult = await importSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(importResult.success).toBe(true) + + // Verify OpenAI Compatible settings are imported to global settings + const importedGlobalSettings = mockContextProxy.setValues.mock.calls[0][0] + expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl).toBe( + "https://new-url.example.com/v1", + ) + expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleModelDimension).toBe(1536) + + // Verify provider profiles do NOT have OpenAI Compatible settings + const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0] + const openaiCompatibleProvider = importedProviderProfiles.apiConfigs["openai-compatible-provider"] + const anthropicProvider = importedProviderProfiles.apiConfigs["anthropic-provider"] + + // Neither provider should have OpenAI Compatible settings + expect(openaiCompatibleProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined() + expect(openaiCompatibleProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined() + expect(anthropicProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined() + expect(anthropicProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined() + }) + + it("should keep OpenAI Compatible settings in global state only - FIXED BEHAVIOR", async () => { + // This test verifies that OpenAI Compatible settings remain in global state + // and are NOT copied to provider profiles + + const exportedSettings = { + providerProfiles: { + currentApiConfigName: "anthropic-provider", + apiConfigs: { + "anthropic-provider": { + apiProvider: "anthropic" as ProviderName, + id: "anthropic-id", + }, + "openai-compatible-provider": { + apiProvider: "openai" as ProviderName, + id: "openai-compatible-id", + // NO OpenAI Compatible settings in provider profiles + }, + }, + modeApiConfigs: {}, + }, + globalSettings: { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexEmbedderBaseUrl: "https://updated.example.com/v1", + codebaseIndexEmbedderModelDimension: 1536, + // OpenAI Compatible settings are stored here + codebaseIndexOpenAiCompatibleBaseUrl: "https://updated.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1536, + }, + }, + } + + const currentProviderProfiles = { + currentApiConfigName: "default", + apiConfigs: { + default: { + apiProvider: "openai" as ProviderName, + id: "default-id", + }, + }, + } + + ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }]) + ;(fs.readFile as Mock).mockResolvedValue(JSON.stringify(exportedSettings)) + + mockProviderSettingsManager.export.mockResolvedValue(currentProviderProfiles) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { name: "anthropic-provider", id: "anthropic-id", apiProvider: "anthropic" as ProviderName }, + { + name: "openai-compatible-provider", + id: "openai-compatible-id", + apiProvider: "openai" as ProviderName, + }, + { name: "default", id: "default-id", apiProvider: "openai" as ProviderName }, + ]) + + const importResult = await importSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(importResult.success).toBe(true) + + // Verify OpenAI Compatible settings are imported to global settings + const importedGlobalSettings = mockContextProxy.setValues.mock.calls[0][0] + expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl).toBe( + "https://updated.example.com/v1", + ) + expect(importedGlobalSettings.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleModelDimension).toBe(1536) + + // Verify NO provider profiles have OpenAI Compatible settings + const importedProviderProfiles = mockProviderSettingsManager.import.mock.calls[0][0] + const anthropicProvider = importedProviderProfiles.apiConfigs["anthropic-provider"] + const openaiCompatibleProvider = importedProviderProfiles.apiConfigs["openai-compatible-provider"] + + // Neither provider should have OpenAI Compatible settings + expect(anthropicProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined() + expect(anthropicProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined() + expect(openaiCompatibleProvider.codebaseIndexOpenAiCompatibleBaseUrl).toBeUndefined() + expect(openaiCompatibleProvider.codebaseIndexOpenAiCompatibleModelDimension).toBeUndefined() + }) + + it("should export OpenAI Compatible settings from global state when provider is openai-compatible", async () => { + // This test reproduces the bug where codebaseIndexEmbedderModelDimension is missing from exported JSON + // when the OpenAI Compatible settings are stored in global state via contextProxy + + ;(vscode.window.showSaveDialog as Mock).mockResolvedValue({ + fsPath: "/mock/path/roo-code-settings.json", + }) + + // Set up provider profiles - note that the OpenAI Compatible provider does NOT have + // the codebaseIndexOpenAiCompatibleBaseUrl and codebaseIndexOpenAiCompatibleModelDimension + // fields in the provider profile itself + const mockProviderProfiles = { + currentApiConfigName: "openrouter-provider", // Current provider is OpenRouter + apiConfigs: { + "openrouter-provider": { + apiProvider: "openrouter" as ProviderName, + id: "openrouter-id", + // OpenRouter doesn't have OpenAI Compatible fields + }, + }, + modeApiConfigs: {}, + } + + // The global settings now include OpenAI Compatible settings directly in codebaseIndexConfig + const mockGlobalSettings = { + mode: "code", + codebaseIndexConfig: { + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: "openai-compatible" as const, + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexEmbedderBaseUrl: "https://custom-api.example.com/v1", + codebaseIndexEmbedderModelDimension: 1536, + // OpenAI Compatible settings are now included directly + codebaseIndexOpenAiCompatibleBaseUrl: "https://custom-api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1536, + }, + } + + mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles) + mockContextProxy.export.mockResolvedValue(mockGlobalSettings) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + await exportSettings({ + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + }) + + // Verify that the exported JSON contains the OpenAI Compatible settings + const exportedData = (safeWriteJson as Mock).mock.calls[0][1] + + // With the fix, these values are now properly exported + expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension).toBe( + 1536, + ) + expect(exportedData.globalSettings.codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl).toBe( + "https://custom-api.example.com/v1", + ) + }) }) }) diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts index 6ffbbf7136..c3d6f9c215 100644 --- a/src/core/config/importExport.ts +++ b/src/core/config/importExport.ts @@ -68,6 +68,9 @@ export async function importSettingsFromPath( (globalSettings.customModes ?? []).map((mode) => customModesManager.updateCustomMode(mode.slug, mode)), ) + // OpenAI Compatible settings are now correctly stored in codebaseIndexConfig + // They will be imported automatically with the config - no special handling needed + await providerSettingsManager.import(providerProfiles) await contextProxy.setValues(globalSettings) @@ -161,10 +164,16 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }: return } + // OpenAI Compatible settings are now correctly stored in codebaseIndexConfig + // No workaround needed - they will be exported automatically with the config + const dirname = path.dirname(uri.fsPath) await fs.mkdir(dirname, { recursive: true }) await safeWriteJson(uri.fsPath, { providerProfiles, globalSettings }) - } catch (e) {} + } catch (e) { + console.error("Failed to export settings:", e) + // Don't re-throw - the UI will handle showing error messages + } } /** diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index fa0bc7bb82..7a3847334d 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -18,6 +18,7 @@ import { arePathsEqual } from "../../utils/path" import { formatResponse } from "../prompts/responses" import { Task } from "../task/Task" +import { formatReminderSection } from "./reminder" export async function getEnvironmentDetails(cline: Task, includeFileDetails: boolean = false) { let details = "" @@ -273,5 +274,6 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo } } - return `\n${details.trim()}\n` + const reminderSection = formatReminderSection(cline.todoList) + return `\n${details.trim()}\n${reminderSection}\n` } diff --git a/src/core/environment/reminder.ts b/src/core/environment/reminder.ts new file mode 100644 index 0000000000..eb1b39dfb5 --- /dev/null +++ b/src/core/environment/reminder.ts @@ -0,0 +1,38 @@ +import { TodoItem, TodoStatus } from "@roo-code/types" + +/** + * Format the reminders section as a markdown block in English, with basic instructions. + */ +export function formatReminderSection(todoList?: TodoItem[]): string { + if (!todoList || todoList.length === 0) { + return "" + } + const statusMap: Record = { + pending: "Pending", + in_progress: "In Progress", + completed: "Completed", + } + const lines: string[] = [ + "====", + "", + "REMINDERS", + "", + "Below is your current list of reminders for this task. Keep them updated as you progress.", + "", + ] + + lines.push("| # | Content | Status |") + lines.push("|---|---------|--------|") + todoList.forEach((item, idx) => { + const escapedContent = item.content.replace(/\\/g, "\\\\").replace(/\|/g, "\\|") + lines.push(`| ${idx + 1} | ${escapedContent} | ${statusMap[item.status] || item.status} |`) + }) + lines.push("") + + lines.push( + "", + "IMPORTANT: When task status changes, remember to call the `update_todo_list` tool to update your progress.", + "", + ) + return lines.join("\n") +} 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 47edfe467a..31b49ec682 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 @@ -368,6 +368,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 e665309432..33b6addf28 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 @@ -265,6 +265,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 c964d5f6ed..b2b0abfad4 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 @@ -417,6 +417,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 5a25768204..0eecf0482e 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 @@ -417,6 +417,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 f74e394d2d..731f4b29a7 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 @@ -373,6 +373,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 47edfe467a..31b49ec682 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 @@ -368,6 +368,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 ecd22a6d6e..fda93c1c77 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 @@ -421,6 +421,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 47edfe467a..31b49ec682 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 @@ -368,6 +368,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 9514c93ea3..8db4d0b377 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 @@ -456,6 +456,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 47edfe467a..31b49ec682 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 @@ -368,6 +368,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 32acd4c169..44d5b58327 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 @@ -421,6 +421,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 5a25768204..0eecf0482e 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 @@ -417,6 +417,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. 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 47edfe467a..31b49ec682 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 @@ -368,6 +368,76 @@ Example: +## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. + + # Tool Use Guidelines 1. In tags, assess what information you already have and what information you need to proceed with the task. diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 61fd9df81e..be3b91f146 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode" import * as os from "os" -import type { ModeConfig, PromptComponent, CustomModePrompts } from "@roo-code/types" +import type { ModeConfig, PromptComponent, CustomModePrompts, TodoItem } from "@roo-code/types" import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes" import { DiffStrategy } from "../../shared/tools" @@ -44,6 +44,7 @@ async function generatePrompt( rooIgnoreInstructions?: string, partialReadsEnabled?: boolean, settings?: Record, + todoList?: TodoItem[], ): Promise { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -122,6 +123,7 @@ export const SYSTEM_PROMPT = async ( rooIgnoreInstructions?: string, partialReadsEnabled?: boolean, settings?: Record, + todoList?: TodoItem[], ): Promise => { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -195,5 +197,6 @@ ${customInstructions}` rooIgnoreInstructions, partialReadsEnabled, settings, + todoList, ) } diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index 736c716a27..3fd5a636a4 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -22,6 +22,7 @@ import { getAccessMcpResourceDescription } from "./access-mcp-resource" import { getSwitchModeDescription } from "./switch-mode" import { getNewTaskDescription } from "./new-task" import { getCodebaseSearchDescription } from "./codebase-search" +import { getUpdateTodoListDescription } from "./update-todo-list" import { CodeIndexManager } from "../../../services/code-index/manager" // Map of tool names to their description functions @@ -45,6 +46,7 @@ const toolDescriptionMap: Record string | undefined> search_and_replace: (args) => getSearchAndReplaceDescription(args), apply_diff: (args) => args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "", + update_todo_list: (args) => getUpdateTodoListDescription(args), } export function getToolDescriptionsForMode( diff --git a/src/core/prompts/tools/update-todo-list.ts b/src/core/prompts/tools/update-todo-list.ts new file mode 100644 index 0000000000..528d5a1b51 --- /dev/null +++ b/src/core/prompts/tools/update-todo-list.ts @@ -0,0 +1,76 @@ +import { ToolArgs } from "./types" + +/** + * Get the description for the update_todo_list tool. + */ +export function getUpdateTodoListDescription(args?: ToolArgs): string { + return `## update_todo_list + +**Description:** +Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks. + +**Checklist Format:** +- Use a single-level markdown checklist (no nesting or subtasks). +- List todos in the intended execution order. +- Status options: + - [ ] Task description (pending) + - [x] Task description (completed) + - [-] Task description (in progress) + +**Status Rules:** +- [ ] = pending (not started) +- [x] = completed (fully finished, no unresolved issues) +- [-] = in_progress (currently being worked on) + +**Core Principles:** +- Before updating, always confirm which todos have been completed since the last update. +- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress). +- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately. +- Do not remove any unfinished todos unless explicitly instructed. +- Always retain all unfinished tasks, updating their status as needed. +- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies). +- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved. +- Remove tasks only if they are no longer relevant or if the user requests deletion. + +**Usage Example:** + + +[x] Analyze requirements +[x] Design architecture +[-] Implement core logic +[ ] Write tests +[ ] Update documentation + + + +*After completing "Implement core logic" and starting "Write tests":* + + +[x] Analyze requirements +[x] Design architecture +[x] Implement core logic +[-] Write tests +[ ] Update documentation +[ ] Add performance benchmarks + + + +**When to Use:** +- The task involves multiple steps or requires ongoing tracking. +- You need to update the status of several todos at once. +- New actionable items are discovered during task execution. +- The user requests a todo list or provides multiple tasks. +- The task is complex and benefits from clear, stepwise progress tracking. + +**When NOT to Use:** +- There is only a single, trivial task. +- The task can be completed in one or two simple steps. +- The request is purely conversational or informational. + +**Task Management Guidelines:** +- Mark task as completed immediately after all work of the current task is done. +- Start the next task by marking it as in_progress. +- Add new todos as soon as they are identified. +- Use clear, descriptive task names. +` +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 4f0d32c8c1..31260cd6fa 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -20,6 +20,7 @@ import { type ToolProgressStatus, type HistoryItem, TelemetryEventName, + TodoItem, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService } from "@roo-code/cloud" @@ -85,6 +86,7 @@ import { processUserContentMentions } from "../mentions/processUserContentMentio import { ApiMessage } from "../task-persistence/apiMessages" import { getMessagesSinceLastSummary, summarizeConversation } from "../condense" import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning" +import { restoreTodoListForTask } from "../tools/updateTodoListTool" // Constants const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes @@ -122,6 +124,7 @@ export type TaskOptions = { } export class Task extends EventEmitter { + todoList?: TodoItem[] readonly taskId: string readonly instanceId: string @@ -370,6 +373,7 @@ export class Task extends EventEmitter { public async overwriteClineMessages(newMessages: ClineMessage[]) { this.clineMessages = newMessages + restoreTodoListForTask(this) await this.saveClineMessages() } @@ -1716,7 +1720,9 @@ export class Task extends EventEmitter { const contextWindow = modelInfo.contextWindow - const currentProfileId = state?.listApiConfigMeta.find((profile) => profile.name === state?.currentApiConfigName)?.id ?? "default"; + const currentProfileId = + state?.listApiConfigMeta.find((profile) => profile.name === state?.currentApiConfigName)?.id ?? + "default" const truncateResult = await truncateConversationIfNeeded({ messages: this.apiConversationHistory, diff --git a/src/core/tools/updateTodoListTool.ts b/src/core/tools/updateTodoListTool.ts new file mode 100644 index 0000000000..cbb90338d3 --- /dev/null +++ b/src/core/tools/updateTodoListTool.ts @@ -0,0 +1,236 @@ +import { Task } from "../task/Task" +import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { formatResponse } from "../prompts/responses" + +import cloneDeep from "clone-deep" +import crypto from "crypto" +import { TodoItem, TodoStatus, todoStatusSchema } from "@roo-code/types" +import { getLatestTodo } from "../../shared/todo" + +let approvedTodoList: TodoItem[] | undefined = undefined + +/** + * Add a todo item to the task's todoList. + */ +export function addTodoToTask(cline: Task, content: string, status: TodoStatus = "pending", id?: string): TodoItem { + const todo: TodoItem = { + id: id ?? crypto.randomUUID(), + content, + status, + } + if (!cline.todoList) cline.todoList = [] + cline.todoList.push(todo) + return todo +} + +/** + * Update the status of a todo item by id. + */ +export function updateTodoStatusForTask(cline: Task, id: string, nextStatus: TodoStatus): boolean { + if (!cline.todoList) return false + const idx = cline.todoList.findIndex((t) => t.id === id) + if (idx === -1) return false + const current = cline.todoList[idx] + if ( + (current.status === "pending" && nextStatus === "in_progress") || + (current.status === "in_progress" && nextStatus === "completed") || + current.status === nextStatus + ) { + cline.todoList[idx] = { ...current, status: nextStatus } + return true + } + return false +} + +/** + * Remove a todo item by id. + */ +export function removeTodoFromTask(cline: Task, id: string): boolean { + if (!cline.todoList) return false + const idx = cline.todoList.findIndex((t) => t.id === id) + if (idx === -1) return false + cline.todoList.splice(idx, 1) + return true +} + +/** + * Get a copy of the todoList. + */ +export function getTodoListForTask(cline: Task): TodoItem[] | undefined { + return cline.todoList?.slice() +} + +/** + * Set the todoList for the task. + */ +export async function setTodoListForTask(cline?: Task, todos?: TodoItem[]) { + if (cline === undefined) return + cline.todoList = Array.isArray(todos) ? todos : [] +} + +/** + * Restore the todoList from argument or from clineMessages. + */ +export function restoreTodoListForTask(cline: Task, todoList?: TodoItem[]) { + if (todoList) { + cline.todoList = Array.isArray(todoList) ? todoList : [] + return + } + cline.todoList = getLatestTodo(cline.clineMessages) +} +/** + * Convert TodoItem[] to markdown checklist string. + * @param todos TodoItem array + * @returns markdown checklist string + */ +function todoListToMarkdown(todos: TodoItem[]): string { + return todos + .map((t) => { + let box = "[ ]" + if (t.status === "completed") box = "[x]" + else if (t.status === "in_progress") box = "[-]" + return `${box} ${t.content}` + }) + .join("\n") +} + +function normalizeStatus(status: string | undefined): TodoStatus { + if (status === "completed") return "completed" + if (status === "in_progress") return "in_progress" + return "pending" +} + +function parseMarkdownChecklist(md: string): TodoItem[] { + if (typeof md !== "string") return [] + const lines = md + .split(/\r?\n/) + .map((l) => l.trim()) + .filter(Boolean) + const todos: TodoItem[] = [] + for (const line of lines) { + const match = line.match(/^\[\s*([ xX\-~])\s*\]\s+(.+)$/) + if (!match) continue + let status: TodoStatus = "pending" + if (match[1] === "x" || match[1] === "X") status = "completed" + else if (match[1] === "-" || match[1] === "~") status = "in_progress" + const id = crypto + .createHash("md5") + .update(match[2] + status) + .digest("hex") + todos.push({ + id, + content: match[2], + status, + }) + } + return todos +} + +export function setPendingTodoList(todos: TodoItem[]) { + approvedTodoList = todos +} + +function validateTodos(todos: any[]): { valid: boolean; error?: string } { + if (!Array.isArray(todos)) return { valid: false, error: "todos must be an array" } + for (const [i, t] of todos.entries()) { + if (!t || typeof t !== "object") return { valid: false, error: `Item ${i + 1} is not an object` } + if (!t.id || typeof t.id !== "string") return { valid: false, error: `Item ${i + 1} is missing id` } + if (!t.content || typeof t.content !== "string") + return { valid: false, error: `Item ${i + 1} is missing content` } + if (t.status && !todoStatusSchema.options.includes(t.status as TodoStatus)) + return { valid: false, error: `Item ${i + 1} has invalid status` } + } + return { valid: true } +} + +/** + * Update the todo list for a task. + * @param cline Task instance + * @param block ToolUse block + * @param askApproval AskApproval function + * @param handleError HandleError function + * @param pushToolResult PushToolResult function + * @param removeClosingTag RemoveClosingTag function + * @param userEdited If true, only show "User Edit Succeeded" and do nothing else + */ +export async function updateTodoListTool( + cline: Task, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, + userEdited?: boolean, +) { + // If userEdited is true, only show "User Edit Succeeded" and do nothing else + if (userEdited === true) { + pushToolResult("User Edit Succeeded") + return + } + try { + const todosRaw = block.params.todos + + let todos: TodoItem[] + try { + todos = parseMarkdownChecklist(todosRaw || "") + } catch { + cline.consecutiveMistakeCount++ + cline.recordToolError("update_todo_list") + pushToolResult(formatResponse.toolError("The todos parameter is not valid markdown checklist or JSON")) + return + } + + const { valid, error } = validateTodos(todos) + if (!valid && !block.partial) { + cline.consecutiveMistakeCount++ + cline.recordToolError("update_todo_list") + pushToolResult(formatResponse.toolError(error || "todos parameter validation failed")) + return + } + + let normalizedTodos: TodoItem[] = todos.map((t) => ({ + id: t.id, + content: t.content, + status: normalizeStatus(t.status), + })) + + const approvalMsg = JSON.stringify({ + tool: "updateTodoList", + todos: normalizedTodos, + }) + if (block.partial) { + await cline.ask("tool", approvalMsg, block.partial).catch(() => {}) + return + } + approvedTodoList = cloneDeep(normalizedTodos) + const didApprove = await askApproval("tool", approvalMsg) + if (!didApprove) { + pushToolResult("User declined to update the todoList.") + return + } + const isTodoListChanged = + approvedTodoList !== undefined && JSON.stringify(normalizedTodos) !== JSON.stringify(approvedTodoList) + if (isTodoListChanged) { + normalizedTodos = approvedTodoList ?? [] + cline.say( + "user_edit_todos", + JSON.stringify({ + tool: "updateTodoList", + todos: normalizedTodos, + }), + ) + } + + await setTodoListForTask(cline, normalizedTodos) + + // If todo list changed, output new todo list in markdown format + if (isTodoListChanged) { + const md = todoListToMarkdown(normalizedTodos) + pushToolResult(formatResponse.toolResult("User edits todo:\n\n" + md)) + } else { + pushToolResult(formatResponse.toolResult("Todo list updated successfully.")) + } + } catch (error) { + await handleError("update todo list", error) + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d8aca8fbcb..8eb2e70a8c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1347,6 +1347,7 @@ export class ClineProvider alwaysAllowMcp, alwaysAllowModeSwitch, alwaysAllowSubtasks, + alwaysAllowUpdateTodoList, allowedMaxRequests, autoCondenseContext, autoCondenseContextPercent, @@ -1433,6 +1434,7 @@ export class ClineProvider alwaysAllowMcp: alwaysAllowMcp ?? false, alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false, alwaysAllowSubtasks: alwaysAllowSubtasks ?? false, + alwaysAllowUpdateTodoList: alwaysAllowUpdateTodoList ?? false, allowedMaxRequests, autoCondenseContext: autoCondenseContext ?? true, autoCondenseContextPercent: autoCondenseContextPercent ?? 100, @@ -1601,6 +1603,7 @@ export class ClineProvider alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false, alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false, alwaysAllowFollowupQuestions: stateValues.alwaysAllowFollowupQuestions ?? false, + alwaysAllowUpdateTodoList: stateValues.alwaysAllowUpdateTodoList ?? false, followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000, allowedMaxRequests: stateValues.allowedMaxRequests, autoCondenseContext: stateValues.autoCondenseContext ?? true, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 0ec14ca27e..b8e21e6040 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -46,6 +46,7 @@ import { getCommand } from "../../utils/commands" const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"]) import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace" +import { setPendingTodoList } from "../tools/updateTodoListTool" export const webviewMessageHandler = async ( provider: ClineProvider, @@ -185,6 +186,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("alwaysAllowSubtasks", message.bool) await provider.postStateToWebview() break + case "alwaysAllowUpdateTodoList": + await updateGlobalState("alwaysAllowUpdateTodoList", message.bool) + await provider.postStateToWebview() + break case "askResponse": provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) break @@ -1317,6 +1322,14 @@ export const webviewMessageHandler = async ( } break } + case "updateTodoList": { + const payload = message.payload as { todos?: any[] } + const todos = payload?.todos + if (Array.isArray(todos)) { + await setPendingTodoList(todos) + } + break + } case "saveApiConfiguration": if (message.text && message.apiConfiguration) { try { diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index 29c4f979dd..994f228e52 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -96,10 +96,10 @@ describe("CodeIndexConfigManager", () => { codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "text-embedding-3-large", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", } mockContextProxy.getGlobalState.mockImplementation((key: string) => { if (key === "codebaseIndexConfig") return mockGlobalState - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" return undefined }) @@ -134,11 +134,11 @@ describe("CodeIndexConfigManager", () => { codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "custom-model", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1024, } mockContextProxy.getGlobalState.mockImplementation((key: string) => { if (key === "codebaseIndexConfig") return mockGlobalState - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" - if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024 return undefined }) setupSecretMocks({ @@ -173,11 +173,11 @@ describe("CodeIndexConfigManager", () => { codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "custom-model", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + // modelDimension is not set } mockContextProxy.getGlobalState.mockImplementation((key: string) => { if (key === "codebaseIndexConfig") return mockGlobalState - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" - if (key === "codebaseIndexOpenAiCompatibleModelDimension") return undefined return undefined }) setupSecretMocks({ @@ -197,6 +197,7 @@ describe("CodeIndexConfigManager", () => { openAiCompatibleOptions: { baseUrl: "https://api.example.com/v1", apiKey: "test-openai-compatible-key", + // modelDimension is undefined when not set }, qdrantUrl: "http://qdrant.local", qdrantApiKey: "test-qdrant-key", @@ -211,11 +212,11 @@ describe("CodeIndexConfigManager", () => { codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "custom-model", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: "invalid-dimension", // Invalid type } mockContextProxy.getGlobalState.mockImplementation((key: string) => { if (key === "codebaseIndexConfig") return mockGlobalState - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" - if (key === "codebaseIndexOpenAiCompatibleModelDimension") return "invalid-dimension" return undefined }) setupSecretMocks({ @@ -461,9 +462,9 @@ describe("CodeIndexConfigManager", () => { codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexOpenAiCompatibleBaseUrl: "https://old-api.example.com/v1", } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://old-api.example.com/v1" return undefined }) setupSecretMocks({ @@ -481,9 +482,9 @@ describe("CodeIndexConfigManager", () => { codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexOpenAiCompatibleBaseUrl: "https://new-api.example.com/v1", } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://new-api.example.com/v1" return undefined }) @@ -500,9 +501,9 @@ describe("CodeIndexConfigManager", () => { codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" return undefined }) setupSecretMocks({ @@ -531,10 +532,10 @@ describe("CodeIndexConfigManager", () => { codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderModelId: "custom-model", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1024, } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" - if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024 return undefined }) setupSecretMocks({ @@ -552,10 +553,10 @@ describe("CodeIndexConfigManager", () => { codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderModelId: "custom-model", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 2048, } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" - if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 2048 return undefined }) @@ -614,10 +615,10 @@ describe("CodeIndexConfigManager", () => { codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderModelId: "custom-model", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + // modelDimension not set initially } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" - if (key === "codebaseIndexOpenAiCompatibleModelDimension") return undefined return undefined }) setupSecretMocks({ @@ -635,10 +636,10 @@ describe("CodeIndexConfigManager", () => { codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderModelId: "custom-model", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1024, } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" - if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024 return undefined }) @@ -655,10 +656,10 @@ describe("CodeIndexConfigManager", () => { codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderModelId: "custom-model", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + codebaseIndexOpenAiCompatibleModelDimension: 1024, } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" - if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024 return undefined }) setupSecretMocks({ @@ -676,10 +677,10 @@ describe("CodeIndexConfigManager", () => { codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", codebaseIndexEmbedderModelId: "custom-model", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", + // modelDimension removed } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" - if (key === "codebaseIndexOpenAiCompatibleModelDimension") return undefined return undefined }) @@ -1082,9 +1083,9 @@ describe("CodeIndexConfigManager", () => { codebaseIndexEnabled: true, codebaseIndexQdrantUrl: "http://qdrant.local", codebaseIndexEmbedderProvider: "openai-compatible", + codebaseIndexOpenAiCompatibleBaseUrl: "https://api.example.com/v1", } } - if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" return undefined }) setupSecretMocks({ diff --git a/src/services/code-index/__tests__/manager.spec.ts b/src/services/code-index/__tests__/manager.spec.ts index 1f5b636387..b52f92f8ff 100644 --- a/src/services/code-index/__tests__/manager.spec.ts +++ b/src/services/code-index/__tests__/manager.spec.ts @@ -1,18 +1,31 @@ -import { CodeIndexManager } from "../manager" +// Mock vscode module +vi.mock("vscode", () => ({ + workspace: { + workspaceFolders: [ + { + uri: { fsPath: "/test/workspace" }, + name: "test", + index: 0, + }, + ], + }, +})) // Mock only the essential dependencies -vitest.mock("../../../utils/path", () => ({ - getWorkspacePath: vitest.fn(() => "/test/workspace"), +vi.mock("../../../utils/path", () => ({ + getWorkspacePath: vi.fn(() => "/test/workspace"), })) -vitest.mock("../state-manager", () => ({ - CodeIndexStateManager: vitest.fn().mockImplementation(() => ({ - onProgressUpdate: vitest.fn(), - getCurrentStatus: vitest.fn(), - dispose: vitest.fn(), +vi.mock("../state-manager", () => ({ + CodeIndexStateManager: vi.fn().mockImplementation(() => ({ + onProgressUpdate: vi.fn(), + getCurrentStatus: vi.fn(), + dispose: vi.fn(), })), })) +import { CodeIndexManager } from "../manager" + describe("CodeIndexManager - handleSettingsChange regression", () => { let mockContext: any let manager: CodeIndexManager @@ -27,7 +40,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { globalState: {} as any, extensionUri: {} as any, extensionPath: "/test/extension", - asAbsolutePath: vitest.fn(), + asAbsolutePath: vi.fn(), storageUri: {} as any, storagePath: "/test/storage", globalStorageUri: {} as any, @@ -58,13 +71,13 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { // Mock a minimal config manager that simulates first-time configuration const mockConfigManager = { - loadConfiguration: vitest.fn().mockResolvedValue({ requiresRestart: true }), + loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: true }), } ;(manager as any)._configManager = mockConfigManager // Mock the feature state to simulate valid configuration that would normally trigger restart - vitest.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true) - vitest.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true) + vi.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true) + vi.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true) // The key test: this should NOT throw "CodeIndexManager not initialized" error await expect(manager.handleSettingsChange()).resolves.not.toThrow() @@ -76,10 +89,10 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { it("should work normally when manager is initialized", async () => { // Mock a complete config manager with all required properties const mockConfigManager = { - loadConfiguration: vitest.fn().mockResolvedValue({ requiresRestart: true }), + loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: true }), isFeatureConfigured: true, isFeatureEnabled: true, - getConfig: vitest.fn().mockReturnValue({ + getConfig: vi.fn().mockReturnValue({ isEnabled: true, isConfigured: true, embedderProvider: "openai", @@ -93,7 +106,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { ;(manager as any)._configManager = mockConfigManager // Simulate an initialized manager by setting the required properties - ;(manager as any)._orchestrator = { stopWatcher: vitest.fn() } + ;(manager as any)._orchestrator = { stopWatcher: vi.fn() } ;(manager as any)._searchService = {} ;(manager as any)._cacheManager = {} @@ -101,12 +114,12 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { expect(manager.isInitialized).toBe(true) // Mock the methods that would be called during restart - const recreateServicesSpy = vitest.spyOn(manager as any, "_recreateServices").mockImplementation(() => {}) - const startIndexingSpy = vitest.spyOn(manager, "startIndexing").mockResolvedValue() + const recreateServicesSpy = vi.spyOn(manager as any, "_recreateServices").mockImplementation(() => {}) + const startIndexingSpy = vi.spyOn(manager, "startIndexing").mockResolvedValue() // Mock the feature state - vitest.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true) - vitest.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true) + vi.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true) + vi.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true) await manager.handleSettingsChange() diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 4797f95d01..26ea0e2a48 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -62,11 +62,12 @@ export class CodeIndexConfigManager { const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? "" const qdrantApiKey = this.contextProxy?.getSecret("codeIndexQdrantApiKey") ?? "" - const openAiCompatibleBaseUrl = this.contextProxy?.getGlobalState("codebaseIndexOpenAiCompatibleBaseUrl") ?? "" + // Fix: Read OpenAI Compatible settings from the correct location within codebaseIndexConfig + const openAiCompatibleBaseUrl = codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl ?? "" const openAiCompatibleApiKey = this.contextProxy?.getSecret("codebaseIndexOpenAiCompatibleApiKey") ?? "" - const openAiCompatibleModelDimension = this.contextProxy?.getGlobalState( - "codebaseIndexOpenAiCompatibleModelDimension", - ) as number | undefined + const openAiCompatibleModelDimension = codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension as + | number + | undefined const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? "" // Update instance variables with configuration diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index 02c55f72e2..aa58469077 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -26,12 +26,17 @@ export class CodeIndexManager { private _cacheManager: CacheManager | undefined public static getInstance(context: vscode.ExtensionContext): CodeIndexManager | undefined { - const workspacePath = getWorkspacePath() // Assumes single workspace for now - - if (!workspacePath) { + // Use first workspace folder consistently + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { return undefined } + // Always use the first workspace folder for consistency across all indexing operations. + // This ensures that the same workspace context is used throughout the indexing pipeline, + // preventing path resolution errors in multi-workspace scenarios. + const workspacePath = workspaceFolders[0].uri.fsPath + if (!CodeIndexManager.instances.has(workspacePath)) { CodeIndexManager.instances.set(workspacePath, new CodeIndexManager(workspacePath, context)) } diff --git a/src/services/code-index/processors/file-watcher.ts b/src/services/code-index/processors/file-watcher.ts index 9a1fc3c9af..a12752bea6 100644 --- a/src/services/code-index/processors/file-watcher.ts +++ b/src/services/code-index/processors/file-watcher.ts @@ -464,7 +464,7 @@ export class FileWatcher implements IFileWatcher { } // Check if file should be ignored - const relativeFilePath = generateRelativeFilePath(filePath) + const relativeFilePath = generateRelativeFilePath(filePath, this.workspacePath) if ( !this.ignoreController.validateAccess(filePath) || (this.ignoreInstance && this.ignoreInstance.ignores(relativeFilePath)) @@ -512,7 +512,7 @@ export class FileWatcher implements IFileWatcher { const { embeddings } = await this.embedder.createEmbeddings(texts) pointsToUpsert = blocks.map((block, index) => { - const normalizedAbsolutePath = generateNormalizedAbsolutePath(block.file_path) + const normalizedAbsolutePath = generateNormalizedAbsolutePath(block.file_path, this.workspacePath) const stableName = `${normalizedAbsolutePath}:${block.start_line}` const pointId = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE) @@ -520,7 +520,7 @@ export class FileWatcher implements IFileWatcher { id: pointId, vector: embeddings[index], payload: { - filePath: generateRelativeFilePath(normalizedAbsolutePath), + filePath: generateRelativeFilePath(normalizedAbsolutePath, this.workspacePath), codeChunk: block.content, startLine: block.start_line, endLine: block.end_line, diff --git a/src/services/code-index/processors/scanner.ts b/src/services/code-index/processors/scanner.ts index f24650f4bc..7b79a1c87d 100644 --- a/src/services/code-index/processors/scanner.ts +++ b/src/services/code-index/processors/scanner.ts @@ -4,6 +4,7 @@ import { RooIgnoreController } from "../../../core/ignore/RooIgnoreController" import { stat } from "fs/promises" import * as path from "path" import { generateNormalizedAbsolutePath, generateRelativeFilePath } from "../shared/get-relative-path" +import { getWorkspacePathForContext } from "../../../utils/path" import { scannerExtensions } from "../shared/supported-extensions" import * as vscode from "vscode" import { CodeBlock, ICodeParser, IEmbedder, IVectorStore, IDirectoryScanner } from "../interfaces" @@ -49,6 +50,9 @@ export class DirectoryScanner implements IDirectoryScanner { onFileParsed?: (fileBlockCount: number) => void, ): Promise<{ codeBlocks: CodeBlock[]; stats: { processed: number; skipped: number }; totalBlockCount: number }> { const directoryPath = directory + // Capture workspace context at scan start + const scanWorkspace = getWorkspacePathForContext(directoryPath) + // Get all files recursively (handles .gitignore automatically) const [allPaths, _] = await listFiles(directoryPath, true, MAX_LIST_FILES_LIMIT) @@ -66,7 +70,7 @@ export class DirectoryScanner implements IDirectoryScanner { // Filter by supported extensions, ignore patterns, and excluded directories const supportedPaths = allowedPaths.filter((filePath) => { const ext = path.extname(filePath).toLowerCase() - const relativeFilePath = generateRelativeFilePath(filePath) + const relativeFilePath = generateRelativeFilePath(filePath, scanWorkspace) // Check if file is in an ignored directory using the shared helper if (isPathInIgnoredDirectory(filePath)) { @@ -169,6 +173,7 @@ export class DirectoryScanner implements IDirectoryScanner { batchBlocks, batchTexts, batchFileInfos, + scanWorkspace, onError, onBlocksIndexed, ), @@ -185,12 +190,15 @@ export class DirectoryScanner implements IDirectoryScanner { await this.cacheManager.updateHash(filePath, currentFileHash) } } catch (error) { - console.error(`Error processing file ${filePath}:`, error) + console.error(`Error processing file ${filePath} in workspace ${scanWorkspace}:`, error) if (onError) { onError( error instanceof Error - ? error - : new Error(t("embeddings:scanner.unknownErrorProcessingFile", { filePath })), + ? new Error(`${error.message} (Workspace: ${scanWorkspace}, File: ${filePath})`) + : new Error( + t("embeddings:scanner.unknownErrorProcessingFile", { filePath }) + + ` (Workspace: ${scanWorkspace})`, + ), ) } } @@ -214,7 +222,7 @@ export class DirectoryScanner implements IDirectoryScanner { // Queue final batch processing const batchPromise = batchLimiter(() => - this.processBatch(batchBlocks, batchTexts, batchFileInfos, onError, onBlocksIndexed), + this.processBatch(batchBlocks, batchTexts, batchFileInfos, scanWorkspace, onError, onBlocksIndexed), ) activeBatchPromises.push(batchPromise) } finally { @@ -235,15 +243,20 @@ export class DirectoryScanner implements IDirectoryScanner { await this.qdrantClient.deletePointsByFilePath(cachedFilePath) await this.cacheManager.deleteHash(cachedFilePath) } catch (error) { - console.error(`[DirectoryScanner] Failed to delete points for ${cachedFilePath}:`, error) + console.error( + `[DirectoryScanner] Failed to delete points for ${cachedFilePath} in workspace ${scanWorkspace}:`, + error, + ) if (onError) { onError( error instanceof Error - ? error + ? new Error( + `${error.message} (Workspace: ${scanWorkspace}, File: ${cachedFilePath})`, + ) : new Error( t("embeddings:scanner.unknownErrorDeletingPoints", { filePath: cachedFilePath, - }), + }) + ` (Workspace: ${scanWorkspace})`, ), ) } @@ -267,6 +280,7 @@ export class DirectoryScanner implements IDirectoryScanner { batchBlocks: CodeBlock[], batchTexts: string[], batchFileInfos: { filePath: string; fileHash: string; isNew: boolean }[], + scanWorkspace: string, onError?: (error: Error) => void, onBlocksIndexed?: (indexedCount: number) => void, ): Promise { @@ -292,11 +306,14 @@ export class DirectoryScanner implements IDirectoryScanner { await this.qdrantClient.deletePointsByMultipleFilePaths(uniqueFilePaths) } catch (deleteError) { console.error( - `[DirectoryScanner] Failed to delete points for ${uniqueFilePaths.length} files before upsert:`, + `[DirectoryScanner] Failed to delete points for ${uniqueFilePaths.length} files before upsert in workspace ${scanWorkspace}:`, deleteError, ) - // Re-throw the error to stop processing this batch attempt - throw deleteError + // Re-throw the error with workspace context + throw new Error( + `Failed to delete points for ${uniqueFilePaths.length} files. Workspace: ${scanWorkspace}. ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`, + { cause: deleteError }, + ) } } // --- End Deletion Step --- @@ -306,7 +323,7 @@ export class DirectoryScanner implements IDirectoryScanner { // Prepare points for Qdrant const points = batchBlocks.map((block, index) => { - const normalizedAbsolutePath = generateNormalizedAbsolutePath(block.file_path) + const normalizedAbsolutePath = generateNormalizedAbsolutePath(block.file_path, scanWorkspace) // Use segmentHash for unique ID generation to handle multiple segments from same line const pointId = uuidv5(block.segmentHash, QDRANT_CODE_BLOCK_NAMESPACE) @@ -315,7 +332,7 @@ export class DirectoryScanner implements IDirectoryScanner { id: pointId, vector: embeddings[index], payload: { - filePath: generateRelativeFilePath(normalizedAbsolutePath), + filePath: generateRelativeFilePath(normalizedAbsolutePath, scanWorkspace), codeChunk: block.content, startLine: block.start_line, endLine: block.end_line, @@ -335,7 +352,10 @@ export class DirectoryScanner implements IDirectoryScanner { success = true } catch (error) { lastError = error as Error - console.error(`[DirectoryScanner] Error processing batch (attempt ${attempts}):`, error) + console.error( + `[DirectoryScanner] Error processing batch (attempt ${attempts}) in workspace ${scanWorkspace}:`, + error, + ) if (attempts < MAX_BATCH_RETRIES) { const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempts - 1) diff --git a/src/services/code-index/shared/__tests__/get-relative-path.spec.ts b/src/services/code-index/shared/__tests__/get-relative-path.spec.ts new file mode 100644 index 0000000000..5226fbf6e6 --- /dev/null +++ b/src/services/code-index/shared/__tests__/get-relative-path.spec.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest" +import path from "path" +import { generateNormalizedAbsolutePath, generateRelativeFilePath } from "../get-relative-path" + +describe("get-relative-path", () => { + describe("generateNormalizedAbsolutePath", () => { + it("should use provided workspace root", () => { + const filePath = "src/file.ts" + const workspaceRoot = path.join(path.sep, "custom", "workspace") + const result = generateNormalizedAbsolutePath(filePath, workspaceRoot) + // On Windows, path.resolve adds the drive letter, so we need to use path.resolve for the expected value + expect(result).toBe(path.resolve(workspaceRoot, filePath)) + }) + + it("should handle absolute paths", () => { + const filePath = path.join(path.sep, "absolute", "path", "file.ts") + const workspaceRoot = path.join(path.sep, "custom", "workspace") + const result = generateNormalizedAbsolutePath(filePath, workspaceRoot) + // When an absolute path is provided, it should be resolved to include drive letter on Windows + expect(result).toBe(path.resolve(filePath)) + }) + + it("should normalize paths with . and .. segments", () => { + const filePath = "./src/../src/file.ts" + const workspaceRoot = path.join(path.sep, "custom", "workspace") + const result = generateNormalizedAbsolutePath(filePath, workspaceRoot) + // Use path.resolve to get the expected normalized absolute path + expect(result).toBe(path.resolve(workspaceRoot, "src", "file.ts")) + }) + }) + + describe("generateRelativeFilePath", () => { + it("should use provided workspace root", () => { + const workspaceRoot = path.join(path.sep, "custom", "workspace") + const absolutePath = path.join(workspaceRoot, "src", "file.ts") + const result = generateRelativeFilePath(absolutePath, workspaceRoot) + expect(result).toBe(path.join("src", "file.ts")) + }) + + it("should handle paths outside workspace", () => { + const absolutePath = path.join(path.sep, "outside", "workspace", "file.ts") + const workspaceRoot = path.join(path.sep, "custom", "workspace") + const result = generateRelativeFilePath(absolutePath, workspaceRoot) + // The result will have .. segments to navigate outside + expect(result).toContain("..") + }) + + it("should handle same path as workspace", () => { + const workspaceRoot = path.join(path.sep, "custom", "workspace") + const absolutePath = workspaceRoot + const result = generateRelativeFilePath(absolutePath, workspaceRoot) + expect(result).toBe(".") + }) + + it("should handle multi-workspace scenarios", () => { + // Simulate the error scenario from the issue + const workspaceRoot = path.join(path.sep, "Users", "test", "project") + const absolutePath = path.join(path.sep, "Users", "test", "admin", ".prettierrc.json") + const result = generateRelativeFilePath(absolutePath, workspaceRoot) + // Should generate a valid relative path, not throw an error + expect(result).toBe(path.join("..", "admin", ".prettierrc.json")) + }) + }) +}) diff --git a/src/services/code-index/shared/get-relative-path.ts b/src/services/code-index/shared/get-relative-path.ts index 564afee955..642a207208 100644 --- a/src/services/code-index/shared/get-relative-path.ts +++ b/src/services/code-index/shared/get-relative-path.ts @@ -1,16 +1,14 @@ import path from "path" -import { getWorkspacePath } from "../../../utils/path" /** * Generates a normalized absolute path from a given file path and workspace root. * Handles path resolution and normalization to ensure consistent absolute paths. * * @param filePath - The file path to normalize (can be relative or absolute) - * @param workspaceRoot - The root directory of the workspace + * @param workspaceRoot - The root directory of the workspace (required) * @returns The normalized absolute path */ -export function generateNormalizedAbsolutePath(filePath: string): string { - const workspaceRoot = getWorkspacePath() +export function generateNormalizedAbsolutePath(filePath: string, workspaceRoot: string): string { // Resolve the path to make it absolute if it's relative const resolvedPath = path.resolve(workspaceRoot, filePath) // Normalize to handle any . or .. segments and duplicate slashes @@ -22,11 +20,10 @@ export function generateNormalizedAbsolutePath(filePath: string): string { * Ensures consistent relative path generation across different platforms. * * @param normalizedAbsolutePath - The normalized absolute path to convert - * @param workspaceRoot - The root directory of the workspace + * @param workspaceRoot - The root directory of the workspace (required) * @returns The relative path from workspaceRoot to the file */ -export function generateRelativeFilePath(normalizedAbsolutePath: string): string { - const workspaceRoot = getWorkspacePath() +export function generateRelativeFilePath(normalizedAbsolutePath: string, workspaceRoot: string): string { // Generate the relative path const relativePath = path.relative(workspaceRoot, normalizedAbsolutePath) // Normalize to ensure consistent path separators diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 9db0889c88..953c0c1070 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -181,6 +181,7 @@ export type ExtensionState = Pick< | "alwaysAllowModeSwitch" | "alwaysAllowSubtasks" | "alwaysAllowExecute" + | "alwaysAllowUpdateTodoList" | "allowedCommands" | "allowedMaxRequests" | "browserToolEnabled" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index a50e30b67e..89fa21b7b7 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -18,8 +18,13 @@ export type PromptMode = Mode | "enhance" export type AudioType = "notification" | "celebration" | "progress_loop" +export interface UpdateTodoListPayload { + todos: any[] +} + export interface WebviewMessage { type: + | "updateTodoList" | "deleteMultipleTasksWithIds" | "currentApiConfigName" | "saveApiConfiguration" @@ -38,6 +43,7 @@ export interface WebviewMessage { | "alwaysAllowWriteProtected" | "alwaysAllowExecute" | "alwaysAllowFollowupQuestions" + | "alwaysAllowUpdateTodoList" | "followupAutoApproveTimeoutMs" | "webviewDidLaunch" | "newTask" @@ -73,6 +79,7 @@ export interface WebviewMessage { | "alwaysAllowModeSwitch" | "allowedMaxRequests" | "alwaysAllowSubtasks" + | "alwaysAllowUpdateTodoList" | "autoCondenseContext" | "autoCondenseContextPercent" | "condensingApiConfigId" @@ -287,3 +294,4 @@ export type WebViewMessagePayload = | IndexingStatusPayload | IndexClearedPayload | InstallMarketplaceItemWithParametersPayload + | UpdateTodoListPayload diff --git a/src/shared/todo.ts b/src/shared/todo.ts new file mode 100644 index 0000000000..16e7d085e2 --- /dev/null +++ b/src/shared/todo.ts @@ -0,0 +1,23 @@ +import { ClineMessage } from "@roo-code/types" +export function getLatestTodo(clineMessages: ClineMessage[]) { + const todos = clineMessages + .filter( + (msg) => + (msg.type === "ask" && msg.ask === "tool") || (msg.type === "say" && msg.say === "user_edit_todos"), + ) + .map((msg) => { + try { + return JSON.parse(msg.text ?? "{}") + } catch { + return null + } + }) + .filter((item) => item && item.tool === "updateTodoList" && Array.isArray(item.todos)) + .map((item) => item.todos) + .pop() + if (todos) { + return todos + } else { + return [] + } +} diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 0725e2e4d6..67972243fe 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -64,6 +64,7 @@ export const toolParamNames = [ "end_line", "query", "args", + "todos", ] as const export type ToolParamName = (typeof toolParamNames)[number] @@ -188,6 +189,7 @@ export const TOOL_DISPLAY_NAMES: Record = { insert_content: "insert content", search_and_replace: "search and replace", codebase_search: "codebase search", + update_todo_list: "update todo list", } as const // Define available tool groups. @@ -226,6 +228,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ "attempt_completion", "switch_mode", "new_task", + "update_todo_list", ] as const export type DiffResult = diff --git a/src/utils/path.ts b/src/utils/path.ts index a58d630172..48e2ce6673 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -115,3 +115,18 @@ export const getWorkspacePath = (defaultCwdPath = "") => { } return cwdPath } + +export const getWorkspacePathForContext = (contextPath?: string): string => { + // If context path provided, find its workspace + if (contextPath) { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(contextPath)) + if (workspaceFolder) { + return workspaceFolder.uri.fsPath + } + // Debug logging when falling back + console.debug(`[CodeIndex] No workspace found for context path: ${contextPath}, falling back to default`) + } + + // Fall back to current behavior + return getWorkspacePath() +} diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index c25feef0d1..ae363a7b63 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -26,6 +26,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { alwaysAllowSubtasks, alwaysApproveResubmit, alwaysAllowFollowupQuestions, + alwaysAllowUpdateTodoList, allowedMaxRequests, setAlwaysAllowReadOnly, setAlwaysAllowWrite, @@ -36,6 +37,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAlwaysAllowSubtasks, setAlwaysApproveResubmit, setAlwaysAllowFollowupQuestions, + setAlwaysAllowUpdateTodoList, setAllowedMaxRequests, } = useExtensionState() @@ -73,6 +75,9 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { case "alwaysAllowFollowupQuestions": setAlwaysAllowFollowupQuestions(value) break + case "alwaysAllowUpdateTodoList": + setAlwaysAllowUpdateTodoList(value) + break } }, [ @@ -85,6 +90,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAlwaysAllowSubtasks, setAlwaysApproveResubmit, setAlwaysAllowFollowupQuestions, + setAlwaysAllowUpdateTodoList, ], ) @@ -101,6 +107,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { alwaysAllowSubtasks: alwaysAllowSubtasks, alwaysApproveResubmit: alwaysApproveResubmit, alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions, + alwaysAllowUpdateTodoList: alwaysAllowUpdateTodoList, }), [ alwaysAllowReadOnly, @@ -112,6 +119,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { alwaysAllowSubtasks, alwaysApproveResubmit, alwaysAllowFollowupQuestions, + alwaysAllowUpdateTodoList, ], ) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index f61eb8c5e0..9c90da503f 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -21,6 +21,7 @@ import { getLanguageFromPath } from "@src/utils/getLanguageFromPath" import { Button } from "@src/components/ui" import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" +import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock" import CodeAccordian from "../common/CodeAccordian" import CodeBlock from "../common/CodeBlock" import MarkdownBlock from "../common/MarkdownBlock" @@ -52,6 +53,7 @@ interface ChatRowProps { onSuggestionClick?: (suggestion: SuggestionItem, event?: React.MouseEvent) => void onBatchFileResponse?: (response: { [key: string]: boolean }) => void onFollowUpUnmount?: () => void + editable?: boolean } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -102,6 +104,7 @@ export const ChatRowContent = ({ onSuggestionClick, onFollowUpUnmount, onBatchFileResponse, + editable, }: ChatRowContentProps) => { const { t } = useTranslation() const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState() @@ -431,6 +434,21 @@ export const ChatRowContent = ({ ) } + case "updateTodoList" as any: { + const todos = (tool as any).todos || [] + return ( + { + if (typeof vscode !== "undefined" && vscode?.postMessage) { + vscode.postMessage({ type: "updateTodoList", payload: { todos: updatedTodos } }) + } + }} + editable={editable && isLast} + /> + ) + } case "newFileCreated": return ( <> @@ -1089,6 +1107,8 @@ export const ChatRowContent = ({ const { query = "", results = [] } = parsed?.content || {} return + case "user_edit_todos": + return {}} /> default: return ( <> diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 39e593f2aa..84bb7b89a2 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -46,6 +46,7 @@ import AutoApproveMenu from "./AutoApproveMenu" import SystemPromptWarning from "./SystemPromptWarning" import ProfileViolationWarning from "./ProfileViolationWarning" import { CheckpointWarning } from "./CheckpointWarning" +import { getLatestTodo } from "@roo/todo" export interface ChatViewProps { isHidden: boolean @@ -96,6 +97,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction messages.at(0), [messages]) + const latestTodos = useMemo(() => { + return getLatestTodo(messages) + }, [messages]) + const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages]) // Has to be after api_req_finished are all reduced into api_req_started messages. @@ -907,6 +913,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + let tool: any = {} + try { + tool = JSON.parse(messageOrGroup.text || "{}") + } catch (_) { + if (messageOrGroup.text?.includes("updateTodoList")) { + tool = { tool: "updateTodoList" } + } + } + if (tool.tool === "updateTodoList" && alwaysAllowUpdateTodoList) { + return false + } + return tool.tool === "updateTodoList" && enableButtons && !!primaryButtonText + })() + } /> ) }, @@ -1293,6 +1322,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction {hasSystemPromptOverride && ( diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 22b4d28192..6c9ea1c204 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -121,9 +121,10 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexConfig.codebaseIndexSearchMinScore ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE, codeIndexOpenAiKey: "", codeIndexQdrantApiKey: "", - codebaseIndexOpenAiCompatibleBaseUrl: "", + codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl || "", codebaseIndexOpenAiCompatibleApiKey: "", - codebaseIndexOpenAiCompatibleModelDimension: undefined, + codebaseIndexOpenAiCompatibleModelDimension: + codebaseIndexConfig.codebaseIndexOpenAiCompatibleModelDimension || undefined, codebaseIndexGeminiApiKey: "", } setInitialSettings(settings) diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 0d75f8f250..1896df486b 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -20,6 +20,7 @@ import { TaskActions } from "./TaskActions" import { ShareButton } from "./ShareButton" import { ContextWindowProgress } from "./ContextWindowProgress" import { Mention } from "./Mention" +import { TodoListDisplay } from "./TodoListDisplay" export interface TaskHeaderProps { task: ClineMessage @@ -32,6 +33,7 @@ export interface TaskHeaderProps { buttonsDisabled: boolean handleCondenseContext: (taskId: string) => void onClose: () => void + todos?: any[] } const TaskHeader = ({ @@ -45,6 +47,7 @@ const TaskHeader = ({ buttonsDisabled, handleCondenseContext, onClose, + todos, }: TaskHeaderProps) => { const { t } = useTranslation() const { apiConfiguration, currentTaskItem } = useExtensionState() @@ -68,11 +71,14 @@ const TaskHeader = ({ ) + const hasTodos = todos && Array.isArray(todos) && todos.length > 0 + return (
)}
+
) } diff --git a/webview-ui/src/components/chat/TodoListDisplay.tsx b/webview-ui/src/components/chat/TodoListDisplay.tsx new file mode 100644 index 0000000000..c9a4d8f4d0 --- /dev/null +++ b/webview-ui/src/components/chat/TodoListDisplay.tsx @@ -0,0 +1,354 @@ +import { useState, useRef, useMemo, useEffect } from "react" + +export function TodoListDisplay({ todos }: { todos: any[] }) { + const [isCollapsed, setIsCollapsed] = useState(true) + const ulRef = useRef(null) + const itemRefs = useRef<(HTMLLIElement | null)[]>([]) + const scrollIndex = useMemo(() => { + const inProgressIdx = todos.findIndex((todo: any) => todo.status === "in_progress") + if (inProgressIdx !== -1) return inProgressIdx + return todos.findIndex((todo: any) => todo.status !== "completed") + }, [todos]) + + // Find the most important todo to display when collapsed + const mostImportantTodo = useMemo(() => { + const inProgress = todos.find((todo: any) => todo.status === "in_progress") + if (inProgress) return inProgress + return todos.find((todo: any) => todo.status !== "completed") + }, [todos]) + useEffect(() => { + if (isCollapsed) return + if (!ulRef.current) return + if (scrollIndex === -1) return + const target = itemRefs.current[scrollIndex] + if (target && ulRef.current) { + const ul = ulRef.current + const targetTop = target.offsetTop - ul.offsetTop + const targetHeight = target.offsetHeight + const ulHeight = ul.clientHeight + const scrollTo = targetTop - (ulHeight / 2 - targetHeight / 2) + ul.scrollTop = scrollTo + } + }, [todos, isCollapsed, scrollIndex]) + if (!Array.isArray(todos) || todos.length === 0) return null + + const totalCount = todos.length + const completedCount = todos.filter((todo: any) => todo.status === "completed").length + + const allCompleted = completedCount === totalCount && totalCount > 0 + + // Create the status icon for the most important todo + const getMostImportantTodoIcon = () => { + if (allCompleted) { + return ( + + ) + } + + if (!mostImportantTodo) { + return ( + + ) + } + + if (mostImportantTodo.status === "completed") { + return ( + + ) + } + + if (mostImportantTodo.status === "in_progress") { + return ( + + ) + } + + // Default not-started todo + return ( + + ) + } + + return ( +
+
setIsCollapsed((v) => !v)}> + {getMostImportantTodoIcon()} + + {allCompleted ? "All tasks completed!" : mostImportantTodo?.content || "No pending tasks"} + +
+ + + {completedCount}/{totalCount} + +
+
+ {/* Floating panel for expanded state */} + {!isCollapsed && ( + <> + {/* Backdrop */} +
setIsCollapsed(true)} + /> + {/* Floating panel */} +
+ {/* Panel header */} +
+
+ + Todo List + + {completedCount}/{totalCount} + +
+ { + e.stopPropagation() + setIsCollapsed(true) + }} + onMouseEnter={(e) => { + e.currentTarget.style.opacity = "1" + e.currentTarget.style.background = "var(--vscode-toolbar-hoverBackground)" + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = "0.7" + e.currentTarget.style.background = "transparent" + }} + /> +
+ {/* Todo list */} +
    + {todos.map((todo: any, idx: number) => { + let icon + if (todo.status === "completed") { + icon = ( + + ) + } else if (todo.status === "in_progress") { + icon = ( + + ) + } else { + icon = ( + + ) + } + return ( +
  • (itemRefs.current[idx] = el)} + style={{ + marginBottom: 8, + display: "flex", + alignItems: "flex-start", + minHeight: 20, + lineHeight: "1.4", + }}> + {icon} + + {todo.content} + +
  • + ) + })} +
+
+ + )} +
+ ) +} diff --git a/webview-ui/src/components/chat/UpdateTodoListToolBlock.tsx b/webview-ui/src/components/chat/UpdateTodoListToolBlock.tsx new file mode 100644 index 0000000000..e851284505 --- /dev/null +++ b/webview-ui/src/components/chat/UpdateTodoListToolBlock.tsx @@ -0,0 +1,498 @@ +import React, { useState, useEffect, useRef } from "react" +import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" +import MarkdownBlock from "../common/MarkdownBlock" + +interface TodoItem { + id?: string + content: string + status?: "completed" | "in_progress" | string +} + +/** + * @description + * Editable Todo List component. Each time the todo list changes (edit, add, delete, status switch), the parent component will be notified via the onChange callback. + * The parent component should synchronize the latest todos to the model in onChange. + */ +interface UpdateTodoListToolBlockProps { + todos?: TodoItem[] + content?: string + /** + * Callback when todos change, be sure to implement and notify the model with the latest todos + * @param todos Latest todo list + */ + onChange: (todos: TodoItem[]) => void + /** Whether editing is allowed (controlled externally) */ + editable?: boolean + userEdited?: boolean +} + +const STATUS_OPTIONS = [ + { value: "", label: "Not Started", color: "var(--vscode-foreground)", border: "#bbb", bg: "transparent" }, + { + value: "in_progress", + label: "In Progress", + color: "var(--vscode-charts-yellow)", + border: "var(--vscode-charts-yellow)", + bg: "rgba(255, 221, 51, 0.15)", + }, + { + value: "completed", + label: "Completed", + color: "var(--vscode-charts-green)", + border: "var(--vscode-charts-green)", + bg: "var(--vscode-charts-green)", + }, +] + +const genId = () => Math.random().toString(36).slice(2, 10) + +const UpdateTodoListToolBlock: React.FC = ({ + todos = [], + content, + onChange, + editable = true, + userEdited = false, +}) => { + const [editTodos, setEditTodos] = useState( + todos.length > 0 ? todos.map((todo) => ({ ...todo, id: todo.id || genId() })) : [], + ) + const [adding, setAdding] = useState(false) + const [newContent, setNewContent] = useState("") + const newInputRef = useRef(null) + const [deleteId, setDeleteId] = useState(null) + const [isEditing, setIsEditing] = useState(false) + + // Automatically exit edit mode when external editable becomes false + useEffect(() => { + if (!editable && isEditing) { + setIsEditing(false) + } + }, [editable, isEditing]) + + // Check if onChange is passed + useEffect(() => { + if (typeof onChange !== "function") { + console.warn( + "UpdateTodoListToolBlock: onChange callback not passed, cannot notify model after todo changes!", + ) + } + // Only check once on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // Sync when external props.todos changes + useEffect(() => { + setEditTodos(todos.length > 0 ? todos.map((todo) => ({ ...todo, id: todo.id || genId() })) : []) + }, [todos]) + + // Auto focus on new item + useEffect(() => { + if (adding && newInputRef.current) { + newInputRef.current.focus() + } + }, [adding]) + + // Edit content + const handleContentChange = (id: string, value: string) => { + const newTodos = editTodos.map((todo) => (todo.id === id ? { ...todo, content: value } : todo)) + setEditTodos(newTodos) + onChange?.(newTodos) + } + + // Change status + const handleStatusChange = (id: string, status: string) => { + const newTodos = editTodos.map((todo) => (todo.id === id ? { ...todo, status } : todo)) + setEditTodos(newTodos) + onChange?.(newTodos) + } + + // Delete (confirmation dialog) + const handleDelete = (id: string) => { + setDeleteId(id) + } + const confirmDelete = () => { + if (!deleteId) return + const newTodos = editTodos.filter((todo) => todo.id !== deleteId) + setEditTodos(newTodos) + onChange?.(newTodos) + setDeleteId(null) + } + const cancelDelete = () => setDeleteId(null) + + // Add + const handleAdd = () => { + if (!newContent.trim()) return + const newTodo: TodoItem = { + id: genId(), + content: newContent.trim(), + status: "", + } + const newTodos = [...editTodos, newTodo] + setEditTodos(newTodos) + onChange?.(newTodos) + setNewContent("") + setAdding(false) + } + + // Add on Enter + const handleNewInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleAdd() + } else if (e.key === "Escape") { + setAdding(false) + setNewContent("") + } + } + + if (userEdited) { + return ( + + +
+ + + User Edit + +
+
+ +
+ User Edits +
+ + ) + } + + return ( + <> + + +
+ + + Todo List Updated + +
+ {editable && ( + + )} +
+ +
+ {Array.isArray(editTodos) && editTodos.length > 0 ? ( +
    + {editTodos.map((todo, idx) => { + let icon + if (todo.status === "completed") { + icon = ( + + ) + } else if (todo.status === "in_progress") { + icon = ( + + ) + } else { + icon = ( + + ) + } + return ( +
  • + {icon} + {isEditing ? ( + handleContentChange(todo.id!, e.target.value)} + style={{ + flex: 1, + minWidth: 0, + fontWeight: 500, + color: "var(--vscode-input-foreground)", + background: "var(--vscode-input-background)", + border: "none", + outline: "none", + fontSize: 13, + marginRight: 6, + padding: "1px 3px", + borderBottom: "1px solid var(--vscode-input-border)", + }} + onBlur={(e) => { + if (!e.target.value.trim()) { + handleDelete(todo.id!) + } + }} + /> + ) : ( + + {todo.content} + + )} + {isEditing && ( + + )} + {isEditing && ( + + )} +
  • + ) + })} + {adding ? ( +
  • + + setNewContent(e.target.value)} + onKeyDown={handleNewInputKeyDown} + style={{ + flex: 1, + minWidth: 0, + fontWeight: 500, + color: "var(--vscode-foreground)", + background: "transparent", + border: "none", + outline: "none", + fontSize: 13, + marginRight: 6, + padding: "1px 3px", + borderBottom: "1px solid #eee", + }} + /> + + +
  • + ) : ( +
  • + {isEditing && ( + + )} +
  • + )} +
+ ) : ( + + )} +
+ {/* Delete confirmation dialog */} + {deleteId && ( +
+
e.stopPropagation()}> +
+ Are you sure you want to delete this todo item? +
+
+ + +
+
+
+ )} + + + ) +} + +export default UpdateTodoListToolBlock diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index 1f9bb282a8..71283a833b 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -45,6 +45,7 @@ type AutoApproveSettingsProps = HTMLAttributes & { | "alwaysAllowFollowupQuestions" | "followupAutoApproveTimeoutMs" | "allowedCommands" + | "alwaysAllowUpdateTodoList" > } diff --git a/webview-ui/src/components/settings/AutoApproveToggle.tsx b/webview-ui/src/components/settings/AutoApproveToggle.tsx index 6c82d3c984..e8b51b01ef 100644 --- a/webview-ui/src/components/settings/AutoApproveToggle.tsx +++ b/webview-ui/src/components/settings/AutoApproveToggle.tsx @@ -15,6 +15,7 @@ type AutoApproveToggles = Pick< | "alwaysAllowSubtasks" | "alwaysAllowExecute" | "alwaysAllowFollowupQuestions" + | "alwaysAllowUpdateTodoList" > export type AutoApproveSetting = keyof AutoApproveToggles @@ -91,6 +92,13 @@ export const autoApproveSettingsConfig: Record { @@ -87,28 +87,25 @@ export const ModelPicker = ({ } // Delay to ensure the popover is closed before setting the search value. - selectTimeoutRef.current = setTimeout(() => setSearchValue(modelId), 100) + selectTimeoutRef.current = setTimeout(() => setSearchValue(""), 100) }, [modelIdKey, setApiConfigurationField], ) - const onOpenChange = useCallback( - (open: boolean) => { - setOpen(open) + const onOpenChange = useCallback((open: boolean) => { + setOpen(open) - // Abandon the current search if the popover is closed. - if (!open) { - // Clear any existing timeout - if (closeTimeoutRef.current) { - clearTimeout(closeTimeoutRef.current) - } - - // Delay to ensure the popover is closed before setting the search value. - closeTimeoutRef.current = setTimeout(() => setSearchValue(selectedModelId), 100) + // Abandon the current search if the popover is closed. + if (!open) { + // Clear any existing timeout + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current) } - }, - [selectedModelId], - ) + + // Clear the search value when closing instead of prefilling it + closeTimeoutRef.current = setTimeout(() => setSearchValue(""), 100) + } + }, []) const onClearSearch = useCallback(() => { setSearchValue("") diff --git a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx index ac8c054f65..270ed305ea 100644 --- a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx @@ -26,6 +26,7 @@ describe("AutoApproveToggle", () => { alwaysAllowSubtasks: false, alwaysAllowExecute: true, alwaysAllowFollowupQuestions: false, + alwaysAllowUpdateTodoList: true, onToggle: mockOnToggle, } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 3e33c2d6ec..df7cee5627 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -129,6 +129,8 @@ export interface ExtensionStateContextType extends ExtensionState { autoCondenseContextPercent: number setAutoCondenseContextPercent: (value: number) => void routerModels?: RouterModels + alwaysAllowUpdateTodoList?: boolean + setAlwaysAllowUpdateTodoList: (value: boolean) => void } export const ExtensionStateContext = createContext(undefined) @@ -221,6 +223,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode codebaseIndexSearchMinScore: undefined, }, codebaseIndexModels: { ollama: {}, openai: {} }, + alwaysAllowUpdateTodoList: true, }) const [didHydrateState, setDidHydrateState] = useState(false) @@ -459,6 +462,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setCustomCondensingPrompt: (value) => setState((prevState) => ({ ...prevState, customCondensingPrompt: value })), setProfileThresholds: (value) => setState((prevState) => ({ ...prevState, profileThresholds: value })), + alwaysAllowUpdateTodoList: state.alwaysAllowUpdateTodoList, + setAlwaysAllowUpdateTodoList: (value) => { + setState((prevState) => ({ ...prevState, alwaysAllowUpdateTodoList: value })) + }, } return {children} diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 634ac28d32..2e2197f123 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Introduïu prefix de comanda (ex. 'git ')", "addButton": "Afegir" }, + "updateTodoList": { + "label": "Todo", + "description": "La llista de tasques es actualitza automàticament sense aprovació" + }, "apiRequestLimit": { "title": "Màximes Sol·licituds", "description": "Fes aquesta quantitat de sol·licituds API automàticament abans de demanar aprovació per continuar amb la tasca.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 7c328cacec..14af077b23 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Befehlspräfix eingeben (z.B. 'git ')", "addButton": "Hinzufügen" }, + "updateTodoList": { + "label": "Todo", + "description": "To-Do-Liste wird automatisch aktualisiert, ohne dass du zustimmen musst" + }, "apiRequestLimit": { "title": "Maximale Anfragen", "description": "Automatisch so viele API-Anfragen stellen, bevor du um die Erlaubnis gebeten wirst, mit der Aufgabe fortzufahren.", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index a849df5b7d..ea7191f224 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Enter command prefix (e.g., 'git ')", "addButton": "Add" }, + "updateTodoList": { + "label": "Todo", + "description": "Automatically update the to-do list without requiring approval" + }, "apiRequestLimit": { "title": "Max Requests", "description": "Automatically make this many API requests before asking for approval to continue with the task.", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 180d91060a..d6cebea206 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Ingrese prefijo de comando (ej. 'git ')", "addButton": "Añadir" }, + "updateTodoList": { + "label": "Todo", + "description": "La lista de tareas se actualiza automáticamente sin aprobación" + }, "apiRequestLimit": { "title": "Solicitudes máximas", "description": "Realizar automáticamente esta cantidad de solicitudes a la API antes de pedir aprobación para continuar con la tarea.", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 4546e595ad..281fb705fc 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Entrez le préfixe de commande (ex. 'git ')", "addButton": "Ajouter" }, + "updateTodoList": { + "label": "Todo", + "description": "La liste de tâches est mise à jour automatiquement sans approbation" + }, "apiRequestLimit": { "title": "Requêtes maximales", "description": "Effectuer automatiquement ce nombre de requêtes API avant de demander l'approbation pour continuer la tâche.", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 249973fa98..4b8132768f 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "कमांड प्रीफिक्स दर्ज करें (उदा. 'git ')", "addButton": "जोड़ें" }, + "updateTodoList": { + "label": "टूडू", + "description": "अनुमोदन की आवश्यकता के बिना स्वचालित रूप से टूडू सूची अपडेट करें" + }, "apiRequestLimit": { "title": "अधिकतम अनुरोध", "description": "कार्य जारी रखने के लिए अनुमति मांगने से पहले स्वचालित रूप से इतने API अनुरोध करें।", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 52a339330e..ac1454ab7b 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -166,6 +166,10 @@ "label": "Tampilkan menu auto-approve di tampilan chat", "description": "Ketika diaktifkan, menu auto-approve akan ditampilkan di bagian bawah tampilan chat, memungkinkan akses cepat ke pengaturan auto-approve" }, + "updateTodoList": { + "label": "Todo", + "description": "Daftar tugas diperbarui secara otomatis tanpa persetujuan" + }, "apiRequestLimit": { "title": "Permintaan Maks", "description": "Secara otomatis membuat sejumlah permintaan API ini sebelum meminta persetujuan untuk melanjutkan tugas.", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 2206822dfc..7627486863 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Inserisci prefisso comando (es. 'git ')", "addButton": "Aggiungi" }, + "updateTodoList": { + "label": "Todo", + "description": "La lista delle cose da fare viene aggiornata automaticamente senza approvazione" + }, "apiRequestLimit": { "title": "Richieste massime", "description": "Esegui automaticamente questo numero di richieste API prima di chiedere l'approvazione per continuare con l'attività.", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 94b93b043a..f779749cd4 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "コマンドプレフィックスを入力(例:'git ')", "addButton": "追加" }, + "updateTodoList": { + "label": "Todo", + "description": "承認なしで自動的にToDoリストを更新" + }, "apiRequestLimit": { "title": "最大リクエスト数", "description": "タスクを続行するための承認を求める前に、自動的にこの数のAPIリクエストを行います。", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index d6436cc6e2..2ea16f7834 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "명령 접두사 입력(예: 'git ')", "addButton": "추가" }, + "updateTodoList": { + "label": "Todo", + "description": "승인 없이 자동으로 할 일 목록이 업데이트됩니다" + }, "apiRequestLimit": { "title": "최대 요청 수", "description": "작업을 계속하기 위한 승인을 요청하기 전에 자동으로 이 수의 API 요청을 수행합니다.", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index ea8c75d974..92b00cf3ff 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Voer commando-prefix in (bijv. 'git ')", "addButton": "Toevoegen" }, + "updateTodoList": { + "label": "Todo", + "description": "De takenlijst wordt automatisch bijgewerkt zonder goedkeuring" + }, "apiRequestLimit": { "title": "Maximale verzoeken", "description": "Voer automatisch dit aantal API-verzoeken uit voordat om goedkeuring wordt gevraagd om door te gaan met de taak.", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index bd15c4a2c0..34e0ac52ef 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Wprowadź prefiks polecenia (np. 'git ')", "addButton": "Dodaj" }, + "updateTodoList": { + "label": "Todo", + "description": "Lista zadań jest automatycznie aktualizowana bez zatwierdzenia" + }, "apiRequestLimit": { "title": "Maksymalna liczba żądań", "description": "Automatycznie wykonaj tyle żądań API przed poproszeniem o zgodę na kontynuowanie zadania.", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index b826bcc06b..e696ab0f77 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Digite o prefixo do comando (ex. 'git ')", "addButton": "Adicionar" }, + "updateTodoList": { + "label": "Todo", + "description": "A lista de tarefas é atualizada automaticamente sem aprovação" + }, "apiRequestLimit": { "title": "Máximo de Solicitações", "description": "Fazer automaticamente este número de requisições à API antes de pedir aprovação para continuar com a tarefa.", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 9dfaf0bccb..3e6a7a9d76 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Введите префикс команды (например, 'git ')", "addButton": "Добавить" }, + "updateTodoList": { + "label": "Todo", + "description": "Список дел обновляется автоматически без подтверждения" + }, "apiRequestLimit": { "title": "Максимум запросов", "description": "Автоматически выполнять это количество API-запросов перед запросом разрешения на продолжение задачи.", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 7ceaf46a32..c0a3b8f501 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Komut öneki girin (örn. 'git ')", "addButton": "Ekle" }, + "updateTodoList": { + "label": "Todo", + "description": "Yapılacaklar listesi onay gerektirmeden otomatik olarak güncellenir" + }, "apiRequestLimit": { "title": "Maksimum İstek", "description": "Göreve devam etmek için onay istemeden önce bu sayıda API isteği otomatik olarak yap.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 134c4f476e..e553f5d0c2 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "Nhập tiền tố lệnh (ví dụ: 'git ')", "addButton": "Thêm" }, + "updateTodoList": { + "label": "Todo", + "description": "Danh sách việc cần làm được cập nhật tự động mà không cần phê duyệt" + }, "apiRequestLimit": { "title": "Số lượng yêu cầu tối đa", "description": "Tự động thực hiện số lượng API request này trước khi yêu cầu phê duyệt để tiếp tục với nhiệm vụ.", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 9007569757..d8d875c85f 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "输入命令前缀(例如 'git ')", "addButton": "添加" }, + "updateTodoList": { + "label": "待办", + "description": "无需批准即可自动更新待办清单" + }, "apiRequestLimit": { "title": "最大请求数", "description": "在请求批准以继续执行任务之前,自动发出此数量的 API 请求。", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 332a234f86..f08cb0dbba 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -162,6 +162,10 @@ "commandPlaceholder": "輸入命令前綴(例如 'git ')", "addButton": "新增" }, + "updateTodoList": { + "label": "待辦", + "description": "自動更新待辦清單無需批准" + }, "apiRequestLimit": { "title": "最大請求數", "description": "在請求批准以繼續執行工作之前,自動發出此數量的 API 請求。",