mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
feat: add hidden submodes functionality for complex Roo modes
- Add hidden and parent fields to ModeConfig type - Update getAllModes to support filtering hidden modes - Hide submodes from mode selector dropdowns - Add parent mode validation in switch_mode tool - Include hidden modes in settings view for management - Add comprehensive tests for hidden mode functionality - Create example .roomodes file demonstrating usage Closes #9446
This commit is contained in:
parent
1589cc1849
commit
4fc71f790b
9 changed files with 253 additions and 14 deletions
91
examples/hidden-submodes.roomodes
Normal file
91
examples/hidden-submodes.roomodes
Normal file
|
|
@ -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
|
||||
|
|
@ -70,6 +70,8 @@ export const modeConfigSchema = z.object({
|
|||
customInstructions: z.string().optional(),
|
||||
groups: groupEntryArraySchema,
|
||||
source: z.enum(["global", "project"]).optional(),
|
||||
hidden: z.boolean().optional(),
|
||||
parent: z.string().optional(),
|
||||
})
|
||||
|
||||
export type ModeConfig = z.infer<typeof modeConfigSchema>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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}` : ""}.`,
|
||||
)
|
||||
|
|
|
|||
118
src/shared/__tests__/modes.hidden.spec.ts
Normal file
118
src/shared/__tests__/modes.hidden.spec.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -86,7 +86,7 @@ export function getModeConfig(slug: string, customModes?: ModeConfig[]): ModeCon
|
|||
}
|
||||
|
||||
// Get all available modes, with custom modes overriding built-in modes
|
||||
export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] {
|
||||
export function getAllModes(customModes?: ModeConfig[], includeHidden: boolean = false): ModeConfig[] {
|
||||
if (!customModes?.length) {
|
||||
return [...modes]
|
||||
}
|
||||
|
|
@ -106,6 +106,11 @@ export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] {
|
|||
}
|
||||
})
|
||||
|
||||
// Filter out hidden modes unless explicitly requested
|
||||
if (!includeHidden) {
|
||||
return allModes.filter((mode) => !mode.hidden)
|
||||
}
|
||||
|
||||
return allModes
|
||||
}
|
||||
|
||||
|
|
@ -284,11 +289,14 @@ export const defaultPrompts: Readonly<CustomModePrompts> = Object.freeze(
|
|||
)
|
||||
|
||||
// Helper function to get all modes with their prompt overrides from extension state
|
||||
export async function getAllModesWithPrompts(context: vscode.ExtensionContext): Promise<ModeConfig[]> {
|
||||
export async function getAllModesWithPrompts(
|
||||
context: vscode.ExtensionContext,
|
||||
includeHidden: boolean = false,
|
||||
): Promise<ModeConfig[]> {
|
||||
const customModes = (await context.globalState.get<ModeConfig[]>("customModes")) || []
|
||||
const customModePrompts = (await context.globalState.get<CustomModePrompts>("customModePrompts")) || {}
|
||||
|
||||
const allModes = getAllModes(customModes)
|
||||
const allModes = getAllModes(customModes, includeHidden)
|
||||
return allModes.map((mode) => ({
|
||||
...mode,
|
||||
roleDefinition: customModePrompts[mode.slug]?.roleDefinition ?? mode.roleDefinition,
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
}
|
||||
}, [inputValue, setInputValue, t])
|
||||
|
||||
const allModes = useMemo(() => getAllModes(customModes), [customModes])
|
||||
const allModes = useMemo(() => getAllModes(customModes, false), [customModes])
|
||||
|
||||
// Memoized check for whether the input has content (text or images)
|
||||
const hasInputContent = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -1289,7 +1289,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
|
||||
// Function to handle mode switching
|
||||
const switchToNextMode = useCallback(() => {
|
||||
const allModes = getAllModes(customModes)
|
||||
const allModes = getAllModes(customModes, false)
|
||||
const currentModeIndex = allModes.findIndex((m) => m.slug === mode)
|
||||
const nextModeIndex = (currentModeIndex + 1) % allModes.length
|
||||
// Update local state and notify extension to sync mode change
|
||||
|
|
@ -1298,7 +1298,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
|
||||
// Function to handle switching to previous mode
|
||||
const switchToPreviousMode = useCallback(() => {
|
||||
const allModes = getAllModes(customModes)
|
||||
const allModes = getAllModes(customModes, false)
|
||||
const currentModeIndex = allModes.findIndex((m) => m.slug === mode)
|
||||
const previousModeIndex = (currentModeIndex - 1 + allModes.length) % allModes.length
|
||||
// Update local state and notify extension to sync mode change
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ export const ModeSelector = ({
|
|||
|
||||
// Get all modes including custom modes and merge custom prompt descriptions.
|
||||
const modes = React.useMemo(() => {
|
||||
const allModes = getAllModes(customModes)
|
||||
// Don't include hidden modes in the selector dropdown
|
||||
const allModes = getAllModes(customModes, false)
|
||||
|
||||
return allModes.map((mode) => ({
|
||||
...mode,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
const [visualMode, setVisualMode] = useState(mode)
|
||||
|
||||
// Build modes fresh each render so search reflects inline rename updates immediately
|
||||
const modes = getAllModes(customModes)
|
||||
// Include hidden modes in the settings view so they can be managed
|
||||
const modes = getAllModes(customModes, true)
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [selectedPromptContent, setSelectedPromptContent] = useState("")
|
||||
|
|
@ -537,8 +538,8 @@ const ModesView = ({ onDone }: ModesViewProps) => {
|
|||
if (message.success) {
|
||||
const { slug } = message as ImportModeResult
|
||||
if (slug) {
|
||||
// Try switching using the freshest mode list available
|
||||
const all = getAllModes(customModesRef.current)
|
||||
// Try switching using the freshest mode list available (include hidden modes)
|
||||
const all = getAllModes(customModesRef.current, true)
|
||||
const importedMode = all.find((m) => m.slug === slug)
|
||||
if (importedMode) {
|
||||
handleModeSwitchRef.current(importedMode)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue