Skip to content

Commit 864d0d7

Browse files
committed
feat: enhance custom modes management and UI interactions
1 parent dd3d8bc commit 864d0d7

File tree

5 files changed

+122
-57
lines changed

5 files changed

+122
-57
lines changed

src/core/config/CustomModesManager.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,8 @@ export class CustomModesManager {
488488
}
489489

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

@@ -1047,12 +1049,32 @@ export class CustomModesManager {
10471049
try {
10481050
const modes = await this.getCustomModes()
10491051
const updatesByFile = new Map<string, Array<{ slug: string; disabled: boolean }>>()
1052+
// Collect built-in modes that need to be copied into the global custom modes file
1053+
const addsForSettings: ModeConfig[] = []
10501054

10511055
// Group updates by source/file
10521056
for (const update of updates) {
10531057
const mode = modes.find((m) => m.slug === update.slug)
10541058
if (!mode) {
1055-
console.warn(`Mode not found: ${update.slug}`)
1059+
// If mode isn't present in custom modes, it might be a built-in mode.
1060+
// Try loading built-in definitions and create a custom copy in the global file.
1061+
try {
1062+
const { modes: builtInModes } = await import("../../shared/modes")
1063+
const builtIn = builtInModes.find((b: any) => b.slug === update.slug)
1064+
if (builtIn) {
1065+
// Create a global-scoped custom mode based on the built-in definition
1066+
const modeToAdd: ModeConfig = {
1067+
...builtIn,
1068+
disabled: update.disabled,
1069+
source: "global",
1070+
}
1071+
addsForSettings.push(modeToAdd)
1072+
continue
1073+
}
1074+
console.warn(`Mode not found: ${update.slug}`)
1075+
} catch (e) {
1076+
console.warn(`Mode not found and failed to load built-ins: ${update.slug}`)
1077+
}
10561078
continue
10571079
}
10581080

@@ -1086,6 +1108,18 @@ export class CustomModesManager {
10861108
})
10871109
}
10881110

1111+
// If we found built-in modes that need to be copied to settings, add them now
1112+
if (addsForSettings.length > 0) {
1113+
const settingsPath = await this.getCustomModesFilePath()
1114+
await this.queueWrite(async () => {
1115+
await this.updateModesInFile(settingsPath, (modes) => {
1116+
// Remove any existing entries with the same slugs, then append new ones
1117+
const filtered = modes.filter((m) => !addsForSettings.some((a) => a.slug === m.slug))
1118+
return [...filtered, ...addsForSettings]
1119+
})
1120+
})
1121+
}
1122+
10891123
await this.refreshMergedState()
10901124
} catch (error) {
10911125
const errorMessage = error instanceof Error ? error.message : String(error)

src/core/webview/webviewMessageHandler.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2660,8 +2660,6 @@ export const webviewMessageHandler = async (
26602660
await provider.postStateToWebview()
26612661

26622662
await provider.postMessageToWebview({ type: "modeDisabledStatesUpdated", success: true })
2663-
2664-
vscode.window.showInformationMessage(t("common:info.modes_updated"))
26652663
} catch (error) {
26662664
provider.log(`Error updating mode disabled states: ${error}`)
26672665

webview-ui/src/App.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,22 @@ const App = () => {
165165
setModeDialogOpen(true)
166166
}
167167

168+
// Internal request from child components to open the delete flow in Modes settings
169+
if ((message as any).type === "openDeleteModeInSettingsRequest" && (message as any).slug) {
170+
// Switch to modes tab so ModesView is mounted and can receive the forwarded message
171+
switchTab("modes")
172+
// Forward the request to ModesView to trigger its delete flow
173+
window.postMessage(
174+
{ type: "openDeleteModeInSettings", slug: (message as any).slug, name: (message as any).name },
175+
"*",
176+
)
177+
}
178+
// Internal request from ModesView to open the enable/disable dialog
179+
if ((message as any).type === "showModeEnableDisableDialogRequest" && (message as any).modes) {
180+
setModeDialogModes((message as any).modes as any[])
181+
setModeDialogOpen(true)
182+
}
183+
168184
if (message.type === "showDeleteMessageDialog" && message.messageTs) {
169185
setDeleteMessageDialogState({ isOpen: true, messageTs: message.messageTs })
170186
}

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

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect } from "react"
2-
import { Check, X, Eye, EyeOff, HelpCircle } from "lucide-react"
2+
import { Eye, EyeOff, HelpCircle } from "lucide-react"
33
import {
44
Dialog,
55
DialogContent,
@@ -234,18 +234,19 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
234234
<div className="status-indicator flex items-center gap-1 flex-shrink-0">
235235
{mode.disabled ? <EyeOff className="size-4 disabled" /> : <Eye className="size-4 enabled" />}
236236
{/* Show delete for global custom modes (they override built-in or are user-created) */}
237-
{mode.source === "global" && (
237+
{(mode.source === "global" || mode.source === "project") && (
238238
<div className="flex items-center gap-1">
239239
<Button
240240
variant="ghost"
241241
size="icon"
242242
onClick={() => {
243-
// Ask the extension to check for rules folder and return path via message
244-
window.parent.postMessage(
245-
{ type: "deleteCustomMode", slug: mode.slug, checkOnly: true },
243+
// Request the App to switch to Modes and forward the delete request
244+
window.postMessage(
245+
{ type: "openDeleteModeInSettingsRequest", slug: mode.slug, name: mode.name },
246246
"*",
247247
)
248-
}}>
248+
}}
249+
className="text-vscode-foreground">
249250
<span className="codicon codicon-trash"></span>
250251
</Button>
251252

@@ -279,8 +280,6 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
279280
const allEnabled = enabled === total
280281
const noneEnabled = enabled === 0
281282

282-
if (modes.length === 0) return null
283-
284283
return (
285284
<div className="space-y-3">
286285
<div className="flex items-center justify-between">
@@ -301,24 +300,30 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
301300
size="sm"
302301
onClick={() => attemptToggleSourceGroup(source, true)}
303302
disabled={allEnabled}
304-
className="enable-disable-button text-xs h-7 px-2">
303+
className="text-xs h-7 px-2 text-vscode-foreground">
305304
Enable All
306305
</Button>
307-
<Button
308-
variant="outline"
309-
size="sm"
310-
onClick={() => attemptToggleSourceGroup(source, false)}
311-
disabled={noneEnabled}
312-
className="enable-disable-button text-xs h-7 px-2">
313-
Disable All
314-
</Button>
306+
{source !== "builtin" && (
307+
<Button
308+
variant="outline"
309+
size="sm"
310+
onClick={() => attemptToggleSourceGroup(source, false)}
311+
disabled={noneEnabled}
312+
className="text-xs h-7 px-2 text-vscode-foreground">
313+
Disable All
314+
</Button>
315+
)}
315316
</div>
316317
</div>
317-
<div className="space-y-2">
318-
{modes.map((mode) => (
319-
<ModeItem key={mode.slug} mode={mode} />
320-
))}
321-
</div>
318+
{modes.length === 0 ? (
319+
<div className="py-4 text-sm text-vscode-descriptionForeground">Nothing to see here.</div>
320+
) : (
321+
<div className="space-y-2">
322+
{modes.map((mode) => (
323+
<ModeItem key={mode.slug} mode={mode} />
324+
))}
325+
</div>
326+
)}
322327
</div>
323328
)
324329
}
@@ -338,15 +343,15 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
338343
</DialogHeader>
339344

340345
{/* Statistics and bulk actions */}
341-
<div className="flex items-center justify-between py-3 px-4 bg-gray-50 rounded-lg">
346+
<div className="flex items-center justify-between py-3 px-4 bg-vscode-editor-background border border-vscode-input-border rounded-lg">
342347
<div className="flex items-center gap-4 text-sm">
343-
<span className="text-gray-700">
348+
<span className="text-vscode-foreground">
344349
<strong>{stats.total}</strong> total modes
345350
</span>
346-
<span className="text-green-700">
351+
<span className="text-vscode-descriptionForeground">
347352
<strong>{stats.enabled}</strong> enabled
348353
</span>
349-
<span className="text-red-700">
354+
<span className="text-vscode-descriptionForeground">
350355
<strong>{stats.disabled}</strong> disabled
351356
</span>
352357
</div>
@@ -356,19 +361,10 @@ export const ModeEnableDisableDialog: React.FC<ModeEnableDisableDialogProps> = (
356361
size="sm"
357362
onClick={() => attemptToggleAllModes(true)}
358363
disabled={stats.enabled === stats.total}
359-
className="enable-disable-button text-xs">
360-
<Check className="size-3 mr-1" />
364+
className="text-xs h-7 px-2">
361365
Enable All
362366
</Button>
363-
<Button
364-
variant="outline"
365-
size="sm"
366-
onClick={() => attemptToggleAllModes(false)}
367-
disabled={stats.disabled === stats.total}
368-
className="enable-disable-button text-xs">
369-
<X className="size-3 mr-1" />
370-
Disable All
371-
</Button>
367+
{/* Per design, omit a global "Disable All" here */}
372368
</div>
373369
</div>
374370

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

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,13 @@ const ModesView = ({ onDone }: ModesViewProps) => {
471471
...prev,
472472
[message.slug]: message.hasContent,
473473
}))
474+
} else if (message.type === "openDeleteModeInSettings") {
475+
// Received forwarded request to open the delete flow for a specific mode.
476+
if (message.slug && message.name) {
477+
setModeToDelete({ slug: message.slug, name: message.name, source: "global" })
478+
// Ask the extension to check for rules folder and return path via message
479+
vscode.postMessage({ type: "deleteCustomMode", slug: message.slug, checkOnly: true })
480+
}
474481
} else if (message.type === "deleteCustomModeCheck") {
475482
// Handle the check response
476483
// Use the ref to get the current modeToDelete value
@@ -523,23 +530,7 @@ const ModesView = ({ onDone }: ModesViewProps) => {
523530
</Button>
524531
</StandardTooltip>
525532
<div className="relative inline-block">
526-
<StandardTooltip content={t("prompts:modes.editModesConfig")}>
527-
<Button
528-
variant="ghost"
529-
size="icon"
530-
className="flex"
531-
onClick={(e: React.MouseEvent) => {
532-
e.preventDefault()
533-
e.stopPropagation()
534-
setShowConfigMenu((prev) => !prev)
535-
}}
536-
onBlur={() => {
537-
// Add slight delay to allow menu item clicks to register
538-
setTimeout(() => setShowConfigMenu(false), 200)
539-
}}>
540-
<span className="codicon codicon-json"></span>
541-
</Button>
542-
</StandardTooltip>
533+
{/* Single Edit modes configuration button (removed duplicate) */}
543534
{showConfigMenu && (
544535
<div
545536
onClick={(e) => e.stopPropagation()}
@@ -577,6 +568,36 @@ const ModesView = ({ onDone }: ModesViewProps) => {
577568
</div>
578569
)}
579570
</div>
571+
<StandardTooltip content={t("prompts:modes.editModesConfig")}>
572+
<Button
573+
variant="ghost"
574+
size="icon"
575+
className="flex"
576+
onClick={(e: React.MouseEvent) => {
577+
e.preventDefault()
578+
e.stopPropagation()
579+
setShowConfigMenu((prev) => !prev)
580+
}}
581+
onBlur={() => {
582+
// Add slight delay to allow menu item clicks to register
583+
setTimeout(() => setShowConfigMenu(false), 200)
584+
}}>
585+
<span className="codicon codicon-json"></span>
586+
</Button>
587+
</StandardTooltip>
588+
{/* Enable/Disable Modes button - placed between Edit config and Marketplace */}
589+
<StandardTooltip content={t("prompts:modes.enableDisable")}>
590+
<Button
591+
variant="ghost"
592+
size="icon"
593+
onClick={() => {
594+
// Ask the App to open the Enable/Disable dialog with current modes.
595+
window.postMessage({ type: "openModeEnableDisableDialog", modes }, "*")
596+
}}
597+
className="flex">
598+
<span className="codicon codicon-eye"></span>
599+
</Button>
600+
</StandardTooltip>
580601
<StandardTooltip content={t("chat:modeSelector.marketplace")}>
581602
<Button
582603
variant="ghost"

0 commit comments

Comments
 (0)