-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat: add hidden submodes functionality for complex Roo modes #9447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # Example .roomodes file demonstrating hidden submodes feature | ||
| # Hidden modes are not shown in the mode selector dropdown but can be accessed | ||
| # programmatically by their parent mode using the switch_mode tool | ||
|
|
||
| customModes: | ||
| # Parent mode that can delegate to specialized submodes | ||
| - slug: complex-task-handler | ||
| name: 🎯 Complex Task Handler | ||
| roleDefinition: |- | ||
| You are a complex task handler that breaks down large tasks into specialized subtasks. | ||
| You can delegate specific work to hidden specialized submodes. | ||
| whenToUse: Use this mode for complex tasks that require multiple specialized approaches | ||
| description: Handles complex multi-step tasks | ||
| groups: | ||
| - read | ||
| - edit | ||
| - command | ||
| customInstructions: |- | ||
| When handling complex tasks: | ||
| 1. Analyze the task requirements | ||
| 2. Identify specialized subtasks | ||
| 3. Use switch_mode to delegate to appropriate hidden submodes: | ||
| - data-analyzer: For data analysis tasks | ||
| - code-generator: For code generation tasks | ||
| - test-writer: For test writing tasks | ||
| 4. Coordinate results from submodes | ||
| 5. Provide comprehensive solution | ||
|
|
||
| # Hidden submode for data analysis (only accessible from complex-task-handler) | ||
| - slug: data-analyzer | ||
| name: 📊 Data Analyzer | ||
| roleDefinition: Specialized mode for analyzing data structures and patterns | ||
| description: Analyzes data and provides insights | ||
| groups: | ||
| - read | ||
| hidden: true | ||
| parent: complex-task-handler | ||
| customInstructions: |- | ||
| Focus exclusively on data analysis: | ||
| - Examine data structures | ||
| - Identify patterns | ||
| - Generate insights | ||
| - Return findings to parent mode | ||
|
|
||
| # Hidden submode for code generation (only accessible from complex-task-handler) | ||
| - slug: code-generator | ||
| name: ⚙️ Code Generator | ||
| roleDefinition: Specialized mode for generating optimized code | ||
| description: Generates code based on specifications | ||
| groups: | ||
| - read | ||
| - edit | ||
| hidden: true | ||
| parent: complex-task-handler | ||
| customInstructions: |- | ||
| Focus exclusively on code generation: | ||
| - Generate clean, efficient code | ||
| - Follow best practices | ||
| - Add appropriate comments | ||
| - Return to parent mode when complete | ||
|
|
||
| # Hidden submode for test writing (only accessible from complex-task-handler) | ||
| - slug: test-writer | ||
| name: 🧪 Test Writer | ||
| roleDefinition: Specialized mode for writing comprehensive tests | ||
| description: Writes unit and integration tests | ||
| groups: | ||
| - read | ||
| - edit | ||
| hidden: true | ||
| parent: complex-task-handler | ||
| customInstructions: |- | ||
| Focus exclusively on test creation: | ||
| - Write comprehensive test cases | ||
| - Cover edge cases | ||
| - Ensure good test coverage | ||
| - Return to parent mode when complete | ||
|
|
||
| # Regular visible mode (shown in dropdown) | ||
| - slug: documentation-mode | ||
| name: 📚 Documentation | ||
| roleDefinition: Mode for creating and updating documentation | ||
| description: Creates and maintains documentation | ||
| groups: | ||
| - read | ||
| - edit | ||
| customInstructions: |- | ||
| Focus on documentation tasks: | ||
| - Write clear, comprehensive docs | ||
| - Update existing documentation | ||
| - Create examples and tutorials |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,8 +35,11 @@ export class SwitchModeTool extends BaseTool<"switch_mode"> { | |
|
|
||
| task.consecutiveMistakeCount = 0 | ||
|
|
||
| // Verify the mode exists | ||
| const targetMode = getModeBySlug(mode_slug, (await task.providerRef.deref()?.getState())?.customModes) | ||
| const state = await task.providerRef.deref()?.getState() | ||
| const customModes = state?.customModes | ||
|
|
||
| // Verify the mode exists (including hidden modes) | ||
| const targetMode = getModeBySlug(mode_slug, customModes) | ||
|
|
||
| if (!targetMode) { | ||
| task.recordToolError("switch_mode") | ||
|
|
@@ -45,7 +48,22 @@ export class SwitchModeTool extends BaseTool<"switch_mode"> { | |
| } | ||
|
|
||
| // Check if already in requested mode | ||
| const currentMode = (await task.providerRef.deref()?.getState())?.mode ?? defaultModeSlug | ||
| const currentMode = state?.mode ?? defaultModeSlug | ||
| const currentModeConfig = getModeBySlug(currentMode, customModes) | ||
|
|
||
| // Check if the target mode is hidden | ||
| if (targetMode.hidden) { | ||
| // Hidden modes can only be accessed by their parent mode | ||
| if (!targetMode.parent || targetMode.parent !== currentMode) { | ||
| task.recordToolError("switch_mode") | ||
| pushToolResult( | ||
| formatResponse.toolError( | ||
| `Mode '${mode_slug}' is not accessible from the current mode '${currentMode}'.`, | ||
| ), | ||
| ) | ||
| return | ||
| } | ||
| } | ||
|
Comment on lines
+54
to
+66
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code checks if Consider adding validation to ensure the parent mode exists: if (targetMode.hidden) {
if (!targetMode.parent) {
// error: hidden mode must have parent
}
const parentMode = getModeBySlug(targetMode.parent, customModes)
if (!parentMode) {
// error: parent mode does not exist
}
if (targetMode.parent !== currentMode) {
// error: can only be accessed by parent
}
}Fix it with Roo Code or mention @roomote and request a fix. |
||
|
|
||
| if (currentMode === mode_slug) { | ||
| task.recordToolError("switch_mode") | ||
|
|
@@ -64,7 +82,7 @@ export class SwitchModeTool extends BaseTool<"switch_mode"> { | |
| await task.providerRef.deref()?.handleModeSwitch(mode_slug) | ||
|
|
||
| pushToolResult( | ||
| `Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${ | ||
| `Successfully switched from ${currentModeConfig?.name ?? currentMode} mode to ${ | ||
| targetMode.name | ||
| } mode${reason ? ` because: ${reason}` : ""}.`, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import { describe, it, expect } from "vitest" | ||
| import { type ModeConfig } from "@roo-code/types" | ||
| import { getAllModes, getModeBySlug } from "../modes" | ||
|
|
||
| describe("Hidden Modes", () => { | ||
| const customModes: ModeConfig[] = [ | ||
| { | ||
| slug: "parent-mode", | ||
| name: "Parent Mode", | ||
| roleDefinition: "Parent mode role", | ||
| groups: ["read", "edit"], | ||
| }, | ||
| { | ||
| slug: "hidden-submode", | ||
| name: "Hidden Submode", | ||
| roleDefinition: "Hidden submode role", | ||
| groups: ["read"], | ||
| hidden: true, | ||
| parent: "parent-mode", | ||
| }, | ||
| { | ||
| slug: "visible-mode", | ||
| name: "Visible Mode", | ||
| roleDefinition: "Visible mode role", | ||
| groups: ["read"], | ||
| }, | ||
| ] | ||
|
|
||
| describe("getAllModes", () => { | ||
| it("should exclude hidden modes by default", () => { | ||
| const modes = getAllModes(customModes) | ||
| const slugs = modes.map((m) => m.slug) | ||
|
|
||
| expect(slugs).toContain("parent-mode") | ||
| expect(slugs).toContain("visible-mode") | ||
| expect(slugs).not.toContain("hidden-submode") | ||
| }) | ||
|
|
||
| it("should include hidden modes when includeHidden is true", () => { | ||
| const modes = getAllModes(customModes, true) | ||
| const slugs = modes.map((m) => m.slug) | ||
|
|
||
| expect(slugs).toContain("parent-mode") | ||
| expect(slugs).toContain("visible-mode") | ||
| expect(slugs).toContain("hidden-submode") | ||
| }) | ||
|
|
||
| it("should return all built-in modes when no custom modes provided", () => { | ||
| const modes = getAllModes() | ||
| expect(modes.length).toBeGreaterThan(0) | ||
| expect(modes.every((m) => !m.hidden)).toBe(true) | ||
| }) | ||
|
|
||
| it("should override built-in modes with custom modes of same slug", () => { | ||
| const customWithOverride: ModeConfig[] = [ | ||
| { | ||
| slug: "code", | ||
| name: "Custom Code Mode", | ||
| roleDefinition: "Custom code role", | ||
| groups: ["read"], | ||
| }, | ||
| ] | ||
|
|
||
| const modes = getAllModes(customWithOverride) | ||
| const codeMode = modes.find((m) => m.slug === "code") | ||
|
|
||
| expect(codeMode?.name).toBe("Custom Code Mode") | ||
| }) | ||
| }) | ||
|
|
||
| describe("getModeBySlug", () => { | ||
| it("should find hidden modes", () => { | ||
| const mode = getModeBySlug("hidden-submode", customModes) | ||
| expect(mode).toBeDefined() | ||
| expect(mode?.hidden).toBe(true) | ||
| expect(mode?.parent).toBe("parent-mode") | ||
| }) | ||
|
|
||
| it("should find visible modes", () => { | ||
| const mode = getModeBySlug("parent-mode", customModes) | ||
| expect(mode).toBeDefined() | ||
| expect(mode?.hidden).toBeUndefined() | ||
| }) | ||
|
|
||
| it("should return undefined for non-existent mode", () => { | ||
| const mode = getModeBySlug("non-existent", customModes) | ||
| expect(mode).toBeUndefined() | ||
| }) | ||
| }) | ||
|
|
||
| describe("Hidden mode parent-child relationships", () => { | ||
| it("should correctly identify parent-child relationships", () => { | ||
| const hiddenMode = getModeBySlug("hidden-submode", customModes) | ||
| const parentMode = getModeBySlug("parent-mode", customModes) | ||
|
|
||
| expect(hiddenMode?.parent).toBe(parentMode?.slug) | ||
| }) | ||
|
|
||
| it("should allow multiple hidden modes with same parent", () => { | ||
| const modesWithMultipleChildren: ModeConfig[] = [ | ||
| ...customModes, | ||
| { | ||
| slug: "hidden-submode-2", | ||
| name: "Hidden Submode 2", | ||
| roleDefinition: "Hidden submode 2 role", | ||
| groups: ["read"], | ||
| hidden: true, | ||
| parent: "parent-mode", | ||
| }, | ||
| ] | ||
|
|
||
| const modes = getAllModes(modesWithMultipleChildren, true) | ||
| const hiddenModes = modes.filter((m) => m.hidden && m.parent === "parent-mode") | ||
|
|
||
| expect(hiddenModes.length).toBe(2) | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The schema allows
hidden: truewithout requiringparent, which would create modes that are completely inaccessible. If a mode is hidden but has no parent, there's no way to switch to it. The schema should enforce that ifhiddenis true, thenparentmust be provided.Consider adding a
.refine()validation like:Fix it with Roo Code or mention @roomote and request a fix.