Skip to content

Commit 6a564fb

Browse files
committed
feat: Add folder deletion when custom mode is deleted (#5210)
1 parent 951f2d0 commit 6a564fb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+718
-48
lines changed

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,75 @@ const mockGetModels = getModels as Mock<typeof getModels>
1414
const mockClineProvider = {
1515
getState: vi.fn(),
1616
postMessageToWebview: vi.fn(),
17+
customModesManager: {
18+
getCustomModes: vi.fn(),
19+
deleteCustomMode: vi.fn(),
20+
},
21+
context: {
22+
extensionPath: "/mock/extension/path",
23+
globalStorageUri: { fsPath: "/mock/global/storage" },
24+
},
25+
contextProxy: {
26+
context: {
27+
extensionPath: "/mock/extension/path",
28+
globalStorageUri: { fsPath: "/mock/global/storage" },
29+
},
30+
setValue: vi.fn(),
31+
},
32+
log: vi.fn(),
33+
postStateToWebview: vi.fn(),
1734
} as unknown as ClineProvider
1835

36+
import { t } from "../../../i18n"
37+
38+
vi.mock("vscode", () => ({
39+
window: {
40+
showInformationMessage: vi.fn(),
41+
showErrorMessage: vi.fn(),
42+
},
43+
workspace: {
44+
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
45+
},
46+
}))
47+
48+
vi.mock("../../../i18n", () => ({
49+
t: vi.fn((key: string, args?: Record<string, any>) => {
50+
// For the delete confirmation with rules, we need to return the interpolated string
51+
if (key === "common:confirmation.delete_custom_mode_with_rules" && args) {
52+
return `Are you sure you want to delete this ${args.scope} mode?\n\nThis will also delete the associated rules folder at:\n${args.rulesFolderPath}`
53+
}
54+
// Return the translated value for "Yes"
55+
if (key === "common:answers.yes") {
56+
return "Yes"
57+
}
58+
// Return the translated value for "Cancel"
59+
if (key === "common:answers.cancel") {
60+
return "Cancel"
61+
}
62+
return key
63+
}),
64+
}))
65+
66+
vi.mock("fs/promises", () => ({
67+
default: {
68+
rm: vi.fn().mockResolvedValue(undefined),
69+
mkdir: vi.fn().mockResolvedValue(undefined),
70+
},
71+
rm: vi.fn().mockResolvedValue(undefined),
72+
mkdir: vi.fn().mockResolvedValue(undefined),
73+
}))
74+
75+
import * as vscode from "vscode"
76+
import fs from "fs/promises"
77+
import * as fsUtils from "../../../utils/fs"
78+
import { getWorkspacePath } from "../../../utils/path"
79+
import { ensureSettingsDirectoryExists } from "../../../utils/globalContext"
80+
import type { ModeConfig } from "@roo-code/types"
81+
82+
vi.mock("../../../utils/fs")
83+
vi.mock("../../../utils/path")
84+
vi.mock("../../../utils/globalContext")
85+
1986
describe("webviewMessageHandler - requestRouterModels", () => {
2087
beforeEach(() => {
2188
vi.clearAllMocks()
@@ -295,3 +362,108 @@ describe("webviewMessageHandler - requestRouterModels", () => {
295362
})
296363
})
297364
})
365+
366+
describe("webviewMessageHandler - deleteCustomMode", () => {
367+
beforeEach(() => {
368+
vi.clearAllMocks()
369+
vi.mocked(getWorkspacePath).mockReturnValue("/mock/workspace")
370+
vi.mocked(vscode.window.showErrorMessage).mockResolvedValue(undefined)
371+
vi.mocked(ensureSettingsDirectoryExists).mockResolvedValue("/mock/global/storage/.roo")
372+
})
373+
374+
it("should delete a project mode and its rules folder", async () => {
375+
const slug = "test-project-mode"
376+
const rulesFolderPath = `/mock/workspace/.roo/rules-${slug}`
377+
378+
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
379+
{
380+
name: "Test Project Mode",
381+
slug,
382+
roleDefinition: "Test Role",
383+
groups: [],
384+
source: "project",
385+
} as ModeConfig,
386+
])
387+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true)
388+
vi.mocked(mockClineProvider.customModesManager.deleteCustomMode).mockResolvedValue(undefined)
389+
390+
await webviewMessageHandler(mockClineProvider, { type: "deleteCustomMode", slug })
391+
392+
// The confirmation dialog is now handled in the webview, so we don't expect showInformationMessage to be called
393+
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
394+
expect(mockClineProvider.customModesManager.deleteCustomMode).toHaveBeenCalledWith(slug)
395+
expect(fs.rm).toHaveBeenCalledWith(rulesFolderPath, { recursive: true, force: true })
396+
})
397+
398+
it("should delete a global mode and its rules folder", async () => {
399+
const slug = "test-global-mode"
400+
const rulesFolderPath = `/Users/hrudolph/.roo/rules-${slug}`
401+
402+
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
403+
{
404+
name: "Test Global Mode",
405+
slug,
406+
roleDefinition: "Test Role",
407+
groups: [],
408+
source: "global",
409+
} as ModeConfig,
410+
])
411+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true)
412+
vi.mocked(mockClineProvider.customModesManager.deleteCustomMode).mockResolvedValue(undefined)
413+
414+
await webviewMessageHandler(mockClineProvider, { type: "deleteCustomMode", slug })
415+
416+
// The confirmation dialog is now handled in the webview, so we don't expect showInformationMessage to be called
417+
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
418+
expect(mockClineProvider.customModesManager.deleteCustomMode).toHaveBeenCalledWith(slug)
419+
expect(fs.rm).toHaveBeenCalledWith(rulesFolderPath, { recursive: true, force: true })
420+
})
421+
422+
it("should only delete the mode when rules folder does not exist", async () => {
423+
const slug = "test-mode-no-rules"
424+
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
425+
{
426+
name: "Test Mode No Rules",
427+
slug,
428+
roleDefinition: "Test Role",
429+
groups: [],
430+
source: "project",
431+
} as ModeConfig,
432+
])
433+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false)
434+
vi.mocked(mockClineProvider.customModesManager.deleteCustomMode).mockResolvedValue(undefined)
435+
436+
await webviewMessageHandler(mockClineProvider, { type: "deleteCustomMode", slug })
437+
438+
// The confirmation dialog is now handled in the webview, so we don't expect showInformationMessage to be called
439+
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
440+
expect(mockClineProvider.customModesManager.deleteCustomMode).toHaveBeenCalledWith(slug)
441+
expect(fs.rm).not.toHaveBeenCalled()
442+
})
443+
444+
it("should handle errors when deleting rules folder", async () => {
445+
const slug = "test-mode-error"
446+
const rulesFolderPath = `/mock/workspace/.roo/rules-${slug}`
447+
const error = new Error("Permission denied")
448+
449+
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
450+
{
451+
name: "Test Mode Error",
452+
slug,
453+
roleDefinition: "Test Role",
454+
groups: [],
455+
source: "project",
456+
} as ModeConfig,
457+
])
458+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true)
459+
vi.mocked(mockClineProvider.customModesManager.deleteCustomMode).mockResolvedValue(undefined)
460+
vi.mocked(fs.rm).mockRejectedValue(error)
461+
462+
await webviewMessageHandler(mockClineProvider, { type: "deleteCustomMode", slug })
463+
464+
expect(mockClineProvider.customModesManager.deleteCustomMode).toHaveBeenCalledWith(slug)
465+
expect(fs.rm).toHaveBeenCalledWith(rulesFolderPath, { recursive: true, force: true })
466+
// No error response is sent anymore - we just continue with deletion
467+
expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalled()
468+
})
469+
})

