Skip to content

Commit 13370a2

Browse files
roomote[bot]roomotemrubens
authored
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 <[email protected]> Co-authored-by: Matt Rubens <[email protected]>
1 parent 2eebf3c commit 13370a2

File tree

12 files changed

+363
-69
lines changed

12 files changed

+363
-69
lines changed

.roo/commands/release.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
description: "Create a new release of the Roo Code extension"
33
argument-hint: patch | minor | major
4+
mode: code
45
---
56

67
1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt`

src/__tests__/command-mentions.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe("Command Mentions", () => {
2727

2828
// Helper function to call parseMentions with required parameters
2929
const callParseMentions = async (text: string) => {
30-
return await parseMentions(
30+
const result = await parseMentions(
3131
text,
3232
"/test/cwd", // cwd
3333
mockUrlContentFetcher, // urlContentFetcher
@@ -38,6 +38,8 @@ describe("Command Mentions", () => {
3838
50, // maxDiagnosticMessages
3939
undefined, // maxReadFileLine
4040
)
41+
// Return just the text for backward compatibility with existing tests
42+
return result.text
4143
}
4244

4345
describe("parseMentions with command support", () => {

src/core/mentions/__tests__/index.spec.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe("parseMentions - URL error handling", () => {
4040

4141
expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching URL https://example.com:", timeoutError)
4242
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
43-
expect(result).toContain("Error fetching content: Navigation timeout of 30000 ms exceeded")
43+
expect(result.text).toContain("Error fetching content: Navigation timeout of 30000 ms exceeded")
4444
})
4545

4646
it("should handle DNS resolution errors", async () => {
@@ -50,7 +50,7 @@ describe("parseMentions - URL error handling", () => {
5050
const result = await parseMentions("Check @https://nonexistent.example", "/test", mockUrlContentFetcher)
5151

5252
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
53-
expect(result).toContain("Error fetching content: net::ERR_NAME_NOT_RESOLVED")
53+
expect(result.text).toContain("Error fetching content: net::ERR_NAME_NOT_RESOLVED")
5454
})
5555

5656
it("should handle network disconnection errors", async () => {
@@ -60,7 +60,7 @@ describe("parseMentions - URL error handling", () => {
6060
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
6161

6262
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
63-
expect(result).toContain("Error fetching content: net::ERR_INTERNET_DISCONNECTED")
63+
expect(result.text).toContain("Error fetching content: net::ERR_INTERNET_DISCONNECTED")
6464
})
6565

6666
it("should handle 403 Forbidden errors", async () => {
@@ -70,7 +70,7 @@ describe("parseMentions - URL error handling", () => {
7070
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
7171

7272
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
73-
expect(result).toContain("Error fetching content: 403 Forbidden")
73+
expect(result.text).toContain("Error fetching content: 403 Forbidden")
7474
})
7575

7676
it("should handle 404 Not Found errors", async () => {
@@ -80,7 +80,7 @@ describe("parseMentions - URL error handling", () => {
8080
const result = await parseMentions("Check @https://example.com/missing", "/test", mockUrlContentFetcher)
8181

8282
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
83-
expect(result).toContain("Error fetching content: 404 Not Found")
83+
expect(result.text).toContain("Error fetching content: 404 Not Found")
8484
})
8585

8686
it("should handle generic errors with fallback message", async () => {
@@ -90,7 +90,7 @@ describe("parseMentions - URL error handling", () => {
9090
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
9191

9292
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
93-
expect(result).toContain("Error fetching content: Some unexpected error")
93+
expect(result.text).toContain("Error fetching content: Some unexpected error")
9494
})
9595

9696
it("should handle non-Error objects thrown", async () => {
@@ -100,7 +100,7 @@ describe("parseMentions - URL error handling", () => {
100100
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
101101

102102
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url")
103-
expect(result).toContain("Error fetching content:")
103+
expect(result.text).toContain("Error fetching content:")
104104
})
105105

106106
it("should handle browser launch errors correctly", async () => {
@@ -112,7 +112,7 @@ describe("parseMentions - URL error handling", () => {
112112
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
113113
"Error fetching content for https://example.com: Failed to launch browser",
114114
)
115-
expect(result).toContain("Error fetching content: Failed to launch browser")
115+
expect(result.text).toContain("Error fetching content: Failed to launch browser")
116116
// Should not attempt to fetch URL if browser launch failed
117117
expect(mockUrlContentFetcher.urlToMarkdown).not.toHaveBeenCalled()
118118
})
@@ -126,7 +126,7 @@ describe("parseMentions - URL error handling", () => {
126126
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
127127
"Error fetching content for https://example.com: String error",
128128
)
129-
expect(result).toContain("Error fetching content: String error")
129+
expect(result.text).toContain("Error fetching content: String error")
130130
})
131131

132132
it("should successfully fetch URL content when no errors occur", async () => {
@@ -135,9 +135,9 @@ describe("parseMentions - URL error handling", () => {
135135
const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher)
136136

137137
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
138-
expect(result).toContain('<url_content url="https://example.com">')
139-
expect(result).toContain("# Example Content\n\nThis is the content.")
140-
expect(result).toContain("</url_content>")
138+
expect(result.text).toContain('<url_content url="https://example.com">')
139+
expect(result.text).toContain("# Example Content\n\nThis is the content.")
140+
expect(result.text).toContain("</url_content>")
141141
})
142142

143143
it("should handle multiple URLs with mixed success and failure", async () => {
@@ -151,9 +151,9 @@ describe("parseMentions - URL error handling", () => {
151151
mockUrlContentFetcher,
152152
)
153153

154-
expect(result).toContain('<url_content url="https://example1.com">')
155-
expect(result).toContain("# First Site")
156-
expect(result).toContain('<url_content url="https://example2.com">')
157-
expect(result).toContain("Error fetching content: timeout")
154+
expect(result.text).toContain('<url_content url="https://example1.com">')
155+
expect(result.text).toContain("# First Site")
156+
expect(result.text).toContain('<url_content url="https://example2.com">')
157+
expect(result.text).toContain("Error fetching content: timeout")
158158
})
159159
})

src/core/mentions/__tests__/processUserContentMentions.spec.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ describe("processUserContentMentions", () => {
2222
mockFileContextTracker = {} as FileContextTracker
2323
mockRooIgnoreController = {}
2424

25-
// Default mock implementation
26-
vi.mocked(parseMentions).mockImplementation(async (text) => `parsed: ${text}`)
25+
// Default mock implementation - returns ParseMentionsResult object
26+
vi.mocked(parseMentions).mockImplementation(async (text) => ({
27+
text: `parsed: ${text}`,
28+
mode: undefined,
29+
}))
2730
})
2831

2932
describe("maxReadFileLine parameter", () => {
@@ -134,10 +137,11 @@ describe("processUserContentMentions", () => {
134137
})
135138

136139
expect(parseMentions).toHaveBeenCalled()
137-
expect(result[0]).toEqual({
140+
expect(result.content[0]).toEqual({
138141
type: "text",
139142
text: "parsed: <task>Do something</task>",
140143
})
144+
expect(result.mode).toBeUndefined()
141145
})
142146

143147
it("should process text blocks with <feedback> tags", async () => {
@@ -156,10 +160,11 @@ describe("processUserContentMentions", () => {
156160
})
157161

158162
expect(parseMentions).toHaveBeenCalled()
159-
expect(result[0]).toEqual({
163+
expect(result.content[0]).toEqual({
160164
type: "text",
161165
text: "parsed: <feedback>Fix this issue</feedback>",
162166
})
167+
expect(result.mode).toBeUndefined()
163168
})
164169

165170
it("should not process text blocks without task or feedback tags", async () => {
@@ -178,7 +183,8 @@ describe("processUserContentMentions", () => {
178183
})
179184

180185
expect(parseMentions).not.toHaveBeenCalled()
181-
expect(result[0]).toEqual(userContent[0])
186+
expect(result.content[0]).toEqual(userContent[0])
187+
expect(result.mode).toBeUndefined()
182188
})
183189

184190
it("should process tool_result blocks with string content", async () => {
@@ -198,11 +204,12 @@ describe("processUserContentMentions", () => {
198204
})
199205

200206
expect(parseMentions).toHaveBeenCalled()
201-
expect(result[0]).toEqual({
207+
expect(result.content[0]).toEqual({
202208
type: "tool_result",
203209
tool_use_id: "123",
204210
content: "parsed: <feedback>Tool feedback</feedback>",
205211
})
212+
expect(result.mode).toBeUndefined()
206213
})
207214

208215
it("should process tool_result blocks with array content", async () => {
@@ -231,7 +238,7 @@ describe("processUserContentMentions", () => {
231238
})
232239

233240
expect(parseMentions).toHaveBeenCalledTimes(1)
234-
expect(result[0]).toEqual({
241+
expect(result.content[0]).toEqual({
235242
type: "tool_result",
236243
tool_use_id: "123",
237244
content: [
@@ -245,6 +252,7 @@ describe("processUserContentMentions", () => {
245252
},
246253
],
247254
})
255+
expect(result.mode).toBeUndefined()
248256
})
249257

250258
it("should handle mixed content types", async () => {
@@ -277,17 +285,18 @@ describe("processUserContentMentions", () => {
277285
})
278286

279287
expect(parseMentions).toHaveBeenCalledTimes(2)
280-
expect(result).toHaveLength(3)
281-
expect(result[0]).toEqual({
288+
expect(result.content).toHaveLength(3)
289+
expect(result.content[0]).toEqual({
282290
type: "text",
283291
text: "parsed: <task>First task</task>",
284292
})
285-
expect(result[1]).toEqual(userContent[1]) // Image block unchanged
286-
expect(result[2]).toEqual({
293+
expect(result.content[1]).toEqual(userContent[1]) // Image block unchanged
294+
expect(result.content[2]).toEqual({
287295
type: "tool_result",
288296
tool_use_id: "456",
289297
content: "parsed: <feedback>Feedback</feedback>",
290298
})
299+
expect(result.mode).toBeUndefined()
291300
})
292301
})
293302

src/core/mentions/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ export async function openMention(cwd: string, mention?: string): Promise<void>
7171
}
7272
}
7373

74+
export interface ParseMentionsResult {
75+
text: string
76+
mode?: string // Mode from the first slash command that has one
77+
}
78+
7479
export async function parseMentions(
7580
text: string,
7681
cwd: string,
@@ -81,9 +86,10 @@ export async function parseMentions(
8186
includeDiagnosticMessages: boolean = true,
8287
maxDiagnosticMessages: number = 50,
8388
maxReadFileLine?: number,
84-
): Promise<string> {
89+
): Promise<ParseMentionsResult> {
8590
const mentions: Set<string> = new Set()
8691
const validCommands: Map<string, Command> = new Map()
92+
let commandMode: string | undefined // Track mode from the first slash command that has one
8793

8894
// First pass: check which command mentions exist and cache the results
8995
const commandMatches = Array.from(text.matchAll(commandRegexGlobal))
@@ -101,10 +107,14 @@ export async function parseMentions(
101107
}),
102108
)
103109

104-
// Store valid commands for later use
110+
// Store valid commands for later use and capture the first mode found
105111
for (const { commandName, command } of commandExistenceChecks) {
106112
if (command) {
107113
validCommands.set(commandName, command)
114+
// Capture the mode from the first command that has one
115+
if (!commandMode && command.mode) {
116+
commandMode = command.mode
117+
}
108118
}
109119
}
110120

@@ -257,7 +267,7 @@ export async function parseMentions(
257267
}
258268
}
259269

260-
return parsedText
270+
return { text: parsedText, mode: commandMode }
261271
}
262272

263273
async function getFileOrFolderContent(
@@ -410,3 +420,4 @@ export async function getLatestTerminalOutput(): Promise<string> {
410420

411421
// Export processUserContentMentions from its own file
412422
export { processUserContentMentions } from "./processUserContentMentions"
423+
export type { ProcessUserContentMentionsResult } from "./processUserContentMentions"

0 commit comments

Comments
 (0)