diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index c50996585fe7..e9528231fcc0 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo, useCallback, useRef } from "react" +import React, { useState, useEffect, useCallback, useRef } from "react" import { VSCodeCheckbox, VSCodeRadioGroup, @@ -30,11 +30,6 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { Tab, TabContent, TabHeader } from "@src/components/common/Tab" import { Button, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, Popover, PopoverContent, PopoverTrigger, @@ -67,15 +62,7 @@ function getGroupName(group: GroupEntry): ToolGroup { const ModesView = ({ onDone }: ModesViewProps) => { const { t } = useAppTranslation() - const { - customModePrompts, - listApiConfigMeta, - currentApiConfigName, - mode, - customInstructions, - setCustomInstructions, - customModes, - } = useExtensionState() + const { customModePrompts, mode, customInstructions, setCustomInstructions, customModes } = useExtensionState() // Use a local state to track the visually active mode // This prevents flickering when switching modes rapidly by: @@ -84,8 +71,8 @@ const ModesView = ({ onDone }: ModesViewProps) => { // 3. Still sending the mode change to the backend for persistence const [visualMode, setVisualMode] = useState(mode) - // Memoize modes to preserve array order - const modes = useMemo(() => getAllModes(customModes), [customModes]) + // Build modes fresh each render so search reflects inline rename updates immediately + const modes = getAllModes(customModes) const [isDialogOpen, setIsDialogOpen] = useState(false) const [selectedPromptContent, setSelectedPromptContent] = useState("") @@ -97,6 +84,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { const [isExporting, setIsExporting] = useState(false) const [isImporting, setIsImporting] = useState(false) const [showImportDialog, setShowImportDialog] = useState(false) + const [importLevel, setImportLevel] = useState<"global" | "project">("project") const [hasRulesToExport, setHasRulesToExport] = useState>({}) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [modeToDelete, setModeToDelete] = useState<{ @@ -111,9 +99,17 @@ const ModesView = ({ onDone }: ModesViewProps) => { const [searchValue, setSearchValue] = useState("") const searchInputRef = useRef(null) - // Local state for mode name input to allow visual emptying - const [localModeName, setLocalModeName] = useState("") - const [currentEditingModeSlug, setCurrentEditingModeSlug] = useState(null) + // removed unused local name state (replaced by inline rename UX) + + // Inline rename state for the mode dropdown row + const [isRenamingMode, setIsRenamingMode] = useState(false) + const [renameInputValue, setRenameInputValue] = useState("") + const renameInputRef = useRef(null) + + // Optimistic rename map so search reflects new names immediately + const [localRenames, setLocalRenames] = useState>({}) + // Display list that overlays optimistic names + const displayModes = (modes || []).map((m) => (localRenames[m.slug] ? { ...m, name: localRenames[m.slug] } : m)) // Direct update functions const updateAgentPrompt = useCallback( @@ -204,6 +200,52 @@ const ModesView = ({ onDone }: ModesViewProps) => { searchInputRef.current?.focus() }, []) + // Focus rename input when entering rename mode + useEffect(() => { + if (isRenamingMode) { + const id = setTimeout(() => renameInputRef.current?.focus(), 0) + return () => clearTimeout(id) + } + }, [isRenamingMode]) + + const handleStartRenameMode = useCallback(() => { + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + setIsRenamingMode(true) + setRenameInputValue(customMode.name) + } + }, [visualMode, customModes, findModeBySlug]) + + const handleCancelRenameMode = useCallback(() => { + setIsRenamingMode(false) + setRenameInputValue("") + }, []) + + const handleSaveRenameMode = useCallback(() => { + const customMode = findModeBySlug(visualMode, customModes) + const trimmed = renameInputValue.trim() + if (!customMode || !trimmed) { + setIsRenamingMode(false) + return + } + // Prevent duplicate names against other modes + const nameTaken = modes.some( + (m) => m.name.toLowerCase() === trimmed.toLowerCase() && m.slug !== customMode.slug, + ) + if (nameTaken) { + // simple guard: do nothing if taken + return + } + updateCustomMode(visualMode, { + ...customMode, + name: trimmed, + source: customMode.source || "global", + }) + // Optimistically reflect rename in UI/search immediately + setLocalRenames((prev) => ({ ...prev, [visualMode]: trimmed })) + setIsRenamingMode(false) + }, [visualMode, customModes, renameInputValue, modes, updateCustomMode, findModeBySlug]) + // Helper function to get current mode's config const getCurrentMode = useCallback((): ModeConfig | undefined => { const findMode = (m: ModeConfig): boolean => m.slug === visualMode @@ -226,22 +268,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { } }, [getCurrentMode, checkRulesDirectory, hasRulesToExport]) - // Reset local name state when mode changes - useEffect(() => { - if (currentEditingModeSlug && currentEditingModeSlug !== visualMode) { - setCurrentEditingModeSlug(null) - setLocalModeName("") - } - }, [visualMode, currentEditingModeSlug]) - - // Helper function to safely access mode properties - const getModeProperty = ( - mode: ModeConfig | undefined, - property: T, - ): ModeConfig[T] | undefined => { - return mode?.[property] - } - // State for create mode dialog const [newModeName, setNewModeName] = useState("") const [newModeSlug, setNewModeSlug] = useState("") @@ -285,6 +311,13 @@ const ModesView = ({ onDone }: ModesViewProps) => { } }, [isCreateModeDialogOpen, resetFormState]) + // Ensure import dialog defaults to "project" each open + useEffect(() => { + if (showImportDialog) { + setImportLevel("project") + } + }, [showImportDialog]) + // Helper function to generate a unique slug from a name const generateSlug = useCallback((name: string, attempt = 0): string => { const baseSlug = name @@ -354,6 +387,8 @@ const ModesView = ({ onDone }: ModesViewProps) => { } updateCustomMode(newModeSlug, newMode) + // Immediately select the newly created mode in the UI + setVisualMode(newModeSlug) switchMode(newModeSlug) setIsCreateModeDialogOpen(false) resetFormState() @@ -466,6 +501,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { console.error("Failed to import mode:", message.error) } } + // Note: Auto-select after import will be handled by PR #9003 } else if (message.type === "checkRulesDirectoryResult") { setHasRulesToExport((prev) => ({ ...prev, @@ -487,7 +523,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { window.addEventListener("message", handler) return () => window.removeEventListener("message", handler) - }, []) // Empty dependency array - only register once + }, [checkRulesDirectory, switchMode]) const handleAgentReset = ( modeSlug: string, @@ -517,11 +553,6 @@ const ModesView = ({ onDone }: ModesViewProps) => {
e.stopPropagation()} className="flex justify-between items-center mb-3">

{t("prompts:modes.title")}

- - -
+ + + +
@@ -611,577 +654,568 @@ const ModesView = ({ onDone }: ModesViewProps) => {
- - - - - - -
- - {searchValue.length > 0 && ( -
- + { + const target = e as { target: { value: string } } + setRenameInputValue(target.target.value) + }} + className="grow" + placeholder={t("prompts:createModeDialog.name.placeholder")} + /> + + + + + + + + ) : ( + <> + + + + + + +
+ + {searchValue.length > 0 && ( +
+ +
+ )}
- )} -
- - - {searchValue && ( -
- {t("prompts:modes.noMatchFound")} -
- )} -
- - {modes - .filter((modeConfig) => - searchValue - ? modeConfig.name - .toLowerCase() - .includes(searchValue.toLowerCase()) - : true, - ) - .map((modeConfig) => ( - { - handleModeSwitch(modeConfig) - setOpen(false) - }} - data-testid={`mode-option-${modeConfig.slug}`}> -
- - {modeConfig.name} - - - {modeConfig.slug} - + + + {searchValue && ( +
+ {t("prompts:modes.noMatchFound")}
- - ))} - -
- - - -
- {/* API Configuration - Moved Here */} -
-
{t("prompts:apiConfiguration.title")}
-
- {t("prompts:apiConfiguration.select")} -
-
- -
-
-
- - {/* Name section */} -
- {/* Only show name and delete for custom modes */} - {visualMode && findModeBySlug(visualMode, customModes) && ( -
-
-
{t("prompts:createModeDialog.name.label")}
-
- { - const customMode = findModeBySlug(visualMode, customModes) - if (customMode) { - setCurrentEditingModeSlug(visualMode) - setLocalModeName(customMode.name) - } - }} - onChange={(e) => { - const newName = e.target.value - // Allow users to type freely, including emptying the field - setLocalModeName(newName) - }} - onBlur={() => { - const customMode = findModeBySlug(visualMode, customModes) - if (customMode) { - const trimmedName = localModeName.trim() - // Only update if the name is not empty - if (trimmedName) { - updateCustomMode(visualMode, { - ...customMode, - name: trimmedName, - source: customMode.source || "global", - }) - } else { - // Revert to the original name if empty - setLocalModeName(customMode.name) - } - } - // Clear the editing state - setCurrentEditingModeSlug(null) - }} - className="w-full" - /> - - - -
-
-
- )} + )} + + + {displayModes + .filter((modeConfig) => + searchValue + ? modeConfig.name + .toLowerCase() + .includes(searchValue.toLowerCase()) + : true, + ) + .map((modeConfig) => ( + { + handleModeSwitch(modeConfig) + setOpen(false) + }} + data-testid={`mode-option-${modeConfig.slug}`}> +
+ + {modeConfig.name} + + + {modeConfig.slug} + +
+
+ ))} +
+ + + + + + {/* New mode (+) moved here from the top bar */} + + + - {/* Role Definition section */} -
-
-
{t("prompts:roleDefinition.title")}
- {!findModeBySlug(visualMode, customModes) && ( - + {/* Edit (rename) mode - only enabled for custom modes */} + - )} -
-
- {t("prompts:roleDefinition.description")} -
- { - const customMode = findModeBySlug(visualMode, customModes) - const prompt = customModePrompts?.[visualMode] as PromptComponent - return ( - customMode?.roleDefinition ?? - prompt?.roleDefinition ?? - getRoleDefinition(visualMode) - ) - })()} - onChange={(e) => { - const value = - (e as unknown as CustomEvent)?.detail?.target?.value ?? - ((e as any).target as HTMLTextAreaElement).value - const customMode = findModeBySlug(visualMode, customModes) - if (customMode) { - // For custom modes, update the JSON file - updateCustomMode(visualMode, { - ...customMode, - roleDefinition: value.trim() || "", - source: customMode.source || "global", - }) - } else { - // For built-in modes, update the prompts - updateAgentPrompt(visualMode, { - roleDefinition: value.trim() || undefined, - }) - } - }} - className="w-full" - rows={5} - data-testid={`${getCurrentMode()?.slug || "code"}-prompt-textarea`} - /> -
- {/* Description section */} -
-
-
{t("prompts:description.title")}
- {!findModeBySlug(visualMode, customModes) && ( - + {/* Delete mode - disabled for built-in modes */} + - )} -
-
- {t("prompts:description.description")} -
- { - const customMode = findModeBySlug(visualMode, customModes) - const prompt = customModePrompts?.[visualMode] as PromptComponent - return customMode?.description ?? prompt?.description ?? getDescription(visualMode) - })()} - onChange={(e) => { - const value = - (e as unknown as CustomEvent)?.detail?.target?.value ?? - ((e as any).target as HTMLTextAreaElement).value - const customMode = findModeBySlug(visualMode, customModes) - if (customMode) { - // For custom modes, update the JSON file - updateCustomMode(visualMode, { - ...customMode, - description: value.trim() || undefined, - source: customMode.source || "global", - }) - } else { - // For built-in modes, update the prompts - updateAgentPrompt(visualMode, { - description: value.trim() || undefined, - }) - } - }} - className="w-full" - data-testid={`${getCurrentMode()?.slug || "code"}-description-textfield`} - /> -
- {/* When to Use section */} -
-
-
{t("prompts:whenToUse.title")}
- {!findModeBySlug(visualMode, customModes) && ( - + {/* Export mode (kept here to the right of the dropdown) */} + - )} -
-
- {t("prompts:whenToUse.description")} -
- { - const customMode = findModeBySlug(visualMode, customModes) - const prompt = customModePrompts?.[visualMode] as PromptComponent - return customMode?.whenToUse ?? prompt?.whenToUse ?? getWhenToUse(visualMode) - })()} - onChange={(e) => { - const value = - (e as unknown as CustomEvent)?.detail?.target?.value ?? - ((e as any).target as HTMLTextAreaElement).value - const customMode = findModeBySlug(visualMode, customModes) - if (customMode) { - // For custom modes, update the JSON file - updateCustomMode(visualMode, { - ...customMode, - whenToUse: value.trim() || undefined, - source: customMode.source || "global", - }) - } else { - // For built-in modes, update the prompts - updateAgentPrompt(visualMode, { - whenToUse: value.trim() || undefined, - }) - } - }} - className="w-full" - rows={4} - data-testid={`${getCurrentMode()?.slug || "code"}-when-to-use-textarea`} - /> + + )}
+
- {/* Mode settings */} - <> - {/* Show tools for all modes */} -
-
-
{t("prompts:tools.title")}
- {findModeBySlug(visualMode, customModes) && ( - - - - )} -
- {!findModeBySlug(visualMode, customModes) && ( -
- {t("prompts:tools.builtInModesText")} -
- )} - {isToolsEditMode && findModeBySlug(visualMode, customModes) ? ( -
- {availableGroups.map((group) => { - const currentMode = getCurrentMode() - const isCustomMode = findModeBySlug(visualMode, customModes) - const customMode = isCustomMode - const isGroupEnabled = isCustomMode - ? customMode?.groups?.some((g) => getGroupName(g) === group) - : currentMode?.groups?.some((g) => getGroupName(g) === group) - - return ( - - {t(`prompts:tools.toolNames.${group}`)} - {group === "edit" && ( -
- {t("prompts:tools.allowedFiles")}{" "} - {(() => { - const currentMode = getCurrentMode() - const editGroup = currentMode?.groups?.find( - (g) => - Array.isArray(g) && - g[0] === "edit" && - g[1]?.fileRegex, - ) - if (!Array.isArray(editGroup)) return t("prompts:allFiles") - return ( - editGroup[1].description || - `/${editGroup[1].fileRegex}/` - ) - })()} -
- )} -
- ) - })} -
- ) : ( -
- {(() => { + {/* Role Definition section */} +
+
+
{t("prompts:roleDefinition.title")}
+ {!findModeBySlug(visualMode, customModes) && ( + + + + )} +
+
+ {t("prompts:roleDefinition.description")} +
+ { + const customMode = findModeBySlug(visualMode, customModes) + const prompt = customModePrompts?.[visualMode] as PromptComponent + return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(visualMode) + })()} + onChange={(e) => { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value ?? + ((e as any).target as HTMLTextAreaElement).value + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + // For custom modes, update the JSON file + updateCustomMode(visualMode, { + ...customMode, + roleDefinition: value.trim() || "", + source: customMode.source || "global", + }) + } else { + // For built-in modes, update the prompts + updateAgentPrompt(visualMode, { + roleDefinition: value.trim() || undefined, + }) + } + }} + className="w-full" + rows={5} + data-testid={`${getCurrentMode()?.slug || "code"}-prompt-textarea`} + /> +
- // If there are no enabled groups, display translated "None" - if (enabledGroups.length === 0) { - return t("prompts:tools.noTools") + {/* Description section */} +
+
+
{t("prompts:description.title")}
+ {!findModeBySlug(visualMode, customModes) && ( + + + + )} +
+
+ {t("prompts:description.description")} +
+ { + const customMode = findModeBySlug(visualMode, customModes) + const prompt = customModePrompts?.[visualMode] as PromptComponent + return customMode?.description ?? prompt?.description ?? getDescription(visualMode) + })()} + onChange={(e) => { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value ?? + ((e as any).target as HTMLTextAreaElement).value + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + // For custom modes, update the JSON file + updateCustomMode(visualMode, { + ...customMode, + description: value.trim() || undefined, + source: customMode.source || "global", + }) + } else { + // For built-in modes, update the prompts + updateAgentPrompt(visualMode, { + description: value.trim() || undefined, + }) + } + }} + className="w-full" + data-testid={`${getCurrentMode()?.slug || "code"}-description-textfield`} + /> +
- return enabledGroups - .map((group) => { - const groupName = getGroupName(group) - const displayName = t(`prompts:tools.toolNames.${groupName}`) - if (Array.isArray(group) && group[1]?.fileRegex) { - const description = - group[1].description || `/${group[1].fileRegex}/` - return `${displayName} (${description})` - } - return displayName - }) - .join(", ") - })()} -
- )} -
- + {/* When to Use section */} +
+
+
{t("prompts:whenToUse.title")}
+ {!findModeBySlug(visualMode, customModes) && ( + + + + )} +
+
+ {t("prompts:whenToUse.description")} +
+ { + const customMode = findModeBySlug(visualMode, customModes) + const prompt = customModePrompts?.[visualMode] as PromptComponent + return customMode?.whenToUse ?? prompt?.whenToUse ?? getWhenToUse(visualMode) + })()} + onChange={(e) => { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value ?? + ((e as any).target as HTMLTextAreaElement).value + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + // For custom modes, update the JSON file + updateCustomMode(visualMode, { + ...customMode, + whenToUse: value.trim() || undefined, + source: customMode.source || "global", + }) + } else { + // For built-in modes, update the prompts + updateAgentPrompt(visualMode, { + whenToUse: value.trim() || undefined, + }) + } + }} + className="w-full" + rows={4} + data-testid={`${getCurrentMode()?.slug || "code"}-when-to-use-textarea`} + /> +
- {/* Role definition for both built-in and custom modes */} -
+ {/* Mode settings */} + <> + {/* Show tools for all modes */} +
-
{t("prompts:customInstructions.title")}
- {!findModeBySlug(visualMode, customModes) && ( - +
{t("prompts:tools.title")}
+ {findModeBySlug(visualMode, customModes) && ( + )}
-
- {t("prompts:customInstructions.description", { - modeName: getCurrentMode()?.name || "Code", - })} -
- { - const customMode = findModeBySlug(visualMode, customModes) - const prompt = customModePrompts?.[visualMode] as PromptComponent - return ( - customMode?.customInstructions ?? - prompt?.customInstructions ?? - getCustomInstructions(mode, customModes) - ) - })()} - onChange={(e) => { - const value = - (e as unknown as CustomEvent)?.detail?.target?.value ?? - ((e as any).target as HTMLTextAreaElement).value - const customMode = findModeBySlug(visualMode, customModes) - if (customMode) { - // For custom modes, update the JSON file - updateCustomMode(visualMode, { - ...customMode, - // Preserve empty string; only treat null/undefined as unset - customInstructions: value ?? undefined, - source: customMode.source || "global", - }) - } else { - // For built-in modes, update the prompts - const existingPrompt = customModePrompts?.[visualMode] as PromptComponent - updateAgentPrompt(visualMode, { - ...existingPrompt, - customInstructions: value.trim(), - }) - } - }} - rows={10} - className="w-full" - data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`} - /> -
- { - const currentMode = getCurrentMode() - if (!currentMode) return + {!findModeBySlug(visualMode, customModes) && ( +
+ {t("prompts:tools.builtInModesText")} +
+ )} + {isToolsEditMode && findModeBySlug(visualMode, customModes) ? ( +
+ {availableGroups.map((group) => { + const currentMode = getCurrentMode() + const isCustomMode = findModeBySlug(visualMode, customModes) + const customMode = isCustomMode + const isGroupEnabled = isCustomMode + ? customMode?.groups?.some((g) => getGroupName(g) === group) + : currentMode?.groups?.some((g) => getGroupName(g) === group) - // Open or create an empty file - vscode.postMessage({ - type: "openFile", - text: `./.roo/rules-${currentMode.slug}/rules.md`, - values: { - create: true, - content: "", - }, - }) - }} - /> - ), - "0": ( - + {t(`prompts:tools.toolNames.${group}`)} + {group === "edit" && ( +
+ {t("prompts:tools.allowedFiles")}{" "} + {(() => { + const currentMode = getCurrentMode() + const editGroup = currentMode?.groups?.find( + (g) => + Array.isArray(g) && g[0] === "edit" && g[1]?.fileRegex, + ) + if (!Array.isArray(editGroup)) return t("prompts:allFiles") + return editGroup[1].description || `/${editGroup[1].fileRegex}/` + })()} +
)} - style={{ display: "inline" }} - aria-label="Learn about global custom instructions for modes" - /> - ), - }} - /> -
+ + ) + })} +
+ ) : ( +
+ {(() => { + const currentMode = getCurrentMode() + const enabledGroups = currentMode?.groups || [] + + // If there are no enabled groups, display translated "None" + if (enabledGroups.length === 0) { + return t("prompts:tools.noTools") + } + + return enabledGroups + .map((group) => { + const groupName = getGroupName(group) + const displayName = t(`prompts:tools.toolNames.${groupName}`) + if (Array.isArray(group) && group[1]?.fileRegex) { + const description = group[1].description || `/${group[1].fileRegex}/` + return `${displayName} (${description})` + } + return displayName + }) + .join(", ") + })()} +
+ )} +
+ + + {/* Role definition for both built-in and custom modes */} +
+
+
{t("prompts:customInstructions.title")}
+ {!findModeBySlug(visualMode, customModes) && ( + + + + )} +
+
+ {t("prompts:customInstructions.description", { + modeName: getCurrentMode()?.name || "Code", + })} +
+ { + const customMode = findModeBySlug(visualMode, customModes) + const prompt = customModePrompts?.[visualMode] as PromptComponent + return ( + customMode?.customInstructions ?? + prompt?.customInstructions ?? + getCustomInstructions(visualMode, customModes) + ) + })()} + onChange={(e) => { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value ?? + ((e as any).target as HTMLTextAreaElement).value + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + // For custom modes, update the JSON file + updateCustomMode(visualMode, { + ...customMode, + // Preserve empty string; only treat null/undefined as unset + customInstructions: value ?? undefined, + source: customMode.source || "global", + }) + } else { + // For built-in modes, update the prompts + const existingPrompt = customModePrompts?.[visualMode] as PromptComponent + updateAgentPrompt(visualMode, { + ...existingPrompt, + customInstructions: value.trim() || undefined, + }) + } + }} + rows={10} + className="w-full" + data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`} + /> +
+ { + const currentMode = getCurrentMode() + if (!currentMode) return + + // Open or create an empty file + vscode.postMessage({ + type: "openFile", + text: `./.roo/rules-${currentMode.slug}/rules.md`, + values: { + create: true, + content: "", + }, + }) + }} + /> + ), + "0": ( + + ), + }} + />
@@ -1220,41 +1254,6 @@ const ModesView = ({ onDone }: ModesViewProps) => {
- {/* Export/Import Mode Buttons */} -
- {/* Export button - visible when any mode is selected */} - {getCurrentMode() && ( - - )} - {/* Import button - always visible */} - -
- {/* Advanced Features Disclosure */}