mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add optional mode field to slash command front matter (#10344)
* feat: add optional mode field to slash command front matter - Add mode field to Command interface - Update command parsing to extract mode from frontmatter - Modify RunSlashCommandTool to automatically switch mode when specified - Add comprehensive tests for mode field parsing and switching - Update existing tests to include mode field * Make it work for manual slash commands too --------- Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
parent
2eebf3c2c4
commit
13370a2ad1
12 changed files with 363 additions and 69 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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('<url_content url="https://example.com">')
|
||||
expect(result).toContain("# Example Content\n\nThis is the content.")
|
||||
expect(result).toContain("</url_content>")
|
||||
expect(result.text).toContain('<url_content url="https://example.com">')
|
||||
expect(result.text).toContain("# Example Content\n\nThis is the content.")
|
||||
expect(result.text).toContain("</url_content>")
|
||||
})
|
||||
|
||||
it("should handle multiple URLs with mixed success and failure", async () => {
|
||||
|
|
@ -151,9 +151,9 @@ describe("parseMentions - URL error handling", () => {
|
|||
mockUrlContentFetcher,
|
||||
)
|
||||
|
||||
expect(result).toContain('<url_content url="https://example1.com">')
|
||||
expect(result).toContain("# First Site")
|
||||
expect(result).toContain('<url_content url="https://example2.com">')
|
||||
expect(result).toContain("Error fetching content: timeout")
|
||||
expect(result.text).toContain('<url_content url="https://example1.com">')
|
||||
expect(result.text).toContain("# First Site")
|
||||
expect(result.text).toContain('<url_content url="https://example2.com">')
|
||||
expect(result.text).toContain("Error fetching content: timeout")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: <task>Do something</task>",
|
||||
})
|
||||
expect(result.mode).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should process text blocks with <feedback> tags", async () => {
|
||||
|
|
@ -156,10 +160,11 @@ describe("processUserContentMentions", () => {
|
|||
})
|
||||
|
||||
expect(parseMentions).toHaveBeenCalled()
|
||||
expect(result[0]).toEqual({
|
||||
expect(result.content[0]).toEqual({
|
||||
type: "text",
|
||||
text: "parsed: <feedback>Fix this issue</feedback>",
|
||||
})
|
||||
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: <feedback>Tool feedback</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: <task>First task</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>Feedback</feedback>",
|
||||
})
|
||||
expect(result.mode).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ export async function openMention(cwd: string, mention?: string): Promise<void>
|
|||
}
|
||||
}
|
||||
|
||||
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<string> {
|
||||
): Promise<ParseMentionsResult> {
|
||||
const mentions: Set<string> = new Set()
|
||||
const validCommands: Map<string, Command> = 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<string> {
|
|||
|
||||
// Export processUserContentMentions from its own file
|
||||
export { processUserContentMentions } from "./processUserContentMentions"
|
||||
export type { ProcessUserContentMentionsResult } from "./processUserContentMentions"
|
||||
|
|
|
|||
|
|
@ -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<ProcessUserContentMentionsResult> {
|
||||
// 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("<task>") ||
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2357,7 +2357,7 @@ export class Task extends EventEmitter<TaskEvents> 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<TaskEvents> 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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue