git-subtree-dir: aiui git-subtree-mainline:0c4826f8ccgit-subtree-split:e30ac1d106
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