Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
efaaed9
feat: add Issue Fixer Orchestrator mode
MuriloFP Jul 3, 2025
57d3fbe
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 3, 2025
ef61905
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 4, 2025
f5a51c4
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 4, 2025
bcbf329
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 5, 2025
80413c0
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 5, 2025
ab10140
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 7, 2025
39c5cf7
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 7, 2025
00a0b63
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 8, 2025
080b61b
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 8, 2025
7a5ad14
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 8, 2025
2c73ff2
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 9, 2025
05ccf57
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 10, 2025
fdb1f35
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 11, 2025
10ce509
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 14, 2025
ab1f9fc
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 15, 2025
74fd8b4
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 15, 2025
6745c8f
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 16, 2025
faf2ee5
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 17, 2025
b2dadf9
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 17, 2025
f648e4c
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 17, 2025
a6d1e60
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 21, 2025
be90907
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 21, 2025
ed3a077
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 22, 2025
856313f
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 24, 2025
4dd68ea
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 28, 2025
b10fa5e
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 29, 2025
f016d7b
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 30, 2025
23855f2
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 31, 2025
3803c29
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 31, 2025
9a7a2a7
feat: add visual indicators for global/project custom modes (#6502)
MuriloFP Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions webview-ui/src/components/chat/ModeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ export const ModeSelector = ({
const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState()
const { t } = useAppTranslation()

// Helper to determine if a mode is custom and get its source
const getModeSource = (mode: ModeConfig): string | null => {
const isCustom = customModes?.some((m) => m.slug === mode.slug)
if (!isCustom) return null
return mode.source || "global" // Default to global if source is undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting the source type for better type safety. You could define:

Then use it in the ModeConfig interface. This would prevent typos and make the code more maintainable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting this default value to a constant:

This would make it easier to maintain and update if needed.

}

// Helper to get display text for source
const getSourceDisplayText = (source: string | null, isShort: boolean = false): string => {
if (!source) return ""
if (isShort) {
return source === "global" ? t("chat:modeSelector.globalShort") : t("chat:modeSelector.projectShort")
}
return source === "global" ? t("chat:modeSelector.global") : t("chat:modeSelector.project")
}

const trackModeSelectorOpened = React.useCallback(() => {
// Track telemetry every time the mode selector is opened
telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED)
Expand Down Expand Up @@ -159,6 +175,13 @@ export const ModeSelector = ({
// Combine instruction text for tooltip
const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}`

// Helper function to render source indicator
const renderSourceIndicator = (mode: ModeConfig, isShort: boolean = false) => {
const source = getModeSource(mode)
if (!source) return null
return <span className="ml-1 text-vscode-descriptionForeground">({getSourceDisplayText(source, isShort)})</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For better accessibility, consider adding an aria-label to help screen reader users:
custom mode

}

const trigger = (
<PopoverTrigger
disabled={disabled}
Expand All @@ -176,7 +199,10 @@ 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 || ""}
{selectedMode && renderSourceIndicator(selectedMode, false)}
</span>
</PopoverTrigger>
)

Expand Down Expand Up @@ -238,7 +264,10 @@ export const ModeSelector = ({
)}
data-testid="mode-selector-item">
<div className="flex-1 min-w-0">
<div className="font-bold truncate">{mode.name}</div>
<div className="font-bold truncate">
{mode.name}
{renderSourceIndicator(mode, true)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the mode list grows significantly, you might want to consider memoizing these source indicators to avoid recalculating them on every render. Though the current performance impact is minimal.

</div>
{mode.description && (
<div className="text-xs text-vscode-descriptionForeground truncate">
{mode.description}
Expand Down
147 changes: 143 additions & 4 deletions webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,21 @@ vi.mock("@/context/ExtensionStateContext", () => ({

vi.mock("@/i18n/TranslationContext", () => ({
useAppTranslation: () => ({
t: (key: string) => key,
t: (key: string) => {
const translations: Record<string, string> = {
"chat:modeSelector.global": "Global",
"chat:modeSelector.project": "Project",
"chat:modeSelector.globalShort": "G",
"chat:modeSelector.projectShort": "P",
"chat:modeSelector.description": "Select a mode to change how the assistant responds.",
"chat:modeSelector.searchPlaceholder": "Search modes...",
"chat:modeSelector.noResults": "No modes found",
"chat:modeSelector.marketplace": "Browse Marketplace",
"chat:modeSelector.settings": "Mode Settings",
"chat:modeSelector.title": "Modes",
}
return translations[key] || key
},
}),
}))

Expand Down Expand Up @@ -93,7 +107,7 @@ describe("ModeSelector", () => {
expect(screen.getByTestId("mode-search-input")).toBeInTheDocument()

// Info icon should be visible
expect(screen.getByText("chat:modeSelector.title")).toBeInTheDocument()
expect(screen.getByText("Modes")).toBeInTheDocument()
const infoIcon = document.querySelector(".codicon-info")
expect(infoIcon).toBeInTheDocument()
})
Expand All @@ -117,7 +131,7 @@ describe("ModeSelector", () => {
expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument()

// Info blurb should be visible
expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument()
expect(screen.getByText(/Select a mode to change how the assistant responds./)).toBeInTheDocument()

// Info icon should NOT be visible
const infoIcon = document.querySelector(".codicon-info")
Expand Down Expand Up @@ -169,7 +183,7 @@ describe("ModeSelector", () => {
expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument()

// Info blurb should be visible instead
expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument()
expect(screen.getByText(/Select a mode to change how the assistant responds./)).toBeInTheDocument()

// Info icon should NOT be visible
const infoIcon = document.querySelector(".codicon-info")
Expand Down Expand Up @@ -199,4 +213,129 @@ describe("ModeSelector", () => {
const infoIcon = document.querySelector(".codicon-info")
expect(infoIcon).toBeInTheDocument()
})

test("shows source indicator for custom modes", () => {
const customModesWithSource: ModeConfig[] = [
{
slug: "custom-global",
name: "Custom Global Mode",
roleDefinition: "Role",
groups: ["read"] as ModeConfig["groups"],
source: "global",
},
{
slug: "custom-project",
name: "Custom Project Mode",
roleDefinition: "Role",
groups: ["read"] as ModeConfig["groups"],
source: "project",
},
]

// Set up mock to return custom modes
mockModes = [
...customModesWithSource,
{
slug: "code",
name: "Code",
roleDefinition: "Role",
groups: ["read"] as ModeConfig["groups"],
},
]

render(
<ModeSelector
value={"custom-global" as Mode}
onChange={vi.fn()}
modeShortcutText="Ctrl+M"
customModes={customModesWithSource}
/>,
)

// Check selected mode shows full indicator
const trigger = screen.getByTestId("mode-selector-trigger")
expect(trigger).toHaveTextContent("Custom Global Mode")
expect(trigger).toHaveTextContent("(Global)")

// Open dropdown
fireEvent.click(trigger)

// Check dropdown shows short indicators
const items = screen.getAllByTestId("mode-selector-item")
const globalItem = items.find((item) => item.textContent?.includes("Custom Global Mode"))
const projectItem = items.find((item) => item.textContent?.includes("Custom Project Mode"))

expect(globalItem).toHaveTextContent("(G)")
expect(projectItem).toHaveTextContent("(P)")
})

test("shows no indicator for built-in modes", () => {
// Set up mock to return only built-in modes
mockModes = [
{
slug: "code",
name: "Code",
roleDefinition: "Role",
groups: ["read"] as ModeConfig["groups"],
},
{
slug: "architect",
name: "Architect",
roleDefinition: "Role",
groups: ["read"] as ModeConfig["groups"],
},
]

render(<ModeSelector value={"code" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)

const trigger = screen.getByTestId("mode-selector-trigger")
expect(trigger).toHaveTextContent("Code")
expect(trigger).not.toHaveTextContent("(")

// Open dropdown
fireEvent.click(trigger)

// Check that no items have indicators
const items = screen.getAllByTestId("mode-selector-item")
items.forEach((item) => {
expect(item).not.toHaveTextContent("(Global")
expect(item).not.toHaveTextContent("(Project")
})
})

test("defaults to global for custom modes without source", () => {
const customModesNoSource: ModeConfig[] = [
{
slug: "custom-old",
name: "Old Custom Mode",
roleDefinition: "Role",
groups: ["read"] as ModeConfig["groups"],
// No source field
},
]

// Set up mock to include the custom mode
mockModes = [
...customModesNoSource,
{
slug: "code",
name: "Code",
roleDefinition: "Role",
groups: ["read"] as ModeConfig["groups"],
},
]

render(
<ModeSelector
value={"custom-old" as Mode}
onChange={vi.fn()}
modeShortcutText="Ctrl+M"
customModes={customModesNoSource}
/>,
)

const trigger = screen.getByTestId("mode-selector-trigger")
expect(trigger).toHaveTextContent("Old Custom Mode")
expect(trigger).toHaveTextContent("(Global)")
})
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good test coverage! Consider adding an edge case test for when the prop is undefined/null to ensure the helper functions handle this gracefully.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/ca/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/de/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/es/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/fr/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/hi/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/id/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/it/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/ja/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/ko/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/nl/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion webview-ui/src/i18n/locales/pl/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading