Browse Source

feat: Add search functionality to mode selector popup and reorganize layout (#6140)

Co-authored-by: Roo Code Translate Mode
Co-authored-by: Daniel Riccio <[email protected]>
Hannes Rudolph 7 months ago
parent
commit
8625b451f4

+ 200 - 76
webview-ui/src/components/chat/ModeSelector.tsx

@@ -1,5 +1,5 @@
 import React from "react"
-import { ChevronUp, Check } from "lucide-react"
+import { ChevronUp, Check, X } from "lucide-react"
 import { cn } from "@/lib/utils"
 import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
 import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
@@ -11,6 +11,10 @@ import { Mode, getAllModes } from "@roo/modes"
 import { ModeConfig, CustomModePrompts } from "@roo-code/types"
 import { telemetryClient } from "@/utils/TelemetryClient"
 import { TelemetryEventName } from "@roo-code/types"
+import { Fzf } from "fzf"
+
+// Minimum number of modes required to show search functionality
+const SEARCH_THRESHOLD = 6
 
 interface ModeSelectorProps {
 	value: Mode
@@ -21,6 +25,7 @@ interface ModeSelectorProps {
 	modeShortcutText: string
 	customModes?: ModeConfig[]
 	customModePrompts?: CustomModePrompts
+	disableSearch?: boolean
 }
 
 export const ModeSelector = ({
@@ -32,13 +37,16 @@ export const ModeSelector = ({
 	modeShortcutText,
 	customModes,
 	customModePrompts,
+	disableSearch = false,
 }: ModeSelectorProps) => {
 	const [open, setOpen] = React.useState(false)
+	const [searchValue, setSearchValue] = React.useState("")
+	const searchInputRef = React.useRef<HTMLInputElement>(null)
 	const portalContainer = useRooPortal("roo-portal")
 	const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState()
 	const { t } = useAppTranslation()
 
-	const trackModeSelectorOpened = () => {
+	const trackModeSelectorOpened = React.useCallback(() => {
 		// Track telemetry every time the mode selector is opened
 		telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED)
 
@@ -47,7 +55,7 @@ export const ModeSelector = ({
 			setHasOpenedModeSelector(true)
 			vscode.postMessage({ type: "hasOpenedModeSelector", bool: true })
 		}
-	}
+	}, [hasOpenedModeSelector, setHasOpenedModeSelector])
 
 	// Get all modes including custom modes and merge custom prompt descriptions
 	const modes = React.useMemo(() => {
@@ -61,6 +69,96 @@ export const ModeSelector = ({
 	// Find the selected mode
 	const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value])
 
+	// Memoize searchable items for fuzzy search with separate name and description search
+	const nameSearchItems = React.useMemo(() => {
+		return modes.map((mode) => ({
+			original: mode,
+			searchStr: [mode.name, mode.slug].filter(Boolean).join(" "),
+		}))
+	}, [modes])
+
+	const descriptionSearchItems = React.useMemo(() => {
+		return modes.map((mode) => ({
+			original: mode,
+			searchStr: mode.description || "",
+		}))
+	}, [modes])
+
+	// Create memoized Fzf instances for name and description searches
+	const nameFzfInstance = React.useMemo(() => {
+		return new Fzf(nameSearchItems, {
+			selector: (item) => item.searchStr,
+		})
+	}, [nameSearchItems])
+
+	const descriptionFzfInstance = React.useMemo(() => {
+		return new Fzf(descriptionSearchItems, {
+			selector: (item) => item.searchStr,
+		})
+	}, [descriptionSearchItems])
+
+	// Filter modes based on search value using fuzzy search with priority
+	const filteredModes = React.useMemo(() => {
+		if (!searchValue) return modes
+
+		// First search in names/slugs
+		const nameMatches = nameFzfInstance.find(searchValue)
+		const nameMatchedModes = new Set(nameMatches.map((result) => result.item.original.slug))
+
+		// Then search in descriptions
+		const descriptionMatches = descriptionFzfInstance.find(searchValue)
+
+		// Combine results: name matches first, then description matches
+		const combinedResults = [
+			...nameMatches.map((result) => result.item.original),
+			...descriptionMatches
+				.filter((result) => !nameMatchedModes.has(result.item.original.slug))
+				.map((result) => result.item.original),
+		]
+
+		return combinedResults
+	}, [modes, searchValue, nameFzfInstance, descriptionFzfInstance])
+
+	const onClearSearch = React.useCallback(() => {
+		setSearchValue("")
+		searchInputRef.current?.focus()
+	}, [])
+
+	const handleSelect = React.useCallback(
+		(modeSlug: string) => {
+			onChange(modeSlug as Mode)
+			setOpen(false)
+			// Clear search after selection
+			setSearchValue("")
+		},
+		[onChange],
+	)
+
+	const onOpenChange = React.useCallback(
+		(isOpen: boolean) => {
+			if (isOpen) trackModeSelectorOpened()
+			setOpen(isOpen)
+			// Clear search when closing
+			if (!isOpen) {
+				setSearchValue("")
+			}
+		},
+		[trackModeSelectorOpened],
+	)
+
+	// Auto-focus search input when popover opens
+	React.useEffect(() => {
+		if (open && searchInputRef.current) {
+			searchInputRef.current.focus()
+		}
+	}, [open])
+
+	// Determine if search should be shown
+	const showSearch = !disableSearch && modes.length > SEARCH_THRESHOLD
+
+	// Combine instruction text for tooltip
+	const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}`
+
 	const trigger = (
 		<PopoverTrigger
 			disabled={disabled}
@@ -83,13 +181,7 @@ export const ModeSelector = ({
 	)
 
 	return (
-		<Popover
-			open={open}
-			onOpenChange={(isOpen) => {
-				if (isOpen) trackModeSelectorOpened()
-				setOpen(isOpen)
-			}}
-			data-testid="mode-selector-root">
+		<Popover open={open} onOpenChange={onOpenChange} data-testid="mode-selector-root">
 			{title ? <StandardTooltip content={title}>{trigger}</StandardTooltip> : trigger}
 
 			<PopoverContent
@@ -98,78 +190,110 @@ export const ModeSelector = ({
 				container={portalContainer}
 				className="p-0 overflow-hidden min-w-80 max-w-9/10">
 				<div className="flex flex-col w-full">
-					<div className="p-3 border-b border-vscode-dropdown-border cursor-default">
-						<div className="flex flex-row items-center gap-1 p-0 mt-0 mb-1 w-full">
-							<h4 className="m-0 pb-2 flex-1">{t("chat:modeSelector.title")}</h4>
-							<div className="flex flex-row gap-1 ml-auto mb-1">
-								<IconButton
-									iconClass="codicon-extensions"
-									title={t("chat:modeSelector.marketplace")}
-									onClick={() => {
-										window.postMessage(
-											{
-												type: "action",
-												action: "marketplaceButtonClicked",
-												values: { marketplaceTab: "mode" },
-											},
-											"*",
-										)
-
-										setOpen(false)
-									}}
-								/>
-								<IconButton
-									iconClass="codicon-settings-gear"
-									title={t("chat:modeSelector.settings")}
-									onClick={() => {
-										vscode.postMessage({
-											type: "switchTab",
-											tab: "modes",
-										})
-										setOpen(false)
-									}}
-								/>
-							</div>
+					{/* Show search bar only when there are more than SEARCH_THRESHOLD items, otherwise show info blurb */}
+					{showSearch ? (
+						<div className="relative p-2 border-b border-vscode-dropdown-border">
+							<input
+								aria-label="Search modes"
+								ref={searchInputRef}
+								value={searchValue}
+								onChange={(e) => setSearchValue(e.target.value)}
+								placeholder={t("chat:modeSelector.searchPlaceholder")}
+								className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0"
+								data-testid="mode-search-input"
+							/>
+							{searchValue.length > 0 && (
+								<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
+									<X
+										className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
+										onClick={onClearSearch}
+									/>
+								</div>
+							)}
 						</div>
-						<p className="my-0 pr-4 text-sm w-full">
-							{t("chat:modeSelector.description")}
-							<br />
-							{modeShortcutText}
-						</p>
-					</div>
+					) : (
+						<div className="p-3 border-b border-vscode-dropdown-border">
+							<p className="m-0 text-xs text-vscode-descriptionForeground">{instructionText}</p>
+						</div>
+					)}
 
 					{/* Mode List */}
-					<div className="max-h-[400px] overflow-y-auto py-0">
-						{modes.map((mode) => (
-							<div
-								className={cn(
-									"p-2 text-sm cursor-pointer flex flex-row gap-4 items-center",
-									"hover:bg-vscode-list-hoverBackground",
-									mode.slug === value
-										? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
-										: "",
-								)}
-								key={mode.slug}
+					<div className="max-h-[300px] overflow-y-auto">
+						{filteredModes.length === 0 && searchValue ? (
+							<div className="py-2 px-3 text-sm text-vscode-foreground/70">
+								{t("chat:modeSelector.noResults")}
+							</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>
+											)}
+										</div>
+										{mode.slug === value && <Check className="ml-auto size-4 p-0.5" />}
+									</div>
+								))}
+							</div>
+						)}
+					</div>
+
+					{/* Bottom bar with buttons on left and title on right */}
+					<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
+						<div className="flex flex-row gap-1">
+							<IconButton
+								iconClass="codicon-extensions"
+								title={t("chat:modeSelector.marketplace")}
 								onClick={() => {
-									onChange(mode.slug as Mode)
+									window.postMessage(
+										{
+											type: "action",
+											action: "marketplaceButtonClicked",
+											values: { marketplaceTab: "mode" },
+										},
+										"*",
+									)
 									setOpen(false)
 								}}
-								data-testid="mode-selector-item">
-								<div className="flex-grow">
-									<p className="m-0 mb-0 font-bold">{mode.name}</p>
-									{mode.description && (
-										<p className="m-0 py-0 pl-4 h-4 flex-1 text-xs overflow-hidden">
-											{mode.description}
-										</p>
-									)}
-								</div>
-								{mode.slug === value ? (
-									<Check className="m-0 size-4 p-0.5" />
-								) : (
-									<div className="size-4" />
-								)}
-							</div>
-						))}
+							/>
+							<IconButton
+								iconClass="codicon-settings-gear"
+								title={t("chat:modeSelector.settings")}
+								onClick={() => {
+									vscode.postMessage({
+										type: "switchTab",
+										tab: "modes",
+									})
+									setOpen(false)
+								}}
+							/>
+						</div>
+
+						{/* Info icon and title on the right - only show info icon when search bar is visible */}
+						<div className="flex items-center gap-1 pr-1">
+							{showSearch && (
+								<StandardTooltip content={instructionText}>
+									<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
+								</StandardTooltip>
+							)}
+							<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
+								{t("chat:modeSelector.title")}
+							</h4>
+						</div>
 					</div>
 				</div>
 			</PopoverContent>

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

@@ -1,8 +1,9 @@
 import React from "react"
-import { render, screen } from "@/utils/test-utils"
+import { render, screen, fireEvent } from "@/utils/test-utils"
 import { describe, test, expect, vi } from "vitest"
 import ModeSelector from "../ModeSelector"
 import { Mode } from "@roo/modes"
+import { ModeConfig } from "@roo-code/types"
 
 // Mock the dependencies
 vi.mock("@/utils/vscode", () => ({
@@ -28,6 +29,23 @@ vi.mock("@/components/ui/hooks/useRooPortal", () => ({
 	useRooPortal: () => document.body,
 }))
 
+vi.mock("@/utils/TelemetryClient", () => ({
+	telemetryClient: {
+		capture: vi.fn(),
+	},
+}))
+
+// Create a variable to control what getAllModes returns
+let mockModes: ModeConfig[] = []
+
+vi.mock("@roo/modes", async () => {
+	const actual = await vi.importActual<typeof import("@roo/modes")>("@roo/modes")
+	return {
+		...actual,
+		getAllModes: () => mockModes,
+	}
+})
+
 describe("ModeSelector", () => {
 	test("shows custom description from customModePrompts", () => {
 		const customModePrompts = {
@@ -55,4 +73,130 @@ describe("ModeSelector", () => {
 		// The component should be rendered
 		expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument()
 	})
+
+	test("shows search bar when there are more than 6 modes", () => {
+		// Set up mock to return 7 modes
+		mockModes = Array.from({ length: 7 }, (_, i) => ({
+			slug: `mode-${i}`,
+			name: `Mode ${i}`,
+			description: `Description for mode ${i}`,
+			roleDefinition: "Role definition",
+			groups: ["read", "edit"],
+		}))
+
+		render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
+
+		// Click to open the popover
+		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
+
+		// Search input should be visible
+		expect(screen.getByTestId("mode-search-input")).toBeInTheDocument()
+
+		// Info icon should be visible
+		expect(screen.getByText("chat:modeSelector.title")).toBeInTheDocument()
+		const infoIcon = document.querySelector(".codicon-info")
+		expect(infoIcon).toBeInTheDocument()
+	})
+
+	test("shows info blurb instead of search bar when there are 6 or fewer modes", () => {
+		// Set up mock to return 5 modes
+		mockModes = Array.from({ length: 5 }, (_, i) => ({
+			slug: `mode-${i}`,
+			name: `Mode ${i}`,
+			description: `Description for mode ${i}`,
+			roleDefinition: "Role definition",
+			groups: ["read", "edit"],
+		}))
+
+		render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
+
+		// Click to open the popover
+		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
+
+		// Search input should NOT be visible
+		expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument()
+
+		// Info blurb should be visible
+		expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument()
+
+		// Info icon should NOT be visible
+		const infoIcon = document.querySelector(".codicon-info")
+		expect(infoIcon).not.toBeInTheDocument()
+	})
+
+	test("filters modes correctly when searching", () => {
+		// Set up mock to return 7 modes to enable search
+		mockModes = Array.from({ length: 7 }, (_, i) => ({
+			slug: `mode-${i}`,
+			name: `Mode ${i}`,
+			description: `Description for mode ${i}`,
+			roleDefinition: "Role definition",
+			groups: ["read", "edit"],
+		}))
+
+		render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
+
+		// Click to open the popover
+		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
+
+		// Type in search
+		const searchInput = screen.getByTestId("mode-search-input")
+		fireEvent.change(searchInput, { target: { value: "Mode 3" } })
+
+		// Should show filtered results
+		const modeItems = screen.getAllByTestId("mode-selector-item")
+		expect(modeItems.length).toBeLessThan(7) // Should have filtered some out
+	})
+
+	test("respects disableSearch prop even when there are more than 6 modes", () => {
+		// Set up mock to return 10 modes
+		mockModes = Array.from({ length: 10 }, (_, i) => ({
+			slug: `mode-${i}`,
+			name: `Mode ${i}`,
+			description: `Description for mode ${i}`,
+			roleDefinition: "Role definition",
+			groups: ["read", "edit"],
+		}))
+
+		render(
+			<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" disableSearch={true} />,
+		)
+
+		// Click to open the popover
+		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
+
+		// Search input should NOT be visible even with 10 modes
+		expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument()
+
+		// Info blurb should be visible instead
+		expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument()
+
+		// Info icon should NOT be visible
+		const infoIcon = document.querySelector(".codicon-info")
+		expect(infoIcon).not.toBeInTheDocument()
+	})
+
+	test("shows search when disableSearch is false (default) and modes > 6", () => {
+		// Set up mock to return 8 modes
+		mockModes = Array.from({ length: 8 }, (_, i) => ({
+			slug: `mode-${i}`,
+			name: `Mode ${i}`,
+			description: `Description for mode ${i}`,
+			roleDefinition: "Role definition",
+			groups: ["read", "edit"],
+		}))
+
+		// Don't pass disableSearch prop (should default to false)
+		render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
+
+		// Click to open the popover
+		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
+
+		// Search input should be visible
+		expect(screen.getByTestId("mode-search-input")).toBeInTheDocument()
+
+		// Info icon should be visible
+		const infoIcon = document.querySelector(".codicon-info")
+		expect(infoIcon).toBeInTheDocument()
+	})
 })

+ 3 - 1
webview-ui/src/i18n/locales/ca/chat.json

@@ -116,7 +116,9 @@
 		"title": "Modes",
 		"marketplace": "Marketplace de Modes",
 		"settings": "Configuració de Modes",
-		"description": "Personalitats especialitzades que adapten el comportament de Roo."
+		"description": "Personalitats especialitzades que adapten el comportament de Roo.",
+		"searchPlaceholder": "Cerca modes...",
+		"noResults": "No s'han trobat resultats"
 	},
 	"errorReadingFile": "Error en llegir el fitxer:",
 	"noValidImages": "No s'ha processat cap imatge vàlida",

+ 3 - 1
webview-ui/src/i18n/locales/de/chat.json

@@ -116,7 +116,9 @@
 		"title": "Modi",
 		"marketplace": "Modus-Marketplace",
 		"settings": "Modus-Einstellungen",
-		"description": "Spezialisierte Personas, die Roos Verhalten anpassen."
+		"description": "Spezialisierte Personas, die Roos Verhalten anpassen.",
+		"searchPlaceholder": "Modi suchen...",
+		"noResults": "Keine Ergebnisse gefunden"
 	},
 	"errorReadingFile": "Fehler beim Lesen der Datei:",
 	"noValidImages": "Keine gültigen Bilder wurden verarbeitet",

+ 3 - 1
webview-ui/src/i18n/locales/en/chat.json

@@ -118,7 +118,9 @@
 		"title": "Modes",
 		"marketplace": "Mode Marketplace",
 		"settings": "Mode Settings",
-		"description": "Specialized personas that tailor Roo's behavior."
+		"description": "Specialized personas that tailor Roo's behavior.",
+		"searchPlaceholder": "Search modes...",
+		"noResults": "No results found"
 	},
 	"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",

+ 3 - 1
webview-ui/src/i18n/locales/es/chat.json

@@ -116,7 +116,9 @@
 		"title": "Modos",
 		"marketplace": "Marketplace de Modos",
 		"settings": "Configuración de Modos",
-		"description": "Personalidades especializadas que adaptan el comportamiento de Roo."
+		"description": "Personalidades especializadas que adaptan el comportamiento de Roo.",
+		"searchPlaceholder": "Buscar modos...",
+		"noResults": "No se encontraron resultados"
 	},
 	"errorReadingFile": "Error al leer el archivo:",
 	"noValidImages": "No se procesaron imágenes válidas",

+ 3 - 1
webview-ui/src/i18n/locales/fr/chat.json

@@ -116,7 +116,9 @@
 		"title": "Modes",
 		"marketplace": "Marketplace de Modes",
 		"settings": "Paramètres des Modes",
-		"description": "Personas spécialisés qui adaptent le comportement de Roo."
+		"description": "Personas spécialisés qui adaptent le comportement de Roo.",
+		"searchPlaceholder": "Rechercher des modes...",
+		"noResults": "Aucun résultat trouvé"
 	},
 	"errorReadingFile": "Erreur lors de la lecture du fichier :",
 	"noValidImages": "Aucune image valide n'a été traitée",

+ 3 - 1
webview-ui/src/i18n/locales/hi/chat.json

@@ -116,7 +116,9 @@
 		"title": "मोड्स",
 		"marketplace": "मोड मार्केटप्लेस",
 		"settings": "मोड सेटिंग्स",
-		"description": "विशेष व्यक्तित्व जो Roo के व्यवहार को अनुकूलित करते हैं।"
+		"description": "विशेष व्यक्तित्व जो Roo के व्यवहार को अनुकूलित करते हैं।",
+		"searchPlaceholder": "मोड खोजें...",
+		"noResults": "कोई परिणाम नहीं मिला"
 	},
 	"errorReadingFile": "फ़ाइल पढ़ने में त्रुटि:",
 	"noValidImages": "कोई मान्य चित्र प्रोसेस नहीं किया गया",

+ 3 - 1
webview-ui/src/i18n/locales/id/chat.json

@@ -122,7 +122,9 @@
 		"title": "Mode",
 		"marketplace": "Marketplace Mode",
 		"settings": "Pengaturan Mode",
-		"description": "Persona khusus yang menyesuaikan perilaku Roo."
+		"description": "Persona khusus yang menyesuaikan perilaku Roo.",
+		"searchPlaceholder": "Cari mode...",
+		"noResults": "Tidak ada hasil yang ditemukan"
 	},
 	"addImages": "Tambahkan gambar ke pesan",
 	"sendMessage": "Kirim pesan",

+ 3 - 1
webview-ui/src/i18n/locales/it/chat.json

@@ -116,7 +116,9 @@
 		"title": "Modalità",
 		"marketplace": "Marketplace delle Modalità",
 		"settings": "Impostazioni Modalità",
-		"description": "Personalità specializzate che adattano il comportamento di Roo."
+		"description": "Personalità specializzate che adattano il comportamento di Roo.",
+		"searchPlaceholder": "Cerca modalità...",
+		"noResults": "Nessun risultato trovato"
 	},
 	"errorReadingFile": "Errore nella lettura del file:",
 	"noValidImages": "Nessuna immagine valida è stata elaborata",

+ 3 - 1
webview-ui/src/i18n/locales/ja/chat.json

@@ -116,7 +116,9 @@
 		"title": "モード",
 		"marketplace": "モードマーケットプレイス",
 		"settings": "モード設定",
-		"description": "Rooの動作をカスタマイズする専門的なペルソナ。"
+		"description": "Rooの動作をカスタマイズする専門的なペルソナ。",
+		"searchPlaceholder": "モードを検索...",
+		"noResults": "結果が見つかりません"
 	},
 	"errorReadingFile": "ファイル読み込みエラー:",
 	"noValidImages": "有効な画像が処理されませんでした",

+ 3 - 1
webview-ui/src/i18n/locales/ko/chat.json

@@ -116,7 +116,9 @@
 		"title": "모드",
 		"marketplace": "모드 마켓플레이스",
 		"settings": "모드 설정",
-		"description": "Roo의 행동을 맞춤화하는 전문화된 페르소나."
+		"description": "Roo의 행동을 맞춤화하는 전문화된 페르소나.",
+		"searchPlaceholder": "모드 검색...",
+		"noResults": "결과를 찾을 수 없습니다"
 	},
 	"errorReadingFile": "파일 읽기 오류:",
 	"noValidImages": "처리된 유효한 이미지가 없습니다",

+ 3 - 1
webview-ui/src/i18n/locales/nl/chat.json

@@ -108,7 +108,9 @@
 		"title": "Modi",
 		"marketplace": "Modus Marktplaats",
 		"settings": "Modus Instellingen",
-		"description": "Gespecialiseerde persona's die het gedrag van Roo aanpassen."
+		"description": "Gespecialiseerde persona's die het gedrag van Roo aanpassen.",
+		"searchPlaceholder": "Zoek modi...",
+		"noResults": "Geen resultaten gevonden"
 	},
 	"addImages": "Afbeeldingen toevoegen aan bericht",
 	"sendMessage": "Bericht verzenden",

+ 3 - 1
webview-ui/src/i18n/locales/pl/chat.json

@@ -116,7 +116,9 @@
 		"title": "Tryby",
 		"marketplace": "Marketplace Trybów",
 		"settings": "Ustawienia Trybów",
-		"description": "Wyspecjalizowane persony, które dostosowują zachowanie Roo."
+		"description": "Wyspecjalizowane persony, które dostosowują zachowanie Roo.",
+		"searchPlaceholder": "Szukaj trybów...",
+		"noResults": "Nie znaleziono wyników"
 	},
 	"errorReadingFile": "Błąd odczytu pliku:",
 	"noValidImages": "Nie przetworzono żadnych prawidłowych obrazów",

+ 3 - 1
webview-ui/src/i18n/locales/pt-BR/chat.json

@@ -116,7 +116,9 @@
 		"title": "Modos",
 		"marketplace": "Marketplace de Modos",
 		"settings": "Configurações de Modos",
-		"description": "Personas especializadas que adaptam o comportamento do Roo."
+		"description": "Personas especializadas que adaptam o comportamento do Roo.",
+		"searchPlaceholder": "Pesquisar modos...",
+		"noResults": "Nenhum resultado encontrado"
 	},
 	"errorReadingFile": "Erro ao ler arquivo:",
 	"noValidImages": "Nenhuma imagem válida foi processada",

+ 3 - 1
webview-ui/src/i18n/locales/ru/chat.json

@@ -108,7 +108,9 @@
 		"title": "Режимы",
 		"marketplace": "Маркетплейс режимов",
 		"settings": "Настройки режимов",
-		"description": "Специализированные персоны, которые настраивают поведение Roo."
+		"description": "Специализированные персоны, которые настраивают поведение Roo.",
+		"searchPlaceholder": "Поиск режимов...",
+		"noResults": "Ничего не найдено"
 	},
 	"addImages": "Добавить изображения к сообщению",
 	"sendMessage": "Отправить сообщение",

+ 3 - 1
webview-ui/src/i18n/locales/tr/chat.json

@@ -116,7 +116,9 @@
 		"title": "Modlar",
 		"marketplace": "Mod Pazaryeri",
 		"settings": "Mod Ayarları",
-		"description": "Roo'nun davranışını özelleştiren uzmanlaşmış kişilikler."
+		"description": "Roo'nun davranışını özelleştiren uzmanlaşmış kişilikler.",
+		"searchPlaceholder": "Modları ara...",
+		"noResults": "Sonuç bulunamadı"
 	},
 	"errorReadingFile": "Dosya okuma hatası:",
 	"noValidImages": "Hiçbir geçerli resim işlenmedi",

+ 3 - 1
webview-ui/src/i18n/locales/vi/chat.json

@@ -116,7 +116,9 @@
 		"title": "Chế độ",
 		"marketplace": "Chợ Chế độ",
 		"settings": "Cài đặt Chế độ",
-		"description": "Các nhân cách chuyên biệt điều chỉnh hành vi của Roo."
+		"description": "Các nhân cách chuyên biệt điều chỉnh hành vi của Roo.",
+		"searchPlaceholder": "Tìm kiếm chế độ...",
+		"noResults": "Không tìm thấy kết quả nào"
 	},
 	"errorReadingFile": "Lỗi khi đọc tệp:",
 	"noValidImages": "Không có hình ảnh hợp lệ nào được xử lý",

+ 3 - 1
webview-ui/src/i18n/locales/zh-CN/chat.json

@@ -116,7 +116,9 @@
 		"title": "模式",
 		"marketplace": "模式市场",
 		"settings": "模式设置",
-		"description": "专门定制Roo行为的角色。"
+		"description": "专门定制Roo行为的角色。",
+		"searchPlaceholder": "搜索模式...",
+		"noResults": "未找到结果"
 	},
 	"errorReadingFile": "读取文件时出错:",
 	"noValidImages": "没有处理有效图片",

+ 3 - 1
webview-ui/src/i18n/locales/zh-TW/chat.json

@@ -116,7 +116,9 @@
 		"title": "模式",
 		"marketplace": "模式市集",
 		"settings": "模式設定",
-		"description": "專門定制Roo行為的角色。"
+		"description": "專門定制Roo行為的角色。",
+		"searchPlaceholder": "搜尋模式...",
+		"noResults": "沒有找到結果"
 	},
 	"errorReadingFile": "讀取檔案時發生錯誤:",
 	"noValidImages": "未處理到任何有效圖片",