diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 394c08dbd7..520b2750ed 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "projecte", - "global": "global" + "global": "global", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 0e51a1644d..84b3c61d1f 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "projekt", - "global": "global" + "global": "global", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 57454cbfe6..b21f8e6262 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -154,7 +154,9 @@ }, "scope": { "project": "project", - "global": "global" + "global": "global", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 32ae5f284e..1e92a68787 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "proyecto", - "global": "global" + "global": "global", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 3f256a3488..9356d44e0b 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "projet", - "global": "global" + "global": "global", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 6ffc87a9eb..b66710e2aa 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "परियोजना", - "global": "वैश्विक" + "global": "वैश्विक", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index fdd619ec4d..0b3ae7c92c 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "proyek", - "global": "global" + "global": "global", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 92cbf64316..42f569f010 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "progetto", - "global": "globale" + "global": "globale", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index c82aa6b90d..8d4baed1bc 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "プロジェクト", - "global": "グローバル" + "global": "グローバル", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index d9d178a39c..9f569c8c1b 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "프로젝트", - "global": "글로벌" + "global": "글로벌", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 277b1d7445..d30c5a042f 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "project", - "global": "globaal" + "global": "globaal", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index ce0597e241..3f0182b37b 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "projekt", - "global": "globalny" + "global": "globalny", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 96912bf9a4..89e215c4da 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "projeto", - "global": "global" + "global": "global", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 7f469da787..da8718a06e 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "проект", - "global": "глобальный" + "global": "глобальный", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index c100172e61..387d37bbec 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "proje", - "global": "küresel" + "global": "küresel", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 9a2fe23c77..c5109f3222 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "dự án", - "global": "toàn cầu" + "global": "toàn cầu", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 9dba8dada9..9d5713add2 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -170,7 +170,9 @@ }, "scope": { "project": "项目", - "global": "全局" + "global": "全局", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 1167e49220..2cd4c84e01 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -165,7 +165,9 @@ }, "scope": { "project": "專案", - "global": "全域" + "global": "全域", + "projectShort": "P", + "globalShort": "G" } }, "marketplace": { diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 93dd2f1f4f..45ed92d823 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -46,6 +46,32 @@ export const ModeSelector = ({ const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState() const { t } = useAppTranslation() + // Helper function to get mode source + const getModeSource = React.useCallback( + (modeSlug: string): "global" | "project" | null => { + const customMode = customModes?.find((mode) => mode.slug === modeSlug) + if (!customMode) return null + return customMode.source || "global" // Default to "global" if source is undefined + }, + [customModes], + ) + + // Helper function to get source display text + const getSourceDisplayText = React.useCallback( + (source: "global" | "project" | null, isShort: boolean = false): string => { + if (!source) return "" + if (isShort) { + return source === "global" + ? ` (${t("common:customModes.scope.globalShort")})` + : ` (${t("common:customModes.scope.projectShort")})` + } + return source === "global" + ? ` (${t("common:customModes.scope.global")})` + : ` (${t("common:customModes.scope.project")})` + }, + [t], + ) + const trackModeSelectorOpened = React.useCallback(() => { // Track telemetry every time the mode selector is opened telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED) @@ -159,6 +185,9 @@ export const ModeSelector = ({ // Combine instruction text for tooltip const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}` + // Get source for selected mode + const selectedModeSource = React.useMemo(() => getModeSource(value), [value, getModeSource]) + const trigger = ( - {selectedMode?.name || ""} + + {selectedMode?.name || ""} + {selectedModeSource && ( + + {getSourceDisplayText(selectedModeSource)} + + )} + ) @@ -225,29 +261,39 @@ 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.slug) + 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} + {modeSource && ( + + {getSourceDisplayText(modeSource, true)} + + )} +
+ {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..6edd6dd8d7 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -47,6 +47,157 @@ vi.mock("@roo/modes", async () => { }) describe("ModeSelector", () => { + test("shows source indicators for custom modes", () => { + // Set up mock modes including custom modes with different sources + mockModes = [ + { + slug: "built-in-mode", + name: "Built-in Mode", + description: "A built-in mode", + roleDefinition: "Role definition", + groups: ["read"], + }, + { + slug: "global-custom", + name: "Global Custom", + description: "A global custom mode", + roleDefinition: "Role definition", + groups: ["read"], + source: "global", + }, + { + slug: "project-custom", + name: "Project Custom", + description: "A project custom mode", + roleDefinition: "Role definition", + groups: ["read"], + source: "project", + }, + ] + + const customModes: ModeConfig[] = [ + { + slug: "global-custom", + name: "Global Custom", + description: "A global custom mode", + roleDefinition: "Role definition", + groups: ["read"], + source: "global", + }, + { + slug: "project-custom", + name: "Project Custom", + description: "A project custom mode", + roleDefinition: "Role definition", + groups: ["read"], + source: "project", + }, + ] + + const { rerender } = render( + , + ) + + // Check trigger shows full source text for selected custom mode + const trigger = screen.getByTestId("mode-selector-trigger") + expect(trigger.textContent).toContain("Global Custom") + expect(trigger.textContent).toContain("(common:customModes.scope.global)") + + // Click to open dropdown + fireEvent.click(trigger) + + // Check dropdown items show short indicators + const modeItems = screen.getAllByTestId("mode-selector-item") + + // Built-in mode should not have indicator + expect(modeItems[0].textContent).toContain("Built-in Mode") + expect(modeItems[0].textContent).not.toContain("(") + + // Global custom mode should have (G) indicator + expect(modeItems[1].textContent).toContain("Global Custom") + expect(modeItems[1].textContent).toContain("(common:customModes.scope.globalShort)") + + // Project custom mode should have (P) indicator + expect(modeItems[2].textContent).toContain("Project Custom") + expect(modeItems[2].textContent).toContain("(common:customModes.scope.projectShort)") + + // Close dropdown + fireEvent.click(document.body) + + // Test with project mode selected + rerender( + , + ) + + // Check trigger shows project source + expect(screen.getByTestId("mode-selector-trigger").textContent).toContain("(common:customModes.scope.project)") + }) + + test("does not show source indicators for built-in modes", () => { + mockModes = [ + { + slug: "code", + name: "Code", + description: "Code mode", + roleDefinition: "Role definition", + groups: ["read", "edit"], + }, + ] + + render() + + // Check trigger does not show source indicator + const trigger = screen.getByTestId("mode-selector-trigger") + expect(trigger.textContent).toBe("Code") + expect(trigger.textContent).not.toContain("(") + + // Click to open dropdown + fireEvent.click(trigger) + + // Check dropdown item does not show indicator + const modeItem = screen.getByTestId("mode-selector-item") + expect(modeItem.textContent).toContain("Code") + expect(modeItem.textContent).not.toContain("(") + }) + + test("defaults source to global when custom mode has no source field", () => { + const customModes: ModeConfig[] = [ + { + slug: "custom-no-source", + name: "Custom No Source", + description: "A custom mode without source field", + roleDefinition: "Role definition", + groups: ["read"], + // No source field - should default to "global" + }, + ] + + mockModes = [...customModes] + + render( + , + ) + + // Check trigger shows global source (default) + const trigger = screen.getByTestId("mode-selector-trigger") + expect(trigger.textContent).toContain("(common:customModes.scope.global)") + }) + test("shows custom description from customModePrompts", () => { const customModePrompts = { code: {