src/core/webview/webviewMessageHandler.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
3434
import { openMention } from "../mentions"
3535
import { TelemetrySetting } from "../../shared/TelemetrySetting"
3636
import { getWorkspacePath } from "../../utils/path"
37+
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
3738
import { Mode, defaultModeSlug } from "../../shared/modes"
3839
import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
3940
import { GetModelsOptions } from "../../shared/api"
@@ -1485,17 +1486,60 @@ export const webviewMessageHandler = async (
14851486
break
14861487
case "deleteCustomMode":
14871488
if (message.slug) {
1488-
const answer = await vscode.window.showInformationMessage(
1489-
t("common:confirmation.delete_custom_mode"),
1490-
{ modal: true },
1491-
t("common:answers.yes"),
1492-
)
1489+
// Get the mode details to determine source and rules folder path
1490+
const customModes = await provider.customModesManager.getCustomModes()
1491+
const modeToDelete = customModes.find((mode) => mode.slug === message.slug)
14931492

1494-
if (answer !== t("common:answers.yes")) {
1493+
if (!modeToDelete) {
1494+
break
1495+
}
1496+
1497+
// Determine the scope based on source (project or global)
1498+
const scope = modeToDelete.source || "global"
1499+
1500+
// Determine the rules folder path
1501+
let rulesFolderPath: string
1502+
if (scope === "project") {
1503+
const workspacePath = getWorkspacePath()
1504+
if (workspacePath) {
1505+
rulesFolderPath = path.join(workspacePath, ".roo", `rules-${message.slug}`)
1506+
} else {
1507+
rulesFolderPath = path.join(".roo", `rules-${message.slug}`)
1508+
}
1509+
} else {
1510+
// Global scope - use home directory
1511+
const homeDir = process.env.HOME || process.env.USERPROFILE || ""
1512+
rulesFolderPath = path.join(homeDir, ".roo", `rules-${message.slug}`)
1513+
}
1514+
1515+
// Check if the rules folder exists
1516+
const rulesFolderExists = await fileExistsAtPath(rulesFolderPath)
1517+
1518+
// If this is a check request, send back the folder info
1519+
if (message.checkOnly) {
1520+
await provider.postMessageToWebview({
1521+
type: "deleteCustomModeCheck",
1522+
slug: message.slug,
1523+
rulesFolderExists,
1524+
rulesFolderPath: rulesFolderExists ? rulesFolderPath : undefined,
1525+
})
14951526
break
14961527
}
14971528

1529+
// Delete the mode
14981530
await provider.customModesManager.deleteCustomMode(message.slug)
1531+
1532+
// Delete the rules folder if it exists
1533+
if (rulesFolderExists) {
1534+
try {
1535+
await fs.rm(rulesFolderPath, { recursive: true, force: true })
1536+
provider.log(`Deleted rules folder for mode ${message.slug}: ${rulesFolderPath}`)
1537+
} catch (error) {
1538+
provider.log(`Failed to delete rules folder for mode ${message.slug}: ${error}`)
1539+
// Continue with mode deletion even if folder deletion fails
1540+
}
1541+
}
1542+
14991543
// Switch back to default mode after deletion
15001544
await updateGlobalState("mode", defaultModeSlug)
15011545
await provider.postStateToWebview()

src/i18n/locales/ca/common.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"confirmation": {
2222
"reset_state": "Estàs segur que vols restablir tots els estats i emmagatzematge secret a l'extensió? Això no es pot desfer.",
2323
"delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?",
24-
"delete_custom_mode": "Estàs segur que vols eliminar aquest mode personalitzat?",
24+
"delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}",
2525
"delete_message": "Què vols eliminar?",
2626
"just_this_message": "Només aquest missatge",
2727
"this_and_subsequent": "Aquest i tots els missatges posteriors"
@@ -141,6 +141,10 @@
141141
"resetFailed": "Error en restablir els modes personalitzats: {{error}}",
142142
"modeNotFound": "Error d'escriptura: Mode no trobat",
143143
"noWorkspaceForProject": "No s'ha trobat cap carpeta d'espai de treball per al mode específic del projecte"
144+
},
145+
"scope": {
146+
"project": "projecte",
147+
"global": "global"
144148
}
145149
},
146150
"mdm": {
@@ -149,5 +153,14 @@
149153
"organization_mismatch": "Has d'estar autenticat amb el compte de Roo Code Cloud de la teva organització.",
150154
"verification_failed": "No s'ha pogut verificar l'autenticació de l'organització."
151155
}
156+
},
157+
"prompts": {
158+
"deleteMode": {
159+
"title": "Suprimeix el mode personalitzat",
160+
"description": "Esteu segur que voleu suprimir aquest mode {{scope}}? Això també suprimirà la carpeta de regles associada a: {{rulesFolderPath}}",
161+
"descriptionNoRules": "Esteu segur que voleu suprimir aquest mode personalitzat?",
162+
"cancel": "Cancel·la",
163+
"confirm": "Suprimeix"
164+
}
152165
}
153166
}

