merge latest main for PR 8279 validation

This commit is contained in:
Brennan Benson
2026-07-12 22:12:47 -07:00
34 changed files with 184 additions and 5 deletions
+13
View File
@@ -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 <Image source={source} style={{ width: size, height: size, borderRadius: 2 }} />
}
function FaviconIcon({ domain, size = 16 }: { domain: string; size?: number }) {
return (
<Image
@@ -98,6 +105,12 @@ export function MobileAgentIcon({ agentId, size = 16 }: { agentId: string; size?
return <Terminal size={size} color={colors.textMuted} />
}
// 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 <BundledIcon source={bundledIcon} size={size} />
}
const agent = MOBILE_AGENT_CATALOG.find((entry) => entry.id === agentId)
if (agent?.faviconDomain) {
return <FaviconIcon domain={agent.faviconDomain} size={size} />
@@ -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<TuiAgent>([
'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`
)
}
})
})
@@ -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<Record<TuiAgent, ImageSourcePropType>> = {
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')
}
@@ -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('<img')
})
it('renders bundled favicons for favicon-domain agents instead of the remote Google service', () => {
// 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(<AgentIcon agent={agent} />)
expect(markup).toContain(`/shared/agent-icons/${agent}.png`)
expect(markup).not.toContain('https://www.google.com/s2/favicons')
}
})
})
+16 -5
View File
@@ -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 <OpenCodeIcon size={size} />
}
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 `<img src="">`.
const iconSrc = catalogEntry?.iconUrl ?? bundledFaviconUrl
if (iconSrc) {
return (
<img
src={catalogEntry.iconUrl}
src={iconSrc}
width={size}
height={size}
alt=""
aria-hidden
style={{ borderRadius: 2 }}
/>
)
}
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 (
<img
src={`https://www.google.com/s2/favicons?domain=${catalogEntry.faviconDomain}&sz=64`}
@@ -0,0 +1,57 @@
import type { TuiAgent } from '../../../shared/types'
import grokUrl from '../../../shared/agent-icons/grok.png?url'
import mimoCodeUrl from '../../../shared/agent-icons/mimo-code.png?url'
import anteUrl from '../../../shared/agent-icons/ante.png?url'
import geminiUrl from '../../../shared/agent-icons/gemini.png?url'
import antigravityUrl from '../../../shared/agent-icons/antigravity.png?url'
import gooseUrl from '../../../shared/agent-icons/goose.png?url'
import ampUrl from '../../../shared/agent-icons/amp.png?url'
import kiroUrl from '../../../shared/agent-icons/kiro.png?url'
import crushUrl from '../../../shared/agent-icons/crush.png?url'
import augUrl from '../../../shared/agent-icons/aug.png?url'
import autohandUrl from '../../../shared/agent-icons/autohand.png?url'
import clineUrl from '../../../shared/agent-icons/cline.png?url'
import codebuffUrl from '../../../shared/agent-icons/codebuff.png?url'
import commandCodeUrl from '../../../shared/agent-icons/command-code.png?url'
import continueUrl from '../../../shared/agent-icons/continue.png?url'
import cursorUrl from '../../../shared/agent-icons/cursor.png?url'
import kimiUrl from '../../../shared/agent-icons/kimi.png?url'
import mistralVibeUrl from '../../../shared/agent-icons/mistral-vibe.png?url'
import qwenCodeUrl from '../../../shared/agent-icons/qwen-code.png?url'
import rovoUrl from '../../../shared/agent-icons/rovo.png?url'
import hermesUrl from '../../../shared/agent-icons/hermes.png?url'
import devinUrl from '../../../shared/agent-icons/devin.png?url'
import openclawUrl from '../../../shared/agent-icons/openclaw.png?url'
// Why: these agents have no hand-authored SVG glyph, so previously their icons
// loaded live from Google's favicon service. That service is unreachable in some
// regions (e.g. mainland China) and offline, leaving broken images across the
// agent settings page, tab title bar, and status bar (#8451). Bundle the favicon
// PNGs at build time so the icons render without any network dependency.
// The PNGs live in src/shared/agent-icons so mobile (Metro) can bundle the same
// files; see mobile/src/components/mobile-agent-icon-assets.ts.
export const AGENT_FAVICON_ASSETS: Partial<Record<TuiAgent, string>> = {
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
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B