feat: add ability to hide default modes

- Add hiddenDefaultModes field to global settings schema
- Update ExtensionStateContext to manage hidden modes state
- Modify getAllModes function to filter out hidden default modes
- Add UI controls in ModesView to toggle visibility of default modes
- Update ModeSelector to respect hidden modes setting
- Add backend message handler for hiddenDefaultModes
- Add comprehensive tests for the new functionality
- Add translation keys for new UI elements

Fixes #6638
This commit is contained in:
Roo Code 2025-08-04 02:25:26 +00:00
parent a5b55dac8b
commit 9ebf237f99
10 changed files with 293 additions and 10 deletions

View file

@ -139,6 +139,7 @@ export const globalSettingsSchema = z.object({
customModes: z.array(modeConfigSchema).optional(),
customModePrompts: customModePromptsSchema.optional(),
customSupportPrompts: customSupportPromptsSchema.optional(),
hiddenDefaultModes: z.array(z.string()).optional(),
enhancementApiConfigId: z.string().optional(),
includeTaskHistoryInEnhance: z.boolean().optional(),
historyPreviewCollapsed: z.boolean().optional(),

View file

@ -2568,5 +2568,11 @@ export const webviewMessageHandler = async (
}
break
}
case "hiddenDefaultModes":
if (message.hiddenModes !== undefined) {
await updateGlobalState("hiddenDefaultModes", message.hiddenModes)
await provider.postStateToWebview()
}
break
}
}

View file

@ -262,6 +262,7 @@ export type ExtensionState = Pick<
// | "customModes" // Optional in GlobalSettings, required here.
| "customModePrompts"
| "customSupportPrompts"
| "hiddenDefaultModes"
| "enhancementApiConfigId"
| "condensingApiConfigId"
| "customCondensingPrompt"

View file