src/i18n/locales/de/common.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"confirmation": {
1818
"reset_state": "Möchtest du wirklich alle Zustände und geheimen Speicher in der Erweiterung zurücksetzen? Dies kann nicht rückgängig gemacht werden.",
1919
"delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?",
20-
"delete_custom_mode": "Möchtest du diesen benutzerdefinierten Modus wirklich löschen?",
20+
"delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}",
2121
"delete_message": "Was möchtest du löschen?",
2222
"just_this_message": "Nur diese Nachricht",
2323
"this_and_subsequent": "Diese und alle nachfolgenden Nachrichten"
@@ -141,6 +141,10 @@
141141
"resetFailed": "Fehler beim Zurücksetzen der benutzerdefinierten Modi: {{error}}",
142142
"modeNotFound": "Schreibfehler: Modus nicht gefunden",
143143
"noWorkspaceForProject": "Kein Arbeitsbereich-Ordner für projektspezifischen Modus gefunden"
144+
},
145+
"scope": {
146+
"project": "projekt",
147+
"global": "global"
144148
}
145149
},
146150
"mdm": {
@@ -149,5 +153,14 @@
149153
"organization_mismatch": "Du musst mit dem Roo Code Cloud-Konto deiner Organisation authentifiziert sein.",
150154
"verification_failed": "Die Organisationsauthentifizierung konnte nicht verifiziert werden."
151155
}
156+
},
157+
"prompts": {
158+
"deleteMode": {
159+
"title": "Benutzerdefinierten Modus löschen",
160+
"description": "Bist du sicher, dass du diesen {{scope}}-Modus löschen möchtest? Dadurch wird auch der zugehörige Regelordner unter {{rulesFolderPath}} gelöscht",
161+
"descriptionNoRules": "Bist du sicher, dass du diesen benutzerdefinierten Modus löschen möchtest?",
162+
"cancel": "Abbrechen",
163+
"confirm": "Löschen"
164+
}
152165
}
153166
}

