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

Co-authored-by: Roo Code Translate Mode
Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
Hannes Rudolph 2025-07-26 23:13:57 -06:00 committed by GitHub
parent 5d4e3819e4
commit 8625b451f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 399 additions and 95 deletions

View file

@ -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>

View file

@ -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()
})
})

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

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

View file

@ -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",

View file

@ -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",

View file

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

View file

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

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

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

View file

@ -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",

View file

@ -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ý",

View file

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

View file

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