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
This commit is contained in:
Roo Code 2025-07-31 19:22:25 +00:00
parent 74672fafcb
commit 15c7e8fbe5
6 changed files with 226 additions and 27 deletions

View file

@ -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 = (
<PopoverTrigger
disabled={disabled}
@ -176,7 +206,12 @@ export const ModeSelector = ({
: null,
)}>
<ChevronUp className="pointer-events-none opacity-80 flex-shrink-0 size-3" />
<span className="truncate">{selectedMode?.name || ""}</span>
<span className="truncate">
{selectedMode?.name || ""}
{selectedModeSourceText && (
<span className="text-vscode-descriptionForeground ml-1">({selectedModeSourceText})</span>
)}
</span>
</PopoverTrigger>
)
@ -225,29 +260,41 @@ export const ModeSelector = ({
</div>
) : (
<div className="py-1">
{filteredModes.map((mode) => (
<div
key={mode.slug}
onClick={() => 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">
<div className="flex-1 min-w-0">
<div className="font-bold truncate">{mode.name}</div>
{mode.description && (
<div className="text-xs text-vscode-descriptionForeground truncate">
{mode.description}
</div>
{filteredModes.map((mode) => {
const modeSource = getModeSource(mode, customModes)
const sourceShortText = getSourceDisplayText(modeSource, t, true)
return (
<div
key={mode.slug}
onClick={() => 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">
<div className="flex-1 min-w-0">
<div className="font-bold truncate flex items-center gap-1.5">
{mode.name}
{sourceShortText && (
<span className="text-xs text-vscode-descriptionForeground font-normal">
({sourceShortText})
</span>
)}
</div>
{mode.description && (
<div className="text-xs text-vscode-descriptionForeground truncate">
{mode.description}
</div>
)}
</div>
{mode.slug === value && <Check className="ml-auto size-4 p-0.5" />}
</div>
{mode.slug === value && <Check className="ml-auto size-4 p-0.5" />}
</div>
))}
)
})}
</div>
)}
</div>

View file

@ -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(
<ModeSelector
value={"custom-global" as Mode}
onChange={vi.fn()}
modeShortcutText="Ctrl+M"
customModes={customModes}
/>,
)
// 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(
<ModeSelector
value={"custom-project" as Mode}
onChange={vi.fn()}
modeShortcutText="Ctrl+M"
customModes={customModes}
/>,
)
// 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(<ModeSelector value={"code" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" customModes={[]} />)
// 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)")
})
})

View file

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

View file

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

View file

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

View file

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