Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/free-toes-hammer.md
Original file line number Diff line number Diff line change
@@ -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.
191 changes: 191 additions & 0 deletions src/core/mentions/__tests__/processKiloUserContentMentions.spec.ts
Original file line number Diff line number Diff line change
@@ -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 <user_message> and slash command", async () => {
const userContent = [
{
type: "tool_result" as const,
tool_use_id: "call_123",
content: "<user_message>/just-do-this-workflow.md</user_message>",
},
]

const [result] = await processKiloUserContentMentions({
...defaultParams,
userContent,
})

// Verify parseKiloSlashCommands was called with the text after parseMentions
expect(parseKiloSlashCommands).toHaveBeenCalledWith(
"parsed: <user_message>/just-do-this-workflow.md</user_message>",
{},
{},
)

// The content should be the processed result from parseKiloSlashCommands
expect(result[0]).toEqual({
type: "tool_result",
tool_use_id: "call_123",
content: "parsed: <user_message>/just-do-this-workflow.md</user_message>",
})
})

it("should call parseKiloSlashCommands for tool_result with array content containing <user_message> and slash command", async () => {
const userContent = [
{
type: "tool_result" as const,
tool_use_id: "call_456",
content: [
{
type: "text" as const,
text: "<user_message>/newtask</user_message>",
},
],
},
]

const [result] = await processKiloUserContentMentions({
...defaultParams,
userContent,
})

// Verify parseKiloSlashCommands was called
expect(parseKiloSlashCommands).toHaveBeenCalledWith("parsed: <user_message>/newtask</user_message>", {}, {})

// 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: <user_message>/newtask</user_message>",
},
],
})
})

it("should handle slash command transformation in tool_result", async () => {
// Mock parseKiloSlashCommands to actually transform the slash command
vi.mocked(parseKiloSlashCommands).mockResolvedValue({
processedText:
'<explicit_instructions type="new_task">\n</explicit_instructions>\n<user_message></user_message>',
needsRulesFileCheck: false,
})

const userContent = [
{
type: "tool_result" as const,
tool_use_id: "call_789",
content: "<user_message>/newtask</user_message>",
},
]

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:
'<explicit_instructions type="new_task">\n</explicit_instructions>\n<user_message></user_message>',
})
})

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()
})
})
})
89 changes: 51 additions & 38 deletions src/core/mentions/processKiloUserContentMentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -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,
}
}

Expand All @@ -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,
}
}

Expand Down
Loading