From 0c0c27ac502244e459ca61812c160b5b1e66eee9 Mon Sep 17 00:00:00 2001 From: Daniel Dengler Date: Sun, 8 Feb 2026 15:00:05 +0100 Subject: [PATCH 1/2] fix(mentions): process slash commands in tool_result blocks Previously, parseKiloSlashCommands was only called for text blocks, causing slash commands in tool_result blocks to be ignored. This fix extends the processing to tool_result blocks by using the new processTextContent helper function that combines parseMentions and parseKiloSlashCommands. The regression test ensures that slash commands in tool responses are properly processed and transformed. --- .../processKiloUserContentMentions.spec.ts | 191 ++++++++++++++++++ .../processKiloUserContentMentions.ts | 89 ++++---- 2 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 src/core/mentions/__tests__/processKiloUserContentMentions.spec.ts diff --git a/src/core/mentions/__tests__/processKiloUserContentMentions.spec.ts b/src/core/mentions/__tests__/processKiloUserContentMentions.spec.ts new file mode 100644 index 00000000000..593102d7430 --- /dev/null +++ b/src/core/mentions/__tests__/processKiloUserContentMentions.spec.ts @@ -0,0 +1,191 @@ +// Regression test for slash command processing in tool_result blocks +// This test will FAIL with the current implementation because parseKiloSlashCommands +// is not called for tool_result blocks. It should pass after the bug is fixed. + +// **The Bug:** +// In `src/core/mentions/processKiloUserContentMentions.ts`, the function calls `parseKiloSlashCommands` for `text` blocks but NOT for `tool_result` blocks. This means when a user answers with a slash command to a tool like "ask for feedback", the command is ignored. + +// **Expected Behavior:** +// When the tests pass, the bug will be fixed and slash commands in tool responses will be properly processed. + +import { processKiloUserContentMentions } from "../processKiloUserContentMentions" +import { parseMentions } from "../index" +import { parseKiloSlashCommands } from "../../slash-commands/kilo" +import { refreshWorkflowToggles } from "../../context/instructions/workflows" +import { ensureLocalKilorulesDirExists } from "../../context/instructions/kilo-rules" +import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher" +import { FileContextTracker } from "../../context-tracking/FileContextTracker" +import * as vscode from "vscode" + +// Mock dependencies +vi.mock("../index", () => ({ + parseMentions: vi.fn(), +})) + +vi.mock("../../slash-commands/kilo", () => ({ + parseKiloSlashCommands: vi.fn(), +})) + +vi.mock("../../context/instructions/workflows", () => ({ + refreshWorkflowToggles: vi.fn(), +})) + +vi.mock("../../context/instructions/kilo-rules", () => ({ + ensureLocalKilorulesDirExists: vi.fn(), +})) + +describe("processKiloUserContentMentions - slash command regression", () => { + const mockContext = {} as vscode.ExtensionContext + const mockUrlContentFetcher = {} as UrlContentFetcher + const mockFileContextTracker = {} as FileContextTracker + const mockRooIgnoreController = {} as any + + beforeEach(() => { + vi.clearAllMocks() + + // Default mocks + vi.mocked(parseMentions).mockImplementation(async (text) => ({ + text: `parsed: ${text}`, + mode: undefined, + })) + + vi.mocked(parseKiloSlashCommands).mockImplementation(async (text, localToggles, globalToggles) => ({ + processedText: text, + needsRulesFileCheck: false, + })) + + vi.mocked(refreshWorkflowToggles).mockResolvedValue({ + localWorkflowToggles: {}, + globalWorkflowToggles: {}, + }) + + vi.mocked(ensureLocalKilorulesDirExists).mockResolvedValue(false) + }) + + const defaultParams = { + context: mockContext, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + rooIgnoreController: mockRooIgnoreController, + showRooIgnoredFiles: false, + includeDiagnosticMessages: true, + maxDiagnosticMessages: 50, + } + + describe("slash command processing in tool_result blocks", () => { + it("should call parseKiloSlashCommands for tool_result with string content containing and slash command", async () => { + const userContent = [ + { + type: "tool_result" as const, + tool_use_id: "call_123", + content: "/just-do-this-workflow.md", + }, + ] + + const [result] = await processKiloUserContentMentions({ + ...defaultParams, + userContent, + }) + + // Verify parseKiloSlashCommands was called with the text after parseMentions + expect(parseKiloSlashCommands).toHaveBeenCalledWith( + "parsed: /just-do-this-workflow.md", + {}, + {}, + ) + + // The content should be the processed result from parseKiloSlashCommands + expect(result[0]).toEqual({ + type: "tool_result", + tool_use_id: "call_123", + content: "parsed: /just-do-this-workflow.md", + }) + }) + + it("should call parseKiloSlashCommands for tool_result with array content containing and slash command", async () => { + const userContent = [ + { + type: "tool_result" as const, + tool_use_id: "call_456", + content: [ + { + type: "text" as const, + text: "/newtask", + }, + ], + }, + ] + + const [result] = await processKiloUserContentMentions({ + ...defaultParams, + userContent, + }) + + // Verify parseKiloSlashCommands was called + expect(parseKiloSlashCommands).toHaveBeenCalledWith("parsed: /newtask", {}, {}) + + // The content array should have the processed text + expect(result[0]).toEqual({ + type: "tool_result", + tool_use_id: "call_456", + content: [ + { + type: "text", + text: "parsed: /newtask", + }, + ], + }) + }) + + it("should handle slash command transformation in tool_result", async () => { + // Mock parseKiloSlashCommands to actually transform the slash command + vi.mocked(parseKiloSlashCommands).mockResolvedValue({ + processedText: + '\n\n', + needsRulesFileCheck: false, + }) + + const userContent = [ + { + type: "tool_result" as const, + tool_use_id: "call_789", + content: "/newtask", + }, + ] + + const [result] = await processKiloUserContentMentions({ + ...defaultParams, + userContent, + }) + + // The content should be the transformed text + expect(result[0]).toEqual({ + type: "tool_result", + tool_use_id: "call_789", + content: + '\n\n', + }) + }) + + it("should not call parseKiloSlashCommands for tool_result without mention tags", async () => { + const userContent = [ + { + type: "tool_result" as const, + tool_use_id: "call_999", + content: "Just regular feedback without special tags", + }, + ] + + await processKiloUserContentMentions({ + ...defaultParams, + userContent, + }) + + // parseMentions should not be called because no mention tags + expect(parseMentions).not.toHaveBeenCalled() + // parseKiloSlashCommands should not be called + expect(parseKiloSlashCommands).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/core/mentions/processKiloUserContentMentions.ts b/src/core/mentions/processKiloUserContentMentions.ts index 16fff0e10e6..07b0df514c2 100644 --- a/src/core/mentions/processKiloUserContentMentions.ts +++ b/src/core/mentions/processKiloUserContentMentions.ts @@ -9,6 +9,7 @@ import { parseKiloSlashCommands } from "../slash-commands/kilo" import { refreshWorkflowToggles } from "../context/instructions/workflows" // kilocode_change import * as vscode from "vscode" // kilocode_change +import { ClineRulesToggles } from "../../shared/cline-rules" // This function is a duplicate of processUserContentMentions, but it adds a check for the newrules command // and processes Kilo-specific slash commands. It should be merged with processUserContentMentions in the future. @@ -41,6 +42,35 @@ export async function processKiloUserContentMentions({ // kilocode_change const mentionTagRegex = /<(?:task|feedback|answer|user_message)>/ + // Helper function to process text through parseMentions and parseKiloSlashCommands + // Returns the processed text and whether a kilorules check is needed + const processTextContent = async ( + text: string, + localWorkflowToggles: ClineRulesToggles, + globalWorkflowToggles: ClineRulesToggles, + ): Promise<{ processedText: string; needsRulesFileCheck: boolean }> => { + const parsedText = await parseMentions( + text, + cwd, + urlContentFetcher, + fileContextTracker, + rooIgnoreController, + showRooIgnoredFiles, + includeDiagnosticMessages, + maxDiagnosticMessages, + maxReadFileLine, + ) + + // when parsing slash commands, we still want to allow the user to provide their desired context + const { processedText, needsRulesFileCheck: needsCheck } = await parseKiloSlashCommands( + parsedText.text, + localWorkflowToggles, + globalWorkflowToggles, + ) + + return { processedText, needsRulesFileCheck: needsCheck } + } + const processUserContentMentions = async () => { // Process userContent array, which contains various block types: // TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam. @@ -62,24 +92,10 @@ export async function processKiloUserContentMentions({ if (block.type === "text") { if (shouldProcessMentions(block.text)) { - // kilocode_change begin: pull slash commands from Cline - const parsedText = await parseMentions( + const { processedText, needsRulesFileCheck: needsCheck } = await processTextContent( block.text, - cwd, - urlContentFetcher, - fileContextTracker, - rooIgnoreController, - showRooIgnoredFiles, - includeDiagnosticMessages, - maxDiagnosticMessages, - maxReadFileLine, - ) - - // when parsing slash commands, we still want to allow the user to provide their desired context - const { processedText, needsRulesFileCheck: needsCheck } = await parseKiloSlashCommands( - parsedText.text, - localWorkflowToggles, // kilocode_change - globalWorkflowToggles, // kilocode_change + localWorkflowToggles, + globalWorkflowToggles, ) if (needsCheck) { @@ -90,27 +106,25 @@ export async function processKiloUserContentMentions({ ...block, text: processedText, } - // kilocode_change end } return block } else if (block.type === "tool_result") { if (typeof block.content === "string") { if (shouldProcessMentions(block.content)) { - const parsedResult = await parseMentions( + const { processedText, needsRulesFileCheck: needsCheck } = await processTextContent( block.content, - cwd, - urlContentFetcher, - fileContextTracker, - rooIgnoreController, - showRooIgnoredFiles, - includeDiagnosticMessages, - maxDiagnosticMessages, - maxReadFileLine, + localWorkflowToggles, + globalWorkflowToggles, ) + + if (needsCheck) { + needsRulesFileCheck = true + } + return { ...block, - content: parsedResult.text, + content: processedText, } } @@ -119,20 +133,19 @@ export async function processKiloUserContentMentions({ const parsedContent = await Promise.all( block.content.map(async (contentBlock) => { if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) { - const parsedResult = await parseMentions( + const { processedText, needsRulesFileCheck: needsCheck } = await processTextContent( contentBlock.text, - cwd, - urlContentFetcher, - fileContextTracker, - rooIgnoreController, - showRooIgnoredFiles, - includeDiagnosticMessages, - maxDiagnosticMessages, - maxReadFileLine, + localWorkflowToggles, + globalWorkflowToggles, ) + + if (needsCheck) { + needsRulesFileCheck = true + } + return { ...contentBlock, - text: parsedResult.text, + text: processedText, } } From 3db4b158e331dd9dc376d0eea74ed163db75c7b0 Mon Sep 17 00:00:00 2001 From: Daniel Dengler Date: Sun, 8 Feb 2026 15:07:54 +0100 Subject: [PATCH 2/2] chore(changeset): add changeset for tool_result slash command fix --- .changeset/free-toes-hammer.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/free-toes-hammer.md diff --git a/.changeset/free-toes-hammer.md b/.changeset/free-toes-hammer.md new file mode 100644 index 00000000000..1b960b8217d --- /dev/null +++ b/.changeset/free-toes-hammer.md @@ -0,0 +1,14 @@ +--- +"kilo-code": patch +--- + +fix(mentions): process slash commands in tool_result blocks + +Previously, parseKiloSlashCommands was only called for text blocks, +causing slash commands in tool_result blocks to be ignored. This fix +extends the processing to tool_result blocks by using the new +processTextContent helper function that combines parseMentions and +parseKiloSlashCommands. + +The regression test ensures that slash commands in tool responses are +properly processed and transformed.