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",