# App Icons & Emoji for Dyad Apps

> Generated by swarm planning session on 2026-02-13

## Summary

Add a visual identity system to Dyad apps — every app gets an icon (emoji or GitHub-style generated avatar) that appears in chat tabs, the app list sidebar, and the app details page. Chat tabs become condensed single-line layouts (icon + chat title) for better density. Icons are auto-generated for all apps (including existing ones via backfill) and customizable by the user through a modal picker on the app details page.

## Problem Statement

When users have multiple apps with similar names or many open chat tabs, it's difficult to quickly distinguish between them at a glance. The current two-line tab layout (app name + chat title) consumes significant horizontal space, limiting how many tabs are visible simultaneously. Users lack a fast visual anchor to identify apps — they must read text labels every time.

## Scope

### In Scope (MVP)

- **Generated avatars**: GitHub-style geometric avatars (deterministic from app ID + name, pure CSS/SVG, ~256 unique combinations from 16 colors x 8 patterns x 2 foreground options)
- **Emoji picker**: Full emoji support via emoji-mart library (lazy-loaded), with search, categories, and recently-used section
- **Icon picker modal**: Two-tab modal (Emoji | Avatar) accessible by clicking the icon on the app details page
- **Condensed chat tabs**: Always single-line layout — icon (16px) + chat title, with hover tooltip showing app name
- **App list icons**: Icon displayed next to app name in sidebar (20x20px)
- **App details header icon**: Large icon display with click-to-edit
- **Auto-generation**: New apps get a generated avatar automatically on creation
- **Copy differentiation**: Copied apps always get a different generated avatar than the original
- **Backfill**: All existing apps receive auto-generated avatars via one-time background migration
- **Fallback**: First letter of app name in a deterministic colored circle when icon data is missing/corrupt
- **Accessibility**: ARIA labels on all icons, keyboard navigation in picker, screen reader support, colorblind-safe patterns (shape/pattern variance, not just color)

### Out of Scope (Follow-up)

- Custom image uploads (storage, security, optimization complexity)
- Per-chat icon overrides (app-level only)
- Icon themes or premium icon packs
- AI-generated contextual icons
- Pattern/color customization for avatars (just "Regenerate" button for MVP)
- Adaptive tab layout (show app name when few tabs) — revisit post-launch if needed
- Icons in chat message content
- Window title bar / OS task switcher icons

## User Stories

- **US1**: As a user creating a new app, I want it to automatically have a unique visual identity so I can recognize it immediately without configuration
- **US2**: As a user with many similar apps, I want to customize each app's icon (emoji or avatar) so I can tell them apart at a glance
- **US3**: As a user copying an app, I want the copy to have a different icon so I don't confuse it with the original
- **US4**: As a user with existing apps, I want them to automatically get icons so I don't have to manually configure dozens of apps
- **US5**: As a power user with 15+ tabs open, I want compact single-line tabs so I can see more tabs without scrolling
- **US6**: As a user hovering over a condensed tab, I want to see the full app name in a tooltip so I can confirm which app it belongs to

## UX Design

### User Flow

**Setting an icon (primary flow):**

1. User navigates to app details page
2. Sees current icon (generated avatar by default) prominently displayed in header
3. Hovers icon — sees edit overlay (pencil icon + "Change icon" tooltip)
4. Clicks icon — modal opens with two tabs: "Emoji" and "Avatar"
5. **Emoji tab**: User searches or browses emoji categories, clicks one — modal closes, icon updates immediately
6. **Avatar tab**: User sees large preview, clicks "Regenerate" to cycle through options, clicks "Apply" to save
7. Icon updates across all surfaces (tabs, sidebar, header) via optimistic UI

**New app creation:**

1. User creates app — system auto-generates avatar from `hash(app.id + app.name)`
2. Icon appears immediately in all surfaces, no user action needed

**Copying an app:**

1. User copies app — system generates NEW avatar (different seed from original)
2. Toast: "App copied! Customize its icon in app settings."

**Backfill (one-time, on feature launch):**

1. On first app startup after feature ships, background migration generates avatars for all existing apps
2. If >500ms, show subtle progress: "Setting up app icons..."
3. Apps show first-letter fallback until their avatar is generated
4. Migration persists completion flag — never runs again

### Key States