src/i18n/locales/en/common.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"confirmation": {
1818
"reset_state": "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.",
1919
"delete_config_profile": "Are you sure you want to delete this configuration profile?",
20-
"delete_custom_mode": "Are you sure you want to delete this custom mode?",
20+
"delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}",
2121
"delete_message": "What would you like to delete?",
2222
"just_this_message": "Just this message",
2323
"this_and_subsequent": "This and all subsequent messages"
@@ -130,6 +130,10 @@
130130
"resetFailed": "Failed to reset custom modes: {{error}}",
131131
"modeNotFound": "Write error: Mode not found",
132132
"noWorkspaceForProject": "No workspace folder found for project-specific mode"
133+
},
134+
"scope": {
135+
"project": "project",
136+
"global": "global"
133137
}
134138
},
135139
"mdm": {
@@ -138,5 +142,14 @@
138142
"organization_mismatch": "You must be authenticated with your organization's Roo Code Cloud account.",
139143
"verification_failed": "Unable to verify organization authentication."
140144
}
145+
},
146+
"prompts": {
147+
"deleteMode": {
148+
"title": "Delete Custom Mode",
149+
"description": "Are you sure you want to delete this {{scope}} mode? This will also delete the associated rules folder at: {{rulesFolderPath}}",
150+
"descriptionNoRules": "Are you sure you want to delete this custom mode?",
151+
"cancel": "Cancel",
152+
"confirm": "Delete"
153+
}
141154
}
142155
}

src/i18n/locales/es/common.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"confirmation": {
1818
"reset_state": "¿Estás seguro de que deseas restablecer todo el estado y el almacenamiento secreto en la extensión? Esta acción no se puede deshacer.",
1919
"delete_config_profile": "¿Estás seguro de que deseas eliminar este perfil de configuración?",
20-
"delete_custom_mode": "¿Estás seguro de que deseas eliminar este modo personalizado?",
20+
"delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}",
2121
"delete_message": "¿Qué deseas eliminar?",
2222
"just_this_message": "Solo este mensaje",
2323
"this_and_subsequent": "Este y todos los mensajes posteriores"
@@ -141,6 +141,10 @@
141141
"resetFailed": "Error al restablecer modos personalizados: {{error}}",
142142
"modeNotFound": "Error de escritura: Modo no encontrado",
143143
"noWorkspaceForProject": "No se encontró carpeta de espacio de trabajo para modo específico del proyecto"
144+
},
145+
"scope": {
146+
"project": "proyecto",
147+
"global": "global"
144148
}
145149
},
146150
"mdm": {
@@ -149,5 +153,14 @@
149153
"organization_mismatch": "Debes estar autenticado con la cuenta de Roo Code Cloud de tu organización.",
150154
"verification_failed": "No se pudo verificar la autenticación de la organización."
151155
}
156+
},
157+
"prompts": {
158+
"deleteMode": {
159+
"title": "Eliminar modo personalizado",
160+
"description": "¿Estás seguro de que quieres eliminar este modo {{scope}}? Esto también eliminará la carpeta de reglas asociada en: {{rulesFolderPath}}",
161+
"descriptionNoRules": "¿Estás seguro de que quieres eliminar este modo personalizado?",
162+
"cancel": "Cancelar",
163+
"confirm": "Eliminar"
164+
}
152165
}
153166
}

0 commit comments

Comments
 (0)