cmux next: links in the agent chat show their site's icon

LU
3d ago
+1362-46

Web links in agent replies showed a plain globe, or nothing, so a reply with several sources read as a wall of blue text. Links in a person's own prompt were plain text. This PR draws each external link with its site's favicon, as T3 Code does: in replies, in prompts (a pasted URL becomes a link), and on web fetch tool rows.

  • Host op link.favicons {hosts} (AgentPaneSiteIcons.swift): an icon cmux already has from its own browser tabs comes first, with no network. Else the host asks a public favicon service for the host name (the link's path and query never leave the Mac), through GuardedRemoteFetch: https only, every address of every hop public, only the service and its image servers (www.google.com, t0 to t3.gstatic.com), at most 3 redirects, 64 KiB, 5 s, no cookies or referrer. The answer is decoded off the main actor and redrawn as a PNG of at most 64 px. A dark mark on a clear ground gets a light plate, so it shows on a dark theme.
  • Bounds: one fetch per host, at most 8 at once for all panes, 512 hosts kept. A site with no icon, or a failed fetch, is asked again after 10 minutes, not kept for the app run. agentPane.images.remote = never fetches nothing.
  • Page (chips/SiteIcon.tsx, chips/siteIcons.ts): the mark is a fixed square from the first frame (globe until the icon arrives), so a late icon never reflows the paragraph. Requests are batched, one call in flight. A local or private address (localhost, 192.168.x, .local) keeps the globe and is never sent. The host also never sends reserved names (.corp, .home, .intranet, .private, .alt, ...) or a company's own hosts (jira.corp.acme.com, any corp, internal or intranet label), the same list T3 Code keeps off the service. GitHub and arXiv keep cmux's own marks. The icon is a CSS background, not an <img>, so copying a message copies text only and the composer attaches no images. Only a base64 PNG data URL is accepted from the host.
  • Prompts (UserBubble.tsx): a typed or pasted http(s) address is a link chip with the same mark and hover card. Paths stay plain text.
  • Dev server (webviews/dev-server/faviconHost.ts): the same op for ?mock in a plain browser.

Why a service and not /favicon.ico: measured on 106 common developer sites, /favicon.ico finds 65%, the page's <link rel=icon> 87%, the service 97%. Sites like rust-lang.org declare their icon only in the page, and sites like npmjs.com refuse a direct fetch. The service sees the host names a chat links; that is the trade-off T3 Code makes too.

Changelog

  • Added: links in agent chat replies, prompts and web fetch rows show the site's icon.
Preview (Agent chat, dark theme)
Preview

Tests

  • webviews/test/link-site-icons.test.tsx: globe then favicon from one batched ask; local, private and GitHub links never asked; a prompt's pasted URL becomes a link; a fetch row shows its page's icon; no <img> in the drawn marks; an answer that is not a base64 PNG is no icon; past 512 sites, pending sites still get their icons.
  • Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentPaneSiteIconsTests.swift (stubbed resolver and transport, no network): request validation, only public host names asked, the service URL, the host pin, one fetch per host as a PNG, a page answer is no icon, bytes that are no image are no icon, never fetches nothing, a tab's icon wins without a fetch, a dark mark gets a plate.
  • Updated: link-cards.test.tsx, pr-hover-card.test.tsx, links-blue for the new link mark.

Observed / Not run

  • Observed: tagged build chat-favicons on macOS 26 with Claude Code replies: React, MDN, both Rust sites, Python, npm, OpenAI, Vite, Svelte, Zed and Bun icons load; localhost keeps the globe; the Rust book's black icon shows on its plate.
  • Observed: bun run check, prepush-gate.py, the webviews tests above, Release compile (swift build -c release).
  • Observed: swift test --filter on AgentPaneSiteIconsTests and the 6 neighbouring reply-link suites: 80 tests pass (Xcode 26.6). Other guards (generated files, concurrency, motion, guardrails) pass; app-size and god-file guards warn (+2 App lines); gallery coverage, crash ratchet and localization fail the same way on base.
  • Not run: light theme in the app; the full webviews and Swift suites.
  • Upstream red: cmux-next checks fails only on "Package dependency lists sorted" (CmuxNextHomeEngineTests imports CmuxNextHome without declaring it, from a63986a44b0). It fails the same way on the base; this PR does not touch Package.swift.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Web links in agent replies, prompts, and web fetch tool rows now show the site's favicon instead of a plain globe or plain text, via a new link.favicons host op.

  • The host fetches icons from a public favicon service for public host names only, guarded to https, public hops, at most 3 redirects, 64 KiB and 5 s, with no cookies or referrer, and caches one answer per host per app run.
  • The mark is a fixed square drawn as a CSS background, so a late icon never reflows the paragraph and copying a message copies text only.
  • Local and private addresses are never fetched, GitHub and arXiv keep their own marks, and agentPane.images.remote = never disables icon fetching.
  • Pasted web addresses in prompts become link chips through the same Markdown path as replies, and fetch tool rows draw the page's icon.
  • The service is used instead of /favicon.ico because it finds icons for 97% of 106 common developer sites, against 65% for /favicon.ico and 87% for the page's <link rel=icon>.
  • Webview and Swift tests cover request bounds, public-host validation, caching, batching, prompt links, fetch rows, and the store's size limit; only a base64 PNG data URL is accepted as an icon, whether fetched or already known.