- **Default**: Generated geometric avatar (deterministic from app ID + name)
- **Customized (emoji)**: User-selected emoji character
- **Customized (avatar)**: User-regenerated avatar (different seed stored)
- **Loading**: Skeleton placeholder in icon picker; fade-in animation for tab icons
- **Error/Fallback**: First letter of app name in deterministic colored circle (color from app.id hash using same 16-color palette)
- **Empty**: Should never occur due to backfill — if it does, show generic app icon (Lucide)

### Interaction Details

**Icon picker modal:**

- **Emoji tab**: Search bar at top, category tabs, grid of emoji (40px cells), recently-used section. Clicking emoji immediately applies and closes modal (quick-apply).
- **Avatar tab**: Large centered preview (128px), "Regenerate" button, "Apply" button. Must preview in both light and dark mode side-by-side.
- **Footer**: Cancel (ESC key) closes without changes
- **Tab persistence**: Remember last-used tab in localStorage

**Chat tabs:**

- Layout: `[Icon 16px] [8px gap] Chat Title [Close button]` (single line)
- Hover: Tooltip appears within 300ms showing `**App Name** - Chat Title` (full text, no truncation)
- Tooltip must be keyboard-accessible (focus on tab shows tooltip after 1 second)
- Icon has subtle fade-in animation (150ms ease) on render

**Overflow menu:**

- Show icons alongside text: `[Icon 14px] App Name - Chat Title`
- Keep app name text in overflow menu for clarity (more horizontal space available)

### Accessibility

- **Screen readers**: Emoji wrapped in `<span aria-hidden="true">`, with `<span class="sr-only">[App Name]</span>` for screen reader text. Tabs have `aria-label="App Name: Chat Title"`
- **Keyboard navigation**: Icon picker fully keyboard-navigable (Tab between sections, Arrow keys in emoji grid, Enter to select). Emoji grid supports arrow key navigation like Windows emoji picker.
- **Colorblind safety**: Generated avatars must vary by SHAPE and PATTERN, not just color. Test in grayscale to verify distinctness.
- **Color contrast**: WCAG AA minimum (4.5:1) for icon elements against both light and dark theme backgrounds
- **Motion sensitivity**: Respect `prefers-reduced-motion` — disable scale/fade animations, use instant transitions
- **Touch targets**: Icon in app details minimum 44x44px tap area; emoji grid cells minimum 40x40px; tab icons minimum 32x32px tap area

## Technical Design

### Architecture

Client-side SVG avatar generation using a deterministic algorithm seeded by `hash(app.id + app.name)`. Emoji rendering uses native OS fonts (test cross-platform; if issues found, add Twemoji fallback). Emoji picker (emoji-mart) is lazy-loaded to avoid impacting bundle size. Backfill runs as a one-time async background task using batched DB updates.

### Components Affected

- `src/db/schema.ts` — Add `iconType` and `iconData` columns to `apps` table
- `src/ipc/types/app.ts` — Update `AppBaseSchema` with new icon fields
- `src/ipc/handlers/app_handlers.ts` — Modify `createApp` (auto-generate icon), `copyApp` (generate different icon), add `updateAppIcon` handler
- `src/components/chat/ChatTabs.tsx` — Refactor to single-line layout with icon, reduce `MIN_VISIBLE_TAB_WIDTH_PX`, add hover tooltip
- `src/pages/app-details.tsx` — Add icon display in header with click-to-edit, icon picker modal
- `src/components/AppList.tsx` / `src/components/appItem.tsx` — Add icon rendering next to app name
- **New**: `src/components/ui/AppIcon.tsx` — Shared icon rendering component (handles emoji, avatar, and fallback modes)
- **New**: `src/components/ui/IconPickerModal.tsx` — Modal with emoji and avatar tabs

### Data Model Changes

Add two nullable text columns to the `apps` table:

```sql
ALTER TABLE apps ADD COLUMN icon_type TEXT;
ALTER TABLE apps ADD COLUMN icon_data TEXT;
```

- `icon_type`: `"emoji"` | `"generated"` | `null`
- `icon_data`:
  - For emoji: single UTF-8 emoji character (e.g., `"🚀"`)
  - For generated: JSON string with avatar seed/config (e.g., `{"seed": "a1b2c3", "version": 1}`)
  - `null`: triggers fallback (first-letter colored circle)

