Skip to content

Commit eef33ad

Browse files
committed
feat: implement batch mode updates and delete confirmation dialog in ModeEnableDisableDialog
1 parent 3cb881d commit eef33ad

File tree

3 files changed

+123
-17
lines changed

3 files changed

+123
-17
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2640,39 +2640,38 @@ export const webviewMessageHandler = async (
26402640
// Handle bulk mode enable/disable updates
26412641
if (message.updates && typeof message.updates === "object") {
26422642
try {
2643-
const { ModeManager } = await import("../../services/ModeManager")
2644-
const modeManager = new ModeManager(provider.context, provider.customModesManager)
2645-
2646-
// Process each mode update
2643+
// Convert updates object into array for batch processing
2644+
const updatesArray: Array<{ slug: string; disabled: boolean }> = []
26472645
for (const [slug, disabled] of Object.entries(message.updates)) {
26482646
if (typeof disabled === "boolean") {
2649-
await modeManager.setModeDisabled(slug, disabled)
2647+
updatesArray.push({ slug, disabled })
26502648
}
26512649
}
26522650

2653-
// Update global state after all changes
2651+
if (updatesArray.length > 0) {
2652+
// Use CustomModesManager batch API which groups updates by file and
2653+
// performs each file write in a single queued operation.
2654+
await provider.customModesManager.setMultipleModesDisabled(updatesArray)
2655+
}
2656+
2657+
// Refresh state and notify webview
26542658
const customModes = await provider.customModesManager.getCustomModes()
26552659
await updateGlobalState("customModes", customModes)
26562660
await provider.postStateToWebview()
26572661

2658-
// Send success response
2659-
await provider.postMessageToWebview({
2660-
type: "modeDisabledStatesUpdated",
2661-
success: true,
2662-
})
2662+
await provider.postMessageToWebview({ type: "modeDisabledStatesUpdated", success: true })
26632663

2664-
vscode.window.showInformationMessage("Mode settings updated successfully")
2664+
vscode.window.showInformationMessage(t("common:info.modes_updated"))
26652665
} catch (error) {
26662666
provider.log(`Error updating mode disabled states: ${error}`)
26672667

2668-
// Send error response
26692668
await provider.postMessageToWebview({
26702669
type: "modeDisabledStatesUpdated",
26712670
success: false,
26722671
error: error instanceof Error ? error.message : String(error),
26732672
})
26742673

2675-
vscode.window.showErrorMessage("Failed to update mode settings")
2674+
vscode.window.showErrorMessage(t("common:errors.update_modes_failed"))
26762675
}
26772676
}
26782677
break

webview-ui/src/App.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,23 @@ const App = () => {
307307
onOpenChange={(open: boolean) => setModeDialogOpen(open)}
308308
modes={modeDialogModes}
309309
onSave={(updatedModes: any[]) => {
310-
// Build updates map slug -> disabled
310+
// Only send updates for modes whose disabled state actually changed.
311311
const updates: Record<string, boolean> = {}
312+
const previousModesBySlug: Record<string, any> = {}
313+
for (const m of modeDialogModes) {
314+
previousModesBySlug[m.slug] = m
315+
}
312316
for (const m of updatedModes) {
313-
updates[m.slug] = !!m.disabled
317+
const prev = previousModesBySlug[m.slug]
318+
const prevDisabled = !!(prev && prev.disabled)
319+
const newDisabled = !!m.disabled
320+
if (prevDisabled !== newDisabled) {
321+
updates[m.slug] = newDisabled
322+
}
323+
}
324+
if (Object.keys(updates).length > 0) {
325+
vscode.postMessage({ type: "updateModeDisabledStates", updates })
314326
}
315-
vscode.postMessage({ type: "updateModeDisabledStates", updates })
316327
// Optimistically update local dialog modes
317328
setModeDialogModes(updatedModes)
318329
}}

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

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,18 @@ import {
1212
Badge,
1313
Separator,
1414
StandardTooltip,
15+
AlertDialog,
16+
AlertDialogContent,
17+
AlertDialogHeader,
18+
AlertDialogTitle,
19+
AlertDialogDescription,
20+
AlertDialogFooter,
21+
AlertDialogCancel,
22+
AlertDialogAction,
1523
} from "@src/components/ui"
1624
import { cn } from "@/lib/utils"
1725
import type { ModeConfig } from "@roo-code/types"
26+
import { useAppTranslation } from "@src/i18n/TranslationContext"
1827

1928
const SOURCE_INFO = {
2029
builtin: {
@@ -51,6 +60,11 @@ interface ModeEnableDisableDialogProps {
5160
onSave: (updatedModes: ModeWithSource[]) => void
5261
}
5362

63+
interface DeleteState {
64+
open: boolean
65+
tMode?: { slug: string; name: string; source?: string; rulesFolderPath?: string } | null
66+
}
67+
5468
interface GroupedModes {
5569
builtin: ModeWithSource[]
5670
global: ModeWithSource[]
@@ -87,6 +101,10 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
87101
const [localModes, setLocalModes] = useState<ModeWithSource[]>(modes)
88102
const [hasChanges, setHasChanges] = useState(false)
89103

104+
const { t } = useAppTranslation()
105+
106+
const [deleteState, setDeleteState] = useState<DeleteState>({ open: false, tMode: null })
107+
90108
// Update local state when props change
91109
useEffect(() => {
92110
setLocalModes(modes)
@@ -184,10 +202,55 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
184202
</div>
185203
<div className="status-indicator flex items-center gap-1 flex-shrink-0">
186204
{mode.disabled ? <EyeOff className="size-4 disabled" /> : <Eye className="size-4 enabled" />}
205+
{/* Show delete for global custom modes (they override built-in or are user-created) */}
206+
{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>
224+
)}
187225
</div>
188226
</div>
189227
)
190228

229+
// Listen for delete check responses from extension
230+
useEffect(() => {
231+
const handler = (e: MessageEvent) => {
232+
const message = e.data
233+
if (message.type === "deleteCustomModeCheck") {
234+
if (message.slug && deleteState.tMode && deleteState.tMode.slug === message.slug) {
235+
setDeleteState({
236+
open: true,
237+
tMode: { ...deleteState.tMode, rulesFolderPath: message.rulesFolderPath },
238+
})
239+
}
240+
}
241+
}
242+
window.addEventListener("message", handler)
243+
return () => window.removeEventListener("message", handler)
244+
}, [deleteState.tMode])
245+
246+
const confirmDelete = () => {
247+
if (!deleteState.tMode) return
248+
window.parent.postMessage({ type: "deleteCustomMode", slug: deleteState.tMode.slug }, "*")
249+
setDeleteState({ open: false, tMode: null })
250+
// Close dialog after request; backend will refresh state
251+
onOpenChange(false)
252+
}
253+
191254
// Source group component
192255
const SourceGroup: React.FC<{ source: ModeSource; modes: ModeWithSource[] }> = ({ source, modes }) => {
193256
const sourceInfo = SOURCE_INFO[source]
@@ -317,6 +380,39 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
317380
</Button>
318381
</div>
319382
</DialogFooter>
383+
384+
{/* Delete confirmation dialog for global custom modes */}
385+
<AlertDialog open={!!deleteState.open} onOpenChange={(open) => setDeleteState((s) => ({ ...s, open }))}>
386+
<AlertDialogContent>
387+
<AlertDialogHeader>
388+
<AlertDialogTitle>{t ? t("prompts:deleteMode.title") : "Delete mode"}</AlertDialogTitle>
389+
<AlertDialogDescription>
390+
{deleteState.tMode && (
391+
<>
392+
{t
393+
? t("prompts:deleteMode.message", { modeName: deleteState.tMode.name })
394+
: `Delete ${deleteState.tMode.name}?`}
395+
{deleteState.tMode.rulesFolderPath && (
396+
<div className="mt-2">
397+
{t
398+
? t("prompts:deleteMode.rulesFolder", {
399+
folderPath: deleteState.tMode.rulesFolderPath,
400+
})
401+
: deleteState.tMode.rulesFolderPath}
402+
</div>
403+
)}
404+
</>
405+
)}
406+
</AlertDialogDescription>
407+
</AlertDialogHeader>
408+
<AlertDialogFooter>
409+
<AlertDialogCancel>{t ? t("prompts:deleteMode.cancel") : "Cancel"}</AlertDialogCancel>
410+
<AlertDialogAction onClick={confirmDelete}>
411+
{t ? t("prompts:deleteMode.confirm") : "Delete"}
412+
</AlertDialogAction>
413+
</AlertDialogFooter>
414+
</AlertDialogContent>
415+
</AlertDialog>
320416
</DialogContent>
321417
</Dialog>
322418
)

0 commit comments

Comments
 (0)