# assistant-ui Migration Plan

**Date:** 2026-05-11
**Repo:** `~/personal/codebase/head-of-product-ui-v2`
**Goal:** Stop reinventing chat primitives. Move as much as possible to assistant-ui's official building blocks. Keep what's genuinely ours (Agent SDK backend, skill/file mentions, multi-conversation streamStore).

---

## 1. Capability Map — assistant-ui vs. us

Legend: ✅ already on assistant-ui · 🟡 hybrid (assistant-ui + custom glue) · 🔴 fully hand-rolled · ⚪ not implemented

| # | Capability | assistant-ui export | Our impl | Status | Verdict |
|---|---|---|---|---|---|
| 1 | Thread container, viewport, welcome slot | `ThreadPrimitive` / `<Thread>` | `<Thread>` used in `ChatPane.tsx` | ✅ | Keep |
| 2 | Composer (textarea, send button, attachments row) | `ComposerPrimitive` | Inherited via `<Thread>` | ✅ | Keep |
| 3 | Auto-scroll / follow-tail | Built into `ThreadPrimitive.Viewport` | Inherited | ✅ | Keep |
| 4 | Markdown rendering | `makeMarkdownText` + `react-markdown` styles | `makeMarkdownText({remarkPlugins:[remarkGfm]})` in `ChatPane.tsx:42` | ✅ | Keep |
| 5 | Streaming + cancel/stop | Runtime layer (`thread().cancelRun()`, `onCancel` on external store) | `chatStreamStore.abort()` + `onCancel` wired | 🟡 | Keep — our SSE loop is necessary (Agent SDK, not AI SDK) |
| 6 | External store runtime bridge | `useExternalStoreRuntime` | Used in `ChatPane.tsx:343` | ✅ | Keep |
| 7 | Image attachments | `SimpleImageAttachmentAdapter` | `composerShared.tsx` via `CompositeAttachmentAdapter` | ✅ | Keep |
| 8 | Text-file attachments | `SimpleTextAttachmentAdapter` | Same | ✅ | Keep |
| 9 | PDF attachments | — (no official adapter) | Custom `PdfAttachmentAdapter` in `composerShared.tsx` | 🔴 | Keep custom (no upstream equivalent) |
| 10 | Tool call fallback UI | `ToolFallback` slot on `assistantMessage.components` | `ToolCallChip.tsx` wired as `ToolFallback` | 🟡 | Keep — our custom rendering is the whole point |
| 11 | "Thinking…" placeholder | **`@assistant-ui/react` ChainOfThought** + `Empty` slot | Custom `ThinkingPlaceholder` on `Empty` slot | 🟡 | Keep ours (simpler) OR adopt `ChainOfThought` if we want reasoning grouping |
| 12 | Reasoning blocks (model chain-of-thought) | `Reasoning` UI component, `ChainOfThought` primitive, `reasoning` content part | Not handled | ⚪ | **ADD** when Agent SDK emits reasoning |
| 13 | Message edit + regenerate | `ActionBarPrimitive` (`Edit`, `Reload`) | None | ⚪ | **ADD** — high user value, low effort |
| 14 | Branching (alt replies) | `BranchPickerPrimitive` | None | ⚪ | Skip for now — Agent SDK doesn't fork |
| 15 | Copy message | `ActionBarPrimitive.Copy` | None | ⚪ | **ADD** — trivial |
| 16 | Action bar (per-message toolbar) | `ActionBarPrimitive` | None | ⚪ | **ADD** — host for Copy/Edit/Reload |
| 17 | Suggested starter prompts | `ThreadPrimitive.Suggestion` | None in chat; hero composer is separate | ⚪ | **ADD** to empty-state for new chats |
| 18 | Slash commands | **`Composer Trigger Popover`** + `Directive Text` | Custom `SkillMention.tsx` + regex expansion | 🔴 | **MIGRATE** to assistant-ui's trigger popover; keep our skill source |
| 19 | @-mentions | Same Trigger Popover + `Directive Text` chips | Custom `FileMention.tsx` + DOM portal hack into `.aui-composer-attachments` | 🔴 | **MIGRATE** — kills our portal hack |
| 20 | Attachment chips (visual) | `AttachmentPrimitive` w/ `aui-attachment-*` classes | We already use those classes | ✅ | Keep |
| 21 | Quote / text selection toolbar | `SelectionToolbarPrimitive` | None | ⚪ | Optional — "Quote this" is nice for PM workflows |
| 22 | Error display | `Error` primitive | Inline "Error: …" string in message | 🟡 | **MIGRATE** to `Error` primitive |
| 23 | Multi-thread list | `ThreadListPrimitive` + adapter | Custom `HistoryPanel.tsx` + `loadSession()` + `sessionStore` | 🔴 | **EVALUATE** — see §3 |
| 24 | Persistence / resume | `Cloud Persistence` (AssistantCloud) **OR** custom `ThreadHistoryAdapter` | Custom `/api/chat-session` + JSONL replay | 🔴 | Keep ours — our truth is Agent SDK's JSONL on disk |
| 25 | Resumable streams (server-side) | `Resumable Stream Deployment` + `ResumableStreamStore` | None — stream tied to client | ⚪ | **Optional later** — would let stream survive refresh, not just nav |
| 26 | Token / timing metrics | `Message Timing` UI, `Context Display` UI | None | ⚪ | **ADD** — Agent SDK emits usage; trivial display win |
| 27 | Sources (citations) | `Sources` UI | None | ⚪ | Skip unless we add web search |
| 28 | Voice / TTS | `Voice` UI, dictation adapter | None | ⚪ | Skip |
| 29 | Image preview in message | `Image` UI component | We render via markdown / `aui-attachment-*` | 🟡 | Adopt `Image` UI for fullscreen dialog |
| 30 | Mermaid / diff viewer | `Mermaid Diagrams`, `Diff Viewer` | None | ⚪ | Optional for PM use cases |
| 31 | Syntax highlighting | `@assistant-ui/react-syntax-highlighter` or react-shiki | None — code blocks plain | ⚪ | **ADD** — `react-shiki` integration |
| 32 | AskUserQuestion interactive card | — (no official equivalent) | Custom `AskUserQuestionCard` in `ToolCallChip.tsx` | 🔴 | Keep — this is genuinely ours |
| 33 | LaTeX / KaTeX | Built-in markdown plugin | None | ⚪ | Optional |
| 34 | Persistent streams across nav | — (assistant-ui state dies on unmount) | `chatStreamStore` module-level Map + `useSyncExternalStore` | 🔴 | **Keep — this is the right architecture for our scale (500+ chats).** assistant-ui has no equivalent. |
| 35 | Conversation ID (stable client key) | — | `activeConversationId` in `sessionStore` | 🔴 | Keep — required by streamStore |
| 36 | Composer in 2 places (hero + chat) | `<Composer />` reusable | Shared via `composerShared.tsx` | ✅ | Keep |

