diff --git a/mobile/src/components/MobileAgentIcon.tsx b/mobile/src/components/MobileAgentIcon.tsx index ade35feda45..6c47fce7a5b 100644 --- a/mobile/src/components/MobileAgentIcon.tsx +++ b/mobile/src/components/MobileAgentIcon.tsx @@ -1,8 +1,11 @@ import { Image, StyleSheet, Text, View } from 'react-native' +import type { ImageSourcePropType } from 'react-native' import { Terminal } from 'lucide-react-native' +import type { TuiAgent } from '../../../src/shared/types' import Svg, { Defs, G, LinearGradient, Path, Stop } from 'react-native-svg' import { colors } from '../theme/mobile-theme' import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog' +import { MOBILE_AGENT_ICON_ASSETS } from './mobile-agent-icon-assets' import { ClaudeIcon, OpenAIIcon } from './AgentIcons' // Why: agent branding should match the desktop/new-worktree picker everywhere @@ -49,6 +52,10 @@ function AiderIcon({ size = 16 }: { size?: number }) { ) } +function BundledIcon({ source, size = 16 }: { source: ImageSourcePropType; size?: number }) { + return +} + function FaviconIcon({ domain, size = 16 }: { domain: string; size?: number }) { return ( } + // Why: prefer the favicon bundled into the app over Google's favicon service, + // which is unreachable in some regions and offline (#8451). + const bundledIcon = MOBILE_AGENT_ICON_ASSETS[agentId as TuiAgent] + if (bundledIcon) { + return + } const agent = MOBILE_AGENT_CATALOG.find((entry) => entry.id === agentId) if (agent?.faviconDomain) { return diff --git a/mobile/src/components/mobile-agent-icon-assets.test.ts b/mobile/src/components/mobile-agent-icon-assets.test.ts new file mode 100644 index 00000000000..edbe71760c7 --- /dev/null +++ b/mobile/src/components/mobile-agent-icon-assets.test.ts @@ -0,0 +1,45 @@ +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import type { TuiAgent } from '../../../src/shared/types' +import { MOBILE_TUI_AGENT_FAVICON_DOMAINS } from '../tasks/mobile-tui-agents' + +// Agents that render a hand-authored glyph in MobileAgentIcon and therefore +// never reach the favicon/bundled-asset path. +const GLYPH_AGENT_IDS = new Set([ + 'claude', + 'claude-agent-teams', + 'codex', + 'pi', + 'omp', + 'aider' +]) + +const dirname = import.meta.dirname +const assetsDir = path.resolve(dirname, '../../../src/shared/agent-icons') +const assetsModuleSource = readFileSync(path.join(dirname, 'mobile-agent-icon-assets.ts'), 'utf8') + +// Why: these agents previously loaded from Google's favicon service, which is +// unreachable in some regions/offline (#8451). Every one that lacks a glyph +// must ship a bundled PNG so the icon renders without a network request. +const agentsNeedingBundledIcon = Object.keys(MOBILE_TUI_AGENT_FAVICON_DOMAINS).filter( + (id): id is TuiAgent => !GLYPH_AGENT_IDS.has(id as TuiAgent) +) + +describe('mobile bundled agent icons', () => { + it('ships a bundled PNG on disk for every favicon-path agent', () => { + for (const id of agentsNeedingBundledIcon) { + expect(existsSync(path.join(assetsDir, `${id}.png`)), `missing bundled icon for ${id}`).toBe( + true + ) + } + }) + + it('wires every favicon-path agent into the bundled asset map', () => { + for (const id of agentsNeedingBundledIcon) { + expect(assetsModuleSource, `${id} not mapped in mobile-agent-icon-assets.ts`).toContain( + `agent-icons/${id}.png` + ) + } + }) +}) diff --git a/mobile/src/components/mobile-agent-icon-assets.ts b/mobile/src/components/mobile-agent-icon-assets.ts new file mode 100644 index 00000000000..52615e89df8 --- /dev/null +++ b/mobile/src/components/mobile-agent-icon-assets.ts @@ -0,0 +1,41 @@ +import type { ImageSourcePropType } from 'react-native' +import type { TuiAgent } from '../../../src/shared/types' + +// Why: mobile previously rendered these agent icons from Google's favicon +// service, which is unreachable in some regions (e.g. mainland China) and +// offline, leaving broken images in the agent picker (#8451). Bundle each +// favicon so Metro packages it into the app and the icon renders without any +// network request. The PNGs are shared with desktop in src/shared/agent-icons +// (Metro watches src/shared via metro.config.js sharedRoot). Metro requires +// static literal require() paths, so keep this as an explicit map rather than +// building the path from the agent id. +export const MOBILE_AGENT_ICON_ASSETS: Partial> = { + openclaude: require('../../../src/shared/agent-icons/openclaude.png'), + grok: require('../../../src/shared/agent-icons/grok.png'), + copilot: require('../../../src/shared/agent-icons/copilot.png'), + opencode: require('../../../src/shared/agent-icons/opencode.png'), + 'mimo-code': require('../../../src/shared/agent-icons/mimo-code.png'), + ante: require('../../../src/shared/agent-icons/ante.png'), + gemini: require('../../../src/shared/agent-icons/gemini.png'), + antigravity: require('../../../src/shared/agent-icons/antigravity.png'), + goose: require('../../../src/shared/agent-icons/goose.png'), + amp: require('../../../src/shared/agent-icons/amp.png'), + kilo: require('../../../src/shared/agent-icons/kilo.png'), + kiro: require('../../../src/shared/agent-icons/kiro.png'), + crush: require('../../../src/shared/agent-icons/crush.png'), + aug: require('../../../src/shared/agent-icons/aug.png'), + autohand: require('../../../src/shared/agent-icons/autohand.png'), + cline: require('../../../src/shared/agent-icons/cline.png'), + codebuff: require('../../../src/shared/agent-icons/codebuff.png'), + 'command-code': require('../../../src/shared/agent-icons/command-code.png'), + continue: require('../../../src/shared/agent-icons/continue.png'), + cursor: require('../../../src/shared/agent-icons/cursor.png'), + droid: require('../../../src/shared/agent-icons/droid.png'), + kimi: require('../../../src/shared/agent-icons/kimi.png'), + 'mistral-vibe': require('../../../src/shared/agent-icons/mistral-vibe.png'), + 'qwen-code': require('../../../src/shared/agent-icons/qwen-code.png'), + rovo: require('../../../src/shared/agent-icons/rovo.png'), + hermes: require('../../../src/shared/agent-icons/hermes.png'), + devin: require('../../../src/shared/agent-icons/devin.png'), + openclaw: require('../../../src/shared/agent-icons/openclaw.png') +} diff --git a/src/renderer/src/components/agent/AgentCombobox.test.tsx b/src/renderer/src/components/agent/AgentCombobox.test.tsx index 6547c3704bd..d6fe7590f49 100644 --- a/src/renderer/src/components/agent/AgentCombobox.test.tsx +++ b/src/renderer/src/components/agent/AgentCombobox.test.tsx @@ -1,6 +1,8 @@ import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' +import type { TuiAgent } from '../../../../shared/types' import { AGENT_CATALOG, AgentIcon } from '@/lib/agent-catalog' +import { AGENT_FAVICON_ASSETS } from '@/lib/agent-favicon-assets' import AgentCombobox from './AgentCombobox' describe('AgentCombobox', () => { @@ -37,4 +39,14 @@ describe('AgentCombobox', () => { expect(markup).not.toContain('https://www.google.com/s2/favicons') expect(markup).not.toContain(' { + // Why: previously loaded from Google's favicon service (#8451). Iterate the + // full asset map so missing files/key mismatches fail the test. + for (const agent of Object.keys(AGENT_FAVICON_ASSETS) as TuiAgent[]) { + const markup = renderToStaticMarkup() + expect(markup).toContain(`/shared/agent-icons/${agent}.png`) + expect(markup).not.toContain('https://www.google.com/s2/favicons') + } + }) }) diff --git a/src/renderer/src/lib/agent-catalog.tsx b/src/renderer/src/lib/agent-catalog.tsx index d43401e502f..f9536dc1c34 100644 --- a/src/renderer/src/lib/agent-catalog.tsx +++ b/src/renderer/src/lib/agent-catalog.tsx @@ -14,6 +14,7 @@ import { } from './agent-icon-glyphs' import { translate } from '@/i18n/i18n' import { createLocalizedCatalog } from '@/i18n/localized-catalog' +import { AGENT_FAVICON_ASSETS } from './agent-favicon-assets' export type AgentCatalogEntry = { id: TuiAgent @@ -113,7 +114,8 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] => id: 'omp', label: translate('auto.lib.agent.catalog.09973b4d84', 'OMP'), cmd: 'omp', - faviconDomain: 'omp.sh', + // Why: no faviconDomain — omp renders the hand-authored OmpIcon glyph, so a + // favicon fallback would never be reached. homepageUrl: 'https://omp.sh' }, { @@ -340,20 +342,29 @@ export function AgentIcon({ return } const catalogEntry = getAgentCatalog().find((a) => a.id === agent) - if (catalogEntry?.iconUrl) { + // Why: prefer the favicon bundled at build time so the icon renders without a + // live network request — Google's favicon service is unreachable in some + // regions and offline, which left these icons broken (#8451). + const bundledFaviconUrl = AGENT_FAVICON_ASSETS[agent] + // Why: one resolved src for guard + attribute so empty `iconUrl` cannot pass + // a truthy `||` check while `??` still renders a broken ``. + const iconSrc = catalogEntry?.iconUrl ?? bundledFaviconUrl + if (iconSrc) { return ( ) } if (catalogEntry?.faviconDomain) { - // Why: agents without a published SVG icon use their site favicon via - // Google's favicon service — same source the README uses for the agent badge list. + // Why: agents without a published SVG icon or bundled favicon fall back to + // their site favicon via Google's favicon service — same source the README + // uses for the agent badge list. return ( > = { + grok: grokUrl, + 'mimo-code': mimoCodeUrl, + ante: anteUrl, + gemini: geminiUrl, + antigravity: antigravityUrl, + goose: gooseUrl, + amp: ampUrl, + kiro: kiroUrl, + crush: crushUrl, + aug: augUrl, + autohand: autohandUrl, + cline: clineUrl, + codebuff: codebuffUrl, + 'command-code': commandCodeUrl, + continue: continueUrl, + cursor: cursorUrl, + kimi: kimiUrl, + 'mistral-vibe': mistralVibeUrl, + 'qwen-code': qwenCodeUrl, + rovo: rovoUrl, + hermes: hermesUrl, + devin: devinUrl, + openclaw: openclawUrl +} diff --git a/src/shared/agent-icons/amp.png b/src/shared/agent-icons/amp.png new file mode 100644 index 00000000000..9b57e20ff0f Binary files /dev/null and b/src/shared/agent-icons/amp.png differ diff --git a/src/shared/agent-icons/ante.png b/src/shared/agent-icons/ante.png new file mode 100644 index 00000000000..c3dadcc2cec Binary files /dev/null and b/src/shared/agent-icons/ante.png differ diff --git a/src/shared/agent-icons/antigravity.png b/src/shared/agent-icons/antigravity.png new file mode 100644 index 00000000000..be2992716b5 Binary files /dev/null and b/src/shared/agent-icons/antigravity.png differ diff --git a/src/shared/agent-icons/aug.png b/src/shared/agent-icons/aug.png new file mode 100644 index 00000000000..1664844ad7f Binary files /dev/null and b/src/shared/agent-icons/aug.png differ diff --git a/src/shared/agent-icons/autohand.png b/src/shared/agent-icons/autohand.png new file mode 100644 index 00000000000..0c3326a5705 Binary files /dev/null and b/src/shared/agent-icons/autohand.png differ diff --git a/src/shared/agent-icons/cline.png b/src/shared/agent-icons/cline.png new file mode 100644 index 00000000000..f4bd762dfee Binary files /dev/null and b/src/shared/agent-icons/cline.png differ diff --git a/src/shared/agent-icons/codebuff.png b/src/shared/agent-icons/codebuff.png new file mode 100644 index 00000000000..b81a6fa9c21 Binary files /dev/null and b/src/shared/agent-icons/codebuff.png differ diff --git a/src/shared/agent-icons/command-code.png b/src/shared/agent-icons/command-code.png new file mode 100644 index 00000000000..2066461b996 Binary files /dev/null and b/src/shared/agent-icons/command-code.png differ diff --git a/src/shared/agent-icons/continue.png b/src/shared/agent-icons/continue.png new file mode 100644 index 00000000000..cd2599d6e63 Binary files /dev/null and b/src/shared/agent-icons/continue.png differ diff --git a/src/shared/agent-icons/copilot.png b/src/shared/agent-icons/copilot.png new file mode 100644 index 00000000000..52126e047d0 Binary files /dev/null and b/src/shared/agent-icons/copilot.png differ diff --git a/src/shared/agent-icons/crush.png b/src/shared/agent-icons/crush.png new file mode 100644 index 00000000000..6d880c1305d Binary files /dev/null and b/src/shared/agent-icons/crush.png differ diff --git a/src/shared/agent-icons/cursor.png b/src/shared/agent-icons/cursor.png new file mode 100644 index 00000000000..426cbfa4d0d Binary files /dev/null and b/src/shared/agent-icons/cursor.png differ diff --git a/src/shared/agent-icons/devin.png b/src/shared/agent-icons/devin.png new file mode 100644 index 00000000000..3eee8d0d8d0 Binary files /dev/null and b/src/shared/agent-icons/devin.png differ diff --git a/src/shared/agent-icons/droid.png b/src/shared/agent-icons/droid.png new file mode 100644 index 00000000000..014b63332be Binary files /dev/null and b/src/shared/agent-icons/droid.png differ diff --git a/src/shared/agent-icons/gemini.png b/src/shared/agent-icons/gemini.png new file mode 100644 index 00000000000..84976ebc7fd Binary files /dev/null and b/src/shared/agent-icons/gemini.png differ diff --git a/src/shared/agent-icons/goose.png b/src/shared/agent-icons/goose.png new file mode 100644 index 00000000000..2da26f064ce Binary files /dev/null and b/src/shared/agent-icons/goose.png differ diff --git a/src/shared/agent-icons/grok.png b/src/shared/agent-icons/grok.png new file mode 100644 index 00000000000..954e5b40da2 Binary files /dev/null and b/src/shared/agent-icons/grok.png differ diff --git a/src/shared/agent-icons/hermes.png b/src/shared/agent-icons/hermes.png new file mode 100644 index 00000000000..7d5ba85e54e Binary files /dev/null and b/src/shared/agent-icons/hermes.png differ diff --git a/src/shared/agent-icons/kilo.png b/src/shared/agent-icons/kilo.png new file mode 100644 index 00000000000..053dba3fd76 Binary files /dev/null and b/src/shared/agent-icons/kilo.png differ diff --git a/src/shared/agent-icons/kimi.png b/src/shared/agent-icons/kimi.png new file mode 100644 index 00000000000..3640a259f8d Binary files /dev/null and b/src/shared/agent-icons/kimi.png differ diff --git a/src/shared/agent-icons/kiro.png b/src/shared/agent-icons/kiro.png new file mode 100644 index 00000000000..4a6cc6bbf8c Binary files /dev/null and b/src/shared/agent-icons/kiro.png differ diff --git a/src/shared/agent-icons/mimo-code.png b/src/shared/agent-icons/mimo-code.png new file mode 100644 index 00000000000..45cb84ff516 Binary files /dev/null and b/src/shared/agent-icons/mimo-code.png differ diff --git a/src/shared/agent-icons/mistral-vibe.png b/src/shared/agent-icons/mistral-vibe.png new file mode 100644 index 00000000000..f0f799cf546 Binary files /dev/null and b/src/shared/agent-icons/mistral-vibe.png differ diff --git a/src/shared/agent-icons/openclaude.png b/src/shared/agent-icons/openclaude.png new file mode 100644 index 00000000000..1e660b74090 Binary files /dev/null and b/src/shared/agent-icons/openclaude.png differ diff --git a/src/shared/agent-icons/openclaw.png b/src/shared/agent-icons/openclaw.png new file mode 100644 index 00000000000..03feb5b7638 Binary files /dev/null and b/src/shared/agent-icons/openclaw.png differ diff --git a/src/shared/agent-icons/opencode.png b/src/shared/agent-icons/opencode.png new file mode 100644 index 00000000000..1f45b1faee0 Binary files /dev/null and b/src/shared/agent-icons/opencode.png differ diff --git a/src/shared/agent-icons/qwen-code.png b/src/shared/agent-icons/qwen-code.png new file mode 100644 index 00000000000..9a6346691d4 Binary files /dev/null and b/src/shared/agent-icons/qwen-code.png differ diff --git a/src/shared/agent-icons/rovo.png b/src/shared/agent-icons/rovo.png new file mode 100644 index 00000000000..08a2af624f8 Binary files /dev/null and b/src/shared/agent-icons/rovo.png differ