diff --git a/apps/web-roo-code/src/components/homepage/pillars-section.tsx b/apps/web-roo-code/src/components/homepage/pillars-section.tsx index b363a5e9317..c6c47ea054c 100644 --- a/apps/web-roo-code/src/components/homepage/pillars-section.tsx +++ b/apps/web-roo-code/src/components/homepage/pillars-section.tsx @@ -181,7 +181,7 @@ export function PillarsSection() {

The Roo Code Extension is{" "} - + open source {" "} so you can see for yourself exactly what it's doing and we don't use diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index 5ce94b3cdc8..ea6be536c04 100644 --- a/packages/types/npm/package.metadata.json +++ b/packages/types/npm/package.metadata.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.95.0", + "version": "1.96.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 2ebb8ca86e1..a774c76da9f 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -57,14 +57,16 @@ export const globalSettingsSchema = z.object({ listApiConfigMeta: z.array(providerSettingsEntrySchema).optional(), pinnedApiConfigs: z.record(z.string(), z.boolean()).optional(), // Auto cleanup settings - autoCleanup: z.object({ - enabled: z.boolean().optional(), - strategy: z.nativeEnum(CleanupStrategy).optional(), - retentionDays: z.number().min(1).max(365).optional(), - maxHistoryCount: z.number().min(10).max(500).optional(), - excludeActive: z.boolean().optional(), - cleanupOnStartup: z.boolean().optional(), - }).optional(), + autoCleanup: z + .object({ + enabled: z.boolean().optional(), + strategy: z.nativeEnum(CleanupStrategy).optional(), + retentionDays: z.number().min(1).max(365).optional(), + maxHistoryCount: z.number().min(10).max(500).optional(), + excludeActive: z.boolean().optional(), + cleanupOnStartup: z.boolean().optional(), + }) + .optional(), lastShownAnnouncementId: z.string().optional(), customInstructions: z.string().optional(), diff --git a/packages/types/src/providers/cerebras.ts b/packages/types/src/providers/cerebras.ts index 54b314b6db9..8e0c2f9413c 100644 --- a/packages/types/src/providers/cerebras.ts +++ b/packages/types/src/providers/cerebras.ts @@ -7,7 +7,7 @@ export const cerebrasDefaultModelId: CerebrasModelId = "gpt-oss-120b" export const cerebrasModels = { "zai-glm-4.6": { - maxTokens: 8192, // Conservative default to avoid premature rate limiting (Cerebras reserves quota upfront) + maxTokens: 16384, // Conservative default to avoid premature rate limiting (Cerebras reserves quota upfront) contextWindow: 131072, supportsImages: false, supportsPromptCache: false, @@ -18,7 +18,7 @@ export const cerebrasModels = { description: "Highly intelligent general purpose model with up to 1,000 tokens/s", }, "qwen-3-235b-a22b-instruct-2507": { - maxTokens: 8192, // Conservative default to avoid premature rate limiting + maxTokens: 16384, // Conservative default to avoid premature rate limiting contextWindow: 64000, supportsImages: false, supportsPromptCache: false, @@ -29,7 +29,7 @@ export const cerebrasModels = { description: "Intelligent model with ~1400 tokens/s", }, "llama-3.3-70b": { - maxTokens: 8192, // Conservative default to avoid premature rate limiting + maxTokens: 16384, // Conservative default to avoid premature rate limiting contextWindow: 64000, supportsImages: false, supportsPromptCache: false, @@ -40,7 +40,7 @@ export const cerebrasModels = { description: "Powerful model with ~2600 tokens/s", }, "qwen-3-32b": { - maxTokens: 8192, // Conservative default to avoid premature rate limiting + maxTokens: 16384, // Conservative default to avoid premature rate limiting contextWindow: 64000, supportsImages: false, supportsPromptCache: false, @@ -51,7 +51,7 @@ export const cerebrasModels = { description: "SOTA coding performance with ~2500 tokens/s", }, "gpt-oss-120b": { - maxTokens: 8192, // Conservative default to avoid premature rate limiting + maxTokens: 16384, // Conservative default to avoid premature rate limiting contextWindow: 64000, supportsImages: false, supportsPromptCache: false, diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 6b38ba13ec8..80650b744c5 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -38,6 +38,7 @@ export const toolNames = [ "update_todo_list", "run_slash_command", "generate_image", + "custom_tool", ] as const export const toolNamesSchema = z.enum(toolNames) diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index 8a80f0d7765..d832508f635 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -90,13 +90,7 @@ export abstract class BaseOpenAiCompatibleProvider model, max_tokens, temperature, - // Enable mergeToolResultText to merge environment_details and other text content - // after tool_results into the last tool message. This prevents reasoning/thinking - // models from dropping reasoning_content when they see a user message after tool results. - messages: [ - { role: "system", content: systemPrompt }, - ...convertToOpenAiMessages(messages, { mergeToolResultText: true }), - ], + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, ...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }), diff --git a/src/api/providers/cerebras.ts b/src/api/providers/cerebras.ts index 3c4ca2bec25..88184cc7c8d 100644 --- a/src/api/providers/cerebras.ts +++ b/src/api/providers/cerebras.ts @@ -106,7 +106,7 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan supportsNativeTools && metadata?.tools && metadata.tools.length > 0 && metadata?.toolProtocol !== "xml" // Convert Anthropic messages to OpenAI format (Cerebras is OpenAI-compatible) - const openaiMessages = convertToOpenAiMessages(messages, { mergeToolResultText: true }) + const openaiMessages = convertToOpenAiMessages(messages) // Prepare request body following Cerebras API specification exactly const requestBody: Record = { diff --git a/src/api/providers/chutes.ts b/src/api/providers/chutes.ts index f06e0e76f20..02dcc152a50 100644 --- a/src/api/providers/chutes.ts +++ b/src/api/providers/chutes.ts @@ -44,10 +44,7 @@ export class ChutesHandler extends RouterProvider implements SingleCompletionHan const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model, max_tokens, - messages: [ - { role: "system", content: systemPrompt }, - ...convertToOpenAiMessages(messages, { mergeToolResultText: true }), - ], + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, ...(metadata?.tools && { tools: metadata.tools }), diff --git a/src/api/providers/deepinfra.ts b/src/api/providers/deepinfra.ts index 46def1c09fe..e9bb2961d3d 100644 --- a/src/api/providers/deepinfra.ts +++ b/src/api/providers/deepinfra.ts @@ -72,10 +72,7 @@ export class DeepInfraHandler extends RouterProvider implements SingleCompletion const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelId, - messages: [ - { role: "system", content: systemPrompt }, - ...convertToOpenAiMessages(messages, { mergeToolResultText: true }), - ], + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, reasoning_effort, diff --git a/src/api/providers/featherless.ts b/src/api/providers/featherless.ts index 64df12bc972..3dcd0821b8c 100644 --- a/src/api/providers/featherless.ts +++ b/src/api/providers/featherless.ts @@ -44,10 +44,7 @@ export class FeatherlessHandler extends BaseOpenAiCompatibleProvider name === resolvedName || resolvedName.indexOf(name) > -1) ?? "") as TName - const matchCustomToolName = (customToolRegistry.list().find((name) => name === resolvedName || resolvedName.indexOf(name) > -1) ?? "" ) as TName - + const matchBuiltinToolName = (toolNames.find( + (name) => name === resolvedName || resolvedName.indexOf(name) > -1, + ) ?? "") as TName + const matchCustomToolName = (customToolRegistry + .list() + .find((name) => name === resolvedName || resolvedName.indexOf(name) > -1) ?? "") as TName + const _resolvedName = matchBuiltinToolName || matchCustomToolName if (!_resolvedName) { diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts new file mode 100644 index 00000000000..6ad8c58282c --- /dev/null +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts @@ -0,0 +1,349 @@ +// npx vitest src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts + +import { describe, it, expect, beforeEach, vi } from "vitest" +import { presentAssistantMessage } from "../presentAssistantMessage" + +// Mock dependencies +vi.mock("../../task/Task") +vi.mock("../../tools/validateToolUse", () => ({ + validateToolUse: vi.fn(), +})) + +// Mock custom tool registry - must be done inline without external variable references +vi.mock("@roo-code/core", () => ({ + customToolRegistry: { + has: vi.fn(), + get: vi.fn(), + }, +})) + +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + captureToolUsage: vi.fn(), + captureConsecutiveMistakeError: vi.fn(), + }, + }, +})) + +import { TelemetryService } from "@roo-code/telemetry" +import { customToolRegistry } from "@roo-code/core" + +describe("presentAssistantMessage - Custom Tool Recording", () => { + let mockTask: any + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks() + + // Create a mock Task with minimal properties needed for testing + mockTask = { + taskId: "test-task-id", + instanceId: "test-instance", + abort: false, + presentAssistantMessageLocked: false, + presentAssistantMessageHasPendingUpdates: false, + currentStreamingContentIndex: 0, + assistantMessageContent: [], + userMessageContent: [], + didCompleteReadingStream: false, + didRejectTool: false, + didAlreadyUseTool: false, + diffEnabled: false, + consecutiveMistakeCount: 0, + clineMessages: [], + api: { + getModel: () => ({ id: "test-model", info: {} }), + }, + browserSession: { + closeBrowser: vi.fn().mockResolvedValue(undefined), + }, + recordToolUsage: vi.fn(), + recordToolError: vi.fn(), + toolRepetitionDetector: { + check: vi.fn().mockReturnValue({ allowExecution: true }), + }, + providerRef: { + deref: () => ({ + getState: vi.fn().mockResolvedValue({ + mode: "code", + customModes: [], + experiments: { + customTools: true, // Enable by default + }, + }), + }), + }, + say: vi.fn().mockResolvedValue(undefined), + ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), + } + }) + + describe("Custom tool usage recording", () => { + it("should record custom tool usage as 'custom_tool' when experiment is enabled", async () => { + const toolCallId = "tool_call_custom_123" + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: toolCallId, + name: "my_custom_tool", + params: { value: "test" }, + partial: false, + }, + ] + + // Mock customToolRegistry to recognize this as a custom tool + vi.mocked(customToolRegistry.has).mockReturnValue(true) + vi.mocked(customToolRegistry.get).mockReturnValue({ + name: "my_custom_tool", + description: "A custom tool", + execute: vi.fn().mockResolvedValue("Custom tool result"), + }) + + await presentAssistantMessage(mockTask) + + // Should record as "custom_tool", not "my_custom_tool" + expect(mockTask.recordToolUsage).toHaveBeenCalledWith("custom_tool") + expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith( + mockTask.taskId, + "custom_tool", + "native", + ) + }) + + it("should record custom tool usage as 'custom_tool' in XML protocol", async () => { + mockTask.assistantMessageContent = [ + { + type: "tool_use", + // No ID = XML protocol + name: "my_custom_tool", + params: { value: "test" }, + partial: false, + }, + ] + + vi.mocked(customToolRegistry.has).mockReturnValue(true) + vi.mocked(customToolRegistry.get).mockReturnValue({ + name: "my_custom_tool", + description: "A custom tool", + execute: vi.fn().mockResolvedValue("Custom tool result"), + }) + + await presentAssistantMessage(mockTask) + + expect(mockTask.recordToolUsage).toHaveBeenCalledWith("custom_tool") + expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith( + mockTask.taskId, + "custom_tool", + "xml", + ) + }) + }) + + describe("Custom tool error recording", () => { + it("should record custom tool error as 'custom_tool'", async () => { + const toolCallId = "tool_call_custom_error_123" + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: toolCallId, + name: "failing_custom_tool", + params: {}, + partial: false, + }, + ] + + // Mock customToolRegistry with a tool that throws an error + vi.mocked(customToolRegistry.has).mockReturnValue(true) + vi.mocked(customToolRegistry.get).mockReturnValue({ + name: "failing_custom_tool", + description: "A failing custom tool", + execute: vi.fn().mockRejectedValue(new Error("Custom tool execution failed")), + }) + + await presentAssistantMessage(mockTask) + + // Should record error as "custom_tool", not "failing_custom_tool" + expect(mockTask.recordToolError).toHaveBeenCalledWith("custom_tool", "Custom tool execution failed") + expect(mockTask.consecutiveMistakeCount).toBe(1) + }) + }) + + describe("Regular tool recording", () => { + it("should record regular tool usage with actual tool name", async () => { + const toolCallId = "tool_call_read_file_123" + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: toolCallId, + name: "read_file", + params: { path: "test.txt" }, + partial: false, + }, + ] + + // read_file is not a custom tool + vi.mocked(customToolRegistry.has).mockReturnValue(false) + + await presentAssistantMessage(mockTask) + + // Should record as "read_file", not "custom_tool" + expect(mockTask.recordToolUsage).toHaveBeenCalledWith("read_file") + expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith( + mockTask.taskId, + "read_file", + "native", + ) + }) + + it("should record MCP tool usage as 'use_mcp_tool' (not custom_tool)", async () => { + const toolCallId = "tool_call_mcp_123" + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: toolCallId, + name: "use_mcp_tool", + params: { + server_name: "test-server", + tool_name: "test-tool", + arguments: "{}", + }, + partial: false, + }, + ] + + vi.mocked(customToolRegistry.has).mockReturnValue(false) + + // Mock MCP hub for use_mcp_tool + mockTask.providerRef = { + deref: () => ({ + getState: vi.fn().mockResolvedValue({ + mode: "code", + customModes: [], + experiments: { + customTools: true, + }, + }), + getMcpHub: () => ({ + findServerNameBySanitizedName: () => "test-server", + executeToolCall: vi.fn().mockResolvedValue({ content: [{ type: "text", text: "result" }] }), + }), + }), + } + + await presentAssistantMessage(mockTask) + + // Should record as "use_mcp_tool", not "custom_tool" + expect(mockTask.recordToolUsage).toHaveBeenCalledWith("use_mcp_tool") + expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith( + mockTask.taskId, + "use_mcp_tool", + "native", + ) + }) + }) + + describe("Custom tool experiment gate", () => { + it("should treat custom tool as unknown when experiment is disabled", async () => { + const toolCallId = "tool_call_disabled_123" + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: toolCallId, + name: "my_custom_tool", + params: {}, + partial: false, + }, + ] + + // Mock provider state with customTools experiment DISABLED + mockTask.providerRef = { + deref: () => ({ + getState: vi.fn().mockResolvedValue({ + mode: "code", + customModes: [], + experiments: { + customTools: false, // Disabled + }, + }), + }), + } + + // Even if registry recognizes it, experiment gate should prevent execution + vi.mocked(customToolRegistry.has).mockReturnValue(true) + vi.mocked(customToolRegistry.get).mockReturnValue({ + name: "my_custom_tool", + description: "A custom tool", + execute: vi.fn().mockResolvedValue("Should not execute"), + }) + + await presentAssistantMessage(mockTask) + + // Should be treated as unknown tool (not executed) + expect(mockTask.say).toHaveBeenCalledWith("error", "unknownToolError") + expect(mockTask.consecutiveMistakeCount).toBe(1) + + // Custom tool should NOT have been executed + const getMock = vi.mocked(customToolRegistry.get) + if (getMock.mock.results.length > 0) { + const customTool = getMock.mock.results[0].value + if (customTool) { + expect(customTool.execute).not.toHaveBeenCalled() + } + } + }) + + it("should not call customToolRegistry.has() when experiment is disabled", async () => { + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: "tool_call_123", + name: "some_tool", + params: {}, + partial: false, + }, + ] + + // Disable experiment + mockTask.providerRef = { + deref: () => ({ + getState: vi.fn().mockResolvedValue({ + mode: "code", + customModes: [], + experiments: { + customTools: false, + }, + }), + }), + } + + await presentAssistantMessage(mockTask) + + // When experiment is off, shouldn't even check the registry + // (Code checks stateExperiments?.customTools before calling has()) + expect(customToolRegistry.has).not.toHaveBeenCalled() + }) + }) + + describe("Partial blocks", () => { + it("should not record usage for partial custom tool blocks", async () => { + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: "tool_call_partial_123", + name: "my_custom_tool", + params: { value: "test" }, + partial: true, // Still streaming + }, + ] + + vi.mocked(customToolRegistry.has).mockReturnValue(true) + + await presentAssistantMessage(mockTask) + + // Should not record usage for partial blocks + expect(mockTask.recordToolUsage).not.toHaveBeenCalled() + expect(TelemetryService.instance.captureToolUsage).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index c49eff4deab..369aae3582d 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -727,8 +727,11 @@ export async function presentAssistantMessage(cline: Task) { } if (!block.partial) { - cline.recordToolUsage(block.name) - TelemetryService.instance.captureToolUsage(cline.taskId, block.name, toolProtocol) + // Check if this is a custom tool - if so, record as "custom_tool" (like MCP tools) + const isCustomTool = stateExperiments?.customTools && customToolRegistry.has(block.name) + const recordName = isCustomTool ? "custom_tool" : block.name + cline.recordToolUsage(recordName) + TelemetryService.instance.captureToolUsage(cline.taskId, recordName, toolProtocol) } // Validate tool use before execution - ONLY for complete (non-partial) blocks. @@ -1133,6 +1136,8 @@ export async function presentAssistantMessage(cline: Task) { cline.consecutiveMistakeCount = 0 } catch (executionError: any) { cline.consecutiveMistakeCount++ + // Record custom tool error with static name + cline.recordToolError("custom_tool", executionError.message) await handleError(`executing custom tool "${block.name}"`, executionError) } diff --git a/src/core/diff/strategies/multi-file-search-replace.ts b/src/core/diff/strategies/multi-file-search-replace.ts index 72126de5162..0851779dd41 100644 --- a/src/core/diff/strategies/multi-file-search-replace.ts +++ b/src/core/diff/strategies/multi-file-search-replace.ts @@ -519,6 +519,15 @@ Each file requires its own path, start_line, and diff elements. console.warn( `[MultiFileSearchReplaceDiffStrategy] Skipping replacement at line ${startLine} because search and replace content are identical`, ) + // TODO: Add a warning to the diff results (costrct change) + // diffResults.push({ + // success: false, + // error: + // `Search and replace content are identical - no changes would be made\n\n` + + // `Debug Info:\n` + + // `- Search and replace must be different to make changes\n` + + // `- Use read_file to verify the content you want to change`, + // }) continue } diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index 1c7843790fa..0075d965cf0 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -427,11 +427,21 @@ Only use a single line of '=======' between search and replacement content, beca replaceContent = stripLineNumbers(replaceContent) } + // Validate that search and replace content are not identical if (searchContent === replaceContent) { appliedCount++ console.warn( `[MultiSearchReplaceDiffStrategy] Skipping replacement at line ${startLine} because search and replace content are identical`, ) + // TODO: Add a warning to the diff results (costrct change) + // diffResults.push({ + // success: false, + // error: + // `Search and replace content are identical - no changes would be made\n\n` + + // `Debug Info:\n` + + // `- Search and replace must be different to make changes\n` + + // `- Use read_file to verify the content you want to change`, + // }) continue } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index ca9ab7f544b..16648324772 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -357,7 +357,9 @@ export class Task extends EventEmitter implements TaskLike { assistantMessageContent: AssistantMessageContent[] = [] presentAssistantMessageLocked = false presentAssistantMessageHasPendingUpdates = false - userMessageContent: ((Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolResultBlockParam) & { __isNoToolsUsed?: boolean; })[] = [] + userMessageContent: ((Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolResultBlockParam) & { + __isNoToolsUsed?: boolean + })[] = [] userMessageContentReady = false didRejectTool = false didAlreadyUseTool = false @@ -944,9 +946,7 @@ export class Task extends EventEmitter implements TaskLike { continue } - const hasNoToolsUsedError = msg.content.some( - (block: any) => block.__isNoToolsUsed === true - ) + const hasNoToolsUsedError = msg.content.some((block: any) => block.__isNoToolsUsed === true) if (hasNoToolsUsedError) { errorUserMessageIndices.push(i) } @@ -963,9 +963,9 @@ export class Task extends EventEmitter implements TaskLike { const lastMessageIndex = this.apiConversationHistory.length - 1 if (lastMessageIndex < 0 || this.apiConversationHistory[lastMessageIndex].role !== "assistant") { // Unexpected state: last message should be the successful assistant response - this.providerRef?.deref()?.log( - `Warning: markErrorCorrectionPair called but last message is not from assistant` - ) + this.providerRef + ?.deref() + ?.log(`Warning: markErrorCorrectionPair called but last message is not from assistant`) return } @@ -1594,6 +1594,10 @@ export class Task extends EventEmitter implements TaskLike { } public async condenseContext(): Promise { + // CRITICAL: Flush any pending tool results before condensing + // to ensure tool_use/tool_result pairs are complete in history + await this.flushPendingToolResultsToHistory() + const systemPrompt = await this.getSystemPrompt() // Get condensing configuration @@ -2422,7 +2426,9 @@ export class Task extends EventEmitter implements TaskLike { // Task Loop - private async initiateTaskLoop(userContent: Array): Promise { + private async initiateTaskLoop( + userContent: Array, + ): Promise { // Kicks off the checkpoints initialization process in the background. getCheckpointService(this) diff --git a/src/core/task/__tests__/markErrorCorrectionPair.spec.ts b/src/core/task/__tests__/markErrorCorrectionPair.spec.ts index fb1123f1b63..157de4941a2 100644 --- a/src/core/task/__tests__/markErrorCorrectionPair.spec.ts +++ b/src/core/task/__tests__/markErrorCorrectionPair.spec.ts @@ -23,9 +23,7 @@ describe("markErrorCorrectionPair 优化验证", () => { continue } - const hasNoToolsUsedError = msg.content.some( - (block: any) => block.__isNoToolsUsed === true - ) + const hasNoToolsUsedError = msg.content.some((block: any) => block.__isNoToolsUsed === true) if (hasNoToolsUsedError) { errorUserMessageIndices.push(i) } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index be5e0b3e043..679bc6d5aa0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -348,14 +348,12 @@ export class ClineProvider const taskHistory = this.getGlobalState("taskHistory") ?? [] // Perform cleanup - const result = await this.autoCleanupService.performCleanup( - taskHistory, - settings, - currentTask?.taskId, - ) + const result = await this.autoCleanupService.performCleanup(taskHistory, settings, currentTask?.taskId) if (result.tasksRemoved > 0) { - this.log(`Auto cleanup removed ${result.tasksRemoved} tasks, freed ${AutoCleanupService.formatBytes(result.spaceFreed)}`) + this.log( + `Auto cleanup removed ${result.tasksRemoved} tasks, freed ${AutoCleanupService.formatBytes(result.spaceFreed)}`, + ) // Delete task files for each removed task for (const taskId of result.removedTaskIds) { @@ -2083,7 +2081,7 @@ export class ClineProvider featureRoomoteControlEnabled, isBrowserSessionActive, autoCleanup, - filterErrorCorrectionMessages + filterErrorCorrectionMessages, } = await this.getState() // let cloudOrganizations: CloudOrganizationMembership[] = [] diff --git a/src/core/webview/__tests__/filterErrorCorrectionMessages.spec.ts b/src/core/webview/__tests__/filterErrorCorrectionMessages.spec.ts index bf3a4243ea8..b9972a97c70 100644 --- a/src/core/webview/__tests__/filterErrorCorrectionMessages.spec.ts +++ b/src/core/webview/__tests__/filterErrorCorrectionMessages.spec.ts @@ -28,10 +28,7 @@ describe("filterErrorCorrectionMessages 配置持久化", () => { } // 验证配置被正确保存 - expect(mockContextProxy.setValue).toHaveBeenCalledWith( - "filterErrorCorrectionMessages", - true - ) + expect(mockContextProxy.setValue).toHaveBeenCalledWith("filterErrorCorrectionMessages", true) }) it("应该在 getState 中返回正确的 filterErrorCorrectionMessages 值", async () => { diff --git a/src/core/webview/autoCleanup.ts b/src/core/webview/autoCleanup.ts index 02f93d6a76b..b4873c973a7 100644 --- a/src/core/webview/autoCleanup.ts +++ b/src/core/webview/autoCleanup.ts @@ -159,9 +159,9 @@ export class AutoCleanupService { if (shouldExcludeActive && currentTaskId) { // 找到活跃任务 - const activeTask = sortedHistory.find(t => t.id === currentTaskId) + const activeTask = sortedHistory.find((t) => t.id === currentTaskId) // 其他任务 - const otherTasks = sortedHistory.filter(t => t.id !== currentTaskId) + const otherTasks = sortedHistory.filter((t) => t.id !== currentTaskId) if (activeTask) { // 保留活跃任务 + (maxCount - 1) 个其他最新任务 @@ -169,23 +169,14 @@ export class AutoCleanupService { const keepOthers = otherTasks.slice(0, Math.max(0, maxCount - 1)) const removeOthers = otherTasks.slice(Math.max(0, maxCount - 1)) - return [ - [activeTask, ...keepOthers], - removeOthers.map(t => t.id) - ] + return [[activeTask, ...keepOthers], removeOthers.map((t) => t.id)] } // 如果活跃任务不存在,正常保留 maxCount 个 - return [ - otherTasks.slice(0, maxCount), - otherTasks.slice(maxCount).map(t => t.id) - ] + return [otherTasks.slice(0, maxCount), otherTasks.slice(maxCount).map((t) => t.id)] } // 不需要保护活跃任务,直接保留最新的 maxCount 个 - return [ - sortedHistory.slice(0, maxCount), - sortedHistory.slice(maxCount).map(t => t.id) - ] + return [sortedHistory.slice(0, maxCount), sortedHistory.slice(maxCount).map((t) => t.id)] } /** diff --git a/src/shared/tools.ts b/src/shared/tools.ts index a9112785f98..f58ec4a5e0f 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -283,6 +283,7 @@ export const TOOL_DISPLAY_NAMES: Record = { update_todo_list: "update todo list", run_slash_command: "run slash command", generate_image: "generate images", + custom_tool: "use custom tools", } as const // Define available tool groups. diff --git a/src/utils/resolveToolProtocol.ts b/src/utils/resolveToolProtocol.ts index cc5705c90fe..74297541825 100644 --- a/src/utils/resolveToolProtocol.ts +++ b/src/utils/resolveToolProtocol.ts @@ -40,12 +40,12 @@ export function resolveToolProtocol( } // 2. User preference - second highest priority - if (_providerSettings.toolProtocol) { + if (_providerSettings?.toolProtocol) { return _providerSettings.toolProtocol } // 5. Final fallback - return _providerSettings.apiProvider === "zgsm" ? TOOL_PROTOCOL.XML : TOOL_PROTOCOL.NATIVE + return _providerSettings?.apiProvider === "zgsm" ? TOOL_PROTOCOL.XML : TOOL_PROTOCOL.NATIVE } /** diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 6b914a622e3..ae13e801201 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -221,7 +221,7 @@ export const ChatRowContent = ({ const [editImages, setEditImages] = useState([]) const { copyWithFeedback } = useCopyToClipboard() const userEditRef = useRef(null) - + // Handle message events for image selection during edit mode useEffect(() => { const handleMessage = (event: MessageEvent) => { @@ -1281,14 +1281,18 @@ export const ChatRowContent = ({ style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}> ${Number(cost || 0)?.toFixed(4)}

- {!isApiRequestInProgress && clineMessages.findIndex((m) => m.ts === message.ts) > 1 && - { - e.preventDefault() - e.stopPropagation() - vscode.postMessage({ type: "deleteMessage", value: message.ts }) - }}/> - } + {!isApiRequestInProgress && clineMessages.findIndex((m) => m.ts === message.ts) > 1 && ( + + { + e.preventDefault() + e.stopPropagation() + vscode.postMessage({ type: "deleteMessage", value: message.ts }) + }} + /> + + )} {(selectReason || firstTokenLatency !== undefined || tokensPerSecond !== undefined) && (
diff --git a/webview-ui/src/components/settings/AutoCleanupSettings.tsx b/webview-ui/src/components/settings/AutoCleanupSettings.tsx index f6d2d8dfeda..ccb4ad308e1 100644 --- a/webview-ui/src/components/settings/AutoCleanupSettings.tsx +++ b/webview-ui/src/components/settings/AutoCleanupSettings.tsx @@ -3,7 +3,11 @@ import React, { useCallback } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { Trash2, Clock, ListFilter } from "lucide-react" -import { CleanupStrategy, DEFAULT_AUTO_CLEANUP_SETTINGS, AutoCleanupSettings as AutoCleanupSettingsType } from "@roo-code/types" +import { + CleanupStrategy, + DEFAULT_AUTO_CLEANUP_SETTINGS, + AutoCleanupSettings as AutoCleanupSettingsType, +} from "@roo-code/types" import { cn } from "@/lib/utils" import { Slider, Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, Input } from "@/components/ui" @@ -35,10 +39,13 @@ export const AutoCleanupSettings = ({ } = settings // 更新设置到 cachedState - const handleSettingsUpdate = useCallback((updatedSettings: Partial) => { - const newSettings = { ...settings, ...updatedSettings } - setCachedStateField("autoCleanup", newSettings) - }, [settings, setCachedStateField]) + const handleSettingsUpdate = useCallback( + (updatedSettings: Partial) => { + const newSettings = { ...settings, ...updatedSettings } + setCachedStateField("autoCleanup", newSettings) + }, + [settings, setCachedStateField], + ) return (
@@ -57,9 +64,7 @@ export const AutoCleanupSettings = ({ handleSettingsUpdate({ enabled: e.target.checked }) }} data-testid="auto-cleanup-enabled-checkbox"> - +
{t("settings:autoCleanup.enabled.description")} @@ -69,22 +74,24 @@ export const AutoCleanupSettings = ({ <> {/* Strategy Selection */}
- - {t("settings:autoCleanup.strategy.label")} - + {t("settings:autoCleanup.strategy.label")} onChange?.({ target: { checked: e.target.checked } })} /> + onChange?.({ target: { checked: e.target.checked } })} + /> {children} ), @@ -152,7 +158,9 @@ describe("AutoCleanupSettings", () => { setAutoCleanup: vi.fn(), } as any) - render() + render( + , + ) // Check for max history count input expect(screen.getByTestId("max-history-count-input")).toBeInTheDocument() diff --git a/webview-ui/src/components/welcome/RooTips.tsx b/webview-ui/src/components/welcome/RooTips.tsx index 60667a0f17a..3da01857ac8 100644 --- a/webview-ui/src/components/welcome/RooTips.tsx +++ b/webview-ui/src/components/welcome/RooTips.tsx @@ -180,7 +180,13 @@ const RooTips = () => { const isHalf = provider.layout === "half" return provider.type === "divider" ? ( - + ) : (
setState((prevState) => ({ ...prevState, autoCleanup: value })), filterErrorCorrectionMessages: state.filterErrorCorrectionMessages ?? false, - setFilterErrorCorrectionMessages: (value) => setState((prevState) => ({ ...prevState, filterErrorCorrectionMessages: value })), + setFilterErrorCorrectionMessages: (value) => + setState((prevState) => ({ ...prevState, filterErrorCorrectionMessages: value })), setCustomCondensingPrompt: (value) => setState((prevState) => ({ ...prevState, customCondensingPrompt: value })), setProfileThresholds: (value) => setState((prevState) => ({ ...prevState, profileThresholds: value })),