From 15c7e8fbe5eeaf5b786b5ccadbf53f7fd1103397 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 31 Jul 2025 19:22:25 +0000 Subject: [PATCH] feat: add visual indicators for global/project custom modes in mode selector - Add (G)/(P) indicators in mode dropdown list for custom modes - Show (Global)/(Project) text in selected mode button - Add helper functions to determine mode source and display text - Add translation keys for global/project indicators in EN, FR, ES, DE - Add comprehensive tests for new functionality - Only show indicators for custom modes, not built-in modes Fixes #6502 --- .../src/components/chat/ModeSelector.tsx | 93 +++++++++--- .../chat/__tests__/ModeSelector.spec.tsx | 136 ++++++++++++++++++ webview-ui/src/i18n/locales/de/chat.json | 6 +- webview-ui/src/i18n/locales/en/chat.json | 6 +- webview-ui/src/i18n/locales/es/chat.json | 6 +- webview-ui/src/i18n/locales/fr/chat.json | 6 +- 6 files changed, 226 insertions(+), 27 deletions(-) diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 93dd2f1f4f..ef8ea2dddc 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -7,7 +7,7 @@ import { IconButton } from "./IconButton" import { vscode } from "@/utils/vscode" import { useExtensionState } from "@/context/ExtensionStateContext" import { useAppTranslation } from "@/i18n/TranslationContext" -import { Mode, getAllModes } from "@roo/modes" +import { Mode, getAllModes, isCustomMode } from "@roo/modes" import { ModeConfig, CustomModePrompts } from "@roo-code/types" import { telemetryClient } from "@/utils/TelemetryClient" import { TelemetryEventName } from "@roo-code/types" @@ -16,6 +16,32 @@ import { Fzf } from "fzf" // Minimum number of modes required to show search functionality const SEARCH_THRESHOLD = 6 +// Helper function to get the source of a custom mode +function getModeSource(mode: ModeConfig, customModes?: ModeConfig[]): "global" | "project" | null { + if (!isCustomMode(mode.slug, customModes)) { + return null // Built-in mode, no source indicator needed + } + + // Find the mode in customModes to get its source + const customMode = customModes?.find((m) => m.slug === mode.slug) + return customMode?.source || "global" // Default to global if source is not specified +} + +// Helper function to get the display text for mode source +function getSourceDisplayText( + source: "global" | "project" | null, + t: (key: string) => string, + short: boolean = false, +): string { + if (!source) return "" + + if (short) { + return source === "global" ? t("chat:modeSelector.globalShort") : t("chat:modeSelector.projectShort") + } + + return source === "global" ? t("chat:modeSelector.global") : t("chat:modeSelector.project") +} + interface ModeSelectorProps { value: Mode onChange: (value: Mode) => void @@ -159,6 +185,10 @@ export const ModeSelector = ({ // Combine instruction text for tooltip const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}` + // Get source indicator for selected mode + const selectedModeSource = selectedMode ? getModeSource(selectedMode, customModes) : null + const selectedModeSourceText = getSourceDisplayText(selectedModeSource, t, false) + const trigger = ( - {selectedMode?.name || ""} + + {selectedMode?.name || ""} + {selectedModeSourceText && ( + ({selectedModeSourceText}) + )} + ) @@ -225,29 +260,41 @@ export const ModeSelector = ({ ) : (
- {filteredModes.map((mode) => ( -
handleSelect(mode.slug)} - className={cn( - "px-3 py-1.5 text-sm cursor-pointer flex items-center", - "hover:bg-vscode-list-hoverBackground", - mode.slug === value - ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" - : "", - )} - data-testid="mode-selector-item"> -
-
{mode.name}
- {mode.description && ( -
- {mode.description} -
+ {filteredModes.map((mode) => { + const modeSource = getModeSource(mode, customModes) + const sourceShortText = getSourceDisplayText(modeSource, t, true) + + return ( +
handleSelect(mode.slug)} + className={cn( + "px-3 py-1.5 text-sm cursor-pointer flex items-center", + "hover:bg-vscode-list-hoverBackground", + mode.slug === value + ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" + : "", )} + data-testid="mode-selector-item"> +
+
+ {mode.name} + {sourceShortText && ( + + ({sourceShortText}) + + )} +
+ {mode.description && ( +
+ {mode.description} +
+ )} +
+ {mode.slug === value && }
- {mode.slug === value && } -
- ))} + ) + })}
)}
diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx index a829168893..ff97306165 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -199,4 +199,140 @@ describe("ModeSelector", () => { const infoIcon = document.querySelector(".codicon-info") expect(infoIcon).toBeInTheDocument() }) + + test("shows source indicators for custom modes", () => { + // Set up mock to return custom modes with source + mockModes = [ + { + slug: "custom-global", + name: "Custom Global Mode", + description: "A global custom mode", + roleDefinition: "Role definition", + groups: ["read", "edit"] as const, + source: "global", + }, + { + slug: "custom-project", + name: "Custom Project Mode", + description: "A project custom mode", + roleDefinition: "Role definition", + groups: ["read", "edit"] as const, + source: "project", + }, + { + slug: "code", + name: "Code Mode", + description: "Built-in code mode", + roleDefinition: "Role definition", + groups: ["read", "edit"] as const, + }, + ] + + const customModes: ModeConfig[] = [ + { + slug: "custom-global", + name: "Custom Global Mode", + description: "A global custom mode", + roleDefinition: "Role definition", + groups: ["read", "edit"], + source: "global", + }, + { + slug: "custom-project", + name: "Custom Project Mode", + description: "A project custom mode", + roleDefinition: "Role definition", + groups: ["read", "edit"], + source: "project", + }, + ] + + render( + , + ) + + // Click to open the popover + fireEvent.click(screen.getByTestId("mode-selector-trigger")) + + // Check that custom modes show source indicators in dropdown + const modeItems = screen.getAllByTestId("mode-selector-item") + + // Find the custom modes in the dropdown + const globalModeItem = modeItems.find((item) => item.textContent?.includes("Custom Global Mode")) + const projectModeItem = modeItems.find((item) => item.textContent?.includes("Custom Project Mode")) + const builtinModeItem = modeItems.find((item) => item.textContent?.includes("Code Mode")) + + // Custom modes should show source indicators + expect(globalModeItem?.textContent).toContain("(chat:modeSelector.globalShort)") + expect(projectModeItem?.textContent).toContain("(chat:modeSelector.projectShort)") + + // Built-in mode should not show source indicator + expect(builtinModeItem?.textContent).not.toContain("(chat:modeSelector.globalShort)") + expect(builtinModeItem?.textContent).not.toContain("(chat:modeSelector.projectShort)") + }) + + test("shows source indicator in selected mode button", () => { + // Set up mock to return custom modes with source + mockModes = [ + { + slug: "custom-project", + name: "Custom Project Mode", + description: "A project custom mode", + roleDefinition: "Role definition", + groups: ["read", "edit"] as const, + source: "project", + }, + ] + + const customModes: ModeConfig[] = [ + { + slug: "custom-project", + name: "Custom Project Mode", + description: "A project custom mode", + roleDefinition: "Role definition", + groups: ["read", "edit"], + source: "project", + }, + ] + + render( + , + ) + + // Check that the trigger button shows the source indicator + const trigger = screen.getByTestId("mode-selector-trigger") + expect(trigger.textContent).toContain("Custom Project Mode") + expect(trigger.textContent).toContain("(chat:modeSelector.project)") + }) + + test("does not show source indicator for built-in modes", () => { + // Set up mock to return only built-in modes + mockModes = [ + { + slug: "code", + name: "Code Mode", + description: "Built-in code mode", + roleDefinition: "Role definition", + groups: ["read", "edit"] as const, + }, + ] + + render() + + // Check that the trigger button does not show source indicator + const trigger = screen.getByTestId("mode-selector-trigger") + expect(trigger.textContent).toContain("Code Mode") + expect(trigger.textContent).not.toContain("(Global)") + expect(trigger.textContent).not.toContain("(Project)") + }) }) diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 36c03c9309..e22e4faae4 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -118,7 +118,11 @@ "settings": "Modus-Einstellungen", "description": "Spezialisierte Personas, die Roos Verhalten anpassen.", "searchPlaceholder": "Modi suchen...", - "noResults": "Keine Ergebnisse gefunden" + "noResults": "Keine Ergebnisse gefunden", + "global": "Global", + "project": "Projekt", + "globalShort": "G", + "projectShort": "P" }, "errorReadingFile": "Fehler beim Lesen der Datei:", "noValidImages": "Keine gültigen Bilder wurden verarbeitet", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index b33ddd4ab4..33a11a2d7f 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -120,7 +120,11 @@ "settings": "Mode Settings", "description": "Specialized personas that tailor Roo's behavior.", "searchPlaceholder": "Search modes...", - "noResults": "No results found" + "noResults": "No results found", + "global": "Global", + "project": "Project", + "globalShort": "G", + "projectShort": "P" }, "enhancePromptDescription": "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.", "addImages": "Add images to message", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 156af85012..a1237b5861 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -118,7 +118,11 @@ "settings": "Configuración de Modos", "description": "Personalidades especializadas que adaptan el comportamiento de Roo.", "searchPlaceholder": "Buscar modos...", - "noResults": "No se encontraron resultados" + "noResults": "No se encontraron resultados", + "global": "Global", + "project": "Proyecto", + "globalShort": "G", + "projectShort": "P" }, "errorReadingFile": "Error al leer el archivo:", "noValidImages": "No se procesaron imágenes válidas", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 959db06b39..cab2746290 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -118,7 +118,11 @@ "settings": "Paramètres des Modes", "description": "Personas spécialisés qui adaptent le comportement de Roo.", "searchPlaceholder": "Rechercher des modes...", - "noResults": "Aucun résultat trouvé" + "noResults": "Aucun résultat trouvé", + "global": "Global", + "project": "Projet", + "globalShort": "G", + "projectShort": "P" }, "errorReadingFile": "Erreur lors de la lecture du fichier :", "noValidImages": "Aucune image valide n'a été traitée",