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 &&