mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: support compound slug names for submodes
- Update ModeConfig schema to allow slash in slugs (parent/child format) - Add hidden and parent fields to ModeConfig for submode support - Update getModeBySlug to handle compound slug matching - Add getVisibleModes and getSubmodes helper functions - Add comprehensive tests for compound slug functionality Part of #9446 to enable hidden/submodes feature
This commit is contained in:
parent
2ca9eac9e0
commit
c88b6a8c68
3 changed files with 195 additions and 4 deletions
|
|
@ -62,7 +62,9 @@ const groupEntryArraySchema = z.array(groupEntrySchema).refine(
|
|||
)
|
||||
|
||||
export const modeConfigSchema = z.object({
|
||||
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
|
||||
slug: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9-]+(\/[a-zA-Z0-9-]+)?$/, "Slug must be in format 'slug' or 'parent-slug/child-slug'"),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
roleDefinition: z.string().min(1, "Role definition is required"),
|
||||
whenToUse: z.string().optional(),
|
||||
|
|
@ -70,6 +72,8 @@ export const modeConfigSchema = z.object({
|
|||
customInstructions: z.string().optional(),
|
||||
groups: groupEntryArraySchema,
|
||||
source: z.enum(["global", "project"]).optional(),
|
||||
hidden: z.boolean().optional(), // Hide mode from dropdown selector
|
||||
parent: z.string().optional(), // Parent mode slug for submodes
|
||||
})
|
||||
|
||||
export type ModeConfig = z.infer<typeof modeConfigSchema>
|
||||
|
|
|
|||
158
src/shared/__tests__/modes-compound-slugs.spec.ts
Normal file
158
src/shared/__tests__/modes-compound-slugs.spec.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import { type ModeConfig } from "@roo-code/types"
|
||||
import { getModeBySlug, getVisibleModes, getSubmodes } from "../modes"
|
||||
|
||||
describe("Compound Mode Slugs", () => {
|
||||
const mockCustomModes: ModeConfig[] = [
|
||||
{
|
||||
slug: "parent-mode",
|
||||
name: "Parent Mode",
|
||||
roleDefinition: "Parent mode definition",
|
||||
groups: ["read"],
|
||||
},
|
||||
{
|
||||
slug: "parent-mode/submode1",
|
||||
name: "Submode 1",
|
||||
roleDefinition: "Submode 1 definition",
|
||||
groups: ["read"],
|
||||
hidden: true,
|
||||
parent: "parent-mode",
|
||||
},
|
||||
{
|
||||
slug: "parent-mode/submode2",
|
||||
name: "Submode 2",
|
||||
roleDefinition: "Submode 2 definition",
|
||||
groups: ["edit"],
|
||||
hidden: true,
|
||||
parent: "parent-mode",
|
||||
},
|
||||
{
|
||||
slug: "regular-mode",
|
||||
name: "Regular Mode",
|
||||
roleDefinition: "Regular mode definition",
|
||||
groups: ["read"],
|
||||
},
|
||||
]
|
||||
|
||||
describe("getModeBySlug", () => {
|
||||
it("should find mode with simple slug", () => {
|
||||
const mode = getModeBySlug("parent-mode", mockCustomModes)
|
||||
expect(mode).toBeDefined()
|
||||
expect(mode?.slug).toBe("parent-mode")
|
||||
})
|
||||
|
||||
it("should find mode with compound slug (parent/child format)", () => {
|
||||
const mode = getModeBySlug("parent-mode/submode1", mockCustomModes)
|
||||
expect(mode).toBeDefined()
|
||||
expect(mode?.slug).toBe("parent-mode/submode1")
|
||||
expect(mode?.parent).toBe("parent-mode")
|
||||
})
|
||||
|
||||
it("should return undefined for non-existent compound slug", () => {
|
||||
const mode = getModeBySlug("parent-mode/non-existent", mockCustomModes)
|
||||
expect(mode).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should prioritize exact match over compound slug parsing", () => {
|
||||
const mode = getModeBySlug("regular-mode", mockCustomModes)
|
||||
expect(mode).toBeDefined()
|
||||
expect(mode?.slug).toBe("regular-mode")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getVisibleModes", () => {
|
||||
it("should exclude hidden submodes from visible modes", () => {
|
||||
const visibleModes = getVisibleModes(mockCustomModes)
|
||||
|
||||
// Should include built-in modes plus custom non-hidden modes
|
||||
// Built-in modes: architect, code, ask, debug, orchestrator (5)
|
||||
// Custom non-hidden: parent-mode, regular-mode (2)
|
||||
// Total: 7
|
||||
expect(visibleModes.length).toBeGreaterThan(0)
|
||||
expect(visibleModes.map((m) => m.slug)).toContain("parent-mode")
|
||||
expect(visibleModes.map((m) => m.slug)).toContain("regular-mode")
|
||||
expect(visibleModes.map((m) => m.slug)).not.toContain("parent-mode/submode1")
|
||||
expect(visibleModes.map((m) => m.slug)).not.toContain("parent-mode/submode2")
|
||||
})
|
||||
|
||||
it("should include all modes when none are hidden", () => {
|
||||
const modesWithoutHidden: ModeConfig[] = [
|
||||
{
|
||||
slug: "mode1",
|
||||
name: "Mode 1",
|
||||
roleDefinition: "Definition 1",
|
||||
groups: ["read"],
|
||||
},
|
||||
{
|
||||
slug: "mode2",
|
||||
name: "Mode 2",
|
||||
roleDefinition: "Definition 2",
|
||||
groups: ["edit"],
|
||||
},
|
||||
]
|
||||
|
||||
const visibleModes = getVisibleModes(modesWithoutHidden)
|
||||
// Should include built-in modes plus custom modes (5 + 2 = 7)
|
||||
expect(visibleModes.length).toBeGreaterThan(0)
|
||||
expect(visibleModes.map((m) => m.slug)).toContain("mode1")
|
||||
expect(visibleModes.map((m) => m.slug)).toContain("mode2")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSubmodes", () => {
|
||||
it("should return all submodes for a parent", () => {
|
||||
const submodes = getSubmodes("parent-mode", mockCustomModes)
|
||||
|
||||
expect(submodes).toHaveLength(2)
|
||||
expect(submodes.map((m) => m.slug)).toContain("parent-mode/submode1")
|
||||
expect(submodes.map((m) => m.slug)).toContain("parent-mode/submode2")
|
||||
})
|
||||
|
||||
it("should return empty array for mode with no submodes", () => {
|
||||
const submodes = getSubmodes("regular-mode", mockCustomModes)
|
||||
expect(submodes).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle non-existent parent mode", () => {
|
||||
const submodes = getSubmodes("non-existent", mockCustomModes)
|
||||
expect(submodes).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Slug validation", () => {
|
||||
it("should accept valid simple slugs", () => {
|
||||
const validSlugs = ["code", "architect", "debug-mode", "test-123"]
|
||||
|
||||
validSlugs.forEach((slug) => {
|
||||
const regex = /^[a-zA-Z0-9-]+(\/[a-zA-Z0-9-]+)?$/
|
||||
expect(regex.test(slug)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it("should accept valid compound slugs", () => {
|
||||
const validCompoundSlugs = ["parent/child", "architect/design", "debug-mode/trace", "test-123/subtest-456"]
|
||||
|
||||
validCompoundSlugs.forEach((slug) => {
|
||||
const regex = /^[a-zA-Z0-9-]+(\/[a-zA-Z0-9-]+)?$/
|
||||
expect(regex.test(slug)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it("should reject invalid slugs", () => {
|
||||
const invalidSlugs = [
|
||||
"parent/child/grandchild", // Too many levels
|
||||
"/child", // Starting with slash
|
||||
"parent/", // Ending with slash
|
||||
"parent//child", // Double slash
|
||||
"parent child", // Space
|
||||
"parent@child", // Invalid character
|
||||
"", // Empty
|
||||
]
|
||||
|
||||
invalidSlugs.forEach((slug) => {
|
||||
const regex = /^[a-zA-Z0-9-]+(\/[a-zA-Z0-9-]+)?$/
|
||||
expect(regex.test(slug)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -68,13 +68,26 @@ export const defaultModeSlug = modes[0].slug
|
|||
|
||||
// Helper functions
|
||||
export function getModeBySlug(slug: string, customModes?: ModeConfig[]): ModeConfig | undefined {
|
||||
// Check custom modes first
|
||||
// First try exact match in custom modes
|
||||
const customMode = customModes?.find((mode) => mode.slug === slug)
|
||||
if (customMode) {
|
||||
return customMode
|
||||
}
|
||||
// Then check built-in modes
|
||||
return modes.find((mode) => mode.slug === slug)
|
||||
|
||||
// Then try exact match in built-in modes
|
||||
const builtInMode = modes.find((mode) => mode.slug === slug)
|
||||
if (builtInMode) {
|
||||
return builtInMode
|
||||
}
|
||||
|
||||
// If no exact match and slug contains '/', try to find as compound slug (parent/child)
|
||||
if (slug.includes("/")) {
|
||||
// For compound slugs, search in both custom and built-in modes
|
||||
const allModes = [...(customModes || []), ...modes]
|
||||
return allModes.find((mode) => mode.slug === slug)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getModeConfig(slug: string, customModes?: ModeConfig[]): ModeConfig {
|
||||
|
|
@ -109,6 +122,22 @@ export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] {
|
|||
return allModes
|
||||
}
|
||||
|
||||
// Get visible modes for dropdown selector (excludes hidden submodes)
|
||||
export function getVisibleModes(customModes?: ModeConfig[]): ModeConfig[] {
|
||||
const allModes = getAllModes(customModes)
|
||||
// Filter out hidden modes (submodes)
|
||||
return allModes.filter((mode) => !mode.hidden)
|
||||
}
|
||||
|
||||
// Get submodes for a parent mode
|
||||
export function getSubmodes(parentSlug: string, customModes?: ModeConfig[]): ModeConfig[] {
|
||||
const allModes = getAllModes(customModes)
|
||||
// Return modes that have this parent or compound slugs starting with parent/
|
||||
return allModes.filter(
|
||||
(mode) => mode.parent === parentSlug || (mode.slug.includes("/") && mode.slug.startsWith(`${parentSlug}/`)),
|
||||
)
|
||||
}
|
||||
|
||||
// Check if a mode is custom or an override
|
||||
export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean {
|
||||
return !!customModes?.some((mode) => mode.slug === slug)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue