Skip to content

Commit 3b3da61

Browse files
committed
Fixes #5210: Add warning when deleting modes with associated rules folders
- Enhanced deleteCustomMode confirmation dialog to warn users when .roo/rules-{mode} folder exists - Added new translation key 'delete_custom_mode_with_rules' for enhanced warning message - Modified webview message handler to check for rules folder existence before showing confirmation - Added comprehensive tests covering all scenarios: enhanced warnings, standard warnings, error handling - Preserves existing behavior for modes without rules folders - Handles edge cases gracefully (no workspace, file check failures, mode not found)
1 parent 3a8ba27 commit 3b3da61

File tree

3 files changed

+253
-1
lines changed

3 files changed

+253
-1
lines changed

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

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,33 @@ import type { Mock } from "vitest"
22

33
// Mock dependencies - must come before imports
44
vi.mock("../../../api/providers/fetchers/modelCache")
5+
vi.mock("../../../utils/fs")
6+
vi.mock("../../../utils/path")
7+
vi.mock("../../../i18n", () => ({
8+
t: vi.fn((key: string) => key), // Return the key as-is for testing
9+
}))
10+
vi.mock("vscode", () => ({
11+
window: {
12+
showInformationMessage: vi.fn(),
13+
showErrorMessage: vi.fn(),
14+
},
15+
workspace: {
16+
workspaceFolders: [],
17+
},
18+
}))
519

620
import { webviewMessageHandler } from "../webviewMessageHandler"
721
import type { ClineProvider } from "../ClineProvider"
822
import { getModels } from "../../../api/providers/fetchers/modelCache"
923
import type { ModelRecord } from "../../../shared/api"
24+
import { fileExistsAtPath } from "../../../utils/fs"
25+
import { getWorkspacePath } from "../../../utils/path"
26+
import * as vscode from "vscode"
27+
28+
const mockFileExistsAtPath = fileExistsAtPath as Mock<typeof fileExistsAtPath>
29+
const mockGetWorkspacePath = getWorkspacePath as Mock<typeof getWorkspacePath>
30+
const mockShowInformationMessage = vscode.window.showInformationMessage as Mock
31+
const mockShowErrorMessage = vscode.window.showErrorMessage as Mock
1032

1133
const mockGetModels = getModels as Mock<typeof getModels>
1234

@@ -275,6 +297,199 @@ describe("webviewMessageHandler - requestRouterModels", () => {
275297
})
276298
})
277299

