Files
archy/PLAN.md
T
DorianandClaude Opus 4.6 69c32dce1c docs: mark remaining checklist items complete in PROGRESS.md and PLAN.md
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>
2026-03-03 21:14:07 +00:00

35 KiB
Raw Blame History

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:

  1. Progress tracking automation — PROGRESS.md updated on every commit/push
  2. 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 (M0M7, 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:

  1. Reads stdin JSON, extracts tool_input.command
  2. Checks if command contains git push or git commit
  3. If match: runs git log --oneline for commits on current branch not on main
  4. Reads current PROGRESS.md roadmap section to identify active milestone
  5. Outputs JSON with hookSpecificOutput containing 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:

  1. Create idb-storage.ts with 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 updatedAt for sorted retrieval
  2. Modify chat.ts:

    • Replace saveServerChats() (line 51-62) with saveConversation() calls
    • Replace loadServerChats() (line 64-84) with loadAllConversations()
    • Keep dev-chats middleware as fallback when indexedDB unavailable
    • Debounced save stays at 800ms

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 typecheck passes

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 sections
  • packages/app/src/components/chat/ChatWindow.vue (248 lines) — wrap message list
  • packages/app/src/components/content/ContentPanel.vue — wrap detail views

Implementation:

  1. 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>
    
  2. Wrap in ChatPage.vue:

    • Wrap chat column (around <ChatWindow>)
    • Wrap content panel column (around <ContentPanel>)
    • Wrap detail view column (around <DetailView>)
  3. Wrap in ChatWindow.vue:

    • Wrap the message loop (v-for of ChatMessage)
  4. 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 typecheck passes

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 test exits 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 test exits 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:e2e passes (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-it as dependency

Implementation:

  1. pnpm add markdown-it + pnpm add -D @types/markdown-it in packages/app
  2. In ChatMessage.vue:
    • Import and configure markdown-it with safe defaults (no HTML)
    • After stripContentTags(), render remaining text through markdown-it
    • Use v-html with 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 typecheck passes

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-virtual dependency

Implementation:

  1. pnpm add @tanstack/vue-virtual in packages/app
  2. Replace the message v-for loop with useVirtualizer:
    • 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 typecheck passes

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:

  1. 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)
  2. 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 typecheck passes

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:

  1. 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 }
  2. Update NostrGrid.vue:

    • Use useNostr() composable
    • Display events as cards with author npub (truncated), content, timestamp
    • Lazy-load on tab activation only

Acceptance criteria:

  • Nostr tab shows real posts from public relays
  • Connection/disconnection is clean (no leaked WebSockets)
  • Handles relay errors gracefully
  • pnpm typecheck passes

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:

  1. Create plugins/index.ts:

    export async function initializePlugins() {
      // Register built-in plugins
      const { claudeProvider } = await import('./claude-provider')
      registerPlugin(claudeProvider)
    }
    
  2. Create plugins/claude-provider.ts:

    • Implement AIProviderAdapter interface from @aiui/core
    • Wrap existing useAI.ts streaming logic as a plugin
    • Export as a Tier 1 (trusted) plugin
  3. In main.ts:

    • Call initializePlugins() before app mount
    • Make it async with error handling

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 provider
  • pnpm typecheck passes

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:

  1. 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,
    }
    
  2. Register in plugins/index.ts via registerRenderer()

  3. 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 typecheck passes

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:

  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 <NostrEmbed> 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

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:

    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

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:

  1. Create favorites.ts Pinia 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
    
  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

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:

  1. 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
    
  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

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:

  1. 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'
    
  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)

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:

    // 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:

    // 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 <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 typecheck passes

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:

  1. 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
    }
    
  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 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:

    // 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:

    interface AIAdapter {
      id: string
      name: string
      chat(messages: Message[], options: ChatOptions): AsyncIterable<string>
      models(): Promise<Model[]>
      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

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:

    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>
    }
    
  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

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

  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

Each automated session should:

  1. Read PROGRESS.md to find the next incomplete task
  2. Read this plan file for the task's detailed spec
  3. Execute the task following the spec exactly
  4. Run pnpm typecheck after changes
  5. Run pnpm lint after changes
  6. Run pnpm test if unit tests exist
  7. Commit with conventional format: type(scope): description
  8. Push to current branch
  9. Update PROGRESS.md session log (triggered by hook, or manually)

Verification

After implementing Part 1 (progress automation):

  1. Run pnpm typecheck && pnpm lint — should pass
  2. Commit a change and push — hook should fire
  3. Verify PROGRESS.md gets a session log entry
  4. Test from a different worktree — should work identically

After each M1M3 task:

  1. pnpm typecheck passes
  2. pnpm lint passes
  3. pnpm test passes (if tests exist)
  4. Dev server runs without errors (pnpm dev)
  5. Manual smoke test: send a message, see content render