mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
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:
parent
74672fafcb
commit
15c7e8fbe5
6 changed files with 226 additions and 27 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/de/chat.json
generated
6
webview-ui/src/i18n/locales/de/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/es/chat.json
generated
6
webview-ui/src/i18n/locales/es/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/fr/chat.json
generated
6
webview-ui/src/i18n/locales/fr/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue