Check off E2E test expansion in PROGRESS.md and all M1 overview items in PLAN.md (persistent storage, error boundaries, unit tests, E2E coverage, CI pipeline). Zero unchecked items remain across all files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
35 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 ✅
- [x] Persistent storage (IndexedDB)
- [x] Error boundaries
- [x] Unit tests for composables
- [x] E2E test coverage
- [x] 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
Why: Nostr notes referenced in chat should render as rich embeds, not raw text.
Files to create/modify:
- Create
packages/app/src/components/chat/NostrEmbed.vue - Modify
packages/app/src/components/chat/ChatMessage.vue— detect and render nostr: URIs
Implementation:
-
Create
NostrEmbed.vue:- Accept
noteIdornpubprop - Fetch note from relays (reuse
useNostrcomposable from M2.4) - Display: author npub (truncated), content, timestamp, relay source
- Glass card styling matching existing design system
- Loading skeleton while fetching
- Error state if note not found
- Accept
-
In ChatMessage.vue:
- Regex detect
nostr:note1...,nostr:npub1...,nostr:nevent1...patterns - Replace with
<NostrEmbed>component inline - Handle bech32 decoding (NIP-19) for note/npub/nevent
- Regex detect
Acceptance criteria:
nostr:note1...in chat renders as embedded cardnostr:npub1...renders as profile card- Graceful fallback if relay unreachable
pnpm typecheckpasses
Task M4.2: Federated Search
Why: Search currently only queries web. Should search across all content types simultaneously.
Files to create/modify:
- Create
packages/app/src/composables/useFederatedSearch.ts - Modify
packages/app/src/components/chat/ChatInput.vue— add search mode - Create
packages/app/src/components/ui/SearchResults.vue
Implementation:
-
Create
useFederatedSearch.ts:interface SearchResult { type: 'film' | 'song' | 'podcast' | 'book' | 'article' | 'place' | 'web' title: string subtitle: string thumbnail?: string data: unknown // type-specific payload } export function useFederatedSearch() { // Search across: film library, song library, podcast library, web (DDG/SearXNG) // Return unified results sorted by relevance // Debounce input (150ms) // Cancel previous searches on new input } -
In ChatInput.vue:
- Add
/searchcommand prefix detection - When typing after
/search, show SearchResults overlay above input - Selecting a result inserts it as a content reference in the message
- Add
-
Create SearchResults.vue:
- Grouped by content type with type icons
- Keyboard navigation (arrow keys + enter)
- Glass morphism dropdown styling
Acceptance criteria:
/search matrixreturns films, songs, articles matching "matrix"- Results grouped by type
- Selecting a result works
pnpm typecheckpasses
Task M4.3: Bookmarks/Favorites
Why: Users can't save interesting content items for later.
Files to create/modify:
- Create
packages/app/src/stores/favorites.ts— Pinia store - Create
packages/app/src/components/ui/FavoriteButton.vue - Create
packages/app/src/components/content/FavoritesGrid.vue - Modify content card components (FilmCard, SongCard, etc.) — add favorite button
- Modify
packages/app/src/components/content/ContentPanel.vue— add Favorites tab
Implementation:
-
Create
favorites.tsPinia store:interface FavoriteItem { id: string type: 'film' | 'song' | 'podcast' | 'book' | 'tv' | 'place' | 'article' title: string data: unknown savedAt: number } // Persist to IndexedDB (reuse idb-storage from M1.1) // Methods: addFavorite, removeFavorite, isFavorited, getFavoritesByType -
Create
FavoriteButton.vue:- Heart icon toggle (outline = not saved, filled = saved)
- Animate on toggle (scale bounce)
- Bitcoin orange when favorited
-
Create
FavoritesGrid.vue:- Tab in ContentPanel showing all saved items
- Filter by content type
- Sort by date saved
- Remove from favorites via swipe or button
-
Add FavoriteButton to existing cards: FilmCard, SongCard, BookCard, etc.
Acceptance criteria:
- Can favorite/unfavorite any content item
- Favorites persist across page refresh (IndexedDB)
- Favorites tab shows all saved items
- Filter by type works
pnpm typecheckpasses
M5: Security & Privacy (Infrastructure)
Task M5.1: E2E Encryption
Why: Conversations stored in IndexedDB are plaintext. Need encryption at rest.
Files to create/modify:
- Create
packages/app/src/utils/crypto.ts - Modify
packages/app/src/utils/idb-storage.ts— encrypt before write, decrypt on read
Implementation:
-
Create
crypto.ts:// Use Web Crypto API (no external dependencies) export async function deriveKey(password: string, salt: Uint8Array): Promise<CryptoKey> // PBKDF2, 100K iterations, SHA-256 export async function encrypt(data: string, key: CryptoKey): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array }> // AES-256-GCM, random 12-byte IV export async function decrypt(ciphertext: ArrayBuffer, iv: Uint8Array, key: CryptoKey): Promise<string> // AES-256-GCM decrypt export async function generateSalt(): Promise<Uint8Array> // 16 random bytes -
Modify
idb-storage.ts:- Add optional encryption parameter to save/load functions
- When
VITE_DISABLE_CRYPTO=true(dev mode), skip encryption - Store salt alongside encrypted data
- Key derived from user passphrase (prompted on first use)
Acceptance criteria:
- Conversations encrypted in IndexedDB when crypto enabled
- Dev mode (
VITE_DISABLE_CRYPTO=true) bypasses encryption - Decryption with wrong passphrase fails gracefully
pnpm typecheckpasses- Unit tests for encrypt/decrypt round-trip
Task M5.2: Encrypted Storage Layer
Why: All IndexedDB data (conversations, favorites, settings) should use the encryption layer.
Files to modify:
- Modify
packages/app/src/stores/favorites.ts— use encrypted storage - Create
packages/app/src/components/ui/PassphraseDialog.vue - Modify
packages/app/src/main.ts— prompt for passphrase on startup
Implementation:
-
Create
PassphraseDialog.vue:- Modal dialog with passphrase input
- "Remember for this session" checkbox (holds key in memory)
- Create new / enter existing passphrase flow
- Glass card styling, min 16px font (no iOS zoom)
-
Wire encryption into all storage operations:
- Conversations (chat.ts store)
- Favorites (favorites.ts store)
- Future: settings, API keys
Acceptance criteria:
- First launch prompts for passphrase creation
- Subsequent launches prompt for passphrase entry
- Wrong passphrase shows error, does not corrupt data
- Session key held in memory (not persisted)
pnpm typecheckpasses
Task M5.3: API Key Vault
Why: API keys (Claude, OpenRouter) are currently stored in plaintext localStorage.
Files to create/modify:
- Create
packages/app/src/utils/key-vault.ts - Create
packages/app/src/components/settings/ApiKeyManager.vue - Modify
packages/app/src/composables/useAI.ts— read keys from vault
Implementation:
-
Create
key-vault.ts:// Encrypted storage for API keys using crypto.ts export async function storeApiKey(provider: string, key: string): Promise<void> export async function getApiKey(provider: string): Promise<string | null> export async function deleteApiKey(provider: string): Promise<void> export async function listProviders(): Promise<string[]> // Keys encrypted with session-derived key from passphrase // Stored in dedicated IndexedDB object store: 'api-keys' -
Create
ApiKeyManager.vue:- List configured providers
- Add/remove API keys
- Keys masked in UI (show last 4 chars)
- Test connection button per provider
-
In
useAI.ts:- Replace direct env var / localStorage reads with vault lookups
- Fallback to env vars for dev mode
Acceptance criteria:
- API keys encrypted at rest
- Keys never appear in console/logs
- UI shows masked keys
- Test connection verifies key works
pnpm typecheckpasses
M6: Payments & Identity (UX + Infrastructure)
Task M6.1: Lightning Wallet Deep-links
Why: AIUI is Bitcoin-only. Need to deep-link to external Lightning wallets for payments.
Files to create/modify:
- Create
packages/app/src/utils/lightning.ts - Create
packages/app/src/components/ui/PaymentButton.vue - Create
packages/app/src/components/ui/LightningInvoice.vue
Implementation:
-
Create
lightning.ts:// Generate LNURL-pay links, BIP21 URIs, Lightning: URIs export function createLightningUri(invoice: string): string export function createBip21Uri(address: string, amount?: number, label?: string): string export function detectWallet(): 'strike' | 'muun' | 'phoenix' | 'zeus' | 'generic' // Deep-link formats: lightning:BOLT11, bitcoin:?lightning=BOLT11 -
Create
PaymentButton.vue:- Bitcoin orange gradient button
- Shows sat amount
- On click: generates deep-link URI, opens wallet
- Fallback: show QR code with invoice string
- Copy invoice to clipboard button
-
Create
LightningInvoice.vue:- Display BOLT11 invoice as QR code (use
qrcodelib or canvas) - Show amount in sats
- Expiry countdown
- Copy button
- Display BOLT11 invoice as QR code (use
Acceptance criteria:
- Payment button generates valid Lightning URIs
- Deep-link opens system wallet picker on mobile
- QR fallback for desktop
pnpm typecheckpasses
Task M6.2: Cashu Token Support
Why: Cashu ecash tokens enable offline micropayments. Display and copy Cashu tokens in chat.
Files to create/modify:
- Create
packages/app/src/utils/cashu.ts - Create
packages/app/src/components/chat/CashuToken.vue - Modify
packages/app/src/components/chat/ChatMessage.vue— detect cashu tokens
Implementation:
-
Create
cashu.ts:// Parse Cashu token format (cashuA...) export function parseCashuToken(token: string): { mint: string; amount: number; unit: string } | null export function isCashuToken(text: string): boolean // No wallet functionality — AIUI is never a wallet // Just parse, display, and deep-link to external wallet -
Create
CashuToken.vue:- Detect
cashuA...strings in chat messages - Display as card: amount, mint URL (truncated), copy button
- "Open in wallet" deep-link button
- Glass card styling with Bitcoin orange accent
- Detect
-
In ChatMessage.vue:
- Regex detect Cashu tokens
- Replace inline with
<CashuToken>component
Acceptance criteria:
- Cashu tokens in chat render as rich cards
- Copy token to clipboard works
- Deep-link to wallet works
- Invalid tokens show graceful fallback
pnpm typecheckpasses
Task M6.3: Nostr Identity (NIP-07)
Why: Enable login via Nostr browser extension (nos2x, Alby, etc.) for identity.
Files to create/modify:
- Create
packages/app/src/composables/useNostrIdentity.ts - Create
packages/app/src/components/settings/NostrLogin.vue - Modify
packages/app/src/stores/— add user identity store
Implementation:
-
Create
useNostrIdentity.ts:// NIP-07: window.nostr API export function useNostrIdentity() { const isAvailable: Ref<boolean> // window.nostr exists const pubkey: Ref<string | null> const npub: Ref<string | null> // bech32 encoded async function login(): Promise<void> // calls window.nostr.getPublicKey() async function sign(event: NostrEvent): Promise<NostrEvent> // calls window.nostr.signEvent() function logout(): void } -
Create
NostrLogin.vue:- "Login with Nostr" button (purple/Nostr brand color)
- Shows npub when logged in (truncated with copy)
- Logout button
- Detects if NIP-07 extension is installed
Acceptance criteria:
- Login with nos2x/Alby extension works
- Public key displayed as npub
- Sign events for Nostr posting
- Graceful message if no extension installed
pnpm typecheckpasses
M7: Platform (Infrastructure)
Task M7.1: MCP Server Integration
Why: Model Context Protocol enables rich tool use. AIUI should expose content surfaces as MCP tools.
Files to create/modify:
- Create
packages/app/src/plugins/mcp-server.ts - Modify
packages/app/src/composables/useAI.ts— add MCP tool handling
Implementation:
-
Create
mcp-server.ts:// Expose AIUI capabilities as MCP tools const tools = [ { name: 'search_films', description: 'Search film library', inputSchema: {...} }, { name: 'search_songs', description: 'Search song library', inputSchema: {...} }, { name: 'search_web', description: 'Search the web', inputSchema: {...} }, { name: 'get_nostr_feed', description: 'Fetch Nostr notes', inputSchema: {...} }, ] // Handle tool_use responses from AI and route to appropriate composable -
In useAI.ts:
- Parse tool_use blocks from Claude responses
- Route to appropriate handler (film search, web search, etc.)
- Return tool results back in conversation
Acceptance criteria:
- Claude can call tools via MCP format
- Tool results display as content in panel
pnpm typecheckpasses
Task M7.2: Multi-provider AI Normalization
Why: Different AI providers (Claude, OpenRouter, Ollama) have different APIs. Normalize them.
Files to create/modify:
- Create
packages/app/src/adapters/claude-adapter.ts - Create
packages/app/src/adapters/openrouter-adapter.ts - Create
packages/app/src/adapters/ollama-adapter.ts - Create
packages/app/src/adapters/types.ts— unified interface - Modify
packages/app/src/composables/useAI.ts— use adapter pattern
Implementation:
-
Create
types.ts:interface AIAdapter { id: string name: string chat(messages: Message[], options: ChatOptions): AsyncIterable<string> models(): Promise<Model[]> supportsStreaming: boolean supportsVision: boolean supportsTools: boolean } -
Create adapters for Claude (existing logic), OpenRouter (OpenAI-compatible), Ollama (local).
-
Refactor useAI.ts to select adapter by provider setting.
Acceptance criteria:
- Can switch between Claude/OpenRouter/Ollama
- Streaming works with all providers
- Content extraction works regardless of provider
pnpm typecheckpasses
Task M7.3: Tauri Desktop Build
Why: Desktop app via Tauri for native experience with system tray, global shortcuts.
Files to create/modify:
- Create
src-tauri/directory with Tauri config - Create
src-tauri/tauri.conf.json - Create
src-tauri/src/main.rs - Modify
packages/app/package.json— add tauri scripts
Implementation:
-
Initialize Tauri in the app package:
pnpm add -D @tauri-apps/cli @tauri-apps/apiin packages/app- Configure window: frameless with custom titlebar, transparent background
- System tray with quick-access menu
- Global shortcut (Cmd+Shift+A) to show/hide window
-
Tauri config:
- Window size: 1200x800, min 800x600
- Transparent background (for glass morphism)
- Auto-updater enabled
- File system scope: app data directory only
Acceptance criteria:
pnpm tauri devlaunches desktop app- Glass morphism renders correctly with transparent window
- System tray works
pnpm tauri buildproduces .dmg/.apppnpm typecheckpasses
Task M7.4: Offline Mode
Why: AIUI should work without internet for browsing saved content.
Files to create/modify:
- Modify
packages/app/src/sw.tsor PWA config — cache strategies - Create
packages/app/src/composables/useOffline.ts - Modify UI components — offline indicators
Implementation:
-
Create
useOffline.ts:export function useOffline() { const isOnline: Ref<boolean> // navigator.onLine + event listeners const pendingSync: Ref<number> // count of items waiting to sync function queueForSync(action: SyncAction): void function processSyncQueue(): Promise<void> } -
PWA cache strategies:
- App shell: cache-first (HTML, JS, CSS, fonts)
- API responses: network-first with cache fallback
- Images: cache-first with stale-while-revalidate
- IndexedDB data: always available offline
-
UI indicators:
- Subtle banner when offline ("Offline — browsing saved content")
- Disable AI chat input when offline (grey out with tooltip)
- Show cached content (favorites, saved conversations)
Acceptance criteria:
- App loads without internet
- Saved conversations and favorites accessible offline
- Chat disabled with clear offline indicator
- Reconnection triggers sync
pnpm typecheckpasses
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):
M1: Stability & Polish
- Task M1.2 — Error boundaries (verify existing ErrorBoundary.vue, wrap remaining components)
- Task M1.3 — Unit tests for contentExtraction (create tests/contentExtraction.test.ts)
- Task M1.1 — IndexedDB persistent storage (create idb-storage.ts, modify chat.ts)
- Task M1.4 — Unit tests for useAI (create tests/useAI.test.ts with mocked fetch)
- Task M1.5 — E2E test expansion (8 new tests in content-surfaces.spec.ts)
M2: Content Experience 6. Task M2.1 — Markdown rendering in chat (add markdown-it, modify ChatMessage.vue) 7. Task M2.3 — Music source resolution + queue (fix usePlayer.ts, update PlayerBar.vue) 8. Task M2.2 — Virtual scrolling for chat (add @tanstack/vue-virtual to ChatWindow.vue) 9. Task M2.4 — Nostr feed integration (create useNostr.ts, update NostrGrid.vue)
M3: Plugin System 10. Task M3.1 — Activate plugin registry (create plugins/index.ts, claude-provider.ts) 11. Task M3.2 — Renderer plugin registration (film/song renderer plugins)
M4: Social & Discovery 12. Task M4.1 — Social embeds (NostrEmbed.vue, nostr: URI detection in chat) 13. Task M4.2 — Federated search (useFederatedSearch.ts, /search command) 14. Task M4.3 — Bookmarks/favorites (favorites.ts store, FavoriteButton, FavoritesGrid)
M5: Security & Privacy 15. Task M5.1 — E2E encryption (crypto.ts with Web Crypto API AES-256-GCM) 16. Task M5.2 — Encrypted storage layer (PassphraseDialog, wire encryption to all stores) 17. Task M5.3 — API key vault (key-vault.ts, ApiKeyManager.vue)
M6: Payments & Identity 18. Task M6.1 — Lightning wallet deep-links (lightning.ts, PaymentButton, LightningInvoice) 19. Task M6.2 — Cashu token support (cashu.ts, CashuToken.vue inline in chat) 20. Task M6.3 — Nostr identity NIP-07 (useNostrIdentity.ts, NostrLogin.vue)
M7: Platform 21. Task M7.1 — MCP server integration (mcp-server.ts, tool routing in useAI) 22. Task M7.2 — Multi-provider AI normalization (adapter pattern for Claude/OpenRouter/Ollama) 23. Task M7.3 — Tauri desktop build (src-tauri config, transparent window, system tray) 24. Task M7.4 — Offline mode (useOffline.ts, cache strategies, offline UI indicators)
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