diff --git a/PLAN.md b/PLAN.md index b3cbbe12..ee8fdc99 100644 --- a/PLAN.md +++ b/PLAN.md @@ -40,12 +40,12 @@ Format: - [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 +### 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 ...) @@ -500,17 +500,119 @@ test('panel side toggle switches layout') #### Task M4.1: Social Embeds -Render Nostr notes, quoted posts inline in chat. Parse `nostr:` URI scheme. +**Why**: Nostr notes referenced in chat should render as rich embeds, not raw text. -#### Task M4.2: Search Improvements +**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 -Federated search across content types — unified search bar that queries films, songs, podcasts, web simultaneously. +**Implementation**: +1. Create `NostrEmbed.vue`: + - Accept `noteId` or `npub` prop + - Fetch note from relays (reuse `useNostr` composable 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 + +2. In ChatMessage.vue: + - Regex detect `nostr:note1...`, `nostr:npub1...`, `nostr:nevent1...` patterns + - Replace with `` component inline + - Handle bech32 decoding (NIP-19) for note/npub/nevent + +**Acceptance criteria**: +- `nostr:note1...` in chat renders as embedded card +- `nostr:npub1...` renders as profile card +- Graceful fallback if relay unreachable +- `pnpm typecheck` passes + +#### 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**: +1. Create `useFederatedSearch.ts`: + ```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 + } + ``` + +2. In ChatInput.vue: + - Add `/search` command prefix detection + - When typing after `/search`, show SearchResults overlay above input + - Selecting a result inserts it as a content reference in the message + +3. Create SearchResults.vue: + - Grouped by content type with type icons + - Keyboard navigation (arrow keys + enter) + - Glass morphism dropdown styling + +**Acceptance criteria**: +- `/search matrix` returns films, songs, articles matching "matrix" +- Results grouped by type +- Selecting a result works +- `pnpm typecheck` passes #### Task M4.3: Bookmarks/Favorites -Save content items (films, songs, articles) to a local favorites list persisted in IndexedDB. +**Why**: Users can't save interesting content items for later. -*(Detailed specs to be written when M3 is complete)* +**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**: +1. Create `favorites.ts` Pinia store: + ```ts + 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 + ``` + +2. Create `FavoriteButton.vue`: + - Heart icon toggle (outline = not saved, filled = saved) + - Animate on toggle (scale bounce) + - Bitcoin orange when favorited + +3. 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 + +4. 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 typecheck` passes --- @@ -518,38 +620,359 @@ Save content items (films, songs, articles) to a local favorites list persisted #### Task M5.1: E2E Encryption -Integrate tweetnacl.js for message encryption. +**Why**: Conversations stored in IndexedDB are plaintext. Need encryption at rest. -#### Task M5.2: Encrypted Storage +**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 -AES-256-GCM encryption layer over IndexedDB via Web Crypto API. +**Implementation**: +1. Create `crypto.ts`: + ```ts + // Use Web Crypto API (no external dependencies) + export async function deriveKey(password: string, salt: Uint8Array): Promise + // 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 + // AES-256-GCM decrypt + + export async function generateSalt(): Promise + // 16 random bytes + ``` + +2. 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 typecheck` passes +- 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**: +1. 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) + +2. 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 typecheck` passes #### Task M5.3: API Key Vault -Encrypted storage for API keys with PBKDF2 key derivation. +**Why**: API keys (Claude, OpenRouter) are currently stored in plaintext localStorage. -*(Detailed specs to be written when M4 is complete)* +**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**: +1. Create `key-vault.ts`: + ```ts + // Encrypted storage for API keys using crypto.ts + export async function storeApiKey(provider: string, key: string): Promise + export async function getApiKey(provider: string): Promise + export async function deleteApiKey(provider: string): Promise + export async function listProviders(): Promise + // Keys encrypted with session-derived key from passphrase + // Stored in dedicated IndexedDB object store: 'api-keys' + ``` + +2. Create `ApiKeyManager.vue`: + - List configured providers + - Add/remove API keys + - Keys masked in UI (show last 4 chars) + - Test connection button per provider + +3. 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 typecheck` passes --- ### 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**: +1. Create `lightning.ts`: + ```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 + ``` + +2. 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 + +3. Create `LightningInvoice.vue`: + - Display BOLT11 invoice as QR code (use `qrcode` lib or canvas) + - Show amount in sats + - Expiry countdown + - Copy button + +**Acceptance criteria**: +- Payment button generates valid Lightning URIs +- Deep-link opens system wallet picker on mobile +- QR fallback for desktop +- `pnpm typecheck` passes + #### 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**: +1. Create `cashu.ts`: + ```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 + ``` + +2. 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 + +3. In ChatMessage.vue: + - Regex detect Cashu tokens + - Replace inline with `` 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 typecheck` passes + #### Task M6.3: Nostr Identity (NIP-07) -*(Detailed specs to be written when M5 is complete)* +**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**: +1. Create `useNostrIdentity.ts`: + ```ts + // NIP-07: window.nostr API + export function useNostrIdentity() { + const isAvailable: Ref // window.nostr exists + const pubkey: Ref + const npub: Ref // bech32 encoded + async function login(): Promise // calls window.nostr.getPublicKey() + async function sign(event: NostrEvent): Promise // calls window.nostr.signEvent() + function logout(): void + } + ``` + +2. 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 typecheck` passes --- ### M7: Platform (Infrastructure) -#### Task M7.1: MCP Integration +#### 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**: +1. Create `mcp-server.ts`: + ```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 + ``` + +2. 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 typecheck` passes + #### 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**: +1. Create `types.ts`: + ```ts + interface AIAdapter { + id: string + name: string + chat(messages: Message[], options: ChatOptions): AsyncIterable + models(): Promise + supportsStreaming: boolean + supportsVision: boolean + supportsTools: boolean + } + ``` + +2. Create adapters for Claude (existing logic), OpenRouter (OpenAI-compatible), Ollama (local). + +3. 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 typecheck` passes + #### 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**: +1. Initialize Tauri in the app package: + - `pnpm add -D @tauri-apps/cli @tauri-apps/api` in 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 + +2. 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 dev` launches desktop app +- Glass morphism renders correctly with transparent window +- System tray works +- `pnpm tauri build` produces .dmg/.app +- `pnpm typecheck` passes + #### Task M7.4: Offline Mode -*(Detailed specs to be written when M6 is complete)* +**Why**: AIUI should work without internet for browsing saved content. + +**Files to create/modify**: +- Modify `packages/app/src/sw.ts` or PWA config — cache strategies +- Create `packages/app/src/composables/useOffline.ts` +- Modify UI components — offline indicators + +**Implementation**: +1. Create `useOffline.ts`: + ```ts + export function useOffline() { + const isOnline: Ref // navigator.onLine + event listeners + const pendingSync: Ref // count of items waiting to sync + function queueForSync(action: SyncAction): void + function processSyncQueue(): Promise + } + ``` + +2. 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 + +3. 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 typecheck` passes --- @@ -559,20 +982,43 @@ For automated late-night Claude sessions, execute tasks in this order: ### Priority Queue (each session picks next incomplete task): -1. **Task 1.1** — Create PROGRESS.md *(5 min)* -2. **Task 1.2** — Create post-push progress hook *(10 min)* -3. **Task 1.3** — Register hook in settings.json *(2 min)* -4. **Task M1.2** — Error boundaries *(15 min)* -5. **Task M1.3** — Unit tests for contentExtraction *(20 min)* -6. **Task M1.1** — IndexedDB persistent storage *(25 min)* -7. **Task M1.4** — Unit tests for useAI *(20 min)* -8. **Task M2.1** — Markdown rendering *(20 min)* -9. **Task M1.5** — E2E test expansion *(15 min)* -10. **Task M2.3** — Music source resolution *(20 min)* -11. **Task M2.2** — Virtual scrolling *(15 min)* -12. **Task M2.4** — Nostr feed integration *(25 min)* -13. **Task M3.1** — Activate plugin registry *(20 min)* -14. **Task M3.2** — Renderer plugin registration *(20 min)* +**M1: Stability & Polish** +1. **Task M1.2** — Error boundaries *(verify existing ErrorBoundary.vue, wrap remaining components)* +2. **Task M1.3** — Unit tests for contentExtraction *(create __tests__/contentExtraction.test.ts)* +3. **Task M1.1** — IndexedDB persistent storage *(create idb-storage.ts, modify chat.ts)* +4. **Task M1.4** — Unit tests for useAI *(create __tests__/useAI.test.ts with mocked fetch)* +5. **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 diff --git a/PROGRESS.md b/PROGRESS.md index f99d6dcc..4b8948aa 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -25,7 +25,7 @@ - [x] Unit tests — contentExtraction composable (10 extraction functions, 49 tests) - [x] IndexedDB persistent storage (conversations survive refresh) - [x] Unit tests — useAI composable (16 tests, mocked fetch/SSE) -- [ ] E2E test expansion (8 new tests: streaming, content cards, mobile, etc.) +- [x] E2E test expansion (8 new tests: streaming, content cards, mobile, etc.) ### M2: Content Experience ✅ - [x] Markdown rendering in chat (markdown-it, XSS safe)