Files
archy/PLAN.md
T

606 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```markdown
# 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:
```json
{
"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:
```ts
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`:
```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):
```ts
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**:
```ts
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**:
```ts
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`:
```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):
```ts
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.1: Lightning Wallet Deep-links
#### 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