Skip to content

Commit 650ef0f

Browse files
committed
feat: enhance mode management with source tracking and restore functionality in ModeManager and ModeEnableDisableDialog
1 parent eef33ad commit 650ef0f

File tree

3 files changed

+180
-34
lines changed

3 files changed

+180
-34
lines changed

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,97 @@ Roo Code adapts to your needs with specialized [modes](https://docs.roocode.com/
8686
- **Debug Mode:** For systematic problem diagnosis
8787
- **[Custom Modes](https://docs.roocode.com/advanced-usage/custom-modes):** Create unlimited specialized personas for security auditing, performance optimization, documentation, or any other task
8888

89+
## Overriding Default Modes
90+
91+
You can override Roo Code's built-in modes (for example: `code`, `debug`, `ask`, `architect`, `orchestrator`) by creating a custom mode that uses the same slug as the built-in mode. When a custom mode uses the same slug it takes precedence according to the following order:
92+
93+
- Project-specific override (in the workspace `.roomodes`) — highest precedence
94+
- Global override (in `custom_modes.yaml`) — next
95+
- Built-in mode — fallback
96+
97+
Overriding Modes Globally
98+
To customize a default mode across all your projects:
99+
100+
1. Open the Prompts tab in the Roo Code UI.
101+
2. Open the Global Prompts / Global Modes settings (click the ⋯ / settings menu) and choose "Edit Global Modes" to edit `custom_modes.yaml`.
102+
3. Add a custom mode entry that uses the same `slug` as the built-in you want to override.
103+
104+
Corrected YAML example
105+
106+
customModes:
107+
108+
- slug: code # Matches the default 'code' mode slug
109+
name: "💻 Code (Global Override)"
110+
roleDefinition: "You are a software engineer with global-specific constraints."
111+
whenToUse: "This globally overridden code mode is for JS/TS tasks."
112+
customInstructions: "Focus on project-specific JS/TS development."
113+
groups:
114+
- read
115+
- [ edit, { fileRegex: "\\.(js|ts)$", description: "JS/TS files only" } ]
116+
117+
JSON example (note: escape backslashes appropriately when embedding inside other strings)
118+
119+
{
120+
"customModes": [{
121+
"slug": "code",
122+
"name": "💻 Code (Global Override)",
123+
"roleDefinition": "You are a software engineer with global-specific constraints",
124+
"whenToUse": "This globally overridden code mode is for JS/TS tasks.",
125+
"customInstructions": "Focus on project-specific JS/TS development",
126+
"groups": [
127+
"read",
128+
["edit", { "fileRegex": "\\\\.(js|ts)$", "description": "JS/TS files only" }]
129+
]
130+
}]
131+
}
132+
133+
Project-Specific Mode Override
134+
To override a default mode for just one project:
135+
136+
1. Open the Prompts tab.
137+
2. Open the Project Prompts / Project Modes settings and choose "Edit Project Modes" to edit the `.roomodes` file in the workspace root.
138+
3. Add a `customModes` entry with the same `slug` as the built-in mode.
139+
140+
YAML example (project override):
141+
142+
customModes:
143+
144+
- slug: code
145+
name: "💻 Code (Project-Specific)"
146+
roleDefinition: "You are a software engineer with project-specific constraints for this project."
147+
whenToUse: "This project-specific code mode is for Python tasks within this project."
148+
customInstructions: "Adhere to PEP8 and use type hints."
149+
groups:
150+
- read
151+
- [ edit, { fileRegex: "\\.py$", description: "Python files only" } ]
152+
- command
153+
154+
Project-specific overrides take precedence over global overrides.
155+
156+
## Restore built-in (delete override)
157+
158+
Enabling/disabling an override is different from restoring the built-in mode.
159+
160+
- Enable/Disable: Toggling enabled/disabled updates the custom override entry (preserves your customizations).
161+
- Restore built-in: Deletes the custom override entry (removes your customizations) so the built-in mode is used again.
162+
163+
How to restore the original built-in mode:
164+
165+
1. Open Prompts → Global Modes (Edit Global Modes).
166+
2. Find the global custom mode that uses the same slug as a built-in — a "Restore built-in" action is available for overrides.
167+
3. Click "Restore built-in" and confirm. The extension will delete the custom mode entry from `custom_modes.yaml` and (optionally) remove the associated rules folder. After this, the built-in mode will be used.
168+
169+
Exact file path examples
170+
171+
- Global custom modes file: `{user_home}/.roo/custom_modes.yaml` (the extension writes global overrides here)
172+
- Project overrides file: `{workspace_root}/.roomodes` (or `.roo/.roomodes` depending on workspace layout)
173+
174+
## Rules folder and backups
175+
176+
Custom mode rule files are stored in a rules folder (for example: `~/.roo/rules-<slug>` for global or `.roo/rules-<slug>` for project-scoped rules). When you delete a custom mode (restore built-in), the extension will attempt to remove the associated rules folder. If you want to keep your custom rules, back up the rules folder before restoring/deleting the override.
177+
178+
Tip: When overriding default modes, test carefully. Consider backing up configurations and rules before major changes.
179+
89180
### Smart Tools
90181

91182
Roo Code comes with powerful [tools](https://docs.roocode.com/basic-usage/how-tools-work) that can:

src/services/ModeManager.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export interface ModeState {
1111
isDisabled: boolean
1212
}
1313

14+
// Internal type used when returning modes to callers (webview/handlers).
15+
// Extends the canonical ModeConfig with source information and an optional
16+
// UI-only `overridesBuiltin` flag used by the webview.
17+
export type ModeWithSource = ModeConfig & { source: ModeSource; overridesBuiltin?: boolean }
18+
1419
/**
1520
* ModeManager handles mode operations including enable/disable functionality,
1621
* source identification, and mode filtering
@@ -27,9 +32,9 @@ export class ModeManager {
2732
/**
2833
* Get all modes with their sources (builtin, global, project)
2934
*/
30-
async getAllModesWithSource(): Promise<ModeConfig[]> {
35+
async getAllModesWithSource(): Promise<ModeWithSource[]> {
3136
const customModes = await this.customModesManager.getCustomModes()
32-
const allModes: ModeConfig[] = []
37+
const allModes: ModeWithSource[] = []
3338

3439
// Add built-in modes first
3540
for (const mode of builtinModes) {
@@ -42,9 +47,13 @@ export class ModeManager {
4247

4348
// Add custom modes (they override built-in modes and have source information)
4449
for (const mode of customModes) {
50+
// UI-only flag: indicate when a global custom mode overrides a built-in one.
51+
// The webview uses this to show a "Restore built-in" action.
52+
const overridesBuiltin = !!builtinModes.find((b) => b.slug === mode.slug)
4553
allModes.push({
4654
...mode,
47-
source: mode.source || "global",
55+
source: (mode.source as ModeSource) || "global",
56+
overridesBuiltin,
4857
})
4958
}
5059

@@ -54,15 +63,15 @@ export class ModeManager {
5463
/**
5564
* Get all enabled modes (excluding disabled ones)
5665
*/
57-
async getEnabledModes(): Promise<ModeConfig[]> {
66+
async getEnabledModes(): Promise<ModeWithSource[]> {
5867
const allModes = await this.getAllModesWithSource()
5968
return allModes.filter((mode) => !mode.disabled)
6069
}
6170

6271
/**
6372
* Get all disabled modes
6473
*/
65-
async getDisabledModes(): Promise<ModeConfig[]> {
74+
async getDisabledModes(): Promise<ModeWithSource[]> {
6675
const allModes = await this.getAllModesWithSource()
6776
return allModes.filter((mode) => mode.disabled === true)
6877
}
@@ -109,7 +118,7 @@ export class ModeManager {
109118
/**
110119
* Get mode by slug with source information
111120
*/
112-
async getModeBySlug(slug: string): Promise<ModeConfig | null> {
121+
async getModeBySlug(slug: string): Promise<ModeWithSource | null> {
113122
const allModes = await this.getAllModesWithSource()
114123
return allModes.find((m) => m.slug === slug) || null
115124
}
@@ -140,9 +149,9 @@ export class ModeManager {
140149
* Get modes categorized by source
141150
*/
142151
async getModesBySource(): Promise<{
143-
builtin: ModeConfig[]
144-
global: ModeConfig[]
145-
project: ModeConfig[]
152+
builtin: ModeWithSource[]
153+
global: ModeWithSource[]
154+
project: ModeWithSource[]
146155
}> {
147156
const allModes = await this.getAllModesWithSource()
148157

webview-ui/src/components/modes/ModeEnableDisableDialog.tsx

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ interface ModeEnableDisableDialogProps {
6262

6363
interface DeleteState {
6464
open: boolean
65+
// action: 'delete' | 'restore' - determines dialog wording
66+
action?: "delete" | "restore"
6567
tMode?: { slug: string; name: string; source?: string; rulesFolderPath?: string } | null
6668
}
6769

@@ -204,23 +206,47 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
204206
{mode.disabled ? <EyeOff className="size-4 disabled" /> : <Eye className="size-4 enabled" />}
205207
{/* Show delete for global custom modes (they override built-in or are user-created) */}
206208
{mode.source === "global" && (
207-
<Button
208-
variant="ghost"
209-
size="icon"
210-
onClick={() => {
211-
// Ask the extension to check for rules folder and return path via message
212-
setDeleteState({
213-
open: false,
214-
tMode: { slug: mode.slug, name: mode.name, source: mode.source },
215-
})
216-
// Request checkOnly first
217-
window.parent.postMessage(
218-
{ type: "deleteCustomMode", slug: mode.slug, checkOnly: true },
219-
"*",
220-
)
221-
}}>
222-
<span className="codicon codicon-trash"></span>
223-
</Button>
209+
<div className="flex items-center gap-1">
210+
<Button
211+
variant="ghost"
212+
size="icon"
213+
onClick={() => {
214+
// Ask the extension to check for rules folder and return path via message
215+
setDeleteState({
216+
open: false,
217+
action: "delete",
218+
tMode: { slug: mode.slug, name: mode.name, source: mode.source },
219+
})
220+
// Request checkOnly first
221+
window.parent.postMessage(
222+
{ type: "deleteCustomMode", slug: mode.slug, checkOnly: true },
223+
"*",
224+
)
225+
}}>
226+
<span className="codicon codicon-trash"></span>
227+
</Button>
228+
229+
{(mode as any).overridesBuiltin && (
230+
<Button
231+
variant="ghost"
232+
size="icon"
233+
onClick={() => {
234+
// Ask the extension to check for rules folder and return path via message
235+
setDeleteState({
236+
open: false,
237+
action: "restore",
238+
tMode: { slug: mode.slug, name: mode.name, source: mode.source },
239+
})
240+
// Request checkOnly first - reuse deleteCustomMode flow but interpret as restore in dialog
241+
window.parent.postMessage(
242+
{ type: "deleteCustomMode", slug: mode.slug, checkOnly: true },
243+
"*",
244+
)
245+
}}>
246+
<span className="codicon codicon-restore"></span>
247+
</Button>
248+
)}
249+
</div>
224250
)}
225251
</div>
226252
</div>
@@ -232,10 +258,12 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
232258
const message = e.data
233259
if (message.type === "deleteCustomModeCheck") {
234260
if (message.slug && deleteState.tMode && deleteState.tMode.slug === message.slug) {
235-
setDeleteState({
261+
// Preserve action (delete vs restore) when opening the confirmation dialog
262+
setDeleteState((prev) => ({
263+
...prev,
236264
open: true,
237-
tMode: { ...deleteState.tMode, rulesFolderPath: message.rulesFolderPath },
238-
})
265+
tMode: { ...prev.tMode!, rulesFolderPath: message.rulesFolderPath },
266+
}))
239267
}
240268
}
241269
}
@@ -385,13 +413,25 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
385413
<AlertDialog open={!!deleteState.open} onOpenChange={(open) => setDeleteState((s) => ({ ...s, open }))}>
386414
<AlertDialogContent>
387415
<AlertDialogHeader>
388-
<AlertDialogTitle>{t ? t("prompts:deleteMode.title") : "Delete mode"}</AlertDialogTitle>
416+
<AlertDialogTitle>
417+
{deleteState.action === "restore"
418+
? t
419+
? t("prompts:restoreMode.title")
420+
: "Restore built-in mode"
421+
: t
422+
? t("prompts:deleteMode.title")
423+
: "Delete mode"}
424+
</AlertDialogTitle>
389425
<AlertDialogDescription>
390426
{deleteState.tMode && (
391427
<>
392-
{t
393-
? t("prompts:deleteMode.message", { modeName: deleteState.tMode.name })
394-
: `Delete ${deleteState.tMode.name}?`}
428+
{deleteState.action === "restore"
429+
? t
430+
? t("prompts:restoreMode.message", { modeName: deleteState.tMode.name })
431+
: `Restore built-in mode ${deleteState.tMode.name}?`
432+
: t
433+
? t("prompts:deleteMode.message", { modeName: deleteState.tMode.name })
434+
: `Delete ${deleteState.tMode.name}?`}
395435
{deleteState.tMode.rulesFolderPath && (
396436
<div className="mt-2">
397437
{t
@@ -408,7 +448,13 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
408448
<AlertDialogFooter>
409449
<AlertDialogCancel>{t ? t("prompts:deleteMode.cancel") : "Cancel"}</AlertDialogCancel>
410450
<AlertDialogAction onClick={confirmDelete}>
411-
{t ? t("prompts:deleteMode.confirm") : "Delete"}
451+
{deleteState.action === "restore"
452+
? t
453+
? t("prompts:restoreMode.confirm")
454+
: "Restore"
455+
: t
456+
? t("prompts:deleteMode.confirm")
457+
: "Delete"}
412458
</AlertDialogAction>
413459
</AlertDialogFooter>
414460
</AlertDialogContent>

0 commit comments

Comments
 (0)