merge latest main for PR 8279 validation
@@ -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')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 579 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 587 B |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 915 B |
|
After Width: | Height: | Size: 380 B |
|
After Width: | Height: | Size: 394 B |
|
After Width: | Height: | Size: 813 B |
|
After Width: | Height: | Size: 519 B |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 757 B |
|
After Width: | Height: | Size: 582 B |
|
After Width: | Height: | Size: 685 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 590 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 652 B |
|
After Width: | Height: | Size: 551 B |
|
After Width: | Height: | Size: 377 B |
|
After Width: | Height: | Size: 726 B |
|
After Width: | Height: | Size: 822 B |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 988 B |