**Zod schema update** in `src/ipc/types/app.ts`:

```typescript
iconType: z.enum(["emoji", "generated"]).nullable(),
iconData: z.string().nullable(),
```

**Backfill migration**: One-time background script that:

1. Queries all apps where `icon_type IS NULL`
2. Generates avatar seed from `hash(app.id + app.name)` for each
3. Updates in batches of 10 with yielding to main thread
4. Stores completion flag in app settings/DB to prevent re-running

### API Changes

**New IPC handler — `updateAppIcon`:**

```typescript
{
  channel: "update-app-icon",
  input: z.object({
    appId: z.number(),
    iconType: z.enum(["emoji", "generated"]),
    iconData: z.string(),
  }),
  output: z.void(),
}
```

**Modified handlers:**

- `createApp`: Generate default avatar seed, set `iconType = "generated"` and `iconData = JSON seed`
- `copyApp`: Generate NEW avatar seed (different from original), never copy icon from source app

## Implementation Plan

### Phase 1: Foundation (Backend + Avatar Generation)

- [ ] Add `icon_type` and `icon_data` columns to `apps` table schema
- [ ] Update `AppBaseSchema` Zod type with new icon fields
- [ ] Implement deterministic avatar generation algorithm (pure CSS/SVG, seeded by app ID + name, 16-color palette, 8 geometric patterns)
- [ ] Create shared `AppIcon.tsx` component that renders: emoji (if iconType=emoji), generated avatar (if iconType=generated), or first-letter fallback (if null)
- [ ] Add `updateAppIcon` IPC handler
- [ ] Modify `createApp` handler to auto-generate avatar on app creation
- [ ] Modify `copyApp` handler to generate different avatar for copied apps
- [ ] Implement one-time background backfill migration (batched, async, with progress indicator if >500ms)

### Phase 2: Icon Display Surfaces

- [ ] Add icon to app details page header (large display, clickable with hover edit overlay)
- [ ] Add icon to app list sidebar items (20x20px, left of app name)
- [ ] Refactor chat tabs to single-line layout: icon (16px) + chat title
- [ ] Reduce `MIN_VISIBLE_TAB_WIDTH_PX` (start at 140px, test and adjust)
- [ ] Add hover tooltip on tabs showing `App Name - Chat Title`
- [ ] Add icons to tab overflow menu (14px icon + app name + chat title)

### Phase 3: Icon Picker Modal + Emoji

- [ ] Install and configure emoji-mart (`@emoji-mart/react`, `@emoji-mart/data`) with lazy loading via dynamic import
- [ ] Build `IconPickerModal.tsx` with two tabs (Emoji | Avatar)
- [ ] Emoji tab: search, categories, recently-used, quick-apply on click
- [ ] Avatar tab: large preview (128px) with light/dark mode side-by-side, "Regenerate" button, "Apply" button
- [ ] Persist last-used tab in localStorage
- [ ] Wire modal to `updateAppIcon` handler with optimistic UI updates

### Phase 4: Polish & Testing

- [ ] Dark mode testing for all generated avatar colors (WCAG AA contrast in both themes)
- [ ] Cross-platform emoji rendering verification (macOS, Windows, Linux)
- [ ] Accessibility audit: ARIA labels, keyboard navigation, screen reader testing
- [ ] Performance testing: backfill with 50, 100, 200 apps (must be <3s for 100 apps)
- [ ] Bundle size verification (emoji-mart lazy-loaded, total increase <300KB)
- [ ] Update E2E test snapshots for new tab layout
- [ ] Write new E2E tests (see Testing Strategy)

## Testing Strategy

### Unit Tests

- [ ] Avatar generation determinism: same seed always produces same output
- [ ] Avatar generation uniqueness: sequential app IDs produce visually distinct avatars
- [ ] Copy app produces different icon than original
- [ ] Icon data validation (valid emoji characters, valid generated JSON config)
- [ ] Fallback logic: null iconType renders first-letter circle

### E2E Tests

