Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2080bdd
feat: implement mode enable/disable functionality and associated UI c…
mechanicmuthu Aug 16, 2025
63ff96d
restore .roomodes to original after testing changes.
mechanicmuthu Aug 17, 2025
3cb881d
feat: enhance mode validation and error handling in newTaskTool
mechanicmuthu Aug 17, 2025
eef33ad
feat: implement batch mode updates and delete confirmation dialog in …
mechanicmuthu Aug 17, 2025
650ef0f
feat: enhance mode management with source tracking and restore functi…
mechanicmuthu Aug 17, 2025
4e7d6f1
feat: add confirmation dialogs for disabling built-in modes and refac…
mechanicmuthu Aug 18, 2025
01ee147
Merge branch 'main' into deselect-unwanted-modes
mechanicmuthu Aug 18, 2025
dd3d8bc
Merge branch 'main' into deselect-unwanted-modes
mechanicmuthu Aug 18, 2025
864d0d7
feat: enhance custom modes management and UI interactions
mechanicmuthu Aug 20, 2025
957ecc5
feat: add delay for UI refresh after mode deletion and update setting…
mechanicmuthu Aug 20, 2025
dfef5d5
Merge branch 'main' into deselect-unwanted-modes
mechanicmuthu Aug 20, 2025
331ae9c
Merge branch 'main' into deselect-unwanted-modes
mechanicmuthu Aug 22, 2025
980bac9
Merge branch 'main' into deselect-unwanted-modes
mechanicmuthu Aug 23, 2025
f768787
feat: add getEnabledModes mock to return non-disabled modes
mechanicmuthu Aug 23, 2025
c789545
feat: enhance mode management by mocking ModeManager and adding conte…
mechanicmuthu Aug 25, 2025
2c7ca10
feat: add overriddenBy property to ModeWithSource and display in Mode…
mechanicmuthu Aug 25, 2025
4861664
refactor: update dialog message handling in ModesView and App components
mechanicmuthu Aug 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,97 @@ Roo Code adapts to your needs with specialized [modes](https://docs.roocode.com/
- **Debug Mode:** For systematic problem diagnosis
- **[Custom Modes](https://docs.roocode.com/advanced-usage/custom-modes):** Create unlimited specialized personas for security auditing, performance optimization, documentation, or any other task

## Overriding Default Modes

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:

- Project-specific override (in the workspace `.roomodes`) — highest precedence
- Global override (in `custom_modes.yaml`) — next
- Built-in mode — fallback

Overriding Modes Globally
To customize a default mode across all your projects:

1. Open the Prompts tab in the Roo Code UI.
2. Open the Global Prompts / Global Modes settings (click the ⋯ / settings menu) and choose "Edit Global Modes" to edit `custom_modes.yaml`.
3. Add a custom mode entry that uses the same `slug` as the built-in you want to override.

Corrected YAML example

customModes:

- slug: code # Matches the default 'code' mode slug
name: "💻 Code (Global Override)"
roleDefinition: "You are a software engineer with global-specific constraints."
whenToUse: "This globally overridden code mode is for JS/TS tasks."
customInstructions: "Focus on project-specific JS/TS development."
groups:
- read
- [ edit, { fileRegex: "\\.(js|ts)$", description: "JS/TS files only" } ]

JSON example (note: escape backslashes appropriately when embedding inside other strings)

{
"customModes": [{
"slug": "code",
"name": "💻 Code (Global Override)",
"roleDefinition": "You are a software engineer with global-specific constraints",
"whenToUse": "This globally overridden code mode is for JS/TS tasks.",
"customInstructions": "Focus on project-specific JS/TS development",
"groups": [
"read",
["edit", { "fileRegex": "\\\\.(js|ts)$", "description": "JS/TS files only" }]
]
}]
}

Project-Specific Mode Override
To override a default mode for just one project:

1. Open the Prompts tab.
2. Open the Project Prompts / Project Modes settings and choose "Edit Project Modes" to edit the `.roomodes` file in the workspace root.
3. Add a `customModes` entry with the same `slug` as the built-in mode.

YAML example (project override):

customModes:

- slug: code
name: "💻 Code (Project-Specific)"
roleDefinition: "You are a software engineer with project-specific constraints for this project."
whenToUse: "This project-specific code mode is for Python tasks within this project."
customInstructions: "Adhere to PEP8 and use type hints."
groups:
- read
- [ edit, { fileRegex: "\\.py$", description: "Python files only" } ]
- command

Project-specific overrides take precedence over global overrides.

## Restore built-in (delete override)

Enabling/disabling an override is different from restoring the built-in mode.

- Enable/Disable: Toggling enabled/disabled updates the custom override entry (preserves your customizations).
- Restore built-in: Deletes the custom override entry (removes your customizations) so the built-in mode is used again.

How to restore the original built-in mode:

1. Open Prompts → Global Modes (Edit Global Modes).
2. Find the global custom mode that uses the same slug as a built-in — a "Restore built-in" action is available for overrides.
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.

Exact file path examples

- Global custom modes file: `{user_home}/.roo/custom_modes.yaml` (the extension writes global overrides here)
- Project overrides file: `{workspace_root}/.roomodes` (or `.roo/.roomodes` depending on workspace layout)

## Rules folder and backups

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.

Tip: When overriding default modes, test carefully. Consider backing up configurations and rules before major changes.

### Smart Tools

Roo Code comes with powerful [tools](https://docs.roocode.com/basic-usage/how-tools-work) that can:
Expand Down
3 changes: 2 additions & 1 deletion packages/types/src/mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export const modeConfigSchema = z.object({
description: z.string().optional(),
customInstructions: z.string().optional(),
groups: groupEntryArraySchema,
source: z.enum(["global", "project"]).optional(),
source: z.enum(["builtin", "global", "project"]).optional(),
disabled: z.boolean().optional(),
})

export type ModeConfig = z.infer<typeof modeConfigSchema>
Expand Down
7 changes: 6 additions & 1 deletion src/api/providers/featherless.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { DEEP_SEEK_DEFAULT_TEMPERATURE, type FeatherlessModelId, featherlessDefaultModelId, featherlessModels } from "@roo-code/types"
import {
DEEP_SEEK_DEFAULT_TEMPERATURE,
type FeatherlessModelId,
featherlessDefaultModelId,
featherlessModels,
} from "@roo-code/types"
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"

Expand Down
136 changes: 136 additions & 0 deletions src/core/config/CustomModesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,8 @@ export class CustomModesManager {
}

settings.customModes = operation(settings.customModes)
// Log write operations so extension host logs show when custom modes are persisted.
console.info(`[CustomModesManager] Writing custom modes to: ${filePath}`)
await fs.writeFile(filePath, yaml.stringify(settings, { lineWidth: 0 }), "utf-8")
}

Expand Down Expand Up @@ -543,6 +545,10 @@ export class CustomModesManager {

// Clear cache when modes are deleted
this.clearCache()

// Add a small delay to ensure file operations are complete before refreshing UI
await new Promise((resolve) => setTimeout(resolve, 100))

await this.refreshMergedState()
})
} catch (error) {
Expand Down Expand Up @@ -1002,6 +1008,136 @@ export class CustomModesManager {
this.cachedAt = 0
}

/**
* Set the disabled state of a mode
*/
public async setModeDisabled(slug: string, disabled: boolean): Promise<void> {
try {
const modes = await this.getCustomModes()
const mode = modes.find((m) => m.slug === slug)

if (!mode) {
throw new Error(`Mode not found: ${slug}`)
}

// Determine which file to update based on source
let targetPath: string
if (mode.source === "project") {
const roomodesPath = await this.getWorkspaceRoomodes()
if (!roomodesPath) {
throw new Error("No .roomodes file found in workspace")
}
targetPath = roomodesPath
} else {
targetPath = await this.getCustomModesFilePath()
}

await this.queueWrite(async () => {
await this.updateModesInFile(targetPath, (modes) => {
return modes.map((m) => (m.slug === slug ? { ...m, disabled } : m))
})
})

await this.refreshMergedState()
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
logger.error("Failed to update mode disabled state", { slug, disabled, error: errorMessage })
throw error
}
}

/**
* Set disabled state for multiple modes
*/
public async setMultipleModesDisabled(updates: Array<{ slug: string; disabled: boolean }>): Promise<void> {
try {
const modes = await this.getCustomModes()
const updatesByFile = new Map<string, Array<{ slug: string; disabled: boolean }>>()
// Collect built-in modes that need to be copied into the global custom modes file
const addsForSettings: ModeConfig[] = []

// Group updates by source/file
for (const update of updates) {
const mode = modes.find((m) => m.slug === update.slug)
if (!mode) {
// If mode isn't present in custom modes, it might be a built-in mode.
// Try loading built-in definitions and create a custom copy in the global file.
try {
const { modes: builtInModes } = await import("../../shared/modes")
const builtIn = builtInModes.find((b: any) => b.slug === update.slug)
if (builtIn) {
// Create a global-scoped custom mode based on the built-in definition
const modeToAdd: ModeConfig = {
...builtIn,
disabled: update.disabled,
source: "global",
}
addsForSettings.push(modeToAdd)
// Add a corresponding update entry for the settings file
const settingsPath = await this.getCustomModesFilePath()
if (!updatesByFile.has(settingsPath)) {
updatesByFile.set(settingsPath, [])
}
updatesByFile.get(settingsPath)!.push(update)
continue
}
console.warn(`Mode not found: ${update.slug}`)
} catch (e) {
console.warn(`Mode not found and failed to load built-ins: ${update.slug}`)
}
continue
}

let targetPath: string
if (mode.source === "project") {
const roomodesPath = await this.getWorkspaceRoomodes()
if (!roomodesPath) {
console.warn(`No .roomodes file found for project mode: ${update.slug}`)
continue
}
targetPath = roomodesPath
} else {
targetPath = await this.getCustomModesFilePath()
}

if (!updatesByFile.has(targetPath)) {
updatesByFile.set(targetPath, [])
}
updatesByFile.get(targetPath)!.push(update)
}

// Apply updates to each file
for (const [filePath, fileUpdates] of updatesByFile) {
await this.queueWrite(async () => {
await this.updateModesInFile(filePath, (modes) => {
return modes.map((m) => {
const update = fileUpdates.find((u) => u.slug === m.slug)
return update ? { ...m, disabled: update.disabled } : m
})
})
})
}

// If we found built-in modes that need to be copied to settings, add them now
if (addsForSettings.length > 0) {
const settingsPath = await this.getCustomModesFilePath()
await this.queueWrite(async () => {
await this.updateModesInFile(settingsPath, (modes) => {
// Remove any existing entries with the same slugs, then append new ones
const filtered = modes.filter((m) => !addsForSettings.some((a) => a.slug === m.slug))
return [...filtered, ...addsForSettings]
})
})
}

await this.refreshMergedState()
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
logger.error("Failed to update multiple modes disabled state", { updates, error: errorMessage })
throw error
}
}

dispose(): void {
for (const disposable of this.disposables) {
disposable.dispose()
Expand Down
5 changes: 4 additions & 1 deletion src/core/prompts/sections/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ export async function getModesSection(context: vscode.ExtensionContext): Promise
// Get all modes with their overrides from extension state
const allModes = await getAllModesWithPrompts(context)

// Filter out disabled modes to reduce system prompt size
const enabledModes = allModes.filter((mode) => !mode.disabled)

let modesContent = `====

MODES

- These are the currently available modes:
${allModes
${enabledModes
.map((mode: ModeConfig) => {
let description: string
if (mode.whenToUse && mode.whenToUse.trim() !== "") {
Expand Down
33 changes: 33 additions & 0 deletions src/core/tools/__tests__/newTaskTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,32 @@ vi.mock("vscode", () => ({
vi.mock("../../../shared/modes", () => ({
getModeBySlug: vi.fn(),
defaultModeSlug: "ask",
modes: [
{
slug: "code",
name: "Code Mode",
roleDefinition: "Test role definition",
groups: ["command", "read", "edit"],
},
{ slug: "ask", name: "Ask Mode", roleDefinition: "Test role definition", groups: ["command", "read", "edit"] },
],
}))

// Mock ModeManager - must be hoisted before newTaskTool import
const mockValidateModeSwitch = vi.fn().mockResolvedValue({ isValid: true })
const mockGetEnabledModes = vi.fn().mockResolvedValue([
{ slug: "code", name: "Code Mode" },
{ slug: "ask", name: "Ask Mode" },
])

vi.mock("../../services/ModeManager", () => ({
ModeManager: vi.fn().mockImplementation((context, customModesManager) => {
console.log("Mock ModeManager constructor called with:", { context, customModesManager })
return {
validateModeSwitch: mockValidateModeSwitch,
getEnabledModes: mockGetEnabledModes,
}
}),
}))

vi.mock("../../prompts/responses", () => ({
Expand Down Expand Up @@ -79,6 +105,13 @@ const mockCline = {
getState: vi.fn(() => ({ customModes: [], mode: "ask" })),
handleModeSwitch: vi.fn(),
createTask: mockCreateTask,
context: {
globalState: { get: vi.fn(), update: vi.fn() },
workspaceState: { get: vi.fn(), update: vi.fn() },
},
customModesManager: {
getCustomModes: vi.fn().mockResolvedValue([]),
},
})),
},
}
Expand Down
21 changes: 21 additions & 0 deletions src/core/tools/newTaskTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RooCodeEventName, TodoItem } from "@roo-code/types"
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
import { Task } from "../task/Task"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
import { ModeManager } from "../../services/ModeManager"
import { formatResponse } from "../prompts/responses"
import { t } from "../../i18n"
import { parseMarkdownChecklist } from "./updateTodoListTool"
Expand Down Expand Up @@ -95,6 +96,26 @@ export async function newTaskTool(
return
}

// Validate mode availability (not disabled)
if (!provider.context || !provider.customModesManager) {
pushToolResult(formatResponse.toolError("Unable to access mode configuration."))
return
}

const modeManager = new ModeManager(provider.context, provider.customModesManager)
const validationResult = await modeManager.validateModeSwitch(mode)

if (!validationResult.isValid) {
cline.recordToolError("new_task")
// Provide helpful error message with available modes
const enabledModes = await modeManager.getEnabledModes()
const availableModesList = enabledModes.map((m) => `- ${m.slug}: ${m.name}`).join("\n")

const errorMessage = `${validationResult.errorMessage}\n\nAvailable enabled modes:\n${availableModesList}`
pushToolResult(formatResponse.toolError(errorMessage))
return
}

const toolMessage = JSON.stringify({
tool: "newTask",
mode: targetMode.name,
Expand Down
Loading