diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage.spec.ts new file mode 100644 index 0000000000..02fd22af2a --- /dev/null +++ b/src/core/assistant-message/__tests__/presentAssistantMessage.spec.ts @@ -0,0 +1,271 @@ +// npx vitest src/core/assistant-message/__tests__/presentAssistantMessage.spec.ts + +describe("thinking tag filtering logic", () => { + // Test the thinking tag filtering logic directly without mocking the entire Task class + const filterThinkingTags = (content: string): string => { + if (!content) return content + + // This is the exact logic from presentAssistantMessage.ts + // Remove all instances of ... blocks completely + // while preserving content outside of thinking tags. + content = content.replace(/[\s\S]*?<\/thinking>/g, "") + + // Also handle partial thinking tags at the end of streaming content + // Remove incomplete thinking tags that might appear during streaming + content = content.replace(/\s*$/g, "") + content = content.replace(/\s*<\/thinking>\s*$/g, "") + + return content + } + + describe("thinking tag removal", () => { + it("should remove complete thinking tags and preserve content outside them", () => { + const messageWithThinking = `I'll analyze this step by step. + + +This is internal reasoning that should not be shown to the user. +Let me think about the best approach here. + + +Here's my analysis of the implementation plan: + +1. First, I need to understand the requirements +2. Then I'll create a solution + + +Another internal thought process. +This should also be hidden. + + +The final recommendation is to proceed with the implementation.` + + const expectedContent = `I'll analyze this step by step. + +Here's my analysis of the implementation plan: + +1. First, I need to understand the requirements +2. Then I'll create a solution + +The final recommendation is to proceed with the implementation.` + + const result = filterThinkingTags(messageWithThinking) + expect(result).toBe(expectedContent) + }) + + it("should handle partial thinking tags at the end of streaming content", () => { + const messageWithPartialThinking = `Here's my response. + + +This is a partial thinking tag that hasn't been closed yet because streaming is still in progress` + + const expectedContent = `Here's my response. + +` + + const result = filterThinkingTags(messageWithPartialThinking) + expect(result).toBe(expectedContent) + }) + + it("should handle incomplete closing thinking tags", () => { + const messageWithIncompleteClosing = `Here's my response. + + +Internal reasoning here. + { + const messageWithNestedXML = `I'll help you with that. + + +Let me think about this: + + 1. Understand the problem + 2. Design solution + +This analysis should be hidden. + + +Here's the solution I recommend.` + + const expectedContent = `I'll help you with that. + +Here's the solution I recommend.` + + const result = filterThinkingTags(messageWithNestedXML) + expect(result).toBe(expectedContent) + }) + + it("should handle multiple thinking blocks in sequence", () => { + const messageWithMultipleThinking = `Initial response. + + +First internal thought. + + + +Second internal thought. + + + +Third internal thought. + + +Final response.` + + const expectedContent = `Initial response. + +Final response.` + + const result = filterThinkingTags(messageWithMultipleThinking) + expect(result).toBe(expectedContent) + }) + + it("should preserve content when there are no thinking tags", () => { + const messageWithoutThinking = `This is a normal response without any thinking tags. + +It should be displayed exactly as written.` + + const result = filterThinkingTags(messageWithoutThinking) + expect(result).toBe(messageWithoutThinking) + }) + + it("should handle thinking tags with whitespace variations", () => { + const messageWithWhitespaceThinking = `Response start. + + + Indented thinking content. + + Multiple lines with spaces. + + +Response end.` + + const expectedContent = `Response start. + +Response end.` + + const result = filterThinkingTags(messageWithWhitespaceThinking) + expect(result).toBe(expectedContent) + }) + + it("should handle thinking tags mixed with other XML-like content", () => { + const messageWithThinkingAndXML = `I'll help you read that file. + + +The user wants me to read a file. I should use the read_file tool. + + +example.txt + + +Now I should analyze the content and provide a response. + + +The file has been read successfully.` + + const expectedTextContent = `I'll help you read that file. + +example.txt + +The file has been read successfully.` + + const result = filterThinkingTags(messageWithThinkingAndXML) + expect(result).toBe(expectedTextContent) + }) + }) + + describe("edge cases", () => { + it("should handle empty content", () => { + const emptyMessage = "" + const result = filterThinkingTags(emptyMessage) + expect(result).toBe("") + }) + + it("should handle content that is only thinking tags", () => { + const onlyThinkingMessage = ` +This entire message is just internal reasoning. +Nothing should be displayed to the user. +` + + const result = filterThinkingTags(onlyThinkingMessage) + expect(result).toBe("") + }) + + it("should handle malformed thinking tags", () => { + const malformedThinkingMessage = `Response with malformed tags. + + { + const messageWithSpecialChars = `Start of response. + + +This thinking block contains: +- Special characters: !@#$%^&*() +- Unicode: 🤔💭 +- Code snippets: const x = "test"; +- Multiple newlines + + +And more content. + + +End of response.` + + const expectedContent = `Start of response. + +End of response.` + + const result = filterThinkingTags(messageWithSpecialChars) + expect(result).toBe(expectedContent) + }) + + it("should handle adjacent thinking tags without content between them", () => { + const messageWithAdjacentThinking = `Response start. + + +First thought. + +Second thought immediately after. + + +Response end.` + + const expectedContent = `Response start. + +Response end.` + + const result = filterThinkingTags(messageWithAdjacentThinking) + expect(result).toBe(expectedContent) + }) + + it("should handle thinking tags at the very beginning and end", () => { + const messageWithBoundaryThinking = ` +Thinking at the start. +Middle content. +Thinking at the end. +` + + const expectedContent = `Middle content.` + + const result = filterThinkingTags(messageWithBoundaryThinking) + expect(result).toBe(expectedContent) + }) + }) +}) \ No newline at end of file diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index ee3fa148b4..5d1dde6bfe 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -98,13 +98,22 @@ export async function presentAssistantMessage(cline: Task) { // here for reference. // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?$/, "") // - // Remove all instances of (with optional line break - // after) and (with optional line break before). - // - Needs to be separate since we dont want to remove the line - // break before the first tag. - // - Needs to happen before the xml parsing below. - content = content.replace(/\s?/g, "") - content = content.replace(/\s?<\/thinking>/g, "") + // Remove all instances of ... blocks completely + // while preserving content outside of thinking tags. + // This regex matches the opening tag, any content (including newlines), + // and the closing tag, removing the entire block. + // Also handles extra whitespace to prevent double newlines. + content = content.replace(/[\s\S]*?<\/thinking>/g, "") + + // Handle partial thinking tags at the end of streaming content + // Remove incomplete thinking tags that might appear during streaming + content = content.replace(/[\s\S]*$/g, "") + + // Clean up any remaining partial closing tags + content = content.replace(/\s*<\/thinking>\s*$/g, "") + + // Clean up extra newlines that result from removing thinking blocks + content = content.replace(/\n\n\n+/g, "\n\n") // Remove partial XML tag at the very end of the content (for // tool use and thinking tags), Prevents scrollview from @@ -214,6 +223,8 @@ export async function presentAssistantMessage(cline: Task) { const modeName = getModeBySlug(mode, customModes)?.name ?? mode return `[${block.name} in ${modeName} mode: '${message}']` } + default: + return `[${block.name}]` } } diff --git a/test-thinking-filter.js b/test-thinking-filter.js new file mode 100644 index 0000000000..f0de208470 --- /dev/null +++ b/test-thinking-filter.js @@ -0,0 +1,142 @@ +// Simple test script to verify thinking tag filtering logic +const filterThinkingTags = (content) => { + if (!content) return content + + // This is the exact logic from presentAssistantMessage.ts + // Remove all instances of ... blocks completely + // while preserving content outside of thinking tags. + // Also handles extra whitespace to prevent double newlines. + content = content.replace(/[\s\S]*?<\/thinking>/g, "") + + // Handle partial thinking tags at the end of streaming content + // Remove incomplete thinking tags that might appear during streaming + content = content.replace(/[\s\S]*$/g, "") + + // Clean up any remaining partial closing tags + content = content.replace(/\s*<\/thinking>\s*$/g, "") + + // Clean up extra newlines that result from removing thinking blocks + content = content.replace(/\n\n\n+/g, "\n\n") + + return content +} + +// Test cases +console.log("Testing thinking tag filtering logic...\n") + +// Test 1: Complete thinking tags +const test1 = `I'll analyze this step by step. + + +This is internal reasoning that should not be shown to the user. +Let me think about the best approach here. + + +Here's my analysis of the implementation plan: + +1. First, I need to understand the requirements +2. Then I'll create a solution + + +Another internal thought process. +This should also be hidden. + + +The final recommendation is to proceed with the implementation.` + +const expected1 = `I'll analyze this step by step. + +Here's my analysis of the implementation plan: + +1. First, I need to understand the requirements +2. Then I'll create a solution + +The final recommendation is to proceed with the implementation.` + +const result1 = filterThinkingTags(test1) +console.log("Test 1 - Complete thinking tags:") +console.log("PASS:", result1 === expected1) +if (result1 !== expected1) { + console.log("Expected:", JSON.stringify(expected1)) + console.log("Got:", JSON.stringify(result1)) +} +console.log() + +// Test 2: Partial thinking tag +const test2 = `Here's my response. + + +This is a partial thinking tag that hasn't been closed yet because streaming is still in progress` + +const expected2 = `Here's my response. + +` + +const result2 = filterThinkingTags(test2) +console.log("Test 2 - Partial thinking tag:") +console.log("PASS:", result2 === expected2) +if (result2 !== expected2) { + console.log("Expected:", JSON.stringify(expected2)) + console.log("Got:", JSON.stringify(result2)) +} +console.log() + +// Test 3: Nested XML content +const test3 = `I'll help you with that. + + +Let me think about this: + + 1. Understand the problem + 2. Design solution + +This analysis should be hidden. + + +Here's the solution I recommend.` + +const expected3 = `I'll help you with that. + +Here's the solution I recommend.` + +const result3 = filterThinkingTags(test3) +console.log("Test 3 - Nested XML content:") +console.log("PASS:", result3 === expected3) +if (result3 !== expected3) { + console.log("Expected:", JSON.stringify(expected3)) + console.log("Got:", JSON.stringify(result3)) +} +console.log() + +// Test 4: No thinking tags +const test4 = `This is a normal response without any thinking tags. + +It should be displayed exactly as written.` + +const result4 = filterThinkingTags(test4) +console.log("Test 4 - No thinking tags:") +console.log("PASS:", result4 === test4) +if (result4 !== test4) { + console.log("Expected:", JSON.stringify(test4)) + console.log("Got:", JSON.stringify(result4)) +} +console.log() + +// Test 5: Only thinking tags +const test5 = ` +This entire message is just internal reasoning. +Nothing should be displayed to the user. +` + +const expected5 = `` + +const result5 = filterThinkingTags(test5) +console.log("Test 5 - Only thinking tags:") +console.log("PASS:", result5 === expected5) +if (result5 !== expected5) { + console.log("Expected:", JSON.stringify(expected5)) + console.log("Got:", JSON.stringify(result5)) +} +console.log() + +console.log("All tests completed!") \ No newline at end of file