- [ ] Icon persistence: set emoji, restart app, icon unchanged
- [ ] Copy distinctness: copy app, verify new app has different icon
- [ ] Tab condensation: open 8+ tabs, all show icons + titles in single line
- [ ] Tooltip accuracy: hover tab, tooltip shows correct app name
- [ ] Fallback rendering: corrupt icon data shows first-letter fallback
- [ ] Overflow menu: open 12+ tabs, overflow menu shows icons for hidden tabs
- [ ] Modal keyboard navigation: open modal, Tab/Arrow keys work, Enter applies
- [ ] Dark mode: switch theme, all icons remain visible and readable
- [ ] Screen reader: navigate tabs with VoiceOver, announces app name + chat title (not emoji unicode)
- [ ] Backfill performance: create 100 apps, restart, measure startup time (<3s)

## Risks & Mitigations

| Risk                                                                | Likelihood | Impact | Mitigation                                                                                                     |
| ------------------------------------------------------------------- | ---------- | ------ | -------------------------------------------------------------------------------------------------------------- |
| Startup performance regression from backfilling 100+ apps           | HIGH       | HIGH   | Run migration in background with batching + yielding; show progress indicator; persist completion flag         |
| Chat tab layout regression (drag/drop, overflow, context menu)      | MEDIUM     | HIGH   | Implement tabs last; comprehensive E2E test suite covering all existing tab behaviors before refactoring       |
| Emoji rendering inconsistency across OS (macOS vs Windows vs Linux) | MEDIUM     | MEDIUM | Test on all 3 platforms before launch; if issues found, add Twemoji/emoji image fallback                       |
| emoji-mart bundle size impact (~200KB)                              | MEDIUM     | MEDIUM | Lazy-load via dynamic import; only load when modal opens; monitor bundle size in CI                            |
| Icon similarity causing app misidentification                       | LOW        | HIGH   | Use shape+pattern variance (not just color); seed includes app name for entropy; exact-match duplicate warning |
| Always-condensed tabs reduce scannability for users with few tabs   | MEDIUM     | MEDIUM | Tooltip on hover (critical path); if >10% user complaints, ship adaptive layout patch                          |
| Generated avatars poor contrast in dark mode                        | MEDIUM     | MEDIUM | Test all 16 palette colors against both theme backgrounds; show dual preview in picker                         |

## Open Questions

- **Tab minimum width**: Start at 140px, but measure with real chat titles. May need adjustment to 120-130px based on truncation data. Acceptance: <30% of tabs truncated beyond first 15 characters.
- **Emoji rendering quality**: If native OS emoji looks inconsistent across platforms, do we switch to Twemoji image sprites (adds ~500KB)? Decision deferred to cross-platform testing results.
- **Overflow menu design**: Icons confirmed in overflow menu, but exact layout (icon size, spacing, whether to show both app name and chat title) needs visual design pass during implementation.

## Decision Log

| Decision                                                       | Reasoning                                                                                                                                                                               |
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Both emoji + avatars in MVP                                    | User decision. Emoji adds expressiveness and delight (Notion-like). Avatar provides automatic uniqueness. Use emoji-mart library, lazy-loaded.                                          |
| Auto-backfill all existing apps                                | User decision. Ensures consistent visual experience from day one. Requires background migration with performance safeguards.                                                            |
| Always condensed tabs (no adaptive layout)                     | User decision. Simpler implementation, consistent UX. Tooltip on hover mitigates discoverability concern. Adaptive layout available as fallback if user feedback demands it.            |
| Click icon → modal for editing                                 | User decision. Standard interaction pattern, gives enough space for emoji grid + avatar preview. Quick-apply for emoji (click = apply + close), explicit Apply for avatar regeneration. |
| Icons persist independently of app name                        | Renaming an app does not change its icon. Icons are identity, not derived from name. Avoids surprising users.                                                                           |
| App-level icons only (no per-chat)                             | Simpler mental model. Icon = app identity. Per-chat overrides deferred to v2 if requested.                                                                                              |
| No custom image uploads in v1                                  | Avoids storage, security (SVG XSS), and content moderation complexity. Emoji + avatars provide sufficient customization.                                                                |
| Client-side SVG avatar generation                              | Faster rendering, no IPC overhead, deterministic from seed. No external dependencies needed. Can refactor to shared utility later if backend rendering needed.                          |
| Phased implementation (foundation → display → picker → polish) | Chat tabs are highest-risk surface, done last. Avatar system can be validated independently before touching critical navigation.                                                        |

---

_Generated by dyad:swarm-to-plan_
