diff --git a/.roo/commands/release.md b/.roo/commands/release.md
index 707844cdc4e..2e09783a58e 100644
--- a/.roo/commands/release.md
+++ b/.roo/commands/release.md
@@ -1,6 +1,7 @@
---
description: "Create a new release of the Roo Code extension"
argument-hint: patch | minor | major
+mode: code
---
1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt`
diff --git a/src/__tests__/command-mentions.spec.ts b/src/__tests__/command-mentions.spec.ts
index 7ddaf3d0928..d309045dc90 100644
--- a/src/__tests__/command-mentions.spec.ts
+++ b/src/__tests__/command-mentions.spec.ts
@@ -27,7 +27,7 @@ describe("Command Mentions", () => {
// Helper function to call parseMentions with required parameters
const callParseMentions = async (text: string) => {
- return await parseMentions(
+ const result = await parseMentions(
text,
"/test/cwd", // cwd
mockUrlContentFetcher, // urlContentFetcher
@@ -38,6 +38,8 @@ describe("Command Mentions", () => {
50, // maxDiagnosticMessages
undefined, // maxReadFileLine
)
+ // Return just the text for backward compatibility with existing tests
+ return result.text
}
describe("parseMentions with command support", () => {
diff --git a/src/core/mentions/__tests__/index.spec.ts b/src/core/mentions/__tests__/index.spec.ts
index 2cb24b4502e..8f229c28b87 100644
--- a/src/core/mentions/__tests__/index.spec.ts
+++ b/src/core/mentions/__tests__/index.spec.ts
@@ -40,7 +40,7 @@ describe("parseMentions - URL error handling", () => {
expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching URL https://example.com:", timeoutError)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
- expect(result).toContain("Error fetching content: Navigation timeout of 30000 ms exceeded")
+ expect(result.text).toContain("Error fetching content: Navigation timeout of 30000 ms exceeded")
})
it("should handle DNS resolution errors", async () => {
@@ -50,7 +50,7 @@ describe("parseMentions - URL error handling", () => {
const result = await parseMentions("Check @https://nonexistent.example", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
- expect(result).toContain("Error fetching content: net::ERR_NAME_NOT_RESOLVED")
+ expect(result.text).toContain("Error fetching content: net::ERR_NAME_NOT_RESOLVED")
})
it("should handle network disconnection errors", async () => {
@@ -60,7 +60,7 @@ describe("parseMentions - URL error handling", () => {
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
- expect(result).toContain("Error fetching content: net::ERR_INTERNET_DISCONNECTED")
+ expect(result.text).toContain("Error fetching content: net::ERR_INTERNET_DISCONNECTED")
})
it("should handle 403 Forbidden errors", async () => {
@@ -70,7 +70,7 @@ describe("parseMentions - URL error handling", () => {
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
- expect(result).toContain("Error fetching content: 403 Forbidden")
+ expect(result.text).toContain("Error fetching content: 403 Forbidden")
})
it("should handle 404 Not Found errors", async () => {
@@ -80,7 +80,7 @@ describe("parseMentions - URL error handling", () => {
const result = await parseMentions("Check @https://example.com/missing", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
- expect(result).toContain("Error fetching content: 404 Not Found")
+ expect(result.text).toContain("Error fetching content: 404 Not Found")
})
it("should handle generic errors with fallback message", async () => {
@@ -90,7 +90,7 @@ describe("parseMentions - URL error handling", () => {
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
- expect(result).toContain("Error fetching content: Some unexpected error")
+ expect(result.text).toContain("Error fetching content: Some unexpected error")
})
it("should handle non-Error objects thrown", async () => {
@@ -100,7 +100,7 @@ describe("parseMentions - URL error handling", () => {
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
- expect(result).toContain("Error fetching content:")
+ expect(result.text).toContain("Error fetching content:")
})
it("should handle browser launch errors correctly", async () => {
@@ -112,7 +112,7 @@ describe("parseMentions - URL error handling", () => {
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Error fetching content for https://example.com: Failed to launch browser",
)
- expect(result).toContain("Error fetching content: Failed to launch browser")
+ expect(result.text).toContain("Error fetching content: Failed to launch browser")
// Should not attempt to fetch URL if browser launch failed
expect(mockUrlContentFetcher.urlToMarkdown).not.toHaveBeenCalled()
})
@@ -126,7 +126,7 @@ describe("parseMentions - URL error handling", () => {
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Error fetching content for https://example.com: String error",
)
- expect(result).toContain("Error fetching content: String error")
+ expect(result.text).toContain("Error fetching content: String error")
})
it("should successfully fetch URL content when no errors occur", async () => {
@@ -135,9 +135,9 @@ describe("parseMentions - URL error handling", () => {
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
- expect(result).toContain('')
- expect(result).toContain("# Example Content\n\nThis is the content.")
- expect(result).toContain("")
+ expect(result.text).toContain('')
+ expect(result.text).toContain("# Example Content\n\nThis is the content.")
+ expect(result.text).toContain("")
})
it("should handle multiple URLs with mixed success and failure", async () => {
@@ -151,9 +151,9 @@ describe("parseMentions - URL error handling", () => {
mockUrlContentFetcher,
)
- expect(result).toContain('')
- expect(result).toContain("# First Site")
- expect(result).toContain('')
- expect(result).toContain("Error fetching content: timeout")
+ expect(result.text).toContain('')
+ expect(result.text).toContain("# First Site")
+ expect(result.text).toContain('')
+ expect(result.text).toContain("Error fetching content: timeout")
})
})
diff --git a/src/core/mentions/__tests__/processUserContentMentions.spec.ts b/src/core/mentions/__tests__/processUserContentMentions.spec.ts
index 13c225042d7..ec2e08f92ae 100644
--- a/src/core/mentions/__tests__/processUserContentMentions.spec.ts
+++ b/src/core/mentions/__tests__/processUserContentMentions.spec.ts
@@ -22,8 +22,11 @@ describe("processUserContentMentions", () => {
mockFileContextTracker = {} as FileContextTracker
mockRooIgnoreController = {}
- // Default mock implementation
- vi.mocked(parseMentions).mockImplementation(async (text) => `parsed: ${text}`)
+ // Default mock implementation - returns ParseMentionsResult object
+ vi.mocked(parseMentions).mockImplementation(async (text) => ({
+ text: `parsed: ${text}`,
+ mode: undefined,
+ }))
})
describe("maxReadFileLine parameter", () => {
@@ -134,10 +137,11 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalled()
- expect(result[0]).toEqual({
+ expect(result.content[0]).toEqual({
type: "text",
text: "parsed: Do something",
})
+ expect(result.mode).toBeUndefined()
})
it("should process text blocks with tags", async () => {
@@ -156,10 +160,11 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalled()
- expect(result[0]).toEqual({
+ expect(result.content[0]).toEqual({
type: "text",
text: "parsed: Fix this issue",
})
+ expect(result.mode).toBeUndefined()
})
it("should not process text blocks without task or feedback tags", async () => {
@@ -178,7 +183,8 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).not.toHaveBeenCalled()
- expect(result[0]).toEqual(userContent[0])
+ expect(result.content[0]).toEqual(userContent[0])
+ expect(result.mode).toBeUndefined()
})
it("should process tool_result blocks with string content", async () => {
@@ -198,11 +204,12 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalled()
- expect(result[0]).toEqual({
+ expect(result.content[0]).toEqual({
type: "tool_result",
tool_use_id: "123",
content: "parsed: Tool feedback",
})
+ expect(result.mode).toBeUndefined()
})
it("should process tool_result blocks with array content", async () => {
@@ -231,7 +238,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledTimes(1)
- expect(result[0]).toEqual({
+ expect(result.content[0]).toEqual({
type: "tool_result",
tool_use_id: "123",
content: [
@@ -245,6 +252,7 @@ describe("processUserContentMentions", () => {
},
],
})
+ expect(result.mode).toBeUndefined()
})
it("should handle mixed content types", async () => {
@@ -277,17 +285,18 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledTimes(2)
- expect(result).toHaveLength(3)
- expect(result[0]).toEqual({
+ expect(result.content).toHaveLength(3)
+ expect(result.content[0]).toEqual({
type: "text",
text: "parsed: First task",
})
- expect(result[1]).toEqual(userContent[1]) // Image block unchanged
- expect(result[2]).toEqual({
+ expect(result.content[1]).toEqual(userContent[1]) // Image block unchanged
+ expect(result.content[2]).toEqual({
type: "tool_result",
tool_use_id: "456",
content: "parsed: Feedback",
})
+ expect(result.mode).toBeUndefined()
})
})
diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts
index f038b5b7836..9ee7cece5d0 100644
--- a/src/core/mentions/index.ts
+++ b/src/core/mentions/index.ts
@@ -71,6 +71,11 @@ export async function openMention(cwd: string, mention?: string): Promise
}
}
+export interface ParseMentionsResult {
+ text: string
+ mode?: string // Mode from the first slash command that has one
+}
+
export async function parseMentions(
text: string,
cwd: string,
@@ -81,9 +86,10 @@ export async function parseMentions(
includeDiagnosticMessages: boolean = true,
maxDiagnosticMessages: number = 50,
maxReadFileLine?: number,
-): Promise {
+): Promise {
const mentions: Set = new Set()
const validCommands: Map = new Map()
+ let commandMode: string | undefined // Track mode from the first slash command that has one
// First pass: check which command mentions exist and cache the results
const commandMatches = Array.from(text.matchAll(commandRegexGlobal))
@@ -101,10 +107,14 @@ export async function parseMentions(
}),
)
- // Store valid commands for later use
+ // Store valid commands for later use and capture the first mode found
for (const { commandName, command } of commandExistenceChecks) {
if (command) {
validCommands.set(commandName, command)
+ // Capture the mode from the first command that has one
+ if (!commandMode && command.mode) {
+ commandMode = command.mode
+ }
}
}
@@ -257,7 +267,7 @@ export async function parseMentions(
}
}
- return parsedText
+ return { text: parsedText, mode: commandMode }
}
async function getFileOrFolderContent(
@@ -410,3 +420,4 @@ export async function getLatestTerminalOutput(): Promise {
// Export processUserContentMentions from its own file
export { processUserContentMentions } from "./processUserContentMentions"
+export type { ProcessUserContentMentionsResult } from "./processUserContentMentions"
diff --git a/src/core/mentions/processUserContentMentions.ts b/src/core/mentions/processUserContentMentions.ts
index 4bdb422d48b..5ea78f4dc30 100644
--- a/src/core/mentions/processUserContentMentions.ts
+++ b/src/core/mentions/processUserContentMentions.ts
@@ -1,8 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
-import { parseMentions } from "./index"
+import { parseMentions, ParseMentionsResult } from "./index"
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
+export interface ProcessUserContentMentionsResult {
+ content: Anthropic.Messages.ContentBlockParam[]
+ mode?: string // Mode from the first slash command that has one
+}
+
/**
* Process mentions in user content, specifically within task and feedback tags
*/
@@ -26,7 +31,10 @@ export async function processUserContentMentions({
includeDiagnosticMessages?: boolean
maxDiagnosticMessages?: number
maxReadFileLine?: number
-}) {
+}): Promise {
+ // Track the first mode found from slash commands
+ let commandMode: string | undefined
+
// Process userContent array, which contains various block types:
// TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam.
// We need to apply parseMentions() to:
@@ -37,7 +45,7 @@ export async function processUserContentMentions({
// (see askFollowupQuestion), we place all user generated content in
// these tags so they can effectively be used as markers for when we
// should parse mentions).
- return Promise.all(
+ const content = await Promise.all(
userContent.map(async (block) => {
const shouldProcessMentions = (text: string) =>
text.includes("") ||
@@ -47,10 +55,33 @@ export async function processUserContentMentions({
if (block.type === "text") {
if (shouldProcessMentions(block.text)) {
+ const result = await parseMentions(
+ block.text,
+ cwd,
+ urlContentFetcher,
+ fileContextTracker,
+ rooIgnoreController,
+ showRooIgnoredFiles,
+ includeDiagnosticMessages,
+ maxDiagnosticMessages,
+ maxReadFileLine,
+ )
+ // Capture the first mode found
+ if (!commandMode && result.mode) {
+ commandMode = result.mode
+ }
return {
...block,
- text: await parseMentions(
- block.text,
+ text: result.text,
+ }
+ }
+
+ return block
+ } else if (block.type === "tool_result") {
+ if (typeof block.content === "string") {
+ if (shouldProcessMentions(block.content)) {
+ const result = await parseMentions(
+ block.content,
cwd,
urlContentFetcher,
fileContextTracker,
@@ -59,27 +90,14 @@ export async function processUserContentMentions({
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine,
- ),
- }
- }
-
- return block
- } else if (block.type === "tool_result") {
- if (typeof block.content === "string") {
- if (shouldProcessMentions(block.content)) {
+ )
+ // Capture the first mode found
+ if (!commandMode && result.mode) {
+ commandMode = result.mode
+ }
return {
...block,
- content: await parseMentions(
- block.content,
- cwd,
- urlContentFetcher,
- fileContextTracker,
- rooIgnoreController,
- showRooIgnoredFiles,
- includeDiagnosticMessages,
- maxDiagnosticMessages,
- maxReadFileLine,
- ),
+ content: result.text,
}
}
@@ -88,19 +106,24 @@ export async function processUserContentMentions({
const parsedContent = await Promise.all(
block.content.map(async (contentBlock) => {
if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) {
+ const result = await parseMentions(
+ contentBlock.text,
+ cwd,
+ urlContentFetcher,
+ fileContextTracker,
+ rooIgnoreController,
+ showRooIgnoredFiles,
+ includeDiagnosticMessages,
+ maxDiagnosticMessages,
+ maxReadFileLine,
+ )
+ // Capture the first mode found
+ if (!commandMode && result.mode) {
+ commandMode = result.mode
+ }
return {
...contentBlock,
- text: await parseMentions(
- contentBlock.text,
- cwd,
- urlContentFetcher,
- fileContextTracker,
- rooIgnoreController,
- showRooIgnoredFiles,
- includeDiagnosticMessages,
- maxDiagnosticMessages,
- maxReadFileLine,
- ),
+ text: result.text,
}
}
@@ -117,4 +140,6 @@ export async function processUserContentMentions({
return block
}),
)
+
+ return { content, mode: commandMode }
}
diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts
index d721438589e..1bdbb55ba94 100644
--- a/src/core/task/Task.ts
+++ b/src/core/task/Task.ts
@@ -2357,7 +2357,7 @@ export class Task extends EventEmitter implements TaskLike {
maxReadFileLine = -1,
} = (await this.providerRef.deref()?.getState()) ?? {}
- const parsedUserContent = await processUserContentMentions({
+ const { content: parsedUserContent, mode: slashCommandMode } = await processUserContentMentions({
userContent: currentUserContent,
cwd: this.cwd,
urlContentFetcher: this.urlContentFetcher,
@@ -2369,6 +2369,18 @@ export class Task extends EventEmitter implements TaskLike {
maxReadFileLine,
})
+ // Switch mode if specified in a slash command's frontmatter
+ if (slashCommandMode) {
+ const provider = this.providerRef.deref()
+ if (provider) {
+ const state = await provider.getState()
+ const targetMode = getModeBySlug(slashCommandMode, state?.customModes)
+ if (targetMode) {
+ await provider.handleModeSwitch(slashCommandMode)
+ }
+ }
+ }
+
const environmentDetails = await getEnvironmentDetails(this, currentIncludeFileDetails)
// Remove any existing environment_details blocks before adding fresh ones.
diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts
index 74ef1c6a78e..6ae4b1ecb7a 100644
--- a/src/core/task/__tests__/Task.spec.ts
+++ b/src/core/task/__tests__/Task.spec.ts
@@ -132,7 +132,7 @@ vi.mock("vscode", () => {
vi.mock("../../mentions", () => ({
parseMentions: vi.fn().mockImplementation((text) => {
- return Promise.resolve(`processed: ${text}`)
+ return Promise.resolve({ text: `processed: ${text}`, mode: undefined })
}),
openMention: vi.fn(),
getLatestTerminalOutput: vi.fn(),
@@ -913,7 +913,7 @@ describe("Cline", () => {
} as Anthropic.ToolResultBlockParam,
]
- const processedContent = await processUserContentMentions({
+ const { content: processedContent } = await processUserContentMentions({
userContent,
cwd: cline.cwd,
urlContentFetcher: cline.urlContentFetcher,
diff --git a/src/core/tools/RunSlashCommandTool.ts b/src/core/tools/RunSlashCommandTool.ts
index 8af8bd6e12a..69cb9dde95b 100644
--- a/src/core/tools/RunSlashCommandTool.ts
+++ b/src/core/tools/RunSlashCommandTool.ts
@@ -4,6 +4,7 @@ import { getCommand, getCommandNames } from "../../services/command/commands"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { BaseTool, ToolCallbacks } from "./BaseTool"
import type { ToolUse } from "../../shared/tools"
+import { getModeBySlug } from "../../shared/modes"
interface RunSlashCommandParams {
command: string
@@ -74,6 +75,7 @@ export class RunSlashCommandTool extends BaseTool<"run_slash_command"> {
args: args,
source: command.source,
description: command.description,
+ mode: command.mode,
})
const didApprove = await askApproval("tool", toolMessage)
@@ -82,6 +84,15 @@ export class RunSlashCommandTool extends BaseTool<"run_slash_command"> {
return
}
+ // Switch mode if specified in the command frontmatter
+ if (command.mode) {
+ const provider = task.providerRef.deref()
+ const targetMode = getModeBySlug(command.mode, (await provider?.getState())?.customModes)
+ if (targetMode) {
+ await provider?.handleModeSwitch(command.mode)
+ }
+ }
+
// Build the result message
let result = `Command: /${commandName}`
@@ -93,6 +104,10 @@ export class RunSlashCommandTool extends BaseTool<"run_slash_command"> {
result += `\nArgument hint: ${command.argumentHint}`
}
+ if (command.mode) {
+ result += `\nMode: ${command.mode}`
+ }
+
if (args) {
result += `\nProvided arguments: ${args}`
}
diff --git a/src/core/tools/__tests__/runSlashCommandTool.spec.ts b/src/core/tools/__tests__/runSlashCommandTool.spec.ts
index e3c8180e381..eef6259deb5 100644
--- a/src/core/tools/__tests__/runSlashCommandTool.spec.ts
+++ b/src/core/tools/__tests__/runSlashCommandTool.spec.ts
@@ -307,4 +307,133 @@ Deploy application to production`,
expect(mockTask.consecutiveMistakeCount).toBe(0)
})
+
+ it("should switch mode when mode is specified in command", async () => {
+ const mockHandleModeSwitch = vi.fn()
+ const block: ToolUse<"run_slash_command"> = {
+ type: "tool_use" as const,
+ name: "run_slash_command" as const,
+ params: {
+ command: "debug-app",
+ },
+ partial: false,
+ }
+
+ const mockCommand = {
+ name: "debug-app",
+ content: "Start debugging the application",
+ source: "project" as const,
+ filePath: ".roo/commands/debug-app.md",
+ description: "Debug the application",
+ mode: "debug",
+ }
+
+ mockTask.providerRef.deref = vi.fn().mockReturnValue({
+ getState: vi.fn().mockResolvedValue({
+ experiments: {
+ runSlashCommand: true,
+ },
+ customModes: undefined,
+ }),
+ handleModeSwitch: mockHandleModeSwitch,
+ })
+
+ vi.mocked(getCommand).mockResolvedValue(mockCommand)
+
+ await runSlashCommandTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockHandleModeSwitch).toHaveBeenCalledWith("debug")
+ expect(mockCallbacks.pushToolResult).toHaveBeenCalledWith(
+ `Command: /debug-app
+Description: Debug the application
+Mode: debug
+Source: project
+
+--- Command Content ---
+
+Start debugging the application`,
+ )
+ })
+
+ it("should not switch mode when mode is not specified in command", async () => {
+ const mockHandleModeSwitch = vi.fn()
+ const block: ToolUse<"run_slash_command"> = {
+ type: "tool_use" as const,
+ name: "run_slash_command" as const,
+ params: {
+ command: "test",
+ },
+ partial: false,
+ }
+
+ const mockCommand = {
+ name: "test",
+ content: "Run tests",
+ source: "project" as const,
+ filePath: ".roo/commands/test.md",
+ description: "Run project tests",
+ }
+
+ mockTask.providerRef.deref = vi.fn().mockReturnValue({
+ getState: vi.fn().mockResolvedValue({
+ experiments: {
+ runSlashCommand: true,
+ },
+ customModes: undefined,
+ }),
+ handleModeSwitch: mockHandleModeSwitch,
+ })
+
+ vi.mocked(getCommand).mockResolvedValue(mockCommand)
+
+ await runSlashCommandTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockHandleModeSwitch).not.toHaveBeenCalled()
+ })
+
+ it("should include mode in askApproval message when mode is specified", async () => {
+ const block: ToolUse<"run_slash_command"> = {
+ type: "tool_use" as const,
+ name: "run_slash_command" as const,
+ params: {
+ command: "debug-app",
+ },
+ partial: false,
+ }
+
+ const mockCommand = {
+ name: "debug-app",
+ content: "Start debugging",
+ source: "project" as const,
+ filePath: ".roo/commands/debug-app.md",
+ description: "Debug the application",
+ mode: "debug",
+ }
+
+ mockTask.providerRef.deref = vi.fn().mockReturnValue({
+ getState: vi.fn().mockResolvedValue({
+ experiments: {
+ runSlashCommand: true,
+ },
+ customModes: undefined,
+ }),
+ handleModeSwitch: vi.fn(),
+ })
+
+ vi.mocked(getCommand).mockResolvedValue(mockCommand)
+
+ await runSlashCommandTool.handle(mockTask as Task, block, mockCallbacks)
+
+ expect(mockCallbacks.askApproval).toHaveBeenCalledWith(
+ "tool",
+ JSON.stringify({
+ tool: "runSlashCommand",
+ command: "debug-app",
+ args: undefined,
+ source: "project",
+ description: "Debug the application",
+ mode: "debug",
+ }),
+ )
+ })
})
diff --git a/src/services/command/__tests__/frontmatter-commands.spec.ts b/src/services/command/__tests__/frontmatter-commands.spec.ts
index 40acc8ae848..3f93b55f940 100644
--- a/src/services/command/__tests__/frontmatter-commands.spec.ts
+++ b/src/services/command/__tests__/frontmatter-commands.spec.ts
@@ -49,6 +49,7 @@ npm run build
filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"),
description: "Sets up the development environment",
argumentHint: undefined,
+ mode: undefined,
})
})
@@ -73,6 +74,7 @@ npm run build
filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"),
description: undefined,
argumentHint: undefined,
+ mode: undefined,
})
})
@@ -116,6 +118,7 @@ Command content here.`
filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"),
description: undefined,
argumentHint: undefined,
+ mode: undefined,
})
})
@@ -151,6 +154,7 @@ Global setup instructions.`
filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"),
description: "Project-specific setup",
argumentHint: undefined,
+ mode: undefined,
})
})
@@ -178,6 +182,7 @@ Global setup instructions.`
filePath: expect.stringContaining(path.join(".roo", "commands", "setup.md")),
description: "Global setup command",
argumentHint: undefined,
+ mode: undefined,
})
})
})
@@ -205,6 +210,7 @@ Create a new release.`
filePath: path.join("/test/cwd", ".roo", "commands", "release.md"),
description: "Create a new release of the Roo Code extension",
argumentHint: "patch | minor | major",
+ mode: undefined,
})
})
@@ -231,6 +237,7 @@ Deploy the application.`
filePath: path.join("/test/cwd", ".roo", "commands", "deploy.md"),
description: "Deploy application to environment",
argumentHint: "staging | production",
+ mode: undefined,
})
})
@@ -287,6 +294,77 @@ Test content.`
expect(result?.argumentHint).toBeUndefined()
})
+
+ it("should load command with mode from frontmatter", async () => {
+ const commandContent = `---
+description: Debug the application
+mode: debug
+---
+
+# Debug Command
+
+Start debugging.`
+
+ mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
+ mockFs.readFile = vi.fn().mockResolvedValue(commandContent)
+
+ const result = await getCommand("/test/cwd", "debug-app")
+
+ expect(result).toEqual({
+ name: "debug-app",
+ content: "# Debug Command\n\nStart debugging.",
+ source: "project",
+ filePath: path.join("/test/cwd", ".roo", "commands", "debug-app.md"),
+ description: "Debug the application",
+ argumentHint: undefined,
+ mode: "debug",
+ })
+ })
+
+ it("should handle empty mode in frontmatter", async () => {
+ const commandContent = `---
+description: Test command
+mode: ""
+---
+
+# Test Command
+
+Test content.`
+
+ mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
+ mockFs.readFile = vi.fn().mockResolvedValue(commandContent)
+
+ const result = await getCommand("/test/cwd", "test")
+
+ expect(result?.mode).toBeUndefined()
+ })
+
+ it("should handle command with description, argument-hint, and mode", async () => {
+ const commandContent = `---
+description: Deploy to environment
+argument-hint: staging | production
+mode: code
+---
+
+# Deploy Command
+
+Deploy the application.`
+
+ mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
+ mockFs.readFile = vi.fn().mockResolvedValue(commandContent)
+
+ const result = await getCommand("/test/cwd", "deploy")
+
+ expect(result).toEqual({
+ name: "deploy",
+ content: "# Deploy Command\n\nDeploy the application.",
+ source: "project",
+ filePath: path.join("/test/cwd", ".roo", "commands", "deploy.md"),
+ description: "Deploy to environment",
+ argumentHint: "staging | production",
+ mode: "code",
+ })
+ })
})
describe("getCommands with frontmatter", () => {
diff --git a/src/services/command/commands.ts b/src/services/command/commands.ts
index 6a5b4c6db1c..4e69558dc14 100644
--- a/src/services/command/commands.ts
+++ b/src/services/command/commands.ts
@@ -17,6 +17,7 @@ export interface Command {
filePath: string
description?: string
argumentHint?: string
+ mode?: string
}
/**
@@ -215,6 +216,7 @@ async function tryLoadCommand(
let parsed
let description: string | undefined
let argumentHint: string | undefined
+ let mode: string | undefined
let commandContent: string
try {
@@ -228,11 +230,13 @@ async function tryLoadCommand(
typeof parsed.data["argument-hint"] === "string" && parsed.data["argument-hint"].trim()
? parsed.data["argument-hint"].trim()
: undefined
+ mode = typeof parsed.data.mode === "string" && parsed.data.mode.trim() ? parsed.data.mode.trim() : undefined
commandContent = parsed.content.trim()
} catch {
// If frontmatter parsing fails, treat the entire content as command content
description = undefined
argumentHint = undefined
+ mode = undefined
commandContent = content.trim()
}
@@ -243,6 +247,7 @@ async function tryLoadCommand(
filePath: resolvedPath,
description,
argumentHint,
+ mode,
}
} catch {
// Directory doesn't exist or can't be read
@@ -296,6 +301,7 @@ async function scanCommandDirectory(
let parsed
let description: string | undefined
let argumentHint: string | undefined
+ let mode: string | undefined
let commandContent: string
try {
@@ -309,11 +315,16 @@ async function scanCommandDirectory(
typeof parsed.data["argument-hint"] === "string" && parsed.data["argument-hint"].trim()
? parsed.data["argument-hint"].trim()
: undefined
+ mode =
+ typeof parsed.data.mode === "string" && parsed.data.mode.trim()
+ ? parsed.data.mode.trim()
+ : undefined
commandContent = parsed.content.trim()
} catch {
// If frontmatter parsing fails, treat the entire content as command content
description = undefined
argumentHint = undefined
+ mode = undefined
commandContent = content.trim()
}
@@ -326,6 +337,7 @@ async function scanCommandDirectory(
filePath: resolvedPath,
description,
argumentHint,
+ mode,
})
}
} catch (error) {