mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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:
parent
a5b55dac8b
commit
9ebf237f99
10 changed files with 293 additions and 10 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -2568,5 +2568,11 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
break
|
||||
}
|
||||
case "hiddenDefaultModes":
|
||||
if (message.hiddenModes !== undefined) {
|
||||
await updateGlobalState("hiddenDefaultModes", message.hiddenModes)
|
||||
await provider.postStateToWebview()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ export type ExtensionState = Pick<
|
|||
// | "customModes" // Optional in GlobalSettings, required here.
|
||||
| "customModePrompts"
|
||||
| "customSupportPrompts"
|
||||
| "hiddenDefaultModes"
|
||||
| "enhancementApiConfigId"
|
||||
| "condensingApiConfigId"
|
||||
| "customCondensingPrompt"
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
217
src/shared/__tests__/getAllModes.spec.ts
Normal file
217
src/shared/__tests__/getAllModes.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 })),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue