Files
archy/PLAN.md
T

19 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 🔄
- [ ] Persistent storage (IndexedDB)
- [ ] Error boundaries
- [ ] Unit tests for composables
- [ ] E2E test coverage
- [ ] CI pipeline

(... remaining milestones from Part 2 ...)

## Session Log
<!-- Auto-populated by post-push hook -->

Task 1.2: Create post-push progress hook

File: .claude/hooks/post-push-progress.sh

A PostToolUse hook for Bash that:

  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

Render Nostr notes, quoted posts inline in chat. Parse nostr: URI scheme.

Task M4.2: Search Improvements

Federated search across content types — unified search bar that queries films, songs, podcasts, web simultaneously.

Task M4.3: Bookmarks/Favorites

Save content items (films, songs, articles) to a local favorites list persisted in IndexedDB.

(Detailed specs to be written when M3 is complete)


M5: Security & Privacy (Infrastructure)

Task M5.1: E2E Encryption

Integrate tweetnacl.js for message encryption.

Task M5.2: Encrypted Storage

AES-256-GCM encryption layer over IndexedDB via Web Crypto API.

Task M5.3: API Key Vault

Encrypted storage for API keys with PBKDF2 key derivation.

(Detailed specs to be written when M4 is complete)


M6: Payments & Identity (UX + Infrastructure)

Task M6.2: Cashu Token Support

Task M6.3: Nostr Identity (NIP-07)

(Detailed specs to be written when M5 is complete)


M7: Platform (Infrastructure)

Task M7.1: MCP Integration

Task M7.2: Multi-provider AI Normalization

Task M7.3: Tauri Desktop Build

Task M7.4: Offline Mode

(Detailed specs to be written when M6 is complete)


Part 3: Automated Session Execution Order

For automated late-night Claude sessions, execute tasks in this order:

Priority Queue (each session picks next incomplete task):

  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)

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