@ -210,6 +210,7 @@ export interface WebviewMessage {
| "deleteCommand"
| "createCommand"
| "insertTextIntoTextarea"
| "hiddenDefaultModes"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
@ -272,6 +273,7 @@ export interface WebviewMessage {
codebaseIndexGeminiApiKey?: string
codebaseIndexMistralApiKey?: string
}
hiddenModes?: string[]
}
export const checkoutDiffPayloadSchema = z.object({

View file

@ -0,0 +1,217 @@
// npx vitest run shared/__tests__/getAllModes.spec.ts
import { describe, it, expect } from "vitest"
import { getAllModes } from "../modes"
import { DEFAULT_MODES, type ModeConfig } from "@roo-code/types"
describe("getAllModes", () => {
const customModes: ModeConfig[] = [
{
slug: "custom-mode-1",
name: "Custom Mode 1",
roleDefinition: "Custom role 1",
groups: ["read", "edit"],
},
{
slug: "custom-mode-2",
name: "Custom Mode 2",
roleDefinition: "Custom role 2",
groups: ["read"],
},
]
describe("without hiddenDefaultModes", () => {
it("returns all default modes and custom modes", () => {
const result = getAllModes(customModes)
// Should include all default modes
DEFAULT_MODES.forEach((defaultMode) => {
expect(result).toContainEqual(
expect.objectContaining({
slug: defaultMode.slug,
name: defaultMode.name,
}),
)
})
// Should include all custom modes
customModes.forEach((customMode) => {
expect(result).toContainEqual(
expect.objectContaining({
slug: customMode.slug,
name: customMode.name,
}),
)
})
// Total should be default modes + custom modes
expect(result).toHaveLength(DEFAULT_MODES.length + customModes.length)
})
it("returns only default modes when no custom modes provided", () => {
const result = getAllModes()
expect(result).toHaveLength(DEFAULT_MODES.length)
DEFAULT_MODES.forEach((defaultMode) => {
expect(result).toContainEqual(
expect.objectContaining({
slug: defaultMode.slug,
name: defaultMode.name,
}),
)
})
})
})
describe("with hiddenDefaultModes", () => {
it("filters out hidden default modes", () => {
const hiddenModes = ["code", "debug"]
const result = getAllModes(customModes, hiddenModes)
// Should not include hidden modes
hiddenModes.forEach((hiddenSlug) => {
expect(result).not.toContainEqual(
expect.objectContaining({
slug: hiddenSlug,
}),
)
})
// Should include non-hidden default modes
DEFAULT_MODES.filter((mode) => !hiddenModes.includes(mode.slug)).forEach((defaultMode) => {
expect(result).toContainEqual(
expect.objectContaining({
slug: defaultMode.slug,
name: defaultMode.name,
}),
)
})
// Should include all custom modes
customModes.forEach((customMode) => {
expect(result).toContainEqual(
expect.objectContaining({
slug: customMode.slug,
name: customMode.name,
}),
)
})
// Total should be (default modes - hidden modes) + custom modes
expect(result).toHaveLength(DEFAULT_MODES.length - hiddenModes.length + customModes.length)
})
it("filters out all default modes when all are hidden", () => {
const allDefaultSlugs = DEFAULT_MODES.map((mode) => mode.slug)
const result = getAllModes(customModes, allDefaultSlugs)
// Should not include any default modes
DEFAULT_MODES.forEach((defaultMode) => {
expect(result).not.toContainEqual(
expect.objectContaining({
slug: defaultMode.slug,
}),
)
})
// Should only include custom modes
expect(result).toHaveLength(customModes.length)
customModes.forEach((customMode) => {
expect(result).toContainEqual(
expect.objectContaining({
slug: customMode.slug,
name: customMode.name,
}),
)
})
})
it("handles empty hiddenDefaultModes array", () => {
const result = getAllModes(customModes, [])
// Should include all modes (same as no filter)
expect(result).toHaveLength(DEFAULT_MODES.length + customModes.length)
})
it("ignores non-existent mode slugs in hiddenDefaultModes", () => {
const hiddenModes = ["non-existent-mode", "code"]
const result = getAllModes(customModes, hiddenModes)
// Should only filter out 'code' which exists
expect(result).not.toContainEqual(
expect.objectContaining({
slug: "code",
}),
)
// Should still have all other modes
expect(result).toHaveLength(DEFAULT_MODES.length - 1 + customModes.length)
})
it("does not filter custom modes even if their slugs are in hiddenDefaultModes", () => {
const customModesWithDefaultSlug: ModeConfig[] = [
{
slug: "code", // Same slug as a default mode
name: "Custom Code Mode",
roleDefinition: "Custom code role",
groups: ["read", "edit"],
},
]
const hiddenModes = ["code"]
const result = getAllModes(customModesWithDefaultSlug, hiddenModes)
// Should not include the default 'code' mode
const defaultCodeMode = DEFAULT_MODES.find((m) => m.slug === "code")
expect(result).not.toContainEqual(
expect.objectContaining({
slug: "code",
name: defaultCodeMode?.name,
}),
)
// Should include the custom 'code' mode
expect(result).toContainEqual(
expect.objectContaining({
slug: "code",
name: "Custom Code Mode",
}),
)
})
})
describe("mode ordering", () => {
it("maintains order with default modes first, then custom modes", () => {
const result = getAllModes(customModes)
// First modes should be default modes
const defaultModeCount = DEFAULT_MODES.length
result.slice(0, defaultModeCount).forEach((mode, index) => {
expect(mode.slug).toBe(DEFAULT_MODES[index].slug)
})
// Remaining modes should be custom modes
result.slice(defaultModeCount).forEach((mode, index) => {
expect(mode.slug).toBe(customModes[index].slug)
})
})
it("maintains order when filtering hidden modes", () => {
const hiddenModes = ["code", "ask"]
const result = getAllModes(customModes, hiddenModes)
// Get expected default modes (non-hidden)
const expectedDefaultModes = DEFAULT_MODES.filter((m) => !hiddenModes.includes(m.slug))
// First modes should be non-hidden default modes in original order
result.slice(0, expectedDefaultModes.length).forEach((mode, index) => {
expect(mode.slug).toBe(expectedDefaultModes[index].slug)
})
// Remaining modes should be custom modes
result.slice(expectedDefaultModes.length).forEach((mode, index) => {
expect(mode.slug).toBe(customModes[index].slug)
})
})
})
})

View file

@ -86,13 +86,18 @@ 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[], hiddenDefaultModes?: string[]): ModeConfig[] {
// Start with built-in modes, filtering out hidden ones
const visibleBuiltInModes = hiddenDefaultModes?.length
? modes.filter((mode) => !hiddenDefaultModes.includes(mode.slug))
: [...modes]
if (!customModes?.length) {
return [...modes]
return visibleBuiltInModes
}
// Start with built-in modes
const allModes = [...modes]
// Start with visible built-in modes
const allModes = [...visibleBuiltInModes]
// Process custom modes
customModes.forEach((customMode) => {

View file

@ -43,7 +43,7 @@ export const ModeSelector = ({
const [searchValue, setSearchValue] = React.useState("")
const searchInputRef = React.useRef<HTMLInputElement>(null)
const portalContainer = useRooPortal("roo-portal")
const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState()
const { hasOpenedModeSelector, setHasOpenedModeSelector, hiddenDefaultModes } = useExtensionState()
const { t } = useAppTranslation()
const trackModeSelectorOpened = React.useCallback(() => {
@ -59,12 +59,12 @@ export const ModeSelector = ({
// Get all modes including custom modes and merge custom prompt descriptions
const modes = React.useMemo(() => {
const allModes = getAllModes(customModes)
const allModes = getAllModes(customModes, hiddenDefaultModes)
return allModes.map((mode) => ({
...mode,
description: customModePrompts?.[mode.slug]?.description ?? mode.description,
}))
}, [customModes, customModePrompts])
}, [customModes, customModePrompts, hiddenDefaultModes])
// Find the selected mode
const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value])

View file

@ -10,7 +10,7 @@ import {
import { Trans } from "react-i18next"
import { ChevronDown, X, Upload, Download } from "lucide-react"
import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema } from "@roo-code/types"
import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema, DEFAULT_MODES } from "@roo-code/types"
import {
Mode,
@ -75,6 +75,8 @@ const ModesView = ({ onDone }: ModesViewProps) => {
customInstructions,
setCustomInstructions,
customModes,
hiddenDefaultModes,
setHiddenDefaultModes,
} = useExtensionState()
// Use a local state to track the visually active mode
@ -85,7 +87,7 @@ const ModesView = ({ onDone }: ModesViewProps) => {
const [visualMode, setVisualMode] = useState(mode)
// Memoize modes to preserve array order
const modes = useMemo(() => getAllModes(customModes), [customModes])
const modes = useMemo(() => getAllModes(customModes, hiddenDefaultModes), [customModes, hiddenDefaultModes])
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [selectedPromptContent, setSelectedPromptContent] = useState("")
@ -701,6 +703,48 @@ const ModesView = ({ onDone }: ModesViewProps) => {
</PopoverContent>
</Popover>
</div>
{/* Default Modes Visibility Settings */}
<div className="mb-3">
<div className="font-bold mb-1">{t("prompts:modes.defaultModesVisibility")}</div>
<div className="text-sm text-vscode-descriptionForeground mb-2">
{t("prompts:modes.defaultModesVisibilityDescription")}
</div>
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2">
{DEFAULT_MODES.map((defaultMode) => (
<VSCodeCheckbox
key={defaultMode.slug}
checked={!hiddenDefaultModes.includes(defaultMode.slug)}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
const target =
(e as CustomEvent)?.detail?.target || (e.target as HTMLInputElement)
const checked = target.checked
if (checked) {
// Remove from hidden modes
const newHiddenModes = hiddenDefaultModes.filter(
(slug) => slug !== defaultMode.slug,
)
setHiddenDefaultModes(newHiddenModes)
vscode.postMessage({
type: "hiddenDefaultModes",
hiddenModes: newHiddenModes,
})
} else {
// Add to hidden modes
const newHiddenModes = [...hiddenDefaultModes, defaultMode.slug]
setHiddenDefaultModes(newHiddenModes)
vscode.postMessage({
type: "hiddenDefaultModes",
hiddenModes: newHiddenModes,
})
}
}}>
{defaultMode.name}
</VSCodeCheckbox>
))}
</div>
</div>
{/* API Configuration - Moved Here */}
<div className="mb-3">
<div className="font-bold mb-1">{t("prompts:apiConfiguration.title")}</div>

View file

@ -43,6 +43,8 @@ export interface ExtensionStateContextType extends ExtensionState {
hasOpenedModeSelector: boolean // New property to track if user has opened mode selector
setHasOpenedModeSelector: (value: boolean) => void // Setter for the new property
alwaysAllowFollowupQuestions: boolean // New property for follow-up questions auto-approve
hiddenDefaultModes: string[] // Array of default mode slugs to hide
setHiddenDefaultModes: (modes: string[]) => void // Setter for hidden default modes
setAlwaysAllowFollowupQuestions: (value: boolean) => void // Setter for the new property
followupAutoApproveTimeoutMs: number | undefined // Timeout in ms for auto-approving follow-up questions
setFollowupAutoApproveTimeoutMs: (value: number) => void // Setter for the timeout
@ -208,6 +210,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
customCondensingPrompt: "", // Default empty string for custom condensing prompt
hasOpenedModeSelector: false, // Default to false (not opened yet)
autoApprovalEnabled: false,
hiddenDefaultModes: [], // Default to empty array (no modes hidden)
customModes: [],
maxOpenTabsContext: 20,
maxWorkspaceFiles: 200,
@ -408,6 +411,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
profileThresholds: state.profileThresholds ?? {},
alwaysAllowFollowupQuestions,
followupAutoApproveTimeoutMs,
hiddenDefaultModes: state.hiddenDefaultModes ?? [],
setExperimentEnabled: (id, enabled) =>
setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
setApiConfiguration,
@ -497,6 +501,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })),
setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })),
setAutoCondenseContext: (value) => setState((prevState) => ({ ...prevState, autoCondenseContext: value })),
setHiddenDefaultModes: (modes) => setState((prevState) => ({ ...prevState, hiddenDefaultModes: modes })),
setAutoCondenseContextPercent: (value) =>
setState((prevState) => ({ ...prevState, autoCondenseContextPercent: value })),
setCondensingApiConfigId: (value) => setState((prevState) => ({ ...prevState, condensingApiConfigId: value })),

View file

@ -10,7 +10,9 @@
"editProjectModes": "Edit Project Modes (.roomodes)",
"createModeHelpText": "Modes are specialized personas that tailor Roo's behavior. <0>Learn about Using Modes</0> or <1>Customizing Modes.</1>",
"selectMode": "Search modes",
"noMatchFound": "No modes found"
"noMatchFound": "No modes found",
"defaultModesVisibility": "Default Modes Visibility",
"defaultModesVisibilityDescription": "Choose which default modes to show in the mode selector"
},
"apiConfiguration": {
"title": "API Configuration",