diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-environment-filtering.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-environment-filtering.spec.ts new file mode 100644 index 0000000000..a80a0e3211 --- /dev/null +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-environment-filtering.spec.ts @@ -0,0 +1,313 @@ +// npx vitest src/core/assistant-message/__tests__/presentAssistantMessage-environment-filtering.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(), +})) +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + captureToolUsage: vi.fn(), + captureConsecutiveMistakeError: vi.fn(), + }, + }, +})) + +describe("presentAssistantMessage - Environment Details Filtering", () => { + let mockTask: any + + beforeEach(() => { + // 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, + api: { + getModel: () => ({ id: "xai/grok-code-fast-1", info: {} }), + }, + browserSession: { + closeBrowser: vi.fn().mockResolvedValue(undefined), + }, + recordToolUsage: vi.fn(), + toolRepetitionDetector: { + check: vi.fn().mockReturnValue({ allowExecution: true }), + }, + providerRef: { + deref: () => ({ + getState: vi.fn().mockResolvedValue({ + mode: "code", + customModes: [], + }), + }), + }, + say: vi.fn().mockResolvedValue(undefined), + ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), + } + }) + + it("should filter out complete tags", async () => { + const contentWithEnvironmentDetails = `Here is my response. + +# VSCode Visible Files +src/test.ts +# Current Time +2025-11-20T03:00:00Z + +This is the actual content the user should see.` + + mockTask.assistantMessageContent = [ + { + type: "text", + content: contentWithEnvironmentDetails, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask) + + // Check that say was called with filtered content + expect(mockTask.say).toHaveBeenCalledWith( + "text", + expect.stringContaining("Here is my response."), + undefined, + false, + ) + expect(mockTask.say).toHaveBeenCalledWith( + "text", + expect.stringContaining("This is the actual content the user should see."), + undefined, + false, + ) + expect(mockTask.say).toHaveBeenCalledWith( + "text", + expect.not.stringContaining("environment_details"), + undefined, + false, + ) + expect(mockTask.say).toHaveBeenCalledWith( + "text", + expect.not.stringContaining("VSCode Visible Files"), + undefined, + false, + ) + }) + + it("should filter out partial tag at the end", async () => { + const contentWithPartialTag = `Here is my response to your query. +The task has been completed successfully. + tag", async () => { + const contentWithClosingTag = `Some content here. + +The actual response continues here.` + + mockTask.assistantMessageContent = [ + { + type: "text", + content: contentWithClosingTag, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask) + + // Check that say was called without the closing tag + expect(mockTask.say).toHaveBeenCalledWith( + "text", + expect.not.stringContaining(""), + undefined, + false, + ) + expect(mockTask.say).toHaveBeenCalledWith( + "text", + expect.stringContaining("The actual response continues here."), + undefined, + false, + ) + }) + + it("should filter out multiple environment_details blocks", async () => { + const contentWithMultipleBlocks = `First part of response. + +First block of system info + +Middle content. + +Second block of system info + +Final part of response.` + + mockTask.assistantMessageContent = [ + { + type: "text", + content: contentWithMultipleBlocks, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask) + + // Check that all environment_details blocks are removed + expect(mockTask.say).toHaveBeenCalledWith( + "text", + expect.stringContaining("First part of response."), + undefined, + false, + ) + expect(mockTask.say).toHaveBeenCalledWith("text", expect.stringContaining("Middle content."), undefined, false) + expect(mockTask.say).toHaveBeenCalledWith( + "text", + expect.stringContaining("Final part of response."), + undefined, + false, + ) + expect(mockTask.say).toHaveBeenCalledWith( + "text", + expect.not.stringContaining("environment_details"), + undefined, + false, + ) + expect(mockTask.say).toHaveBeenCalledWith("text", expect.not.stringContaining("system info"), undefined, false) + }) + + it("should handle partial closing tag { + const contentWithPartialClosingTag = `Response text here. +Some more content. + { + const mixedContent = `Assistant: I'll help you with that task. + + +# System information +Current directory: /test + + +Let me analyze your code: +- First point +- Second point + + +More system info here + + +The solution is straightforward.` + + mockTask.assistantMessageContent = [ + { + type: "text", + content: mixedContent, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask) + + const calledContent = mockTask.say.mock.calls[0][1] + + // Check that normal content is preserved + expect(calledContent).toContain("I'll help you with that task") + expect(calledContent).toContain("Let me analyze your code:") + expect(calledContent).toContain("- First point") + expect(calledContent).toContain("- Second point") + expect(calledContent).toContain("The solution is straightforward") + + // Check that environment_details content is removed + expect(calledContent).not.toContain("environment_details") + expect(calledContent).not.toContain("System information") + expect(calledContent).not.toContain("Current directory") + expect(calledContent).not.toContain("More system info") + }) + + it("should handle environment_details tags with whitespace", async () => { + const contentWithWhitespace = `Response start. + +Content to be removed + +Response end.` + + mockTask.assistantMessageContent = [ + { + type: "text", + content: contentWithWhitespace, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask) + + const calledContent = mockTask.say.mock.calls[0][1] + + // Check that tags with whitespace are properly removed + expect(calledContent).toContain("Response start") + expect(calledContent).toContain("Response end") + expect(calledContent).not.toContain("environment_details") + expect(calledContent).not.toContain("Content to be removed") + }) +}) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index fcf7d25cc9..4fac6ebe40 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -123,8 +123,15 @@ export async function presentAssistantMessage(cline: Task) { content = content.replace(/\s?/g, "") content = content.replace(/\s?<\/thinking>/g, "") + // Remove all instances of ... blocks + // This prevents internal system messages from being exposed in model responses + // Use regex to match the entire block including content between tags + content = content.replace(/[\s\S]*?<\/environment_details>/g, "") + // Also remove orphaned closing tags that might appear without opening tags + content = content.replace(/\s?<\/environment_details>/g, "") + // Remove partial XML tag at the very end of the content (for - // tool use and thinking tags), Prevents scrollview from + // tool use, thinking tags, and environment_details tags), Prevents scrollview from // jumping when tags are automatically removed. const lastOpenBracketIndex = content.lastIndexOf("<") @@ -132,7 +139,7 @@ export async function presentAssistantMessage(cline: Task) { const possibleTag = content.slice(lastOpenBracketIndex) // Check if there's a '>' after the last '<' (i.e., if the - // tag is complete) (complete thinking and tool tags will + // tag is complete) (complete thinking, environment_details, and tool tags will // have been removed by now.) const hasCloseBracket = possibleTag.includes(">") @@ -150,14 +157,20 @@ export async function presentAssistantMessage(cline: Task) { // (letters and underscores only). const isLikelyTagName = /^[a-zA-Z_]+$/.test(tagContent) + // Check if it's a partial environment_details tag + const isPartialEnvironmentDetails = + "environment_details".startsWith(tagContent.toLowerCase()) || + (tagContent.toLowerCase().startsWith("e") && + "environment_details".includes(tagContent.toLowerCase())) + // Preemptively remove < or