---

## 2. What we keep, what we migrate, what we add

### Keep as-is (don't fight the architecture)

- **`chatStreamStore` + `useSyncExternalStore`** — assistant-ui has no answer for "subscribe to a stream while ChatPane is unmounted." This is our IP. Don't touch it.
- **Backend (`server/lib/chat.ts`)** — Agent SDK is not AI SDK. `useChatRuntime` does not apply.
- **`useExternalStoreRuntime` bridge** — correct seam.
- **Custom `PdfAttachmentAdapter`** — no upstream.
- **`AskUserQuestionCard`** — domain-specific.
- **Skill source / file source** (where the data comes from) — that's our domain, not the picker UI.

### Migrate (replace hand-rolled with upstream)

| Migrate | From | To | Effort | Win |
|---|---|---|---|---|
| **A. Slash command UI** | `SkillMention.tsx` popover + portal | `Composer Trigger Popover` + `Directive Text` | M | Kills custom popover, anchoring, fuzzy search; we just feed it the skill list |
| **B. @-mention UI** | `FileMention.tsx` popover + DOM portal hack into `.aui-composer-attachments` | Same Trigger Popover | M | Kills the portal hack entirely. Big code reduction. |
| **C. Error rendering** | Inline "Error: …" text | `Error` primitive | XS | Accessibility + consistency |
| **D. Image preview** | Bare `<img>` in markdown | `Image` UI component | XS | Fullscreen dialog, loading states |

