plans/app-icons.md
Generated by swarm planning session on 2026-02-13
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.
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.
Setting an icon (primary flow):
New app creation:
hash(app.id + app.name)Copying an app:
Backfill (one-time, on feature launch):
Icon picker modal:
Chat tabs:
[Icon 16px] [8px gap] Chat Title [Close button] (single line)**App Name** - Chat Title (full text, no truncation)Overflow menu:
[Icon 14px] App Name - Chat Title<span aria-hidden="true">, with <span class="sr-only">[App Name]</span> for screen reader text. Tabs have aria-label="App Name: Chat Title"prefers-reduced-motion — disable scale/fade animations, use instant transitionsClient-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.
src/db/schema.ts — Add iconType and iconData columns to apps tablesrc/ipc/types/app.ts — Update AppBaseSchema with new icon fieldssrc/ipc/handlers/app_handlers.ts — Modify createApp (auto-generate icon), copyApp (generate different icon), add updateAppIcon handlersrc/components/chat/ChatTabs.tsx — Refactor to single-line layout with icon, reduce MIN_VISIBLE_TAB_WIDTH_PX, add hover tooltipsrc/pages/app-details.tsx — Add icon display in header with click-to-edit, icon picker modalsrc/components/AppList.tsx / src/components/appItem.tsx — Add icon rendering next to app namesrc/components/ui/AppIcon.tsx — Shared icon rendering component (handles emoji, avatar, and fallback modes)src/components/ui/IconPickerModal.tsx — Modal with emoji and avatar tabsAdd two nullable text columns to the apps table:
ALTER TABLE apps ADD COLUMN icon_type TEXT;
ALTER TABLE apps ADD COLUMN icon_data TEXT;
icon_type: "emoji" | "generated" | nullicon_data:
"🚀"){"seed": "a1b2c3", "version": 1})null: triggers fallback (first-letter colored circle)Zod schema update in src/ipc/types/app.ts:
iconType: z.enum(["emoji", "generated"]).nullable(),
iconData: z.string().nullable(),
Backfill migration: One-time background script that:
icon_type IS NULLhash(app.id + app.name) for eachNew IPC handler — updateAppIcon:
{
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 seedcopyApp: Generate NEW avatar seed (different from original), never copy icon from source appicon_type and icon_data columns to apps table schemaAppBaseSchema Zod type with new icon fieldsAppIcon.tsx component that renders: emoji (if iconType=emoji), generated avatar (if iconType=generated), or first-letter fallback (if null)updateAppIcon IPC handlercreateApp handler to auto-generate avatar on app creationcopyApp handler to generate different avatar for copied appsMIN_VISIBLE_TAB_WIDTH_PX (start at 140px, test and adjust)App Name - Chat Title@emoji-mart/react, @emoji-mart/data) with lazy loading via dynamic importIconPickerModal.tsx with two tabs (Emoji | Avatar)updateAppIcon handler with optimistic UI updates| 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 |
| 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