diff --git a/Cargo.lock b/Cargo.lock index d970146b..35065644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2548,7 +2548,7 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "mcpmux" -version = "0.0.7" +version = "0.0.10" dependencies = [ "anyhow", "async-trait", @@ -2585,7 +2585,7 @@ dependencies = [ [[package]] name = "mcpmux-core" -version = "0.0.7" +version = "0.0.10" dependencies = [ "anyhow", "async-trait", @@ -2608,7 +2608,7 @@ dependencies = [ [[package]] name = "mcpmux-gateway" -version = "0.0.7" +version = "0.0.10" dependencies = [ "anyhow", "async-stream", @@ -2648,7 +2648,7 @@ dependencies = [ [[package]] name = "mcpmux-mcp" -version = "0.0.7" +version = "0.0.10" dependencies = [ "anyhow", "async-trait", @@ -2667,7 +2667,7 @@ dependencies = [ [[package]] name = "mcpmux-storage" -version = "0.0.7" +version = "0.0.10" dependencies = [ "anyhow", "async-trait", diff --git a/apps/desktop/src/features/clients/ClientsPage.tsx b/apps/desktop/src/features/clients/ClientsPage.tsx index 33eaf143..30b22139 100644 --- a/apps/desktop/src/features/clients/ClientsPage.tsx +++ b/apps/desktop/src/features/clients/ClientsPage.tsx @@ -4,6 +4,7 @@ import cursorIcon from '@/assets/client-icons/cursor.svg'; import vscodeIcon from '@/assets/client-icons/vscode.png'; import claudeIcon from '@/assets/client-icons/claude.svg'; import windsurfIcon from '@/assets/client-icons/windsurf.svg'; +import { resolveKnownClientKey } from '@/lib/clientIcons'; import { Laptop, Loader2, @@ -79,21 +80,18 @@ const CONNECTION_MODES = [ }, ]; -// Bundled icons for well-known AI clients (matched by client_name) -const KNOWN_CLIENT_ICONS: Record = { +// Bundled icons for well-known AI clients (resolved via icon key) +const CLIENT_ICON_ASSETS: Record = { cursor: cursorIcon, - 'vs code': vscodeIcon, vscode: vscodeIcon, - 'visual studio code': vscodeIcon, - 'claude desktop': claudeIcon, claude: claudeIcon, windsurf: windsurfIcon, - codeium: windsurfIcon, }; // Client icon component — uses bundled icon for known clients, falls back to logo_uri, then emoji function ClientIcon({ logo_uri, client_name }: { logo_uri?: string | null; client_name: string }) { - const iconUrl = KNOWN_CLIENT_ICONS[client_name.toLowerCase()] || logo_uri; + const knownKey = resolveKnownClientKey(client_name); + const iconUrl = (knownKey && CLIENT_ICON_ASSETS[knownKey]) || logo_uri; if (iconUrl) { return ( { + describe('exact matches', () => { + it('should resolve "cursor" to cursor', () => { + expect(resolveKnownClientKey('cursor')).toBe('cursor'); + }); + + it('should resolve "Cursor" (case-insensitive) to cursor', () => { + expect(resolveKnownClientKey('Cursor')).toBe('cursor'); + }); + + it('should resolve "claude" to claude', () => { + expect(resolveKnownClientKey('claude')).toBe('claude'); + }); + + it('should resolve "Claude Desktop" to claude', () => { + expect(resolveKnownClientKey('Claude Desktop')).toBe('claude'); + }); + + it('should resolve "vs code" to vscode', () => { + expect(resolveKnownClientKey('vs code')).toBe('vscode'); + }); + + it('should resolve "vscode" to vscode', () => { + expect(resolveKnownClientKey('vscode')).toBe('vscode'); + }); + + it('should resolve "Visual Studio Code" to vscode', () => { + expect(resolveKnownClientKey('Visual Studio Code')).toBe('vscode'); + }); + + it('should resolve "windsurf" to windsurf', () => { + expect(resolveKnownClientKey('windsurf')).toBe('windsurf'); + }); + + it('should resolve "codeium" to windsurf', () => { + expect(resolveKnownClientKey('codeium')).toBe('windsurf'); + }); + }); + + describe('prefix matches with parenthesised suffix', () => { + it('should resolve "Claude Code (mcpmux)" to claude', () => { + expect(resolveKnownClientKey('Claude Code (mcpmux)')).toBe('claude'); + }); + + it('should resolve "Claude Desktop (some-server)" to claude', () => { + expect(resolveKnownClientKey('Claude Desktop (some-server)')).toBe('claude'); + }); + + it('should resolve "Cursor (my-project)" to cursor', () => { + expect(resolveKnownClientKey('Cursor (my-project)')).toBe('cursor'); + }); + + it('should resolve "VS Code (workspace)" to vscode', () => { + expect(resolveKnownClientKey('VS Code (workspace)')).toBe('vscode'); + }); + + it('should resolve "Windsurf (test)" to windsurf', () => { + expect(resolveKnownClientKey('Windsurf (test)')).toBe('windsurf'); + }); + }); + + describe('prefix matches with space-separated suffix', () => { + it('should resolve "Claude Code v2" to claude', () => { + expect(resolveKnownClientKey('Claude Code v2')).toBe('claude'); + }); + + it('should resolve "Cursor beta" to cursor', () => { + expect(resolveKnownClientKey('Cursor beta')).toBe('cursor'); + }); + }); + + describe('non-matching names', () => { + it('should return null for unknown client names', () => { + expect(resolveKnownClientKey('unknown-client')).toBeNull(); + }); + + it('should return null for empty string', () => { + expect(resolveKnownClientKey('')).toBeNull(); + }); + + it('should not match partial names without word boundary', () => { + // "claudeXYZ" should NOT match "claude" — no word boundary + expect(resolveKnownClientKey('claudeXYZ')).toBeNull(); + }); + + it('should not match "cursorify" (no word boundary)', () => { + expect(resolveKnownClientKey('cursorify')).toBeNull(); + }); + }); + + describe('whitespace handling', () => { + it('should trim leading/trailing whitespace', () => { + expect(resolveKnownClientKey(' Cursor ')).toBe('cursor'); + }); + + it('should handle whitespace with suffix', () => { + expect(resolveKnownClientKey(' Claude Code (mcpmux) ')).toBe('claude'); + }); + }); +});