# feat\(mobile\): thread chips show the thread's project icon · PR \#22243 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22243)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: closed

Source repository: Unavailable fork

Source branch: feat/mobile-thread-chip-project-icons

Target branch: main

Head commit: 8b0678c092431fd2e4fbff348099056645540bd7

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22243)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22243/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22243/diffs?format=markdown)

## Description

---

> [!NOTE]
> 🤖 **Claude Opus 5.5 on behalf of Oliver**

> [!IMPORTANT]
> **Stacked PR 2 of 4.** Review and merge in this order:
>
> 1. #22237: Thread links in chat render as chips
> 2. **#22243: Thread chips show the thread's project icon ← this PR**
> 3. #22481: Projects without a favicon show a monogram, like desktop
> 4. #22585: Chat and composer chips stay readable in light mode
>
> Every PR in the stack targets `main` because the branches live on a fork, so this diff also includes #22237. To review only this PR's changes, use [this comparison](https://github.com/flamboh/t3code/compare/fix/mobile-assistant-link-chips...feat/mobile-thread-chip-project-icons).
>
> When a PR earlier in the stack merges, I rebase the ones after it onto `main`.

## Why

On desktop, a thread link shows the thread's project favicon and falls back to a chat glyph ([`MarkdownThreadLink.tsx`](https://github.com/pingdotgg/t3code/blob/main/apps/web/src/components/chat/MarkdownThreadLink.tsx)). On mobile every thread chip showed the same MessagesSquare glyph, so you couldn't tell which project a linked thread belonged to.

## Implementation

Thread chips in mobile chat now show the thread's project icon, the way mobile shows project icons elsewhere. That covers thread context records in user messages and the thread-link chips from #22237. Each chip picks, in order:

| Thread's project | Chip icon |
| --- | --- |
| Thread unknown on this device, or the no-project (scratch) folder | Tinted MessagesSquare chat bubble, as before (desktop and #21546 also use a chat bubble for no-project threads) |
| Custom emoji icon | The emoji |
| Custom monogram icon | That monogram in its color |
| Custom Lucide icon | A monogram of the project title in the icon's color (mobile has no Lucide set, same as the sidebar) |
| No custom icon, has a favicon | The favicon in its own colors |
| No custom icon, no favicon | A monogram of the project title, colored by the same name hash web uses (`projectIdentity.ts`) |

- Favicons come from mobile's existing project favicon pipeline (`projectFaviconUrlAtom`), the same one `ProjectFavicon` uses in thread lists. That gives a cached thumbnail from the on-device favicon cache when there is one, and otherwise the server's signed asset URL. The token is in the URL, so the pill loads it the same way over a local, Tailscale or T3 Connect connection, with no extra headers. Each project resolves once and is shared by every chip and list row that shows it.
- The native pill only accepts a raster image, so monogram and emoji tiles are drawn once per distinct glyph by a hidden `react-native-svg` view mounted at the app root and exported with `toDataURL` as a PNG `data:` URI. Monograms use the sidebar tile's look: rounded tile, 15% tint background, `-500` text. The result is cached in an atom keyed by the glyph and color, so every chip with that glyph shares one image. Nothing is redrawn per chip or per render, and nothing is fetched. The colors don't depend on the theme, so one image serves dark and light.
- Icons keep their own colors (no tint); the label keeps the thread accent. Linked threads are resolved across environments the same way tapping them already does.

## Evidence

One fixture thread with a project in each state: `favicon-app` (favicon), `Auto Monogram` (no icon, no favicon), `custom-mono` (custom monogram "Q7", violet), `emoji-proj` (🚀), `lucide-proj` (Lucide rocket, emerald), the no-project folder, and a link to a thread that doesn't exist. Each appears as a thread context record in the user message and as a `t3-thread://` link in the assistant reply.

Before (dark and light, iOS then Android): every thread chip shows the teal MessagesSquare glyph.

![Before: every thread chip shows the teal MessagesSquare glyph](https://gh-file-drop-api-prod-galwoqjslzlnws6s.oliver-boorstein.workers.dev/f/92b27d66b6ea5cda/prB-before-grid.png)

After, close-up of the context-record chips. From the top: favicon, auto monogram "AM", custom monogram "Q7", emoji 🚀, Lucide as monogram "LP", no project (chat bubble), unknown thread (chat bubble).

![After: context-record chips show favicon, AM, Q7, rocket emoji, LP, then chat bubbles for no project and unknown, on iOS and Android in dark and light](https://gh-file-drop-api-prod-galwoqjslzlnws6s.oliver-boorstein.workers.dev/f/92cac5869b43393f/closeup.png)

After, iOS 27 simulator: context records in the user message and thread links in the assistant reply, dark and light.

![After on iOS: both chip forms show the same icon per state in dark and light](https://gh-file-drop-api-prod-galwoqjslzlnws6s.oliver-boorstein.workers.dev/f/871ae8ece80734d0/ios-grid.png)

After, Android 16 emulator, same layout.

![After on Android: both chip forms show the same icon per state in dark and light](https://gh-file-drop-api-prod-galwoqjslzlnws6s.oliver-boorstein.workers.dev/f/c9ecf1bc8655e345/and-grid.png)

## Limitations

- **Projects with no favicon look different from the mobile sidebar.** Their chips show a title monogram, as desktop does, while mobile thread lists still show a folder for those projects.
- **Monogram font differs slightly from the sidebar.** The tile is drawn by `react-native-svg`, which doesn't pick up the app's `font-mono`. iOS draws the letters in the system sans-serif, and Android draws them in regular-weight monospace instead of bold.
- **A favicon that hasn't loaded yet shows the monogram first.** The chip switches to the favicon once it loads, which happens only the first time that project's favicon is fetched on the device. The thread list shows a folder in that gap.
- **Emoji is drawn by the platform's emoji font**, a little smaller than a full tile, just like the sidebar's emoji icon.
- **Some icons only appear on a full app reload.** If a tile capture ever fails, that glyph's chips keep the chat bubble until the app reloads. This never happened in testing.
- **The no-project chat bubble depends on server config.** It only applies when the server reports a scratch folder (`scratchWorkspaceRoot`). Before the server config arrives, a no-project thread briefly shows the folder's monogram.
- **No relay or tunnel testing.** Tested only against a local dev server; T3 Connect isn't tested. Monogram and emoji tiles are drawn on the device and need no network.

## Validation

- `vp test run` for `apps/mobile/src/lib/projectIcon.test.ts` (which icon a chip gets in each state), `enrichedLinkPresentation.test.ts`, and the React Compiler coverage test. `tsc --noEmit` for `apps/mobile`, and lint on the changed files.
- iOS 27 simulator and Android 16 emulator, dark and light, against an isolated dev server. The JS ran on the installed native clients' base commit with this branch's mobile changes applied.

Made with Claude Opus 5.5 via Claude Code in T3 Code.