300+
describe("webviewMessageHandler - deleteCustomMode", () => {
301+
const mockCustomModesManager = {
302+
getCustomModes: vi.fn(),
303+
deleteCustomMode: vi.fn(),
304+
}
305+
306+
const mockContextProxy = {
307+
setValue: vi.fn(),
308+
}
309+
310+
const mockProvider = {
311+
...mockClineProvider,
312+
customModesManager: mockCustomModesManager,
313+
contextProxy: mockContextProxy,
314+
postStateToWebview: vi.fn(),
315+
} as unknown as ClineProvider
316+
317+
beforeEach(() => {
318+
vi.clearAllMocks()
319+
mockGetWorkspacePath.mockReturnValue("/test/workspace")
320+
})
321+
322+
it("shows enhanced warning when rules folder exists", async () => {
323+
const testMode = {
324+
slug: "test-mode",
325+
name: "Test Mode",
326+
source: "project" as const,
327+
}
328+
329+
mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
330+
mockFileExistsAtPath.mockResolvedValue(true)
331+
mockShowInformationMessage.mockResolvedValue("common:answers.yes")
332+
333+
await webviewMessageHandler(mockProvider, {
334+
type: "deleteCustomMode",
335+
slug: "test-mode",
336+
})
337+
338+
// Verify rules folder check was performed
339+
expect(mockFileExistsAtPath).toHaveBeenCalledWith("/test/workspace/.roo/rules-test-mode")
340+
341+
// Verify enhanced warning message was shown
342+
expect(mockShowInformationMessage).toHaveBeenCalledWith(
343+
"common:confirmation.delete_custom_mode_with_rules",
344+
{ modal: true },
345+
"common:answers.yes",
346+
)
347+
348+
// Verify deletion proceeded
349+
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-mode")
350+
})
351+
352+
it("shows standard warning when rules folder does not exist", async () => {
353+
const testMode = {
354+
slug: "test-mode",
355+
name: "Test Mode",
356+
source: "global" as const,
357+
}
358+
359+
mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
360+
mockFileExistsAtPath.mockResolvedValue(false)
361+
mockShowInformationMessage.mockResolvedValue("common:answers.yes")
362+
363+
await webviewMessageHandler(mockProvider, {
364+
type: "deleteCustomMode",
365+
slug: "test-mode",
366+
})
367+
368+
// Verify standard warning message was shown
369+
expect(mockShowInformationMessage).toHaveBeenCalledWith(
370+
"common:confirmation.delete_custom_mode",
371+
{ modal: true },
372+
"common:answers.yes",
373+
)
374+
375+
// Verify deletion proceeded
376+
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-mode")
377+
})
378+
379+
it("shows standard warning when workspace path is not available", async () => {
380+
const testMode = {
381+
slug: "test-mode",
382+
name: "Test Mode",
383+
source: "project" as const,
384+
}
385+
386+
mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
387+
mockGetWorkspacePath.mockReturnValue("")
388+
mockShowInformationMessage.mockResolvedValue("common:answers.yes")
389+
390+
await webviewMessageHandler(mockProvider, {
391+
type: "deleteCustomMode",
392+
slug: "test-mode",
393+
})
394+
395+
// Verify file check was not performed
396+
expect(mockFileExistsAtPath).not.toHaveBeenCalled()
397+
398+
// Verify standard warning message was shown
399+
expect(mockShowInformationMessage).toHaveBeenCalledWith(
400+
"common:confirmation.delete_custom_mode",
401+
{ modal: true },
402+
"common:answers.yes",
403+
)
404+
})
405+
406+
it("shows standard warning when file check fails", async () => {
407+
const testMode = {
408+
slug: "test-mode",
409+
name: "Test Mode",
410+
source: "project" as const,
411+
}
412+
413+
mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
414+
mockFileExistsAtPath.mockRejectedValue(new Error("File system error"))
415+
mockShowInformationMessage.mockResolvedValue("common:answers.yes")
416+
417+
await webviewMessageHandler(mockProvider, {
418+
type: "deleteCustomMode",
419+
slug: "test-mode",
420+
})
421+
422+
// Verify standard warning message was shown (fallback)
423+
expect(mockShowInformationMessage).toHaveBeenCalledWith(
424+
"common:confirmation.delete_custom_mode",
425+
{ modal: true },
426+
"common:answers.yes",
427+
)
428+
429+
// Verify deletion still proceeded
430+
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-mode")
431+
})
432+
433+
it("does not delete when user cancels", async () => {
434+
const testMode = {
435+
slug: "test-mode",
436+
name: "Test Mode",
437+
source: "project" as const,
438+
}
439+
440+
mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
441+
mockFileExistsAtPath.mockResolvedValue(true)
442+
mockShowInformationMessage.mockResolvedValue(undefined) // User cancelled
443+
444+
await webviewMessageHandler(mockProvider, {
445+
type: "deleteCustomMode",
446+
slug: "test-mode",
447+
})
448+
449+
// Verify deletion was not performed
450+
expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled()
451+
})
452+
453+
it("shows error when mode is not found", async () => {
454+
mockCustomModesManager.getCustomModes.mockResolvedValue([])
455+
456+
await webviewMessageHandler(mockProvider, {
457+
type: "deleteCustomMode",
458+
slug: "nonexistent-mode",
459+
})
460+
461+
// Verify error message was shown
462+
expect(mockShowErrorMessage).toHaveBeenCalledWith("common:customModes.errors.modeNotFound")
463+
464+
// Verify deletion was not attempted
465+
expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled()
466+
})
467+
468+
it("correctly identifies GLOBAL mode source", async () => {
469+
const testMode = {
470+
slug: "global-mode",
471+
name: "Global Mode",
472+
source: "global" as const,
473+
}
474+
475+
mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
476+
mockFileExistsAtPath.mockResolvedValue(true)
477+
mockShowInformationMessage.mockResolvedValue("common:answers.yes")
478+
479+
await webviewMessageHandler(mockProvider, {
480+
type: "deleteCustomMode",
481+
slug: "global-mode",
482+
})
483+
484+
// Verify enhanced warning message shows GLOBAL
485+
expect(mockShowInformationMessage).toHaveBeenCalledWith(
486+
"common:confirmation.delete_custom_mode_with_rules",
487+
{ modal: true },
488+
"common:answers.yes",
489+
)
490+
})
491+
})
492+
278493
it("prefers config values over message values for LiteLLM", async () => {
279494
const mockModels: ModelRecord = {}
280495
mockGetModels.mockResolvedValue(mockModels)

src/core/webview/webviewMessageHandler.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1485,8 +1485,44 @@ export const webviewMessageHandler = async (
14851485
break
14861486
case "deleteCustomMode":
14871487
if (message.slug) {
1488+
// Get the mode details to determine source and name
1489+
const customModes = await provider.customModesManager.getCustomModes()
1490+
const modeToDelete = customModes.find((mode) => mode.slug === message.slug)
1491+
1492+
if (!modeToDelete) {
1493+
vscode.window.showErrorMessage(t("common:customModes.errors.modeNotFound"))
1494+
break
1495+
}
1496+
1497+
// Check if rules folder exists
1498+
const workspacePath = getWorkspacePath()
1499+
let hasRulesFolder = false
1500+
1501+
if (workspacePath) {
1502+
const rulesFolderPath = path.join(workspacePath, ".roo", `rules-${message.slug}`)
1503+
try {
1504+
hasRulesFolder = await fileExistsAtPath(rulesFolderPath)
1505+
} catch (error) {
1506+
// If we can't check the folder, fall back to standard confirmation
1507+
hasRulesFolder = false
1508+
}
1509+
}
1510+
1511+
// Show appropriate confirmation dialog
1512+
let confirmationMessage: string
1513+
if (hasRulesFolder) {
1514+
const sourceText = modeToDelete.source === "project" ? "PROJECT" : "GLOBAL"
1515+
confirmationMessage = t("common:confirmation.delete_custom_mode_with_rules", {
1516+
source: sourceText,
1517+
modeName: modeToDelete.name,
1518+
slug: message.slug,
1519+
})
1520+
} else {
1521+
confirmationMessage = t("common:confirmation.delete_custom_mode")
1522+
}
1523+
14881524
const answer = await vscode.window.showInformationMessage(
1489-
t("common:confirmation.delete_custom_mode"),
1525+
confirmationMessage,
14901526
{ modal: true },
14911527
t("common:answers.yes"),
14921528
)

src/i18n/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
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?",
2020
"delete_custom_mode": "Are you sure you want to delete this custom mode?",
21+
"delete_custom_mode_with_rules": "Are you sure you want to delete this {{source}} mode '{{modeName}}'?\n\nNote: The associated rules folder at .roo/rules-{{slug}} will remain and must be deleted manually.",
2122
"delete_message": "What would you like to delete?",
2223
"just_this_message": "Just this message",
2324
"this_and_subsequent": "This and all subsequent messages"

0 commit comments

Comments
 (0)