Skip to content

Commit 4e7d6f1

Browse files
committed
feat: add confirmation dialogs for disabling built-in modes and refactor toggle functions in ModeEnableDisableDialog
1 parent 650ef0f commit 4e7d6f1

File tree

1 file changed

+103
-115
lines changed

1 file changed

+103
-115
lines changed

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

Lines changed: 103 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,6 @@ import {
1212
Badge,
1313
Separator,
1414
StandardTooltip,
15-
AlertDialog,
16-
AlertDialogContent,
17-
AlertDialogHeader,
18-
AlertDialogTitle,
19-
AlertDialogDescription,
20-
AlertDialogFooter,
21-
AlertDialogCancel,
22-
AlertDialogAction,
2315
} from "@src/components/ui"
2416
import { cn } from "@/lib/utils"
2517
import type { ModeConfig } from "@roo-code/types"
@@ -60,13 +52,6 @@ interface ModeEnableDisableDialogProps {
6052
onSave: (updatedModes: ModeWithSource[]) => void
6153
}
6254

63-
interface DeleteState {
64-
open: boolean
65-
// action: 'delete' | 'restore' - determines dialog wording
66-
action?: "delete" | "restore"
67-
tMode?: { slug: string; name: string; source?: string; rulesFolderPath?: string } | null
68-
}
69-
7055
interface GroupedModes {
7156
builtin: ModeWithSource[]
7257
global: ModeWithSource[]
@@ -102,10 +87,17 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
10287
}) => {
10388
const [localModes, setLocalModes] = useState<ModeWithSource[]>(modes)
10489
const [hasChanges, setHasChanges] = useState(false)
90+
const [confirmOpen, setConfirmOpen] = useState(false)
91+
const [pendingAction, setPendingAction] = useState<
92+
| { type: "mode"; payload: string }
93+
| { type: "source"; payload: { source: ModeSource; enable: boolean } }
94+
| { type: "all"; payload: { enable: boolean } }
95+
| null
96+
>(null)
10597

106-
const { t } = useAppTranslation()
98+
useAppTranslation()
10799

108-
const [deleteState, setDeleteState] = useState<DeleteState>({ open: false, tMode: null })
100+
// Delete handled in the mode settings window; no delete UI in this dialog.
109101

110102
// Update local state when props change
111103
useEffect(() => {
@@ -134,32 +126,69 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
134126
}, [localModes])
135127

136128
// Toggle a single mode's disabled state
137-
const toggleMode = (slug: string) => {
129+
const doToggleModeImmediate = (slug: string) => {
138130
setLocalModes((prev) => {
139131
const updated = prev.map((mode) => (mode.slug === slug ? { ...mode, disabled: !mode.disabled } : mode))
140132
return updated
141133
})
142134
setHasChanges(true)
143135
}
144136

145-
// Toggle all modes in a source group
146-
const toggleSourceGroup = (source: ModeSource, enable: boolean) => {
137+
const attemptToggleMode = (slug: string) => {
138+
const mode = localModes.find((m) => m.slug === slug)
139+
if (!mode) return
140+
// If disabling a builtin that is currently enabled, show confirmation
141+
if (mode.source === "builtin" && !mode.disabled) {
142+
setPendingAction({ type: "mode", payload: slug })
143+
setConfirmOpen(true)
144+
return
145+
}
146+
doToggleModeImmediate(slug)
147+
}
148+
149+
// Toggle all modes in a source group (with confirmation for builtin disables)
150+
const doToggleSourceGroupImmediate = (source: ModeSource, enable: boolean) => {
147151
setLocalModes((prev) => {
148152
const updated = prev.map((mode) => (mode.source === source ? { ...mode, disabled: !enable } : mode))
149153
return updated
150154
})
151155
setHasChanges(true)
152156
}
153157

154-
// Enable/disable all modes
155-
const toggleAllModes = (enable: boolean) => {
158+
const attemptToggleSourceGroup = (source: ModeSource, enable: boolean) => {
159+
if (source === "builtin" && enable === false) {
160+
// find builtin slugs that would be disabled
161+
const toDisable = localModes.filter((m) => m.source === "builtin" && !m.disabled).map((m) => m.slug)
162+
if (toDisable.length > 0) {
163+
setPendingAction({ type: "source", payload: { source, enable } })
164+
setConfirmOpen(true)
165+
return
166+
}
167+
}
168+
doToggleSourceGroupImmediate(source, enable)
169+
}
170+
171+
// Enable/disable all modes (with confirmation for builtin disables)
172+
const doToggleAllModesImmediate = (enable: boolean) => {
156173
setLocalModes((prev) => {
157174
const updated = prev.map((mode) => ({ ...mode, disabled: !enable }))
158175
return updated
159176
})
160177
setHasChanges(true)
161178
}
162179

180+
const attemptToggleAllModes = (enable: boolean) => {
181+
if (enable === false) {
182+
const toDisable = localModes.filter((m) => m.source === "builtin" && !m.disabled).map((m) => m.slug)
183+
if (toDisable.length > 0) {
184+
setPendingAction({ type: "all", payload: { enable } })
185+
setConfirmOpen(true)
186+
return
187+
}
188+
}
189+
doToggleAllModesImmediate(enable)
190+
}
191+
163192
// Handle save
164193
const handleSave = () => {
165194
onSave(localModes)
@@ -184,7 +213,7 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
184213
<div className="flex items-center gap-3 flex-1 min-w-0">
185214
<Checkbox
186215
checked={!mode.disabled}
187-
onCheckedChange={() => toggleMode(mode.slug)}
216+
onCheckedChange={() => attemptToggleMode(mode.slug)}
188217
className="flex-shrink-0"
189218
/>
190219
<div className="flex-1 min-w-0">
@@ -212,12 +241,6 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
212241
size="icon"
213242
onClick={() => {
214243
// 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
221244
window.parent.postMessage(
222245
{ type: "deleteCustomMode", slug: mode.slug, checkOnly: true },
223246
"*",
@@ -232,12 +255,6 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
232255
size="icon"
233256
onClick={() => {
234257
// 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
241258
window.parent.postMessage(
242259
{ type: "deleteCustomMode", slug: mode.slug, checkOnly: true },
243260
"*",
@@ -252,32 +269,7 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
252269
</div>
253270
)
254271

255-
// Listen for delete check responses from extension
256-
useEffect(() => {
257-
const handler = (e: MessageEvent) => {
258-
const message = e.data
259-
if (message.type === "deleteCustomModeCheck") {
260-
if (message.slug && deleteState.tMode && deleteState.tMode.slug === message.slug) {
261-
// Preserve action (delete vs restore) when opening the confirmation dialog
262-
setDeleteState((prev) => ({
263-
...prev,
264-
open: true,
265-
tMode: { ...prev.tMode!, rulesFolderPath: message.rulesFolderPath },
266-
}))
267-
}
268-
}
269-
}
270-
window.addEventListener("message", handler)
271-
return () => window.removeEventListener("message", handler)
272-
}, [deleteState.tMode])
273-
274-
const confirmDelete = () => {
275-
if (!deleteState.tMode) return
276-
window.parent.postMessage({ type: "deleteCustomMode", slug: deleteState.tMode.slug }, "*")
277-
setDeleteState({ open: false, tMode: null })
278-
// Close dialog after request; backend will refresh state
279-
onOpenChange(false)
280-
}
272+
// No delete handlers here — deletion lives in the dedicated mode settings UI.
281273

282274
// Source group component
283275
const SourceGroup: React.FC<{ source: ModeSource; modes: ModeWithSource[] }> = ({ source, modes }) => {
@@ -307,15 +299,15 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
307299
<Button
308300
variant="outline"
309301
size="sm"
310-
onClick={() => toggleSourceGroup(source, true)}
302+
onClick={() => attemptToggleSourceGroup(source, true)}
311303
disabled={allEnabled}
312304
className="enable-disable-button text-xs h-7 px-2">
313305
Enable All
314306
</Button>
315307
<Button
316308
variant="outline"
317309
size="sm"
318-
onClick={() => toggleSourceGroup(source, false)}
310+
onClick={() => attemptToggleSourceGroup(source, false)}
319311
disabled={noneEnabled}
320312
className="enable-disable-button text-xs h-7 px-2">
321313
Disable All
@@ -362,7 +354,7 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
362354
<Button
363355
variant="outline"
364356
size="sm"
365-
onClick={() => toggleAllModes(true)}
357+
onClick={() => attemptToggleAllModes(true)}
366358
disabled={stats.enabled === stats.total}
367359
className="enable-disable-button text-xs">
368360
<Check className="size-3 mr-1" />
@@ -371,7 +363,7 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
371363
<Button
372364
variant="outline"
373365
size="sm"
374-
onClick={() => toggleAllModes(false)}
366+
onClick={() => attemptToggleAllModes(false)}
375367
disabled={stats.disabled === stats.total}
376368
className="enable-disable-button text-xs">
377369
<X className="size-3 mr-1" />
@@ -397,6 +389,51 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
397389
)}
398390
</div>
399391

392+
{/* Confirmation dialog shown when disabling built-in modes */}
393+
{confirmOpen && (
394+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
395+
<div className="bg-white p-6 rounded-lg max-w-lg w-full">
396+
<h3 className="text-lg font-semibold">Disabling built-in mode(s)</h3>
397+
<p className="text-sm text-gray-700 mt-2">
398+
Disabling a built-in mode will copy it to your custom_model.yaml so you can modify or
399+
delete the custom copy. You can restore the original built-in mode later by deleting the
400+
custom mode in Mode Settings.
401+
</p>
402+
<div className="mt-4 flex justify-end gap-2">
403+
<Button
404+
variant="outline"
405+
onClick={() => {
406+
// Cancel confirmation
407+
setConfirmOpen(false)
408+
setPendingAction(null)
409+
}}>
410+
Cancel
411+
</Button>
412+
<Button
413+
onClick={() => {
414+
// Proceed with pending action
415+
if (pendingAction) {
416+
if (pendingAction.type === "mode") {
417+
doToggleModeImmediate(pendingAction.payload)
418+
} else if (pendingAction.type === "source") {
419+
doToggleSourceGroupImmediate(
420+
pendingAction.payload.source,
421+
pendingAction.payload.enable,
422+
)
423+
} else if (pendingAction.type === "all") {
424+
doToggleAllModesImmediate(pendingAction.payload.enable)
425+
}
426+
}
427+
setConfirmOpen(false)
428+
setPendingAction(null)
429+
}}>
430+
Proceed
431+
</Button>
432+
</div>
433+
</div>
434+
</div>
435+
)}
436+
400437
<DialogFooter className="flex items-center justify-between">
401438
<div className="text-xs text-gray-500">{hasChanges && "You have unsaved changes"}</div>
402439
<div className="flex items-center gap-2">
@@ -409,56 +446,7 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
409446
</div>
410447
</DialogFooter>
411448

412-
{/* Delete confirmation dialog for global custom modes */}
413-
<AlertDialog open={!!deleteState.open} onOpenChange={(open) => setDeleteState((s) => ({ ...s, open }))}>
414-
<AlertDialogContent>
415-
<AlertDialogHeader>
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>
425-
<AlertDialogDescription>
426-
{deleteState.tMode && (
427-
<>
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}?`}
435-
{deleteState.tMode.rulesFolderPath && (
436-
<div className="mt-2">
437-
{t
438-
? t("prompts:deleteMode.rulesFolder", {
439-
folderPath: deleteState.tMode.rulesFolderPath,
440-
})
441-
: deleteState.tMode.rulesFolderPath}
442-
</div>
443-
)}
444-
</>
445-
)}
446-
</AlertDialogDescription>
447-
</AlertDialogHeader>
448-
<AlertDialogFooter>
449-
<AlertDialogCancel>{t ? t("prompts:deleteMode.cancel") : "Cancel"}</AlertDialogCancel>
450-
<AlertDialogAction onClick={confirmDelete}>
451-
{deleteState.action === "restore"
452-
? t
453-
? t("prompts:restoreMode.confirm")
454-
: "Restore"
455-
: t
456-
? t("prompts:deleteMode.confirm")
457-
: "Delete"}
458-
</AlertDialogAction>
459-
</AlertDialogFooter>
460-
</AlertDialogContent>
461-
</AlertDialog>
449+
{/* Delete handled in settings; confirmation dialog removed from this popup */}
462450
</DialogContent>
463451
</Dialog>
464452
)

0 commit comments

Comments
 (0)