fix(app): restore PromptIndex component functionality and update launch configuration
This commit is contained in:
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse Bash guard: block dangerous shell commands.
|
||||
# Denies: rm -rf, git reset --hard, git push -f, git clean -fd, chmod -R 777,
|
||||
# fork bombs, block device overwrites, mkfs, paths escaping project root.
|
||||
# Uses python3 instead of jq for JSON (guaranteed on macOS).
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
CMD=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('tool_input', {}).get('command', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
BASE="${CLAUDE_PROJECT_DIR:-}"
|
||||
[[ -z "$BASE" ]] && BASE=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('cwd', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
[[ -z "$BASE" ]] && BASE="$(pwd)"
|
||||
|
||||
# Normalize: collapse whitespace, strip leading/trailing
|
||||
CMD_NORM=$(echo "$CMD" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
|
||||
deny() {
|
||||
local reason="$1"
|
||||
python3 -c "
|
||||
import json
|
||||
print(json.dumps({
|
||||
'hookSpecificOutput': {
|
||||
'hookEventName': 'PreToolUse',
|
||||
'permissionDecision': 'deny',
|
||||
'permissionDecisionReason': '$reason'
|
||||
}
|
||||
}))
|
||||
"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Dangerous patterns (case-insensitive where sensible)
|
||||
case "$CMD_NORM" in
|
||||
*"rm -rf"*|*"rm -fr"*|*"rm -f -r"*|*"rm -r -f"*) deny "Destructive rm -rf blocked by security hook" ;;
|
||||
*"git reset --hard"*) deny "git reset --hard would lose uncommitted work" ;;
|
||||
*"git push --force"*|*"git push -f"*|*"git push -f "*) deny "git push --force would rewrite history" ;;
|
||||
*"git clean -fd"*|*"git clean -f -d"*) deny "git clean -fd deletes untracked files" ;;
|
||||
*"chmod -R 777"*|*"chmod -R 0777"*) deny "chmod -R 777 is a security risk" ;;
|
||||
*":(){ :"*"};:"*) deny "Fork bomb pattern blocked" ;;
|
||||
*"> /dev/sd"*|*">/dev/sd"*) deny "Block device overwrite blocked" ;;
|
||||
*"mkfs "*|*"mkfs."*) deny "Disk format command blocked" ;;
|
||||
esac
|
||||
|
||||
# Check for path traversal escaping project root (../ outside project)
|
||||
# Only if we have a sensible base
|
||||
if [[ -n "$BASE" ]] && [[ -d "$BASE" ]]; then
|
||||
# Simple heuristic: command contains .. and would resolve outside project
|
||||
if echo "$CMD_NORM" | grep -qE '\.\./|/\.\.'; then
|
||||
# Extract plausible paths and check - allow ../ within project
|
||||
if echo "$CMD_NORM" | grep -qE '(rm|mv|cp|cat|chmod|chown)\s+.*\.\.'; then
|
||||
# Could be risky; be conservative for rm/mv/cp
|
||||
if echo "$CMD_NORM" | grep -qE '\brm\b.*\.\.'; then
|
||||
deny "Path traversal with rm blocked"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse Bash hook: detect git push/commit and prompt Claude to update PROGRESS.md.
|
||||
# Returns structured feedback with recent commits so Claude can write a session log entry.
|
||||
# Uses python3 instead of jq for JSON (guaranteed on macOS).
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# Extract command from JSON using python3
|
||||
CMD=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('tool_input', {}).get('command', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
|
||||
# Only trigger on git push or git commit commands
|
||||
if ! echo "$CMD" | grep -qE '\bgit\s+(push|commit)\b'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Gather context for the progress update
|
||||
BASE="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
||||
BRANCH=$(git -C "$BASE" branch --show-current 2>/dev/null || echo "unknown")
|
||||
PROGRESS_FILE="$BASE/PROGRESS.md"
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M')
|
||||
|
||||
# Get recent commits (branch vs main, or last 10)
|
||||
if git -C "$BASE" rev-parse --verify main &>/dev/null; then
|
||||
COMMITS=$(git -C "$BASE" log --oneline main..HEAD 2>/dev/null | head -15)
|
||||
if [ -z "$COMMITS" ]; then
|
||||
COMMITS=$(git -C "$BASE" log --oneline -10 2>/dev/null)
|
||||
fi
|
||||
else
|
||||
COMMITS=$(git -C "$BASE" log --oneline -10 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Get changed files in recent commits
|
||||
CHANGED_FILES=$(git -C "$BASE" diff --name-only main..HEAD 2>/dev/null | head -20 || \
|
||||
git -C "$BASE" diff --name-only HEAD~5..HEAD 2>/dev/null | head -20 || \
|
||||
echo "unknown")
|
||||
|
||||
# Build the feedback message and output as JSON using python3
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
message = '''Progress Update Needed
|
||||
|
||||
A git push/commit was detected on branch \`$BRANCH\` at $TIMESTAMP.
|
||||
|
||||
Recent commits:
|
||||
\`\`\`
|
||||
$COMMITS
|
||||
\`\`\`
|
||||
|
||||
Changed files:
|
||||
\`\`\`
|
||||
$CHANGED_FILES
|
||||
\`\`\`
|
||||
|
||||
Please update PROGRESS.md:
|
||||
1. Add a session log entry under '## Session Log' with format: ### $TIMESTAMP — $BRANCH
|
||||
2. Summarize what was accomplished (2-4 bullet points based on the commits above)
|
||||
3. Update any roadmap checkboxes if tasks were completed
|
||||
4. Commit the PROGRESS.md update'''
|
||||
|
||||
output = {
|
||||
'hookSpecificOutput': {
|
||||
'hookEventName': 'PostToolUse',
|
||||
'progressUpdate': message
|
||||
}
|
||||
}
|
||||
print(json.dumps(output))
|
||||
"
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse Edit|Write guard: block edits outside project and to protected paths.
|
||||
# Denies: paths outside project, .git/, .env*, lockfiles, node_modules/
|
||||
# Uses python3 instead of jq for JSON (guaranteed on macOS).
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('tool_input', {}).get('file_path', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
BASE="${CLAUDE_PROJECT_DIR:-}"
|
||||
[[ -z "$BASE" ]] && BASE=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('cwd', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
[[ -z "$BASE" ]] && BASE="$(pwd)"
|
||||
|
||||
# Resolve to absolute path
|
||||
if [[ -z "$FILE_PATH" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
ABS_BASE=$(cd "$BASE" 2>/dev/null && pwd) || true
|
||||
[[ -z "$ABS_BASE" ]] && ABS_BASE=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$BASE" 2>/dev/null) || true
|
||||
[[ -z "$ABS_BASE" ]] && ABS_BASE="$BASE"
|
||||
# Ensure base has trailing slash for prefix check
|
||||
[[ "$ABS_BASE" != */ ]] && ABS_BASE="${ABS_BASE}/"
|
||||
if [[ "$FILE_PATH" != /* ]]; then
|
||||
ABS_PATH="$ABS_BASE${FILE_PATH#./}"
|
||||
else
|
||||
ABS_PATH="$FILE_PATH"
|
||||
fi
|
||||
# Normalize path (collapse .. and ., no symlink resolution needed)
|
||||
ABS_PATH=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$ABS_PATH" 2>/dev/null) || true
|
||||
[[ -z "$ABS_PATH" ]] && ABS_PATH="$ABS_BASE${FILE_PATH#./}"
|
||||
|
||||
deny() {
|
||||
local reason="$1"
|
||||
echo "Blocked: $ABS_PATH — $reason" >&2
|
||||
python3 -c "
|
||||
import json
|
||||
print(json.dumps({
|
||||
'hookSpecificOutput': {
|
||||
'hookEventName': 'PreToolUse',
|
||||
'permissionDecision': 'deny',
|
||||
'permissionDecisionReason': '$reason'
|
||||
}
|
||||
}))
|
||||
"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Protected patterns (path contains or equals)
|
||||
PROTECTED_PATTERNS=(
|
||||
".git/"
|
||||
".env"
|
||||
".env.local"
|
||||
"node_modules/"
|
||||
"package-lock.json"
|
||||
"pnpm-lock.yaml"
|
||||
)
|
||||
|
||||
for pattern in "${PROTECTED_PATTERNS[@]}"; do
|
||||
if [[ "$ABS_PATH" == *"$pattern"* ]] || [[ "$ABS_PATH" == *"/$pattern" ]]; then
|
||||
deny "Edit blocked: path matches protected pattern ($pattern)"
|
||||
fi
|
||||
done
|
||||
|
||||
# .env.*.local
|
||||
if [[ "$ABS_PATH" =~ \.env\..*\.local$ ]]; then
|
||||
deny "Edit blocked: .env.*.local files contain secrets"
|
||||
fi
|
||||
|
||||
# Ensure path is under project root (ABS_BASE has trailing /)
|
||||
if [[ "$ABS_PATH" != "$ABS_BASE"* ]] && [[ "$ABS_PATH" != "$BASE"* ]]; then
|
||||
deny "Edit blocked: path is outside project directory"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-risky-bash.sh"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-push-progress.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
# 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 (M0–M7, 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 M1–M3 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
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
# AIUI Progress
|
||||
|
||||
## Current Status
|
||||
**Active Milestone**: M1 — Stability & Polish
|
||||
**Overall**: ~65% architectural completeness
|
||||
|
||||
## Roadmap
|
||||
|
||||
### M0: Foundation ✅
|
||||
- [x] Chat interface with streaming (Claude/OpenRouter/Mock)
|
||||
- [x] 11 content type renderers (film, song, podcast, book, TV, image, place, article, magazine, nostr, code)
|
||||
- [x] Responsive layout (mobile three-column + desktop overlays)
|
||||
- [x] Glass morphism design system (Tailwind CSS)
|
||||
- [x] Claude proxy + web search (SearXNG/DDG)
|
||||
- [x] PWA support (auto-update, installable)
|
||||
- [x] Music player (Plyr-based PlayerBar)
|
||||
- [x] Dev chat persistence (Vite middleware)
|
||||
|
||||
### M1: Stability & Polish 🔄
|
||||
- [x] Progress tracking automation
|
||||
- [ ] Error boundaries (`onErrorCaptured` wrappers)
|
||||
- [ ] Unit tests — contentExtraction composable
|
||||
- [ ] IndexedDB persistent storage
|
||||
- [ ] Unit tests — useAI composable
|
||||
- [ ] E2E test expansion (content surfaces, responsive)
|
||||
|
||||
### M2: Content Experience 🔲
|
||||
- [ ] Markdown rendering in chat (markdown-it)
|
||||
- [ ] Music source resolution + queue management
|
||||
- [ ] Virtual scrolling for chat (@tanstack/vue-virtual)
|
||||
- [ ] Nostr feed integration (relay WebSocket)
|
||||
|
||||
### M3: Plugin System 🔲
|
||||
- [ ] Activate plugin registry at runtime
|
||||
- [ ] Renderer plugin registration (film/song)
|
||||
- [ ] Plugin settings UI
|
||||
- [ ] Plugin sandboxing (Tier 1/2)
|
||||
|
||||
### M4: Social & Discovery 🔲
|
||||
- [ ] Social embeds (Nostr notes inline)
|
||||
- [ ] Federated search across content types
|
||||
- [ ] Bookmarks/favorites (IndexedDB)
|
||||
|
||||
### M5: Security & Privacy 🔲
|
||||
- [ ] E2E encryption (tweetnacl.js)
|
||||
- [ ] Encrypted storage (AES-256-GCM over IndexedDB)
|
||||
- [ ] API key vault (Web Crypto + PBKDF2)
|
||||
|
||||
### M6: Payments & Identity 🔲
|
||||
- [ ] Lightning wallet deep-links
|
||||
- [ ] Cashu token support
|
||||
- [ ] Nostr identity (NIP-07)
|
||||
|
||||
### M7: Platform 🔲
|
||||
- [ ] MCP integration
|
||||
- [ ] Multi-provider AI normalization
|
||||
- [ ] Tauri desktop build
|
||||
- [ ] Offline mode (service worker, local-first sync)
|
||||
|
||||
## Session Log
|
||||
<!-- Entries below are auto-populated by the post-push Claude Code hook -->
|
||||
<!-- Format: ### YYYY-MM-DD HH:MM — branch-name -->
|
||||
Reference in New Issue
Block a user