Skip to content

Commit 16cf741

Browse files
committed
fix: process @/file references in slash command content
- Added parseMentions processing to runSlashCommandTool to expand file references - File references in command descriptions and bodies are now properly expanded - Added comprehensive tests for mention processing in slash commands - Handles errors gracefully by falling back to original content if processing fails Fixes #8602
1 parent 3a47c55 commit 16cf741

File tree

2 files changed

+181
-1
lines changed

2 files changed

+181
-1
lines changed

src/core/tools/__tests__/runSlashCommandTool.spec.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,22 @@ import { runSlashCommandTool } from "../runSlashCommandTool"
33
import { Task } from "../../task/Task"
44
import { formatResponse } from "../../prompts/responses"
55
import { getCommand, getCommandNames } from "../../../services/command/commands"
6+
import { parseMentions } from "../../mentions"
67

78
// Mock dependencies
89
vi.mock("../../../services/command/commands", () => ({
910
getCommand: vi.fn(),
1011
getCommandNames: vi.fn(),
1112
}))
1213

14+
vi.mock("../../mentions", () => ({
15+
parseMentions: vi.fn(),
16+
}))
17+
18+
vi.mock("../../../services/browser/UrlContentFetcher", () => ({
19+
UrlContentFetcher: vi.fn().mockImplementation(() => ({})),
20+
}))
21+
1322
describe("runSlashCommandTool", () => {
1423
let mockTask: any
1524
let mockAskApproval: any
@@ -20,6 +29,9 @@ describe("runSlashCommandTool", () => {
2029
beforeEach(() => {
2130
vi.clearAllMocks()
2231

32+
// By default, mock parseMentions to return the original content unchanged
33+
vi.mocked(parseMentions).mockImplementation((content) => Promise.resolve(content))
34+
2335
mockTask = {
2436
consecutiveMistakeCount: 0,
2537
recordToolError: vi.fn(),
@@ -377,4 +389,147 @@ Deploy application to production`,
377389

378390
expect(mockTask.consecutiveMistakeCount).toBe(0)
379391
})
392+
393+
it("should process mentions in command content", async () => {
394+
const mockCommand = {
395+
name: "test",
396+
content: "Check @/README.md for details",
397+
source: "project" as const,
398+
filePath: ".roo/commands/test.md",
399+
description: "Test command with file reference",
400+
}
401+
402+
vi.mocked(getCommand).mockResolvedValue(mockCommand)
403+
vi.mocked(parseMentions).mockResolvedValue(
404+
"Check 'README.md' (see below for file content)\n\n<file_content path=\"README.md\">\n# README\nTest content\n</file_content>",
405+
)
406+
407+
mockAskApproval.mockResolvedValue(true)
408+
409+
const block = {
410+
type: "tool_use" as const,
411+
name: "run_slash_command" as const,
412+
params: {
413+
command: "test",
414+
},
415+
partial: false,
416+
}
417+
418+
await runSlashCommandTool(
419+
mockTask as Task,
420+
block,
421+
mockAskApproval,
422+
mockHandleError,
423+
mockPushToolResult,
424+
mockRemoveClosingTag,
425+
)
426+
427+
// Verify parseMentions was called with the command content
428+
expect(vi.mocked(parseMentions)).toHaveBeenCalledWith(
429+
"Check @/README.md for details",
430+
"/test/project",
431+
expect.any(Object), // UrlContentFetcher instance
432+
undefined,
433+
undefined,
434+
false,
435+
true,
436+
50,
437+
undefined,
438+
)
439+
440+
// Verify the processed content is included in the result
441+
expect(mockPushToolResult).toHaveBeenCalledWith(
442+
expect.stringContaining("Check 'README.md' (see below for file content)"),
443+
)
444+
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining('<file_content path="README.md">'))
445+
})
446+
447+
it("should handle mention processing errors gracefully", async () => {
448+
const mockCommand = {
449+
name: "test",
450+
content: "Check @/README.md for details",
451+
source: "project" as const,
452+
filePath: ".roo/commands/test.md",
453+
description: "Test command with file reference",
454+
}
455+
456+
vi.mocked(getCommand).mockResolvedValue(mockCommand)
457+
vi.mocked(parseMentions).mockRejectedValue(new Error("Failed to process mentions"))
458+
459+
mockAskApproval.mockResolvedValue(true)
460+
461+
const block = {
462+
type: "tool_use" as const,
463+
name: "run_slash_command" as const,
464+
params: {
465+
command: "test",
466+
},
467+
partial: false,
468+
}
469+
470+
// Mock console.warn to verify it's called
471+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
472+
473+
await runSlashCommandTool(
474+
mockTask as Task,
475+
block,
476+
mockAskApproval,
477+
mockHandleError,
478+
mockPushToolResult,
479+
mockRemoveClosingTag,
480+
)
481+
482+
// Should log a warning when mention processing fails
483+
expect(consoleWarnSpy).toHaveBeenCalledWith(
484+
expect.stringContaining("Failed to process mentions in slash command content:"),
485+
)
486+
487+
// Should still return the original content when mention processing fails
488+
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("Check @/README.md for details"))
489+
490+
consoleWarnSpy.mockRestore()
491+
})
492+
493+
it("should process multiple file references in command content", async () => {
494+
const mockCommand = {
495+
name: "docs",
496+
content: "Review @/README.md and @/CONTRIBUTING.md for guidelines",
497+
source: "project" as const,
498+
filePath: ".roo/commands/docs.md",
499+
description: "Documentation command",
500+
}
501+
502+
vi.mocked(getCommand).mockResolvedValue(mockCommand)
503+
vi.mocked(parseMentions).mockResolvedValue(
504+
"Review 'README.md' (see below for file content) and 'CONTRIBUTING.md' (see below for file content)\n\n" +
505+
'<file_content path="README.md">\n# README\n</file_content>\n\n' +
506+
'<file_content path="CONTRIBUTING.md">\n# Contributing\n</file_content>',
507+
)
508+
509+
mockAskApproval.mockResolvedValue(true)
510+
511+
const block = {
512+
type: "tool_use" as const,
513+
name: "run_slash_command" as const,
514+
params: {
515+
command: "docs",
516+
},
517+
partial: false,
518+
}
519+
520+
await runSlashCommandTool(
521+
mockTask as Task,
522+
block,
523+
mockAskApproval,
524+
mockHandleError,
525+
mockPushToolResult,
526+
mockRemoveClosingTag,
527+
)
528+
529+
// Verify both files are included in the processed content
530+
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining('<file_content path="README.md">'))
531+
expect(mockPushToolResult).toHaveBeenCalledWith(
532+
expect.stringContaining('<file_content path="CONTRIBUTING.md">'),
533+
)
534+
})
380535
})

src/core/tools/runSlashCommandTool.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } f
33
import { formatResponse } from "../prompts/responses"
44
import { getCommand, getCommandNames } from "../../services/command/commands"
55
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
6+
import { parseMentions } from "../mentions"
7+
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
68

79
export async function runSlashCommandTool(
810
task: Task,
@@ -78,6 +80,29 @@ export async function runSlashCommandTool(
7880
return
7981
}
8082

83+
// Process mentions in the command content
84+
const provider = task.providerRef.deref()
85+
const urlContentFetcher = new UrlContentFetcher(provider!.context)
86+
let processedContent = command.content
87+
88+
try {
89+
// Process @/file references and other mentions in the command content
90+
processedContent = await parseMentions(
91+
command.content,
92+
task.cwd,
93+
urlContentFetcher,
94+
undefined, // fileContextTracker - not needed for slash commands
95+
undefined, // rooIgnoreController - will use default
96+
false, // showRooIgnoredFiles
97+
true, // includeDiagnosticMessages
98+
50, // maxDiagnosticMessages
99+
undefined, // maxReadFileLine
100+
)
101+
} catch (error) {
102+
// If mention processing fails, log the error but continue with original content
103+
console.warn(`Failed to process mentions in slash command content: ${error}`)
104+
}
105+
81106
// Build the result message
82107
let result = `Command: /${commandName}`
83108

@@ -94,7 +119,7 @@ export async function runSlashCommandTool(
94119
}
95120

96121
result += `\nSource: ${command.source}`
97-
result += `\n\n--- Command Content ---\n\n${command.content}`
122+
result += `\n\n--- Command Content ---\n\n${processedContent}`
98123

99124
// Return the command content as the tool result
100125
pushToolResult(result)

0 commit comments

Comments
 (0)