### Add (free wins from upstream)

| Add | Component | Effort | Why |
|---|---|---|---|
| **E. Action bar (Copy / Edit / Reload)** | `ActionBarPrimitive` | S | Table stakes for chat UX. Edit+regen is huge for PM iteration loops. |
| **F. Suggested prompts** | `ThreadPrimitive.Suggestion` | XS | Empty state for new chats — surface common skill invocations |
| **G. Token / timing footer** | `Message Timing` UI | XS | Agent SDK already emits usage; just render it |
| **H. Code syntax highlighting** | `react-shiki` or `@assistant-ui/react-syntax-highlighter` | S | Massive readability win for any code in responses |
| **I. Reasoning blocks** | `ChainOfThought` + `reasoning` content part | S | Only if we surface Agent SDK's thinking — otherwise skip |

### Defer / Skip

- Branching (no fork model in Agent SDK)
- Voice / TTS
- AssistantCloud persistence (our JSONL replay is fine)
- Resumable streams (nice-to-have; current scope: persist across nav, not across refresh)
- Mermaid, LaTeX, Sources (add when needed)

---

## 3. Hard question: do we replace HistoryPanel with `ThreadListPrimitive`?

**Recommendation: No.** Three reasons:

1. `ThreadListPrimitive` assumes assistant-ui owns thread lifecycle (create / switch / archive / delete). Our source of truth is Agent SDK's JSONL on disk + `/api/projects` listing. Bridging this through assistant-ui's thread-list adapter is more glue than we have today.
2. Our HistoryPanel already does what we need (grouped by time bucket, filter, archive). It's not where the pain is.
3. `activeConversationId` ↔ SDK `sessionId` mirror logic in `sessionStore` does not fit cleanly into `ThreadListItemRuntime`.

**However**, if we ever want native search / archive / rename on threads with proper a11y, revisit.

---

## 4. Migration plan — sequenced

### Phase 1 — Mention systems (biggest code-debt payoff)
1. Adopt `Composer Trigger Popover` for `/` (skills)
2. Adopt same primitive for `@` (files)
3. Delete `FileMention.tsx` portal hack into `.aui-composer-attachments`
4. Keep skill expansion (`expandSkillInvocations`) — that's backend prompt assembly, not UI

**Outcome:** ~300+ LOC deleted; native a11y, keyboard nav, anchoring.

### Phase 2 — Message actions
5. Add `ActionBarPrimitive` to assistant messages: Copy, Reload
6. Add `ActionBarPrimitive` to user messages: Edit (regenerates assistant reply)
7. Wire Edit → `runtime.message(i).edit()` → triggers new run via existing `onNew`

**Outcome:** Real iteration loop. Edit+regen is the #1 missing chat feature.

### Phase 3 — Polish
8. `Error` primitive replaces inline error strings
9. `Image` UI component for attachment previews
10. `ThreadPrimitive.Suggestion` for empty new-chat state (3-4 common PM prompts)
11. `Message Timing` footer (TTFT, tok/s) — opt-in via setting

### Phase 4 — Code rendering (separate ticket)
12. `react-shiki` for code blocks (slot into `makeMarkdownText`'s code component)

### Phase 5 — Optional
- Resumable streams (server-side) if "stream survives page refresh" becomes a goal
- `ChainOfThought` / reasoning if we expose Agent SDK thinking

---

## 5. What I'd cut next session

Phase 1 + Phase 2. That's where 80% of the leverage is. Phase 3 is a single afternoon. Phase 4 is its own ticket.

**Concrete next steps:**

1. **Decide:** Phase 1 first (kill mention hacks) or Phase 2 first (Edit/Reload — visible feature)?
2. Spike Phase 1 in a worktree (we have the pattern from the markdown spike).
3. Confirm: do we want reasoning blocks now or wait until Agent SDK emits them more consistently?

Path to this doc: `~/Library/Mobile\ Documents/com~apple~CloudDocs/claude-output/head-of-product-ui/2026-05-11_assistant-ui-migration-plan.md`
