From 55b1e6a30d0592b330b937d9679c16d669643c12 Mon Sep 17 00:00:00 2001 From: Aashir Athar Date: Fri, 29 May 2026 08:17:21 +0500 Subject: [PATCH] feat(mcp): search + keyboard navigation for installed MCP servers (#2639) Co-authored-by: Claude Opus 4.7 --- .../channels/mcp/InstalledServerList.test.tsx | 290 +++++++++++++++++- .../channels/mcp/InstalledServerList.tsx | 154 +++++++--- .../channels/mcp/McpServerSearch.test.tsx | 63 ++++ .../channels/mcp/McpServerSearch.tsx | 54 ++++ .../components/channels/mcp/McpServersTab.tsx | 12 +- app/src/lib/i18n/chunks/ar-1.ts | 6 + app/src/lib/i18n/chunks/bn-1.ts | 6 + app/src/lib/i18n/chunks/de-1.ts | 6 + app/src/lib/i18n/chunks/en-1.ts | 6 + app/src/lib/i18n/chunks/es-1.ts | 6 + app/src/lib/i18n/chunks/fr-1.ts | 6 + app/src/lib/i18n/chunks/hi-1.ts | 6 + app/src/lib/i18n/chunks/id-1.ts | 6 + app/src/lib/i18n/chunks/it-1.ts | 6 + app/src/lib/i18n/chunks/ko-1.ts | 6 + app/src/lib/i18n/chunks/pt-1.ts | 6 + app/src/lib/i18n/chunks/ru-1.ts | 6 + app/src/lib/i18n/chunks/zh-CN-1.ts | 6 + app/src/lib/i18n/en.ts | 6 + 19 files changed, 614 insertions(+), 43 deletions(-) create mode 100644 app/src/components/channels/mcp/McpServerSearch.test.tsx create mode 100644 app/src/components/channels/mcp/McpServerSearch.tsx diff --git a/app/src/components/channels/mcp/InstalledServerList.test.tsx b/app/src/components/channels/mcp/InstalledServerList.test.tsx index 5f2b65314..5180bd70a 100644 --- a/app/src/components/channels/mcp/InstalledServerList.test.tsx +++ b/app/src/components/channels/mcp/InstalledServerList.test.tsx @@ -183,8 +183,9 @@ describe('InstalledServerList', () => { onBrowseCatalog={() => {}} /> ); - // The status dot has title="error" - expect(screen.getByTitle('error')).toBeInTheDocument(); + // The status dot title is the i18n'd label ('Error' in English) — + // sourced from `channels.status.error` per `STATUS_I18N_KEYS`. + expect(screen.getByTitle('Error')).toBeInTheDocument(); }); it('falls back to disconnected status when no matching status entry', () => { @@ -197,7 +198,7 @@ describe('InstalledServerList', () => { onBrowseCatalog={() => {}} /> ); - expect(screen.getByTitle('disconnected')).toBeInTheDocument(); + expect(screen.getByTitle('Disconnected')).toBeInTheDocument(); }); // ----------------------------------------------------------------------- @@ -234,4 +235,287 @@ describe('InstalledServerList', () => { fireEvent.click(screen.getByRole('button', { name: 'Browse catalog' })); expect(onBrowse).toHaveBeenCalledTimes(1); }); + + // ----------------------------------------------------------------------- + // Filter behaviour (the new search/filter feature) + // ----------------------------------------------------------------------- + + it('shows all servers when filter is the empty string', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="" + /> + ); + expect(screen.getByText('File Server')).toBeInTheDocument(); + expect(screen.getByText('DB Server')).toBeInTheDocument(); + }); + + it('filters by display_name case-insensitively', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="FILE" + /> + ); + expect(screen.getByText('File Server')).toBeInTheDocument(); + expect(screen.queryByText('DB Server')).not.toBeInTheDocument(); + }); + + it('filters by qualified_name', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="db-server" + /> + ); + // SERVER_2 has qualified_name 'acme/db-server' — matched + expect(screen.getByText('DB Server')).toBeInTheDocument(); + expect(screen.queryByText('File Server')).not.toBeInTheDocument(); + }); + + it('filters by description', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="reads files" + /> + ); + // SERVER_1 has description 'Reads files' — matched + expect(screen.getByText('File Server')).toBeInTheDocument(); + expect(screen.queryByText('DB Server')).not.toBeInTheDocument(); + }); + + it('treats undefined description as empty (no false match) without crashing', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="undefined" + /> + ); + // 'undefined' must not match the absent description literally — assertion + // here is that the filter logic doesn't blow up and the no-match path runs. + expect(screen.queryByText('DB Server')).not.toBeInTheDocument(); + }); + + it('trims surrounding whitespace from the filter', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter=" File " + /> + ); + expect(screen.getByText('File Server')).toBeInTheDocument(); + expect(screen.queryByText('DB Server')).not.toBeInTheDocument(); + }); + + it('shows "no matches" message including the query when filter matches nothing', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="zzz-nope" + /> + ); + expect(screen.getByText('No servers match "zzz-nope".')).toBeInTheDocument(); + }); + + it('shows "X of Y servers" count via an aria-live region when filtering', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="File" + /> + ); + // `status` is NOT a "name from content" role per WAI-ARIA, so the + // accessible name doesn't come from text. Query by text and then + // verify the live-region attributes on the same element. + const status = screen.getByText('1 of 2 servers'); + expect(status).toHaveAttribute('role', 'status'); + expect(status).toHaveAttribute('aria-live', 'polite'); + }); + + it('hides the count when filter is empty', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="" + /> + ); + expect(screen.queryByText(/of \d+ servers/)).not.toBeInTheDocument(); + }); + + it('hides the count when filter is only whitespace', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter=" " + /> + ); + expect(screen.queryByText(/of \d+ servers/)).not.toBeInTheDocument(); + }); + + it('keeps the original empty state (not the filtered no-match) when there are zero servers', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="anything" + /> + ); + expect(screen.getByText('No MCP servers installed yet.')).toBeInTheDocument(); + expect(screen.queryByText(/No servers match/)).not.toBeInTheDocument(); + }); + + // ----------------------------------------------------------------------- + // Keyboard navigation (ArrowUp / ArrowDown across server buttons) + // ----------------------------------------------------------------------- + + it('moves focus to the next server on ArrowDown', () => { + render( + {}} + onBrowseCatalog={() => {}} + /> + ); + const first = screen.getByRole('button', { name: /File Server/i }); + const second = screen.getByRole('button', { name: /DB Server/i }); + first.focus(); + expect(first).toHaveFocus(); + fireEvent.keyDown(first, { key: 'ArrowDown' }); + expect(second).toHaveFocus(); + }); + + it('moves focus to the previous server on ArrowUp', () => { + render( + {}} + onBrowseCatalog={() => {}} + /> + ); + const first = screen.getByRole('button', { name: /File Server/i }); + const second = screen.getByRole('button', { name: /DB Server/i }); + second.focus(); + fireEvent.keyDown(second, { key: 'ArrowUp' }); + expect(first).toHaveFocus(); + }); + + it('clamps focus at the last server on ArrowDown', () => { + render( + {}} + onBrowseCatalog={() => {}} + /> + ); + const second = screen.getByRole('button', { name: /DB Server/i }); + second.focus(); + fireEvent.keyDown(second, { key: 'ArrowDown' }); + // No wrap-around. + expect(second).toHaveFocus(); + }); + + it('clamps focus at the first server on ArrowUp', () => { + render( + {}} + onBrowseCatalog={() => {}} + /> + ); + const first = screen.getByRole('button', { name: /File Server/i }); + first.focus(); + fireEvent.keyDown(first, { key: 'ArrowUp' }); + expect(first).toHaveFocus(); + }); + + it('does not move focus or preventDefault for unrelated keys', () => { + render( + {}} + onBrowseCatalog={() => {}} + /> + ); + const first = screen.getByRole('button', { name: /File Server/i }); + first.focus(); + const event = fireEvent.keyDown(first, { key: 'a' }); + // The listener should ignore unrelated keys; focus stays put. + expect(first).toHaveFocus(); + // fireEvent returns false if preventDefault was called — verify it wasn't. + expect(event).toBe(true); + }); + + it('arrow keys traverse only the visible (filtered) items', () => { + render( + {}} + onBrowseCatalog={() => {}} + filter="File" + /> + ); + const visible = screen.getByRole('button', { name: /File Server/i }); + visible.focus(); + // Only one filtered item → ArrowDown should clamp (single visible) + fireEvent.keyDown(visible, { key: 'ArrowDown' }); + expect(visible).toHaveFocus(); + expect(screen.queryByRole('button', { name: /DB Server/i })).not.toBeInTheDocument(); + }); }); diff --git a/app/src/components/channels/mcp/InstalledServerList.tsx b/app/src/components/channels/mcp/InstalledServerList.tsx index 57d52e164..ed6aca55c 100644 --- a/app/src/components/channels/mcp/InstalledServerList.tsx +++ b/app/src/components/channels/mcp/InstalledServerList.tsx @@ -1,6 +1,16 @@ /** * List of installed MCP servers with status dot, name, and tool count. + * + * Supports an optional `filter` prop that case-insensitively matches + * against `display_name`, `qualified_name`, and `description`. When + * filtering is active the list shows a "X of Y servers" count via a + * `role="status"` live region so assistive tech announces the new + * total as the user types. ArrowUp / ArrowDown move focus between + * server buttons (clamped at the edges); Enter/Space activate via + * the underlying ` ) : ( -
    - {servers.map(server => { - const connStatus = statusMap.get(server.server_id); - const status: ServerStatus = connStatus?.status ?? 'disconnected'; - const toolCount = connStatus?.tool_count ?? 0; - const isSelected = selectedId === server.server_id; + <> + {isFiltering && ( +

    + {t('mcp.installed.search.countMatches') + .replace('{shown}', String(filteredServers.length)) + .replace('{total}', String(servers.length))} +

    + )} + {filteredServers.length === 0 ? ( +
    +

    + {t('mcp.installed.search.noMatches').replace('{query}', filter.trim())} +

    +
    + ) : ( +
      + {filteredServers.map(server => { + const connStatus = statusMap.get(server.server_id); + const status: ServerStatus = connStatus?.status ?? 'disconnected'; + const toolCount = connStatus?.tool_count ?? 0; + const isSelected = selectedId === server.server_id; - return ( -
    • - -
    • - ); - })} -
    + + + ); + })} +
+ )} + )} ); diff --git a/app/src/components/channels/mcp/McpServerSearch.test.tsx b/app/src/components/channels/mcp/McpServerSearch.test.tsx new file mode 100644 index 000000000..926de0820 --- /dev/null +++ b/app/src/components/channels/mcp/McpServerSearch.test.tsx @@ -0,0 +1,63 @@ +/** + * Tests for McpServerSearch — controlled filter input with clear button. + */ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import McpServerSearch from './McpServerSearch'; + +describe('McpServerSearch', () => { + it('renders the search landmark with an accessible label', () => { + render( {}} />); + const landmark = screen.getByRole('search', { name: 'Search installed MCP servers' }); + expect(landmark).toBeInTheDocument(); + }); + + it('renders a search-type input with placeholder and aria-label', () => { + render( {}} />); + const input = screen.getByRole('searchbox', { name: 'Filter installed MCP servers by name' }); + expect(input).toHaveAttribute('type', 'search'); + expect(input).toHaveAttribute('placeholder', 'Filter servers…'); + }); + + it('does not render the clear button when value is empty', () => { + render( {}} />); + expect(screen.queryByRole('button', { name: 'Clear filter' })).not.toBeInTheDocument(); + }); + + it('renders the clear button when value is non-empty', () => { + render( {}} />); + expect(screen.getByRole('button', { name: 'Clear filter' })).toBeInTheDocument(); + }); + + it('reflects the current value as the input value', () => { + render( {}} />); + expect( + screen.getByRole('searchbox', { name: 'Filter installed MCP servers by name' }) + ).toHaveValue('redis'); + }); + + it('fires onChange with the new value on typing', () => { + const onChange = vi.fn(); + render(); + const input = screen.getByRole('searchbox', { name: 'Filter installed MCP servers by name' }); + fireEvent.change(input, { target: { value: 'gh' } }); + expect(onChange).toHaveBeenCalledWith('gh'); + }); + + it('fires onChange with an empty string when the clear button is clicked', () => { + const onChange = vi.fn(); + render(); + fireEvent.click(screen.getByRole('button', { name: 'Clear filter' })); + expect(onChange).toHaveBeenCalledWith(''); + }); + + it('renders the clear icon as decorative (aria-hidden)', () => { + const { container } = render( {}} />); + // The svg inside the clear button must be aria-hidden so the button's + // own aria-label is the sole accessible name. + const svg = container.querySelector('button[aria-label="Clear filter"] svg'); + expect(svg).not.toBeNull(); + expect(svg).toHaveAttribute('aria-hidden', 'true'); + }); +}); diff --git a/app/src/components/channels/mcp/McpServerSearch.tsx b/app/src/components/channels/mcp/McpServerSearch.tsx new file mode 100644 index 000000000..be404b8f2 --- /dev/null +++ b/app/src/components/channels/mcp/McpServerSearch.tsx @@ -0,0 +1,54 @@ +/** + * Filter input for the installed MCP server list. + * + * Controlled component — the parent (`McpServersTab`) owns the value + * and pushes it into `InstalledServerList` as the `filter` prop. The + * input exposes a clear button when non-empty and announces itself + * as a `role="search"` landmark so assistive tech can jump to it. + * + * Intentionally has NO global keyboard shortcut binding (e.g. Cmd/Ctrl+K) + * to avoid clashing with the app-wide CommandProvider in `App.tsx`. + * Users focus the input by clicking or tabbing. + */ +import { useT } from '../../../lib/i18n/I18nContext'; + +interface McpServerSearchProps { + value: string; + onChange: (next: string) => void; +} + +const McpServerSearch = ({ value, onChange }: McpServerSearchProps) => { + const { t } = useT(); + const hasValue = value.length > 0; + return ( +
+ onChange(e.target.value)} + placeholder={t('mcp.installed.search.placeholder')} + aria-label={t('mcp.installed.search.inputAria')} + className="w-full rounded-lg border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-1.5 pr-7 text-xs text-stone-800 dark:text-neutral-100 placeholder:text-stone-400 dark:placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:border-primary-400" + /> + {hasValue && ( + + )} +
+ ); +}; + +export default McpServerSearch; diff --git a/app/src/components/channels/mcp/McpServersTab.tsx b/app/src/components/channels/mcp/McpServersTab.tsx index 246a1805a..fbbe96b94 100644 --- a/app/src/components/channels/mcp/McpServersTab.tsx +++ b/app/src/components/channels/mcp/McpServersTab.tsx @@ -14,6 +14,7 @@ import InstalledServerDetail from './InstalledServerDetail'; import InstalledServerList from './InstalledServerList'; import McpCatalogBrowser from './McpCatalogBrowser'; import McpInventoryPanel from './McpInventoryPanel'; +import McpServerSearch from './McpServerSearch'; import type { ConnStatus, InstalledServer } from './types'; const log = debug('mcp-clients:tab'); @@ -32,6 +33,9 @@ const McpServersTab = () => { const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [rightPane, setRightPane] = useState({ mode: 'none' }); + // Local-only filter for the installed-server list. Not persisted — the + // search is a transient scan helper, not a saved view. + const [searchFilter, setSearchFilter] = useState(''); // Sharable Inventory modal toggle. Local state — the manifest UX is // a one-off interaction, not a saved view. const [inventoryOpen, setInventoryOpen] = useState(false); @@ -167,19 +171,25 @@ const McpServersTab = () => {
- {/* Left pane: installed list */} + {/* Left pane: search + installed list */}
{loadError && (
{loadError}
)} + {servers.length > 0 && ( +
+ +
+ )}
diff --git a/app/src/lib/i18n/chunks/ar-1.ts b/app/src/lib/i18n/chunks/ar-1.ts index 279ba48d9..39ff0ad06 100644 --- a/app/src/lib/i18n/chunks/ar-1.ts +++ b/app/src/lib/i18n/chunks/ar-1.ts @@ -1318,6 +1318,12 @@ const ar1: TranslationMap = { 'mcp.installed.empty': 'لم يتم تثبيت خوادم MCP حتى الآن.', 'mcp.installed.toolSingular': 'أداة {count}', 'mcp.installed.toolPlural': 'أدوات {count}', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/bn-1.ts b/app/src/lib/i18n/chunks/bn-1.ts index 2855d7926..4bdb77a61 100644 --- a/app/src/lib/i18n/chunks/bn-1.ts +++ b/app/src/lib/i18n/chunks/bn-1.ts @@ -1327,6 +1327,12 @@ const bn1: TranslationMap = { 'mcp.installed.empty': 'এখনো কোনো MCP সার্ভার ইনস্টল করা হয়নি।', 'mcp.installed.toolSingular': '{count} টুল', 'mcp.installed.toolPlural': '{count} টুল', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/de-1.ts b/app/src/lib/i18n/chunks/de-1.ts index 26239ec86..4f6859945 100644 --- a/app/src/lib/i18n/chunks/de-1.ts +++ b/app/src/lib/i18n/chunks/de-1.ts @@ -1345,6 +1345,12 @@ const de1: TranslationMap = { 'mcp.installed.empty': 'Noch keine MCP-Server installiert.', 'mcp.installed.toolSingular': '{count}-Tool', 'mcp.installed.toolPlural': '{count}-Tools', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/en-1.ts b/app/src/lib/i18n/chunks/en-1.ts index 6fa2410ce..e53de7f22 100644 --- a/app/src/lib/i18n/chunks/en-1.ts +++ b/app/src/lib/i18n/chunks/en-1.ts @@ -1303,6 +1303,12 @@ const en1: TranslationMap = { 'mcp.installed.empty': 'No MCP servers installed yet.', 'mcp.installed.toolSingular': '{count} tool', 'mcp.installed.toolPlural': '{count} tools', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/es-1.ts b/app/src/lib/i18n/chunks/es-1.ts index ec021cc89..b9346f2be 100644 --- a/app/src/lib/i18n/chunks/es-1.ts +++ b/app/src/lib/i18n/chunks/es-1.ts @@ -1342,6 +1342,12 @@ const es1: TranslationMap = { 'mcp.installed.empty': 'Aún no hay servidores MCP instalados.', 'mcp.installed.toolSingular': 'herramienta {count}', 'mcp.installed.toolPlural': '{count} herramientas', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/fr-1.ts b/app/src/lib/i18n/chunks/fr-1.ts index 7c9011c30..c35684801 100644 --- a/app/src/lib/i18n/chunks/fr-1.ts +++ b/app/src/lib/i18n/chunks/fr-1.ts @@ -1347,6 +1347,12 @@ const fr1: TranslationMap = { 'mcp.installed.empty': 'No MCP servers installed yet.', 'mcp.installed.toolSingular': 'Outil {count}', 'mcp.installed.toolPlural': 'Outils {count}', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/hi-1.ts b/app/src/lib/i18n/chunks/hi-1.ts index b24a4c229..bae5fa292 100644 --- a/app/src/lib/i18n/chunks/hi-1.ts +++ b/app/src/lib/i18n/chunks/hi-1.ts @@ -1326,6 +1326,12 @@ const hi1: TranslationMap = { 'mcp.installed.empty': 'अभी तक कोई MCP सर्वर स्थापित नहीं है।', 'mcp.installed.toolSingular': '{count} उपकरण', 'mcp.installed.toolPlural': '{count} उपकरण', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/id-1.ts b/app/src/lib/i18n/chunks/id-1.ts index b8771ce1f..3a140d1e7 100644 --- a/app/src/lib/i18n/chunks/id-1.ts +++ b/app/src/lib/i18n/chunks/id-1.ts @@ -1330,6 +1330,12 @@ const id1: TranslationMap = { 'mcp.installed.empty': 'Belum ada server MCP yang terinstal.', 'mcp.installed.toolSingular': '{count} alat', 'mcp.installed.toolPlural': '{count} alat', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/it-1.ts b/app/src/lib/i18n/chunks/it-1.ts index e5514b47e..88cd01088 100644 --- a/app/src/lib/i18n/chunks/it-1.ts +++ b/app/src/lib/i18n/chunks/it-1.ts @@ -1335,6 +1335,12 @@ const it1: TranslationMap = { 'mcp.installed.empty': 'Nessun server MCP ancora installato.', 'mcp.installed.toolSingular': '{count} strumento', 'mcp.installed.toolPlural': '{count} strumenti', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/ko-1.ts b/app/src/lib/i18n/chunks/ko-1.ts index 13256076d..c11ad9d41 100644 --- a/app/src/lib/i18n/chunks/ko-1.ts +++ b/app/src/lib/i18n/chunks/ko-1.ts @@ -1327,6 +1327,12 @@ const ko1: TranslationMap = { 'mcp.installed.empty': '아직 MCP 서버가 설치되지 않았습니다.', 'mcp.installed.toolSingular': '{count} 도구', 'mcp.installed.toolPlural': '{count} 도구', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/pt-1.ts b/app/src/lib/i18n/chunks/pt-1.ts index 033872e79..a8bc093f4 100644 --- a/app/src/lib/i18n/chunks/pt-1.ts +++ b/app/src/lib/i18n/chunks/pt-1.ts @@ -1340,6 +1340,12 @@ const pt1: TranslationMap = { 'mcp.installed.empty': 'Nenhum servidor MCP instalado ainda.', 'mcp.installed.toolSingular': 'Ferramenta {count}', 'mcp.installed.toolPlural': 'Ferramentas {count}', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/ru-1.ts b/app/src/lib/i18n/chunks/ru-1.ts index f92e54b4c..1a4345bda 100644 --- a/app/src/lib/i18n/chunks/ru-1.ts +++ b/app/src/lib/i18n/chunks/ru-1.ts @@ -1330,6 +1330,12 @@ const ru1: TranslationMap = { 'mcp.installed.empty': 'Серверы MCP пока не установлены.', 'mcp.installed.toolSingular': '{count} инструмент', 'mcp.installed.toolPlural': '{count} инструменты', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/chunks/zh-CN-1.ts b/app/src/lib/i18n/chunks/zh-CN-1.ts index d2107237f..81f8e80a0 100644 --- a/app/src/lib/i18n/chunks/zh-CN-1.ts +++ b/app/src/lib/i18n/chunks/zh-CN-1.ts @@ -1311,6 +1311,12 @@ const zhCN1: TranslationMap = { 'mcp.installed.empty': '尚未安装 MCP 服务器。', 'mcp.installed.toolSingular': '{count} 工具', 'mcp.installed.toolPlural': '{count} 工具', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 53358a237..2b8c6397d 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -927,6 +927,12 @@ const en: TranslationMap = { 'mcp.installed.empty': 'No MCP servers installed yet.', 'mcp.installed.toolSingular': '{count} tool', 'mcp.installed.toolPlural': '{count} tools', + 'mcp.installed.search.landmarkAria': 'Search installed MCP servers', + 'mcp.installed.search.inputAria': 'Filter installed MCP servers by name', + 'mcp.installed.search.placeholder': 'Filter servers…', + 'mcp.installed.search.clearAria': 'Clear filter', + 'mcp.installed.search.countMatches': '{shown} of {total} servers', + 'mcp.installed.search.noMatches': 'No servers match "{query}".', 'mcp.inventory.openButton': 'Inventory', 'mcp.inventory.openAria': 'Open the sharable MCP inventory panel', 'mcp.inventory.title': 'Sharable MCP Inventory',