19 KiB
AIUI Project Plan: Roadmap, Progress Tracking & Automated Coding Tasks
Context
AIUI is at ~65% architectural completeness. The chat + content surface experience is mature (11 content types, streaming, responsive layout, glass morphism design system). However, critical infrastructure is missing: no persistent storage beyond dev mode, zero error boundaries, no unit tests, plugin system spec'd but unused, no encryption.
The user has Claude Code automation set up for late-night unattended runs. This plan provides:
- Progress tracking automation — PROGRESS.md updated on every commit/push
- Detailed task specs — granular enough for automated Claude sessions to execute autonomously
There are 6 active worktrees — all automation must work across branches.
Part 1: Progress Tracking Automation
Task 1.1: Create PROGRESS.md
File: PROGRESS.md (repo root)
Create a structured progress document with:
- Project status summary (current milestone, % complete)
- Roadmap checklist (M0–M7, matching Part 2 below)
- Session log section (auto-populated by hook)
Format:
# AIUI Progress
## Current Status
**Active Milestone**: M1 — Stability & Polish
**Overall**: ~65% architectural completeness
## Roadmap
### M0: Foundation ✅
- [x] Chat interface with streaming
- [x] 11 content type renderers
- [x] Responsive layout (mobile + desktop)
- [x] Glass morphism design system
- [x] Claude proxy + web search
- [x] PWA support
### M1: Stability & Polish 🔄
- [ ] Persistent storage (IndexedDB)
- [ ] Error boundaries
- [ ] Unit tests for composables
- [ ] E2E test coverage
- [ ] CI pipeline
(... remaining milestones from Part 2 ...)
## Session Log
<!-- Auto-populated by post-push hook -->
Task 1.2: Create post-push progress hook
File: .claude/hooks/post-push-progress.sh
A PostToolUse hook for Bash that:
- Reads stdin JSON, extracts
tool_input.command - Checks if command contains
git pushorgit commit - If match: runs
git log --onelinefor commits on current branch not onmain - Reads current PROGRESS.md roadmap section to identify active milestone
- Outputs JSON with
hookSpecificOutputcontaining a message asking Claude to update the Session Log in PROGRESS.md
The hook script should:
- Extract branch name via
git branch --show-current - Get commit list via
git log --oneline main..HEAD(or last 5 commits if no divergence) - Format the output as structured feedback
Task 1.3: Register the hook
File: .claude/settings.json
Add a PostToolUse entry:
{
"hooks": {
"PreToolUse": [ ... existing ... ],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-push-progress.sh"
}
]
}
]
}
}
Part 2: Long-Term Delivery Roadmap
M0: Foundation ✅ (Complete)
All done — chat, renderers, layout, design system, proxy, PWA.
M1: Stability & Polish (Infrastructure)
Task M1.1: IndexedDB Persistent Storage
Why: Conversations are lost on page refresh in production. Currently only persisted via dev-mode /api/dev-chats Vite middleware.
Files to modify:
packages/app/src/stores/chat.ts(198 lines) — add IndexedDB adapter- Create
packages/app/src/utils/idb-storage.ts— IndexedDB wrapper
Implementation:
-
Create
idb-storage.tswith these functions:export async function openDB(): Promise<IDBDatabase> export async function saveConversation(conv: Conversation): Promise<void> export async function loadAllConversations(): Promise<Map<string, Conversation>> export async function deleteConversation(id: string): Promise<void>- DB name:
aiui-store, version 1 - Object store:
conversations, keyPath:id - Index on
updatedAtfor sorted retrieval
- DB name:
-
Modify
chat.ts:- Replace
saveServerChats()(line 51-62) withsaveConversation()calls - Replace
loadServerChats()(line 64-84) withloadAllConversations() - Keep dev-chats middleware as fallback when
indexedDBunavailable - Debounced save stays at 800ms
- Replace
Acceptance criteria:
- Conversations survive page refresh
- New conversations appear after reload
- Delete removes from IndexedDB
- Falls back to dev-chats middleware if IDB unavailable
pnpm typecheckpasses
Task M1.2: Error Boundaries
Why: Zero onErrorCaptured usage. Any component error crashes the whole app.
Files to modify:
- Create
packages/app/src/components/ui/ErrorBoundary.vue packages/app/src/pages/ChatPage.vue(597 lines) — wrap major sectionspackages/app/src/components/chat/ChatWindow.vue(248 lines) — wrap message listpackages/app/src/components/content/ContentPanel.vue— wrap detail views
Implementation:
-
Create
ErrorBoundary.vue:<script setup lang="ts"> import { ref, onErrorCaptured } from 'vue' defineProps<{ fallbackMessage?: string }>() const error = ref<Error | null>(null) const hasError = ref(false) onErrorCaptured((err) => { error.value = err instanceof Error ? err : new Error(String(err)) hasError.value = true return false // prevent propagation }) function retry() { hasError.value = false; error.value = null } </script> <template> <slot v-if="!hasError" /> <div v-else class="glass-card p-4 text-center"> <p class="text-white/60">{{ fallbackMessage || 'Something went wrong' }}</p> <button class="glass-button-sm mt-2" @click="retry">Retry</button> </div> </template> -
Wrap in ChatPage.vue:
- Wrap chat column (around
<ChatWindow>) - Wrap content panel column (around
<ContentPanel>) - Wrap detail view column (around
<DetailView>)
- Wrap chat column (around
-
Wrap in ChatWindow.vue:
- Wrap the message loop (v-for of ChatMessage)
-
Wrap in ContentPanel.vue:
- Wrap each grid component render
- Wrap detail component render
Acceptance criteria:
- A failing renderer shows error card with retry button
- Chat continues working if content panel errors
- Content panel continues if one card errors
pnpm typecheckpasses
Task M1.3: Unit Tests for contentExtraction
Why: contentExtraction.ts is 967 lines of regex parsing with zero tests. It's the most critical composable.
File to create: packages/app/src/__tests__/contentExtraction.test.ts
Tests to write (use vitest):
describe('contentExtraction', () => {
describe('extractAllFilms', () => {
it('extracts film_ext tags with title, year, director')
it('extracts film:id tags and looks up from library')
it('returns empty array for text with no film tags')
it('handles multiple films in one message')
it('handles malformed tags gracefully')
})
describe('extractAllSongs', () => {
it('extracts song_ext tags with title, artist, year')
it('extracts song:id tags from library')
it('extracts songs from markdown bold patterns')
it('deduplicates songs by title+artist')
})
describe('extractAllPodcasts', () => {
it('extracts podcast_ext tags')
it('extracts podcast:id from library')
})
describe('extractAllBooks', () => {
it('extracts book_ext tags with title, author, year')
it('handles optional fields')
})
describe('extractAllTVSeries', () => {
it('extracts tv_ext tags')
it('parses creator and network fields')
})
describe('extractAllPlaces', () => {
it('extracts place_ext tags with all fields')
it('handles missing optional fields (rating, price)')
})
describe('extractMagazineSections', () => {
it('extracts sections from markdown headings')
it('captures content between headings')
it('extracts hero images')
})
describe('stripContentTags', () => {
it('removes all tag types from text')
it('preserves non-tag content')
it('handles nested/adjacent tags')
})
describe('extractBoldDomainLinks', () => {
it('extracts **domain.com** patterns with URLs')
it('extracts markdown links')
})
})
How to run: pnpm test (vitest via turbo)
Acceptance criteria:
- All tests pass
- Covers the 10 main extraction functions
- Tests edge cases (empty input, malformed tags, duplicates)
pnpm testexits 0
Task M1.4: Unit Tests for useAI
File to create: packages/app/src/__tests__/useAI.test.ts
Tests to write:
describe('useAI', () => {
describe('provider selection', () => {
it('defaults to first available provider')
it('switches provider via setActiveProvider')
it('lists available models for active provider')
})
describe('context injection', () => {
it('includes film library in system prompt')
it('includes song library in system prompt')
it('includes content tag format instructions')
})
describe('sendMessage', () => {
it('adds user message to store')
it('creates assistant message placeholder')
it('sets isStreaming to true during stream')
it('sets isStreaming to false after completion')
it('handles stream errors gracefully')
})
describe('stopGeneration', () => {
it('aborts active stream')
it('sets isStreaming to false')
})
})
Note: Will need to mock fetch for streaming tests. Use vitest's vi.fn().
Acceptance criteria:
- All tests pass with mocked fetch/SSE
pnpm testexits 0
Task M1.5: E2E Test Expansion
File to modify: packages/app/e2e/content-surfaces.spec.ts
Tests to add:
test('sends a message and receives streaming response')
test('content panel shows film cards when AI mentions films')
test('clicking a film card opens detail view')
test('mobile viewport shows full-screen overlay for content')
test('stop button halts generation')
test('web search toggle works')
test('new conversation clears messages')
test('panel side toggle switches layout')
Acceptance criteria:
pnpm test:e2epasses (needs dev server running)
M2: Content Experience (UX)
Task M2.1: Markdown Rendering in Chat
Why: Chat messages display plain text. Markdown (bold, italic, links, code blocks, lists) should render properly.
Files to modify:
packages/app/src/components/chat/ChatMessage.vue(333 lines)- Add
markdown-itas dependency
Implementation:
pnpm add markdown-it+pnpm add -D @types/markdown-itinpackages/app- In ChatMessage.vue:
- Import and configure markdown-it with safe defaults (no HTML)
- After
stripContentTags(), render remaining text through markdown-it - Use
v-htmlwith the sanitized markdown output - Add CSS for rendered markdown (code blocks, lists, links) in main.css
- Ensure content tags are extracted BEFORE markdown rendering
Security: markdown-it with html: false prevents XSS. No raw HTML passthrough.
Acceptance criteria:
- Bold, italic, links, code blocks, lists render in chat
- Content tags still extract correctly (films, songs, etc.)
- No XSS possible
pnpm typecheckpasses
Task M2.2: Virtual Scrolling for Chat
Why: Long conversations with many messages cause scroll jank.
Files to modify:
packages/app/src/components/chat/ChatWindow.vue- Add
@tanstack/vue-virtualdependency
Implementation:
pnpm add @tanstack/vue-virtualinpackages/app- Replace the message
v-forloop withuseVirtualizer:- Estimate row heights (user messages ~60px, assistant ~200px)
- Use dynamic measurement for actual heights
- Maintain scroll-to-bottom behavior during streaming
- Keep overscan at 5 items
Acceptance criteria:
- Scrolling is smooth with 100+ messages
- Auto-scroll to bottom during streaming still works
pnpm typecheckpasses
Task M2.3: Music Source Resolution
Why: PlayerBar exists but music source resolution is incomplete. Iframe embedding untested.
Files to modify:
packages/app/src/composables/usePlayer.ts(185 lines)packages/app/src/components/player/PlayerBar.vue(165 lines)
Implementation:
-
In usePlayer.ts:
- Add queue management:
queue: ShallowRef<Song[]>,currentIndex: Ref<number> - Add
playNext(),playPrevious(),addToQueue(song)methods - Fix iframe playback (lines 72-83): create Plyr instance for iframes too
- Add retry logic for failed music searches (try next source)
- Add queue management:
-
In PlayerBar.vue:
- Add next/previous buttons
- Show queue count
- Add queue panel (slide-up from player)
Acceptance criteria:
- Can play songs from search results
- Next/previous navigation works
- Queue persists across song changes
pnpm typecheckpasses
Task M2.4: Nostr Feed Integration
Why: NostrGrid.vue exists but is non-functional. No relay connection.
Files to modify:
packages/app/src/components/content/NostrGrid.vue- Create
packages/app/src/composables/useNostr.ts
Implementation:
-
Create
useNostr.ts:- Connect to public relays (wss://relay.damus.io, wss://nos.lol, wss://relay.snort.social)
- Use raw WebSocket (no nostr-tools dependency to keep bundle small)
- Subscribe to kind:1 (text notes) with limit 50
- Parse NIP-01 event format manually
- Export
useNostr()returning{ events, isConnected, connect, disconnect }
-
Update NostrGrid.vue:
- Use
useNostr()composable - Display events as cards with author npub (truncated), content, timestamp
- Lazy-load on tab activation only
- Use
Acceptance criteria:
- Nostr tab shows real posts from public relays
- Connection/disconnection is clean (no leaked WebSockets)
- Handles relay errors gracefully
pnpm typecheckpasses
M3: Plugin System (Infrastructure)
Task M3.1: Activate Plugin Registry at Runtime
Why: packages/core/src/plugins/registry.ts exists with registerPlugin() but nothing calls it.
Files to modify:
packages/app/src/main.ts(26 lines) — add plugin initialization- Create
packages/app/src/plugins/index.ts— plugin bootstrap - Create
packages/app/src/plugins/claude-provider.ts— first AI provider plugin
Implementation:
-
Create
plugins/index.ts:export async function initializePlugins() { // Register built-in plugins const { claudeProvider } = await import('./claude-provider') registerPlugin(claudeProvider) } -
Create
plugins/claude-provider.ts:- Implement
AIProviderAdapterinterface from@aiui/core - Wrap existing
useAI.tsstreaming logic as a plugin - Export as a Tier 1 (trusted) plugin
- Implement
-
In
main.ts:- Call
initializePlugins()before app mount - Make it async with error handling
- Call
Acceptance criteria:
- Plugin registry has at least 1 registered plugin at runtime
- Chat still works through the plugin adapter
getPluginsByType('ai-provider')returns the Claude providerpnpm typecheckpasses
Task M3.2: Renderer Plugin Registration
Why: Content renderers are hardcoded. Making them pluggable enables community extensions.
Files to modify:
- Create
packages/app/src/plugins/renderers/film-renderer.ts - Create
packages/app/src/plugins/renderers/song-renderer.ts - Modify
packages/app/src/plugins/index.ts— register renderers - Modify
packages/app/src/components/content/ContentPanel.vue— use registry lookups
Implementation:
-
Create renderer plugins for film and song (as examples):
const filmRenderer: RendererDefinition = { id: 'film', name: 'Film Renderer', contentType: 'film', surfaces: ['chat-preview', 'panel-preview', 'panel-play'], chatPreview: FilmCard, panelPreview: FilmGrid, panelPlay: FilmDetail, } -
Register in
plugins/index.tsviaregisterRenderer() -
In ContentPanel.vue, look up renderers via
getRendererForContentType()instead of hardcoded imports (gradual migration — start with film/song, keep others hardcoded)
Acceptance criteria:
- Film and song renderers registered via plugin system
getAllRenderers()returns registered renderers- Content panel still renders correctly
pnpm typecheckpasses
M4: Social & Discovery (UX)
Task M4.1: Social Embeds
Render Nostr notes, quoted posts inline in chat. Parse nostr: URI scheme.
Task M4.2: Search Improvements
Federated search across content types — unified search bar that queries films, songs, podcasts, web simultaneously.
Task M4.3: Bookmarks/Favorites
Save content items (films, songs, articles) to a local favorites list persisted in IndexedDB.
(Detailed specs to be written when M3 is complete)
M5: Security & Privacy (Infrastructure)
Task M5.1: E2E Encryption
Integrate tweetnacl.js for message encryption.
Task M5.2: Encrypted Storage
AES-256-GCM encryption layer over IndexedDB via Web Crypto API.
Task M5.3: API Key Vault
Encrypted storage for API keys with PBKDF2 key derivation.
(Detailed specs to be written when M4 is complete)
M6: Payments & Identity (UX + Infrastructure)
Task M6.1: Lightning Wallet Deep-links
Task M6.2: Cashu Token Support
Task M6.3: Nostr Identity (NIP-07)
(Detailed specs to be written when M5 is complete)
M7: Platform (Infrastructure)
Task M7.1: MCP Integration
Task M7.2: Multi-provider AI Normalization
Task M7.3: Tauri Desktop Build
Task M7.4: Offline Mode
(Detailed specs to be written when M6 is complete)
Part 3: Automated Session Execution Order
For automated late-night Claude sessions, execute tasks in this order:
Priority Queue (each session picks next incomplete task):
- Task 1.1 — Create PROGRESS.md (5 min)
- Task 1.2 — Create post-push progress hook (10 min)
- Task 1.3 — Register hook in settings.json (2 min)
- Task M1.2 — Error boundaries (15 min)
- Task M1.3 — Unit tests for contentExtraction (20 min)
- Task M1.1 — IndexedDB persistent storage (25 min)
- Task M1.4 — Unit tests for useAI (20 min)
- Task M2.1 — Markdown rendering (20 min)
- Task M1.5 — E2E test expansion (15 min)
- Task M2.3 — Music source resolution (20 min)
- Task M2.2 — Virtual scrolling (15 min)
- Task M2.4 — Nostr feed integration (25 min)
- Task M3.1 — Activate plugin registry (20 min)
- Task M3.2 — Renderer plugin registration (20 min)
Session Protocol
Each automated session should:
- Read
PROGRESS.mdto find the next incomplete task - Read this plan file for the task's detailed spec
- Execute the task following the spec exactly
- Run
pnpm typecheckafter changes - Run
pnpm lintafter changes - Run
pnpm testif unit tests exist - Commit with conventional format:
type(scope): description - Push to current branch
- Update PROGRESS.md session log (triggered by hook, or manually)
Verification
After implementing Part 1 (progress automation):
- Run
pnpm typecheck && pnpm lint— should pass - Commit a change and push — hook should fire
- Verify PROGRESS.md gets a session log entry
- Test from a different worktree — should work identically
After each M1–M3 task:
pnpm typecheckpassespnpm lintpassespnpm testpasses (if tests exist)- Dev server runs without errors (
pnpm dev) - Manual smoke test: send a message, see content render