Back to Dyad

App Icons & Emoji for Dyad Apps

plans/app-icons.md

0.44.018.3 KB
Original Source

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

RiskLikelihoodImpactMitigation
Startup performance regression from backfilling 100+ appsHIGHHIGHRun migration in background with batching + yielding; show progress indicator; persist completion flag
Chat tab layout regression (drag/drop, overflow, context menu)MEDIUMHIGHImplement tabs last; comprehensive E2E test suite covering all existing tab behaviors before refactoring
Emoji rendering inconsistency across OS (macOS vs Windows vs Linux)MEDIUMMEDIUMTest on all 3 platforms before launch; if issues found, add Twemoji/emoji image fallback
emoji-mart bundle size impact (~200KB)MEDIUMMEDIUMLazy-load via dynamic import; only load when modal opens; monitor bundle size in CI
Icon similarity causing app misidentificationLOWHIGHUse 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 tabsMEDIUMMEDIUMTooltip on hover (critical path); if >10% user complaints, ship adaptive layout patch
Generated avatars poor contrast in dark modeMEDIUMMEDIUMTest 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

DecisionReasoning
Both emoji + avatars in MVPUser decision. Emoji adds expressiveness and delight (Notion-like). Avatar provides automatic uniqueness. Use emoji-mart library, lazy-loaded.
Auto-backfill all existing appsUser 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 editingUser 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 nameRenaming 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 v1Avoids storage, security (SVG XSS), and content moderation complexity. Emoji + avatars provide sufficient customization.
Client-side SVG avatar generationFaster 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