Skip to content

Commit 15c7e8f

Browse files
committed
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
1 parent 74672fa commit 15c7e8f

File tree

6 files changed

+226
-27
lines changed

6 files changed

+226
-27
lines changed

webview-ui/src/components/chat/ModeSelector.tsx

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { IconButton } from "./IconButton"
77
import { vscode } from "@/utils/vscode"
88
import { useExtensionState } from "@/context/ExtensionStateContext"
99
import { useAppTranslation } from "@/i18n/TranslationContext"
10-
import { Mode, getAllModes } from "@roo/modes"
10+
import { Mode, getAllModes, isCustomMode } from "@roo/modes"
1111
import { ModeConfig, CustomModePrompts } from "@roo-code/types"
1212
import { telemetryClient } from "@/utils/TelemetryClient"
1313
import { TelemetryEventName } from "@roo-code/types"
@@ -16,6 +16,32 @@ import { Fzf } from "fzf"
1616
// Minimum number of modes required to show search functionality
1717
const SEARCH_THRESHOLD = 6
1818

19+
// Helper function to get the source of a custom mode
20+
function getModeSource(mode: ModeConfig, customModes?: ModeConfig[]): "global" | "project" | null {
21+
if (!isCustomMode(mode.slug, customModes)) {
22+
return null // Built-in mode, no source indicator needed
23+
}
24+
25+
// Find the mode in customModes to get its source
26+
const customMode = customModes?.find((m) => m.slug === mode.slug)
27+
return customMode?.source || "global" // Default to global if source is not specified
28+
}
29+
30+
// Helper function to get the display text for mode source
31+
function getSourceDisplayText(
32+
source: "global" | "project" | null,
33+
t: (key: string) => string,
34+
short: boolean = false,
35+
): string {
36+
if (!source) return ""
37+
38+
if (short) {
39+
return source === "global" ? t("chat:modeSelector.globalShort") : t("chat:modeSelector.projectShort")
40+
}
41+
42+
return source === "global" ? t("chat:modeSelector.global") : t("chat:modeSelector.project")
43+
}
44+
1945
interface ModeSelectorProps {
2046
value: Mode
2147
onChange: (value: Mode) => void
@@ -159,6 +185,10 @@ export const ModeSelector = ({
159185
// Combine instruction text for tooltip
160186
const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}`
161187

188+
// Get source indicator for selected mode
189+
const selectedModeSource = selectedMode ? getModeSource(selectedMode, customModes) : null
190+
const selectedModeSourceText = getSourceDisplayText(selectedModeSource, t, false)
191+
162192
const trigger = (
163193
<PopoverTrigger
164194
disabled={disabled}
@@ -176,7 +206,12 @@ export const ModeSelector = ({
176206
: null,
177207
)}>
178208
<ChevronUp className="pointer-events-none opacity-80 flex-shrink-0 size-3" />
179-
<span className="truncate">{selectedMode?.name || ""}</span>
209+
<span className="truncate">
210+
{selectedMode?.name || ""}
211+
{selectedModeSourceText && (
212+
<span className="text-vscode-descriptionForeground ml-1">({selectedModeSourceText})</span>
213+
)}
214+
</span>
180215
</PopoverTrigger>
181216
)
182217

@@ -225,29 +260,41 @@ export const ModeSelector = ({
225260
</div>
226261
) : (
227262
<div className="py-1">
228-
{filteredModes.map((mode) => (
229-
<div
230-
key={mode.slug}
231-
onClick={() => handleSelect(mode.slug)}
232-
className={cn(
233-
"px-3 py-1.5 text-sm cursor-pointer flex items-center",
234-
"hover:bg-vscode-list-hoverBackground",
235-
mode.slug === value
236-
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
237-
: "",
238-
)}
239-
data-testid="mode-selector-item">
240-
<div className="flex-1 min-w-0">
241-
<div className="font-bold truncate">{mode.name}</div>
242-
{mode.description && (
243-
<div className="text-xs text-vscode-descriptionForeground truncate">
244-
{mode.description}
245-
</div>
263+
{filteredModes.map((mode) => {
264+
const modeSource = getModeSource(mode, customModes)
265+
const sourceShortText = getSourceDisplayText(modeSource, t, true)
266+
267+
return (
268+
<div
269+
key={mode.slug}
270+
onClick={() => handleSelect(mode.slug)}
271+
className={cn(
272+
"px-3 py-1.5 text-sm cursor-pointer flex items-center",
273+
"hover:bg-vscode-list-hoverBackground",
274+
mode.slug === value
275+
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
276+
: "",
246277
)}
278+
data-testid="mode-selector-item">
279+
<div className="flex-1 min-w-0">
280+
<div className="font-bold truncate flex items-center gap-1.5">
281+
{mode.name}
282+
{sourceShortText && (
283+
<span className="text-xs text-vscode-descriptionForeground font-normal">
284+
({sourceShortText})
285+
</span>
286+
)}
287+
</div>
288+
{mode.description && (
289+
<div className="text-xs text-vscode-descriptionForeground truncate">
290+
{mode.description}
291+
</div>
292+
)}
293+
</div>
294+
{mode.slug === value && <Check className="ml-auto size-4 p-0.5" />}
247295
</div>
248-
{mode.slug === value && <Check className="ml-auto size-4 p-0.5" />}
249-
</div>
250-
))}
296+
)
297+
})}
251298
</div>
252299
)}
253300
</div>

webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,140 @@ describe("ModeSelector", () => {
199199
const infoIcon = document.querySelector(".codicon-info")
200200
expect(infoIcon).toBeInTheDocument()
201201
})
202+
203+
test("shows source indicators for custom modes", () => {
204+
// Set up mock to return custom modes with source
205+
mockModes = [
206+
{
207+
slug: "custom-global",
208+
name: "Custom Global Mode",
209+
description: "A global custom mode",
210+
roleDefinition: "Role definition",
211+
groups: ["read", "edit"] as const,
212+
source: "global",
213+
},
214+
{
215+
slug: "custom-project",
216+
name: "Custom Project Mode",
217+
description: "A project custom mode",
218+
roleDefinition: "Role definition",
219+
groups: ["read", "edit"] as const,
220+
source: "project",
221+
},
222+
{
223+
slug: "code",
224+
name: "Code Mode",
225+
description: "Built-in code mode",
226+
roleDefinition: "Role definition",
227+
groups: ["read", "edit"] as const,
228+
},
229+
]
230+
231+
const customModes: ModeConfig[] = [
232+
{
233+
slug: "custom-global",
234+
name: "Custom Global Mode",
235+
description: "A global custom mode",
236+
roleDefinition: "Role definition",
237+
groups: ["read", "edit"],
238+
source: "global",
239+
},
240+
{
241+
slug: "custom-project",
242+
name: "Custom Project Mode",
243+
description: "A project custom mode",
244+
roleDefinition: "Role definition",
245+
groups: ["read", "edit"],
246+
source: "project",
247+
},
248+
]
249+
250+
render(
251+
<ModeSelector
252+
value={"custom-global" as Mode}
253+
onChange={vi.fn()}
254+
modeShortcutText="Ctrl+M"
255+
customModes={customModes}
256+
/>,
257+
)
258+
259+
// Click to open the popover
260+
fireEvent.click(screen.getByTestId("mode-selector-trigger"))
261+
262+
// Check that custom modes show source indicators in dropdown
263+
const modeItems = screen.getAllByTestId("mode-selector-item")
264+
265+
// Find the custom modes in the dropdown
266+
const globalModeItem = modeItems.find((item) => item.textContent?.includes("Custom Global Mode"))
267+
const projectModeItem = modeItems.find((item) => item.textContent?.includes("Custom Project Mode"))
268+
const builtinModeItem = modeItems.find((item) => item.textContent?.includes("Code Mode"))
269+
270+
// Custom modes should show source indicators
271+
expect(globalModeItem?.textContent).toContain("(chat:modeSelector.globalShort)")
272+
expect(projectModeItem?.textContent).toContain("(chat:modeSelector.projectShort)")
273+
274+
// Built-in mode should not show source indicator
275+
expect(builtinModeItem?.textContent).not.toContain("(chat:modeSelector.globalShort)")
276+
expect(builtinModeItem?.textContent).not.toContain("(chat:modeSelector.projectShort)")
277+
})
278+
279+
test("shows source indicator in selected mode button", () => {
280+
// Set up mock to return custom modes with source
281+
mockModes = [
282+
{
283+
slug: "custom-project",
284+
name: "Custom Project Mode",
285+
description: "A project custom mode",
286+
roleDefinition: "Role definition",
287+
groups: ["read", "edit"] as const,
288+
source: "project",
289+
},
290+
]
291+
292+
const customModes: ModeConfig[] = [
293+
{
294+
slug: "custom-project",
295+
name: "Custom Project Mode",
296+
description: "A project custom mode",
297+
roleDefinition: "Role definition",
298+
groups: ["read", "edit"],
299+
source: "project",
300+
},
301+
]
302+
303+
render(
304+
<ModeSelector
305+
value={"custom-project" as Mode}
306+
onChange={vi.fn()}
307+
modeShortcutText="Ctrl+M"
308+
customModes={customModes}
309+
/>,
310+
)
311+
312+
// Check that the trigger button shows the source indicator
313+
const trigger = screen.getByTestId("mode-selector-trigger")
314+
expect(trigger.textContent).toContain("Custom Project Mode")
315+
expect(trigger.textContent).toContain("(chat:modeSelector.project)")
316+
})
317+
318+
test("does not show source indicator for built-in modes", () => {
319+
// Set up mock to return only built-in modes
320+
mockModes = [
321+
{
322+
slug: "code",
323+
name: "Code Mode",
324+
description: "Built-in code mode",
325+
roleDefinition: "Role definition",
326+
groups: ["read", "edit"] as const,
327+
},
328+
]
329+
330+
render(<ModeSelector value={"code" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" customModes={[]} />)
331+
332+
// Check that the trigger button does not show source indicator
333+
const trigger = screen.getByTestId("mode-selector-trigger")
334+
expect(trigger.textContent).toContain("Code Mode")
335+
expect(trigger.textContent).not.toContain("(Global)")
336+
expect(trigger.textContent).not.toContain("(Project)")
337+
})
202338
})

webview-ui/src/i18n/locales/de/chat.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@
120120
"settings": "Mode Settings",
121121
"description": "Specialized personas that tailor Roo's behavior.",
122122
"searchPlaceholder": "Search modes...",
123-
"noResults": "No results found"
123+
"noResults": "No results found",
124+
"global": "Global",
125+
"project": "Project",
126+
"globalShort": "G",
127+
"projectShort": "P"
124128
},
125129
"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.",
126130
"addImages": "Add images to message",

webview-ui/src/i18n/locales/es/chat.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/fr/chat.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)