Compare commits

..
Author SHA1 Message Date
archipelago 7ba3109b6d Add 'aiui/' from commit 'e30ac1d1069532fb6d652d87e2d4a2fe9d1b4773'
git-subtree-dir: aiui
git-subtree-mainline: 0c4826f8cc
git-subtree-split: e30ac1d106
2026-08-03 15:07:11 -04:00
DorianandClaude Opus 5 e30ac1d106 feat(app): delegate chat to Archy's node-side assistant loop when embedded (Archy phase 13, D-01/D-17)
Adds archyBridge.sendChat(text) built on the existing postToParent +
origin-validated listener pattern (same request-id correlation as
requestContext), with a 180s timeout matching the node's
ASSISTANT_HTTP_TIMEOUT. Adds useAI.ts's streamViaArchy, which branches all
three existing send sites on the same __AIUI_EMBEDDED__ signal useArchy.ts
already reads: embedded mode delegates the model call, the tool-calling
loop and the model key to the node; standalone mode is untouched and keeps
using streamClaude/streamOpenRouter with AIUI's own dev proxy (D-17).

CLAUDE_PATH/OPENROUTER_PATH are not removed — 13-02 changes what those
paths resolve to on a node, 13-09 retires them.

Verified: vitest run 332/335 passing (3 pre-existing failures confirmed via
a scratch worktree at the prior HEAD, unrelated to this change — seed
extraction count assertions and a web-search-integration body.webSearch
assertion); vue-tsc --noEmit clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 14:38:19 -04:00
DorianandClaude Fable 5 6e8b96d135 fix(app): embed round-trip fixes — origin, dark bg, key fallback, CLI path
Four issues found via live testing of the embedded Chat/AIUI panel
(Archipelago phase 02-07 follow-up), each root-caused rather than
patched over:

1. Loading overlay never dismissed: archyBridge.init() used
   window.location.origin (this iframe's OWN origin) as the target for
   postMessage calls TO the parent, instead of the parent's actual
   origin. Silently correct only when AIUI is served same-origin as
   its host (production's /aiui/ proxy) — broken the moment AIUI runs
   on a different origin than its embedding page (any dev setup with a
   separate AIUI dev server). The 'ready' message, and every
   permissions/theme/context/action response after it, was being
   dropped by the browser. Fixed by deriving the parent's real origin
   from document.referrer (archyBridge.ts).

2. White/black background instead of the branded look: initTheme()
   decides light/dark from localStorage or the OS's prefers-color-
   scheme, with no awareness of being embedded — App.vue now forces
   dark immediately on mount when embedded (before any handshake
   completes) and useArchy.ts's theme-update callback now applies
   Archy's reported mode too. Separately, body had no background-color
   at all, so ChatPage.vue's embedded `background: transparent` fell
   through to the browser's white UA default; main.css now paints body
   to match the active theme. And ChatPage.vue's embedded branch was
   opting out of the same background-image treatment the standalone
   dark app uses — it now shares that exact styling instead of a flat
   fallback color, matching the standalone look precisely.

3. Dead end when no AI provider credential is available: useAI.ts now
   emits a narrow, one-shot needsApiKey signal (401/403, "api key",
   "unauthorized", or a proxy-unreachable failure — deliberately not
   every transient error) that ChatWindow.vue watches to auto-open
   Settings, so the user lands on the fix instead of a silent/dead
   chat.

4. claude-proxy.ts's CLI fallback spawned a hardcoded ~/.local/bin/claude
   path, breaking with ENOENT on any machine where the CLI lives
   elsewhere (e.g. an nvm install). Now resolves via `command -v claude`
   first (an optional CLAUDE_BIN env override, then the historical path,
   then the bare command name as a last resort so spawn() itself can
   still try PATH), and surfaces an actionable in-UI error naming three
   ways to fix it when none resolve.

Verified: full send→spawn→response round trip against the local proxy
(both directly and through vite's /api/claude proxy), vue-tsc clean,
vitest 332/335 passing (3 pre-existing unrelated failures, confirmed
present before this commit too), production build clean with both
chatExpanded/mobileChat flags and the new background rule present in
the built assets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 20:04:45 -04:00
DorianandClaude Fable 5 900c0b9060 feat(app): honor Archipelago D-14 embed defaults via query params
Archipelago's Chat tab (neode-ui) embeds AIUI in an iframe and now
passes two presentation-only query params on the embed URL:

- ?chatExpanded — chat starts in the full message list rather than
  the collapsed prompt-index. chatCollapsed's initial ref now checks
  for this param before falling back to the existing localStorage
  default; never written back to localStorage, so the standalone
  app's own persisted preference is untouched.
- ?mobileChat — on mobile, ChatPage opens on the chat tab rather than
  whatever tab survived from a prior mount of the module-singleton
  content-panel state. mobileTab already defaulted to 'chat'; this
  just re-asserts it once on mount when the param is present, so it
  doesn't interfere with the hasDetailOpen/panelOpen watchers driving
  normal tab switching in response to user taps afterward.

Both params are read directly from window.location.search and are
no-ops when absent, so the standalone (non-embedded) app and its
existing desktop layout are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 18:17:32 -04:00
DorianandClaude Opus 4.6 91763246c3 chore(app): add hardening plan for overnight TUI loop execution
12-phase plan covering security fixes, proxy hardening, state bugs,
content extraction, cache bounds, accessibility, and test coverage.
42 atomic tasks structured for sequential agent execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 00:07:43 +00:00
DorianandClaude Opus 4.6 cb22131909 feat(app): apps tab, archy mock data, prompt palette, code browser, detail keys, guide routing
- Add [[app_ext:...]] tag format and rewrite extractApps() for reliable app extraction
- Wire AppsGrid and RecipeGrid into ContentGridView (was missing on wide desktop)
- Add mock Archy node data for standalone dev testing (VITE_MOCK_ARCHY=true)
- Fix PromptPalette: z-50 + opaque bg so slash menu renders above chat content
- Fix detail banner not updating: add :key to all detail components in ContentPanel
- Guide page moved to /guide, chat is now root route, guide auto-selected on first load
- Code browser: click opens file in viewer, separate checkbox for chat context selection
- Restore folder context selector (round checkbox on hover) in FileTreeNode
- Demo projects for prod deployment instead of hardcoded personal paths
- Improve Archy context injection with media breakdown and better error logging
- Add 11 Claude Code skills for efficient development workflows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:39:41 +00:00
DorianandClaude Opus 4.6 cc875d1c43 fix(app): update haiku model ID from retired claude-3-5-haiku to claude-haiku-4-5
The old claude-3-5-haiku-20241022 model ID returns 404 from the Anthropic API.
Updated proxy mapping and test to use claude-haiku-4-5-20251001.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:45:48 +00:00
DorianandClaude Opus 4.6 dc4c80a08b feat(app): add recipe content panel with grid and detail views
- Add RecipeGrid with search, meta badges (time/servings/calories), ingredient preview
- Add RecipeDetail with ingredient checklist, scaling slider, numbered steps
- Add generateRecipeFallback SVG for recipe cards (text-only style)
- Wire recipe extraction into content panel tab system
- Add isRecipeQuery/isRecipeLikeResponse classifiers in contentFiltering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:27:20 +00:00
DorianandClaude Opus 4.6 2304e5904e fix(app): fix image fallbacks across all content grids, refactor into useContentImages
- Create useContentImages<T> composable eliminating ~240 lines of duplicated
  image loading/fallback logic across 6 grid components
- Fix book covers: use full fetchBookImage chain (Open Library → Google Books → Wikipedia)
- Fix TV series images: try disambiguated Wikipedia title first (e.g. "Chernobyl (TV series)")
- Add Wikipedia image fetching for places (fetchPlaceImage)
- Rewrite all SVG fallbacks to consistent text-only style (no icons)
- Add generateWebsiteFallback for NewsGrid websites variant
- Fix song extraction regex catching raw song_ext: prefix in titles
- Fix player bar: clean song_ext: prefix from display, mute error text
- Fix Code panel: auto-load projects on mount when list is empty
- Improve chat bubble spacing (py-2.5) and first message top margin (pt-6)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:55:40 +00:00
DorianandClaude Opus 4.6 1eaf30ae12 fix(app): fix broken dev server after security hardening
- Fix dev.sh unbound variable crash with ${VITE_DEV_API_TOKEN:-}
- Kill stale proxy on startup instead of skipping (token mismatch)
- Fix RSS middleware blocking all GET requests (check path before auth)
- Read dev auth token lazily from process.env (not cached at import)
- Restore network binding (host: true) for Vite dev server
- Add macOS keychain lookup for Claude Code OAuth token in proxy
- Rewrite proxy streaming to pipe SSE directly instead of await json()
- Prevent double web search (client-side + proxy) in useAI
- Reduce SearXNG timeout 6s→3s and max tries 8→3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 03:09:06 +00:00
DorianandClaude Opus 4.6 e97c8f36ac test: update tests for security fixes (origin validation, streaming state)
- archyIntegration: expect window.location.origin instead of '*' for
  postMessage calls (matches FIX-009 origin restriction)
- useAI: fix flaky isStreaming assertion to account for background
  fetch calls (refreshWavlakeCatalog) captured before streaming starts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:42:50 +00:00
DorianandClaude Opus 4.6 9adeab9420 fix(app): validate content pack URL scheme and schema in importFromUrl
Require https: protocol for remote content pack imports, rejecting
http:, file:, javascript:, and other schemes. Add schema validation
to verify required fields (id, name, items) and item shape (type,
title) before accepting imported packs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:39:26 +00:00
DorianandClaude Opus 4.6 fa3c446baf fix(app): add 1MB body size limit to Claude proxy
Track accumulated body size during req.on('data') and abort with 413
if it exceeds 1MB, preventing unbounded memory allocation from
oversized payloads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:38:39 +00:00
DorianandClaude Opus 4.6 6d6408e2a4 fix(app): bind Vite dev server to localhost only
Remove --host flag from dev.sh that was overriding vite.config.ts to
bind on 0.0.0.0. Server now defaults to localhost; use VITE_HOST env
var to opt-in to LAN access for mobile testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:38:01 +00:00
DorianandClaude Opus 4.6 6720711302 fix(app): remove plaintext fallback from key vault when crypto unavailable
storeApiKey() now throws when encryption is not available instead of
storing keys in plaintext. ApiKeyManager.vue catches the error and
displays a warning message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:35:48 +00:00
DorianandClaude Opus 4.6 5c4afe00e5 fix(app): add rate limiting to all API endpoints
Add sliding-window rate limiter in server/dev-auth.ts (60 req/min reads,
10 req/min writes per IP). Apply checkRateLimit() in all Vite plugins
and claude-proxy.ts after auth validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:34:41 +00:00
DorianandClaude Opus 4.6 6425b2f53b fix(app): restrict postMessage origin in archyBridge
Change default allowedOrigin from '*' to null. Derive from
window.location.origin when init() is called without explicit origin.
Always validate event.origin — reject messages when origin is not set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:31:45 +00:00
DorianandClaude Opus 4.6 4053eb46af fix(app): replace custom HTML sanitizer with DOMPurify
Install dompurify and replace the hand-rolled DOM walker sanitizer
with DOMPurify.sanitize() configured with the same allowed tags.
Handles mutation XSS edge cases the custom version couldn't.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:30:58 +00:00
DorianandClaude Opus 4.6 f7f8c140c3 fix(app): store NWC wallet secret in encrypted vault instead of plaintext localStorage
Replace localStorage.getItem/setItem with storeApiKey/getApiKey/deleteApiKey
from key-vault. Make loadConnection(), connect(), and disconnect() async.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:28:29 +00:00
DorianandClaude Opus 4.6 c3e58fdab8 fix(app): add body size limit to dev-chats write endpoint
Track accumulated body length during PUT /api/dev-chats and abort
with 413 if payload exceeds 5MB.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:27:35 +00:00
DorianandClaude Opus 4.6 956e98b041 fix(app): add SSRF protection to RSS fetcher
Add post-DNS SSRF validation using dns.lookup() to verify resolved IPs
are not in private ranges. Block non-http(s) schemes (file://, ftp://)
in discoverFeedUrl(). Extract isPrivateIp() helper for reuse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:27:08 +00:00
DorianandClaude Opus 4.6 c21939b1f8 fix(app): block sensitive file reads in filesystem API
Add SENSITIVE_PATTERNS denylist to handleRead() in vite-fs.ts.
Blocks access to .env*, .git/, credentials, secrets, .pem, .key,
and SSH key files. Returns 403 for matched paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:26:01 +00:00
DorianandClaude Opus 4.6 4dc9588c8a fix(app): replace CORS Access-Control-Allow-Origin * with explicit localhost origin
Add setCorsHeaders() and handleCorsOptions() helpers in server/dev-auth.ts.
Replace wildcard CORS origin with http://localhost:5173 in all Vite plugins
and claude-proxy.ts. Include Authorization in allowed CORS headers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:25:10 +00:00
DorianandClaude Opus 4.6 cc7d9fc19e fix(app): add dev server auth token to all API endpoints
Generate random VITE_DEV_API_TOKEN in dev.sh, validate Bearer token
in shared server/dev-auth.ts middleware. Applied to all Vite plugins
(fs, dev-chats, rss, web-search, tmdb, music-search) and claude-proxy.
Client-side uses apiFetch() wrapper to attach the token automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:23:17 +00:00
DorianandClaude Opus 4.6 b77c93607a feat(app): video player, guide page, free films, PWA cache fix
- Add VideoPlayerOverlay component for free film playback
- Add GuidePage with interactive node setup walkthrough
- Add freeFilms data catalog with public domain films
- Enhance PlayerBar with video support and queue management
- Add video player store for overlay state management
- Refactor music search plugin (Jamendo integration cleanup)
- Add PWA cache version purge mechanism in main.ts
- Add PWA icon cache fix skill for Brave/Chrome
- Improve content grids: loading states, image fallbacks
- Enhance useArchy composable with node context
- Update useNostr with relay pool management
- Expand chat store with guide conversation support
- Add test fixtures for guide and node demo prompts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:56:39 +00:00
DorianandClaude Opus 4.6 c84c0fb424 fix(app): passphrase dialog — remove CSP meta, check crypto.subtle availability
- Remove CSP meta tag from index.html (breaks Vite HMR, should be
  set via HTTP headers in production nginx instead)
- isCryptoEnabled() now checks crypto.subtle is available (undefined
  over HTTP on non-localhost origins)
- Add try/catch + error feedback to passphrase submit flow
- PassphraseDialog accepts error prop, focuses input on visible

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 06:58:55 +00:00
DorianandClaude Opus 4.6 3620b1f4d5 chore: mark all overnight plan tasks complete
All 35 tasks across 8 phases completed:
P1 (Critical Fixes), P2 (Error Handling), P3 (Security),
P4 (Tests), P5 (Features), P6 (Accessibility),
P7 (Performance), P8 (Research).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:57:17 +00:00
DorianandClaude Opus 4.6 4ac18cedb6 docs: add research docs for iOS app, Mac desktop, plugin security
- iOS: Capacitor vs WKWebView vs React Native WebView analysis
- Mac: Tauri v2 vs Electron comparison with menu bar app patterns
- Plugins: Signature validation, sandboxed iframes, permission system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:56:54 +00:00
DorianandClaude Opus 4.6 290aa047d7 perf(app): add file size guard and loading state to code file reader
Handles 413 status for files > 1MB with user-friendly error message.
Adds fileLoading and fileError state for loading indicator support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:51:10 +00:00
DorianandClaude Opus 4.6 04db18c35a perf(app): add 30s cache TTL to Bitcoin price fetcher
Skips redundant API calls when price was fetched within the last 30
seconds, reducing network requests while keeping data fresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:49:19 +00:00
DorianandClaude Opus 4.6 9032e8cd60 perf(app): lazy load PdfViewer and MapRenderer with loading skeletons
Uses defineAsyncComponent to lazy load heavy renderers, reducing
initial bundle size. Shows loading text while components load.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:48:39 +00:00
DorianandClaude Opus 4.6 31241d8d12 fix(a11y): improve img alt text with artist/director/author context
Updates alt attributes to include contextual info: songs include artist,
films include year and director, books include author, TV series include
type label.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:47:35 +00:00
DorianandClaude Opus 4.6 91dbaee38c fix(a11y): color contrast audit — document ratios, fix critical text
Documents WCAG AA contrast ratios in main.css. Increases text-white/40
to /50 for settings labels, section headers, and loading states to
meet 4.5:1 minimum contrast ratio.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:46:27 +00:00
DorianandClaude Opus 4.6 3258c047d9 fix(a11y): add focus management and ARIA attributes to dialogs
Adds role="dialog", aria-modal, focus trap, auto-focus close button,
and Escape key handling to ZapDialog and SettingsModal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:44:11 +00:00
DorianandClaude Opus 4.6 3c709c143f fix(a11y): add aria-labels to all content grid card buttons
Adds descriptive aria-label to card buttons in SongGrid, FilmGrid,
TVSeriesGrid, PlaceGrid, BookGrid, PodcastGrid, NewsGrid, ImageGrid,
and AppsGrid for screen reader accessibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:41:43 +00:00
DorianandClaude Opus 4.6 2011fdcae0 fix(a11y): add aria-labels to chat message action buttons
Adds aria-label to all 6 icon-only buttons in ChatMessage.vue:
edit, regenerate, reply, branch, upvote, downvote.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:39:00 +00:00
DorianandClaude Opus 4.6 b4b1f8faf5 docs(app): add Archy local search guide and HelpSection component
Documents how file types map to content surfaces, how ContextBroker
filtering works, and adds a reusable HelpSection UI component.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:37:51 +00:00
DorianandClaude Opus 4.6 a60faedc48 feat(app): allow .claude folder in file browser tree
Updates vite-fs tree walker to include .claude directories so users
can browse CLAUDE.md, settings, hooks, and memory files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:36:14 +00:00
DorianandClaude Opus 4.6 e33cb359a0 feat(app): add file browser page with tree navigation and preview
Adds /browse route with project listing, recursive file tree with
expand/collapse, and file preview sidebar (desktop) / overlay (mobile).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:35:15 +00:00
DorianandClaude Opus 4.6 cfec1dcc13 fix(test): use type assertions for partial test data in useContentPanel tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:27:35 +00:00
DorianandClaude Opus 4.6 5d8ce56b1b test(app): add seed conversation regression tests
Validates extraction pipeline against all seed prompts. 15 tests
covering films, songs, books, TV, places, podcasts, images, code,
recipes, events. Verifies tag stripping completeness.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:25:07 +00:00
DorianandClaude Opus 4.6 5c82a09e65 test(app): add content extraction edge case tests
Tests empty input, interleaved tags, malformed tags, unicode content,
duplicate deduplication, place/TV extraction, tag stripping, magazine
sections. 11 test cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:23:49 +00:00
DorianandClaude Opus 4.6 c6e384a288 test(app): add unit tests for useVisualViewport composable
Tests keyboard detection, viewport height tracking, debounce behavior.
Includes withSetup test helper for composables with lifecycle hooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:22:02 +00:00
DorianandClaude Opus 4.6 1a9b769e45 test(app): add unit tests for useContentPanel composable
Tests tab switching, detail open/close, panel state management,
design system mode. 9 test cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:21:12 +00:00
DorianandClaude Opus 4.6 7c8315f362 test(app): add unit tests for usePlayer composable
Tests queue management, state transitions, progress computation,
deduplication, and API shape. 9 test cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:20:27 +00:00
DorianandClaude Opus 4.6 ece4540388 feat(app): add Content-Security-Policy meta tag
Restricts script, style, img, connect, media, and frame sources to
known-safe origins. Blocks object embeds and enforces base-uri self.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:18:58 +00:00
DorianandClaude Opus 4.6 e7496883c4 fix(app): replace innerHTML='' with textContent='' in usePlayer
Safer DOM clearing that avoids innerHTML for content sanitization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:18:29 +00:00
DorianandClaude Opus 4.6 2581f20ebf fix(app): add URL length limit (2048 chars) to extractUrlFromText
Prevents processing excessively long URLs in content extraction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:18:00 +00:00
DorianandClaude Opus 4.6 2ced4830f6 fix(app): add postMessage origin validation to archyBridge
Configurable origin replaces wildcard '*' for both sending and receiving.
Origin check filters incoming messages when a specific origin is set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:17:24 +00:00
DorianandClaude Opus 4.6 f84e68ad06 fix(app): add error handling to PdfViewer renderPage and VideoPlayer initHls
Prevents unhandled promise rejections from async watchers and lifecycle hooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:16:34 +00:00
DorianandClaude Opus 4.6 9b3fd5d973 fix(app): add catch block to SSE readSSE for stream read errors
Reports stream errors to onError callback instead of silently failing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:14:31 +00:00
DorianandClaude Opus 4.6 82f1e99324 fix(app): wrap unprotected JSON.parse calls in try/catch
SSE stream callbacks and AI response parsing now handle malformed data
gracefully instead of throwing unhandled exceptions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:11:18 +00:00
DorianandClaude Opus 4.6 9ba4854024 fix(app): debounce viewport change handler, overflow hidden when keyboard open
50ms debounce prevents jittery resizing on mobile keyboard open/close.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:08:30 +00:00
DorianandClaude Opus 4.6 378b60820a fix(content): add .catch() to cover fetch promise chains in grids
Prevents unhandled promise rejections if fetch throws unexpectedly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:07:32 +00:00
DorianandClaude Opus 4.6 6e671faa3c fix(app): brighten SVG fallback covers — increase lightness by +10%
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:06:04 +00:00
DorianandClaude Opus 4.6 cba448e32a chore: move overnight to skills format
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:37:03 +00:00
DorianandClaude Opus 4.6 9dbc9c24a6 chore: overnight plan 2026-03-04 + /overnight command
- Rewrote loop/plan.md with 34 checkbox tasks across 8 phases
- Updated loop/prompt.md for new overnight scope
- Added .claude/commands/overnight.md skill
- Self-destructing dev SW from previous session

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:34:33 +00:00
DorianandClaude Opus 4.6 8a1c3135dd fix(app): disable PWA service worker in dev, fix deprecated meta tag
Dev mode SW was caching stale responses and breaking page loads.
Disabled devOptions.enabled to prevent SW registration during development.
Added mobile-web-app-capable meta tag (the modern replacement for
apple-mobile-web-app-capable).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:06:41 +00:00
DorianandClaude Opus 4.6 e21d13ab3f revert(app): remove basicSsl — breaks HTTP dev access on LAN
The always-on HTTPS plugin prevented HTTP access at the LAN IP.
PWA install requires HTTPS but that should be handled at deploy time,
not in dev config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:04:37 +00:00
DorianandClaude Opus 4.6 0fa78cbf1d fix(app): keyboard resizes container, tab bar margin, HTTPS for PWA install
- Root container height now bound to visualViewport.height when keyboard
  is open — the whole layout shrinks instead of being pushed offscreen
- Tab bar gets 24px vertical margin (12px top + 12px bottom + safe area)
- Added @vitejs/plugin-basic-ssl for HTTPS dev server — required for PWA
  install on non-localhost origins (LAN IP access)
- Improved useVisualViewport to track fullHeight for accurate keyboard
  offset calculation across orientation changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:01:07 +00:00
DorianandClaude Opus 4.6 a0a9cf8e54 feat(app): mobile improvements — native-feel viewport, keyboard, HIG tabs, PWA fix
- Lock viewport: position:fixed on html/body prevents iOS bounce scroll
- No zoom on input focus: maximum-scale=1, user-scalable=no
- Keyboard-responsive chat: visualViewport API detects keyboard, hides
  tab bar, scrolls chat to bottom, scrollIntoView on input focus
- iOS HIG tab bar: 49pt height, vertical icon+label, safe-area-inset-bottom
- PWA fix: manifest start_url/scope/id changed from '/' to './' for
  subpath deployment compatibility
- PlayerBar: variant prop (fixed/inline), inline on mobile above tab bar,
  fixed on desktop. No more overlap with tab bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:50:06 +00:00
DorianandClaude Opus 4.6 799ca131d9 fix(app): only show gradient overlay on real images, not SVG fallbacks
The from-black/60 gradient overlay was making SVG text fallbacks appear
completely black. Now the gradient only renders when there's an actual
image, letting the designed SVG fallbacks show through properly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:31:11 +00:00
DorianandClaude Opus 4.6 6a1ce2e185 fix(app): brighten SVG text fallbacks so they're visible over dark overlays
All 8 SVG generators (song, podcast, news, image, film, TV, book, place)
had backgrounds at 9-10% lightness which appeared black under grid gradient
overlays. Bumped to 18% with proportionally brighter text and icons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:21:13 +00:00
DorianandClaude Opus 4.6 c88e837cc9 feat(app): add real images to all seed content + universal SVG text fallbacks
- Songs: 19/21 now have iTunes album art URLs (2 niche Bitcoin artists use fallback)
- Podcasts: 21/21 now have iTunes artwork URLs
- News: 21/21 now have Unsplash topic images
- Books: Fixed The Network State cover URL
- Places: 21/21 now have photos (Unsplash + Wikimedia)
- Added generateNewsFallback() and generateImageFallback() SVG generators
- Updated NewsCard, NewsGrid, ImageCard to use SVG text fallback instead of emoji
- Added error handling to PlaceCard and PlaceGrid for failed photo loads

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:11:15 +00:00
DorianandClaude Opus 4.6 982a0c6aa6 fix(app): news tab shows eagerly for news queries, populates async
The news tab was missing because hasNews evaluated to false when web
search results hadn't arrived yet. Now shows the tab eagerly for news
queries — results populate when they arrive via the deep watcher.
Also fixes RSS late-insertion to place before the Prompt tab.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:14:27 +00:00
DorianandClaude Opus 4.6 f0d4ae5acc feat(app): Prompt tab shows both query and response in brief format
The Prompt tab now renders the full AI response using magazine extraction
(extractMagazineSections) to create nicely formatted tiles. The user's
query appears as the hero headline, and the response is broken into
styled sections — same as the Brief tab but always available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:13:37 +00:00
DorianandClaude Opus 4.6 2963bca7fe feat(app): add always-visible Prompt tab with magazine brief style
Adds a 'Prompt' tab that is always present as the rightmost tab in the
content panel. Shows the user's original query as a single magazine-style
tile using the existing MagazineGrid component.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:08:58 +00:00
DorianandClaude Opus 4.6 dfce63de66 refactor(app): consolidate seeds into single conversation
All 15 seed prompts now load as one conversation ("Content Showcase")
instead of 15 separate ones. /seed collapses chat to show PromptIndex
so users can quickly pick any prompt to see its content surface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:38:52 +00:00
DorianandClaude Opus 4.6 1a76efce4a fix(player): raise PlayerBar z-index above chat panel
Chat aside has z-[100], PlayerBar had z-50 so it rendered underneath.
Bumped to z-[999] to ensure the player always floats above all panels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:35:44 +00:00
DorianandClaude Opus 4.6 493657549e fix(app): harden persistence, player performance, content extraction
- Fix chat persistence: unwrap Vue Proxy objects before IDB storage,
  flush pending saves on page unload/visibility change, use immediate
  saves for conversation creation and seed migration
- Fix player performance: parallel music search across providers,
  server-side LRU cache, client-side result cache, audio element reuse,
  instant UI feedback, abort stale searches, next-song prefetch
- Fix content extraction: strip recipe/event tags in stripContentTags,
  extend cleanMagazineContent regex for all _ext patterns
- Fix PlayerBar: move to App.vue root to avoid stacking context clipping,
  remove unused isDark conditionals (dark-only app)
- Add /seed command: loads 15 seed conversations from fixture index,
  opens history panel, switches to first seed conversation
- Add seed prompt index: 15 realistic AI prompt/response pairs covering
  films, songs, books, TV, places, podcasts, code, images, recipes, events
- Add seedExtraction.test.ts: 60 tests validating extraction counts,
  tag stripping completeness, and data integrity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:34:21 +00:00
DorianandClaude Opus 4.6 6c29e9e41d chore(app): iOS HIG Phase 7 — final verification audit passed
- 0 text-[10px], text-[9px], text-[8px], text-[11px] violations remaining
- All inputs verified at text-base (16px) minimum
- All interactive buttons at 44px minimum touch targets
- All button gaps at 8px minimum
- All 226 tests pass, typecheck clean, lint clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:13:58 +00:00
DorianandClaude Opus 4.6 43fa1bac73 fix(app): iOS HIG Phase 6 — modals, dialogs & overlays
- PassphraseDialog: submit button h-10→h-11 (44px)
- ShareToNostr: close/cancel/publish buttons expanded to 44px
- ComparisonView: tab buttons min-h-[44px], gap-1→gap-2
- NostrProfileEditor: publish button min-h-[44px]
- NostrRelayManager: all action buttons expanded to 44px
- ChatHistory: conversation items min-h-[44px]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:12:43 +00:00
DorianandClaude Opus 4.6 a0edbe7d20 fix(app): iOS HIG Phase 5 — glass button system & .touch-target utility
- Add responsive glass-button-sm: 44px min-height on mobile (≤768px)
- Replace !h-7 !min-h-0 overrides with responsive min-h-[44px] md:min-h-0
- Add .touch-target utility class (44px min, inline-flex centered)
- Refactor 21 icon buttons to use .touch-target class

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:09:29 +00:00
DorianandClaude Opus 4.6 c554b0d280 fix(app): iOS HIG Phase 4 — tab bar, gaps, and grid card audit
- ContentPanel: tab gap-1→gap-2, tab buttons min-h-[44px], close button 44px
- Audited all gap-0.5/gap-1: all remaining are between non-interactive elements
- Audited all grid card buttons: no undersized action buttons found

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:05:18 +00:00
DorianandClaude Opus 4.6 a8fc4ab204 fix(app): iOS HIG Phase 3 — content & settings touch targets (44px minimum)
- Detail views: back buttons expanded across all 8 detail components
- SettingsModal/SettingsPanel: close, edit/delete, tab buttons expanded
- MemoryPanel: edit/delete buttons + gap fix
- PersonaSelector: close button expanded
- PluginMarketplace: settings gear button expanded
- NostrGrid: sub-tab and filter buttons expanded
- PlayerBar, MapRenderer, BranchSwitcher, ZapDialog, NostrDMs,
  NostrThread, NostrArticles, DesignSystemDetail/Grid: all remaining
  small interactive buttons expanded to 44px minimum

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:02:15 +00:00
DorianandClaude Opus 4.6 723dd6e4d3 fix(app): iOS HIG Phase 2 — chat interface touch targets (44px minimum)
- ChatMessage: action buttons w-7→44px, gap-0.5→gap-2
- ChatHeader: toolbar buttons w-8/w-9→44px
- ChatInput: attach button w-8→44px, reply close w-5→44px
- ChatSearch: prev/next/close buttons w-6→44px
- ArticleReader: all toolbar buttons w-8→44px
- PdfViewer: all nav/zoom buttons w-8→44px

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:56:24 +00:00
DorianandClaude Opus 4.6 b2fcc23623 fix(app): iOS HIG Phase 1 — input font sizes and text minimums
- Replace all text-[10px] (308 instances) with text-xs (12px)
- Replace all text-[9px] and text-[8px] (133 instances) with text-xs
- Replace all text-[11px] (76 instances) with text-xs
- Bump all input/textarea font sizes to text-base (16px) to prevent iOS auto-zoom
- No visual design changes — only sizing minimums enforced

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:53:25 +00:00
DorianandClaude Opus 4.6 dde3859a2e chore(loop): iOS HIG compliance plan — 24 tasks across 7 phases
Font sizes (text-[10px]→text-xs, text-[8-9px]→text-[11px]),
touch targets (44×44px minimum for all buttons), input zoom
prevention (16px minimum), gap compliance (8px between targets),
glass button system updates, and comprehensive verification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:43:44 +00:00
DorianandClaude Opus 4.6 bda670d8d9 feat(app): base-aware routing, embedded flag, panel slide animation
- Use import.meta.env.BASE_URL for router history (Archy /aiui/ deployments)
- Capture embedded flag from URL params before router init
- Add panelSlideIn animation with prefers-reduced-motion support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:41:26 +00:00
DorianandClaude Opus 4.6 21b43214ac chore(loop): mark all 17 tasks complete in overnight plan
All phases completed:
- Phase 1: ImageGrid/PlaceGrid wired into ContentPanel
- Phase 2: Archy wallet/files context, ArchyAppsGrid, archy-apps data
- Phase 3: 111 extraction quality tests, all passing
- Phase 4: 20 Archy integration tests, all passing
- Phase 5: Code mode Esc key, project pill, design token AI context
- Phase 6: 226 total tests passing, 0 type errors, 0 lint errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:25:34 +00:00
DorianandClaude Opus 4.6 ba0320fa8d feat(code): add Esc to exit code mode, project indicator pill, design token context
- Escape key exits code mode on desktop (T14)
- Show active project name pill near chat input when in code mode (T15)
- Inject selected design tokens, files, and open file content into AI
  system prompt when in code mode (T16)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:24:58 +00:00
DorianandClaude Opus 4.6 decee9163b test(archy): add integration test suite for archyBridge, useArchy, and archy-apps
20 tests covering postMessage protocol, composable API shape,
buildArchyContext format, archy-apps data integrity, and base-aware paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:22:38 +00:00
DorianandClaude Opus 4.6 b8f1e9301b test(extraction): expand test suite to 111 tests covering all content types
Add tests for podcasts, apps, films, magazine sections, websites/domains,
nostr detection, news detection, filterTabsByContext routing, edge cases
(empty input, unicode, long text, malformed tags, mixed content).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:20:43 +00:00
DorianandClaude Opus 4.6 20ef35c8e3 feat(archy): extend useArchy with wallet/files context, add ArchyAppsGrid component
- Add wallet (Lightning balance, channels) and files (Nextcloud) context
  categories to useArchy composable with AI prompt injection
- Create archy-apps.ts data file mapping all 18 Archy services
- Build ArchyAppsGrid component with live status from bridge
- Wire ArchyAppsGrid into ContentPanel when embedded in Archy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:15:33 +00:00
DorianandClaude Opus 4.6 fe94693ef6 feat(content): wire ImageGrid/ImageDetail and PlaceGrid/PlaceDetail into ContentPanel
Add missing content type wiring so images and places render properly
in the content panel with full grid → detail navigation flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:12:34 +00:00
DorianandClaude Opus 4.6 705e184a5b chore(loop): update overnight plan with Archy integration and 17 tasks
6 phases: ContentPanel wiring, Archy service integration, extraction
hardening to 100+ tests, Archy integration tests, code mode UX, and
final verification. Prompt updated with full Archy service inventory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:09:28 +00:00
DorianandClaude Opus 4.6 89885269f0 feat(archy): wire archyBridge into app, base-aware API paths, nginx config
- Create useArchy composable wrapping archyBridge with reactive Vue state
- Initialize bridge in App.vue when ?embedded=true detected
- Inject Archy node context (apps, system, network) into AI system prompt
- Make API paths base-aware (import.meta.env.BASE_URL) for /aiui/ deployment
- Add nginx-archy.conf for production Anthropic API proxy with SSE support
- Fix archyBridge.ts typecheck error, export ActionResponse type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:22:15 +00:00
DorianandClaude Opus 4.6 f0eb4383bd feat(content): comprehensive extraction hardening, code mode UI, overnight plan
- Relax isBookLikeResponse threshold (>=2 to >=1)
- Widen book patterns: inline prose, verb-preceded, numbered bold, em-dash
- Remove overly aggressive film/song gating on book extraction
- Allow songs to coexist with film/book tags (explicit tags always returned)
- Strengthen TV patterns: seasons, created by, standalone "tv" query match
- Fix TV_EXT_RE to handle both Title|Year|Creator and Title|Creator|Year
- Widen place patterns: type word search in descriptions, bold fallback
- Add "pizza" to isPlaceQuery and preferredFirstTab
- Add Code tab: isCodeQuery, isCodeLikeResponse, extractCodeBlocks, wiring
- Fix image threshold: single image with meaningful alt text shown
- Refactor filterTabsByContext: specialized paths now append remaining content
- Add code mode UI: orange input styling, design system selection, file selection
- DesignSystemGrid: selection toggle only on checkmark, card click opens detail
- Add 64-test extraction quality test suite
- Update overnight plan.md and prompt.md for hardening run

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:54:03 +00:00
DorianandClaude Opus 4.6 75e9274582 feat: interactive setup — collects tasks and project rules in terminal
setup.sh now walks the user through entering their tasks and project
context interactively. Writes plan.md and prompt.md from their input.
Offers to launch the loop immediately at the end.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:41:36 +00:00
DorianandClaude Opus 4.6 eda9e48965 refactor: make setup.sh self-contained, remove templates folder
All templates are now embedded inline in setup.sh. Users only need
this single script — run `bash setup.sh` from any project root and
everything is created automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:36:42 +00:00
DorianandClaude Opus 4.6 b96d96b654 docs: add overnight automation guide and setup script for others
Comprehensive standalone guide + setup script so anyone can replicate
the Claude Code overnight automation system for their own projects.
Includes loop.sh, hook templates, plan/prompt templates, and an
interactive setup script. No project-specific content included.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:31:41 +00:00
DorianandClaude Opus 4.6 82718a3453 fix(proxy): unset CLAUDECODE env vars before spawning CLI
Allows the proxy to spawn claude CLI even when the dev server was
started from within a Claude Code session.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:17:32 +00:00
DorianandClaude Opus 4.6 b08fd86f41 revert(proxy): restore working CLI-based proxy from c82b4ef
Reverts all proxy changes back to the last known working version.
The proxy uses Claude CLI for all requests and only uses the Anthropic
API for web search tool calling when a credential is available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:41:47 +00:00
DorianandClaude Opus 4.6 9263d7bb26 refactor(proxy): remove OpenRouter fallback, simplify to Anthropic API + CLI
Removes all OpenRouter proxy code from claude-proxy.ts. The fallback
chain is now just: Anthropic API (key/OAuth) → Claude CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:38:22 +00:00
DorianandClaude Opus 4.6 f1db573f10 fix(proxy): fall back to OpenRouter with Anthropic SSE format conversion
When no Anthropic API credential is available, the proxy now falls back
to OpenRouter before trying the Claude CLI. The new streamViaOpenRouterFallback
function converts OpenRouter's OpenAI-format SSE to Anthropic-format SSE
(content_block_delta with text_delta) so the frontend's Claude provider
can parse it correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:19:55 +00:00
DorianandClaude Opus 4.6 f346992924 feat(chat): slash command palette, action button containers, app detection fix
- Slash commands (/code, /nostr, /design, /search) now appear in the
  prompt palette with descriptions, auto-send on select
- Chat message hover actions wrapped in a proper glass container with
  backdrop blur, divider between actions and feedback thumbs
- Palette has 8px side margins, no scroll limit
- Fix app extraction: queries mentioning known app names (e.g. "start9")
  now surface the Apps tab even without explicit app/nostr query patterns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 05:15:15 +00:00
DorianandClaude Opus 4.6 84ccdc7048 feat(app): content detection overhaul, apps tab, chat UX, web search
- Overhaul content detection: expand all query/response classifiers with
  broader AI response patterns (news, music, books, TV, places, websites)
- Add Nostr detection (isNostrQuery, isNostrLikeResponse) and App detection
  (isAppQuery, isAppLikeResponse) classifiers
- Add bare domain extraction from AI text (e.g. "check out damus.io")
- Add Apps tab with curated database of ~30 Nostr + Bitcoin ecosystem apps
  (clients, wallets, privacy tools, node software, dev tools)
- Create AppsGrid + AppDetail components with search, filtering, how-to
- Wire app extraction and Nostr detection into useContentPanel
- Add PromptIndex badges for Apps and Nostr tabs
- Chat UX: dedicated history button, settings modal (memory + advanced),
  fix collapsed chat v-if/v-else chain bug
- Web search: add Brave Search API as primary backend, expand SearXNG pool
- iOS HIG: comprehensive mobile UX rules in cursor rules + CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 05:03:03 +00:00
DorianandClaude Opus 4.6 5d4367eaff docs: mark all milestones complete (M8-M20) and pass FINAL gate
All 116 tasks implemented across M8-M20. Full suite passing:
- 101 tests (vitest)
- 0 typecheck errors (vue-tsc --noEmit)
- 0 lint errors (eslint)
- Production build succeeds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:23:36 +00:00
DorianandClaude Opus 4.6 b63a1250bb docs: mark M20 complete
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:22:21 +00:00
DorianandClaude Opus 4.6 77bad97377 feat(collab): collaboration & sharing (M20.1-M20.6)
- Share conversations as Nostr kind:30023 articles with NIP-44 encryption
- Read-only conversation viewer page at /view/:nostrAddr
- Collaborative playlists via NIP-51 kind:30004 lists
- Conversation templates (6 built-in + custom)
- Audio podcast export via Web Speech API
- Community content packs with registry and import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:22:01 +00:00
DorianandClaude Opus 4.6 6581b057ac docs: mark M19 complete
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:16:02 +00:00
DorianandClaude Opus 4.6 18b972b351 feat(devx): developer experience & quality (M19.1-M19.8)
- Storybook 8 setup with dark glass canvas, stories for all ui/ components
- Visual regression tests (Playwright screenshots, 0.5% pixel diff)
- Bundle size CI gate (fail > 250KB gzipped)
- Comprehensive mock data: 20+ items for films, songs, books, TV, images,
  places, podcasts, news, nostr events; mock TMDB responses
- E2E cross-browser matrix: Chromium + Firefox + WebKit + iPhone 14 + Galaxy S21
- Proxy integration tests (SSE format, tool_use, error handling, disconnect)
- Lighthouse CI config (LCP < 3s, CLS < 0.15)
- Dependency audit: weekly GitHub Actions workflow with license checker
- CI workflow: lint, typecheck, test, bundle size, e2e, lighthouse, audit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:15:39 +00:00
DorianandClaude Opus 4.6 a01f43c95f docs: mark M18 complete and pass TEST:M18 gate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:59:20 +00:00
DorianandClaude Opus 4.6 1664fbbb6f feat(perf): performance composables for lazy loading, dedup, prefetch, cleanup (M18.1-M18.8)
- Image lazy loading: IntersectionObserver + blur-up placeholder
- Request deduplication: in-flight Promise sharing by URL+body key
- Prefetch on hover: 5-minute cache for pre-fetched detail data
- Memory leak audit: useCleanup() tracks intervals/listeners/observers
- Background sync queue: retry failed IDB saves on visibility change
- Bundle splitting: composable architecture enables tree-shaking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:59:03 +00:00
DorianandClaude Opus 4.6 dd7b694dee docs: mark M17 complete and pass TEST:M17 gate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:57:56 +00:00
DorianandClaude Opus 4.6 725f629f69 feat(a11y): accessibility audit, high contrast, i18n foundation, skip nav (M17.1-M17.8)
- Keyboard navigation: useFocusTrap() for modals, useRovingTabindex() for grids
- ARIA audit: composables for focus trap and roving tabindex patterns
- High contrast mode: @media (prefers-contrast: more) + .high-contrast class
- i18n foundation: en/es/fr locale files, useI18n() composable with auto-detect
- RTL layout support: dir attribute toggling based on locale
- Dyslexia-friendly font: .font-dyslexia CSS class with OpenDyslexic support
- Skip navigation link: .skip-nav CSS with focus-visible positioning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:57:37 +00:00
DorianandClaude Opus 4.6 85936d3005 docs: mark M16 complete and pass TEST:M16 gate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:55:36 +00:00
DorianandClaude Opus 4.6 6be4fbe081 feat(mobile): mobile UX polish with bottom sheet, haptics, gestures, PWA (M16.1-M16.10)
- BottomSheet.vue: gesture-driven with 40/80/100% snap points, backdrop
- Swipe navigation: left/right to switch conversations, 80px threshold
- Pull-to-refresh: custom glass spinner, haptic on release
- Haptic feedback: useHaptics() composable with pattern presets
- Web Share API: native share with glass fallback sheet
- Pinch-to-zoom: usePinchZoom() composable, 1x-4x, double-tap reset
- iOS PWA: safe area insets CSS utilities, black-translucent status bar
- Long-press context menus: 500ms trigger via bottom sheet
- Scroll position memory: per-route Map, auto-restore on mount
- Landscape mode: CSS split layout, orientation change detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:55:19 +00:00
DorianandClaude Opus 4.6 72f93e4e94 docs: mark M15 complete and pass TEST:M15 gate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:52:46 +00:00
DorianandClaude Opus 4.6 1b5debee33 feat(settings): comprehensive settings panel with accent, glass, fonts, shortcuts, notifications (M15.1-M15.10)
- Accent colour picker: 8 presets + custom colour picker, live CSS var update
- Glass intensity slider: Subtle/Default/Strong blur and opacity presets
- Font size settings: Compact 13px / Default 15px / Large 17px
- Content type visibility: toggle each of 11 content tabs
- Keyboard shortcut map: view and rebind all shortcuts, conflict detection
- Browser push notifications: opt-in for generation complete events
- Auto-archive: 7/30/90/never day threshold for old conversations
- Full data export: JSON archive of all localStorage aiui-* keys
- Data wipe: two-step confirm, optional API key vault clearing
- Default conversation settings: model, web search, token counts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:52:29 +00:00
DorianandClaude Opus 4.6 dd559502ae docs: mark M14 complete and pass TEST:M14 gate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:48:47 +00:00
DorianandClaude Opus 4.6 df5b4e04ae feat(plugins): plugin marketplace with discovery, settings, permissions, Wikipedia & OpenLibrary (M14.1-M14.8)
- Plugin Discovery UI: registry fetch, install button, rating display
- Plugin Settings Panel: JSON Schema form, key-value editor
- Plugin Permissions UI: grant/deny dialog per capability
- Plugin Dev Mode: VITE_PLUGIN_DEV flag, error inspector, init timing
- Built-in Wikipedia plugin: REST API search, /wiki command
- Built-in OpenLibrary plugin: book search with cover images
- Plugin Import by URL: fetch manifest, validate, install
- Plugin Versioning: auto-update check, badge, update all button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:48:31 +00:00
DorianandClaude Opus 4.6 2a836b1395 docs: mark M13 complete and pass TEST:M13 gate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:44:46 +00:00
DorianandClaude Opus 4.6 52e62e33cd feat(discover): content discovery panel with For You, tags, playlists, collections (M13.1-M13.8)
- For You feed: frequency map from favorites, sorted by most-favorited type
- Content tagging: user tags on any item, tag cloud, filter by tag
- Smart playlists: recently played, most played, by genre, by decade
- Similar content: background AI call for 3 suggestions, cached 7 days
- Recently viewed history: last 50 items with time-ago display
- Content collections: user-curated mixed-type lists with mosaic grid
- Trending: most-referenced items across 30 days with badge
- Share to Nostr: compose preview, sign via NIP-07, broadcast to relays

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:44:30 +00:00
DorianandClaude Opus 4.6 d5d8a1f8b2 docs: mark M12 complete and pass TEST:M12 gate
74 tests pass, 0 typecheck errors, 0 lint errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:36:15 +00:00
DorianandClaude Opus 4.6 c328d6498c feat(bitcoin): full Bitcoin ecosystem cards and composables (M12.2-M12.8)
M12.2: Fedimint ecash card with Fedi deep-link
M12.3: BOLT12 offer card with QR and wallet deep-link
M12.4: Nostr Wallet Connect (NWC) composable with NIP-47 scaffolding
M12.5: LNURL-auth login composable with challenge generation
M12.6: Live sat/fiat price from mempool.space/api/v1/prices (60s refresh)
M12.7: Mempool.space tx viewer with confirmations, fee rate, block height
M12.8: BOLT11 invoice decoder card with amount, expiry countdown, pay link

All Bitcoin patterns auto-detected in chat messages via useBitcoinDetector.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:35:38 +00:00
DorianandClaude Opus 4.6 3a718f44de feat(bitcoin): on-chain address display with QR and mempool link (M12.1)
Detect bech32, bech32m, P2PKH, P2SH addresses in chat messages.
Show address card with type badge, QR code, copy button, and
mempool.space link. Bitcoin detector utility for all BTC patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:32:59 +00:00
DorianandClaude Opus 4.6 d7d1f1df37 docs: mark M11 complete and pass TEST:M11 gate
74 tests pass, 0 typecheck errors, 0 lint errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:31:23 +00:00
DorianandClaude Opus 4.6 091a1ce4d1 feat(nostr): NIP-23 long-form articles with article renderer (M11.10)
Articles sub-tab fetches kind:30023 events, shows title/summary/date.
Clicking opens full article in ArticleReader (M10.1). Discovery from
relay.nostr.band with 30 most recent articles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:30:35 +00:00
DorianandClaude Opus 4.6 f4e8614730 feat(nostr): NIP-51 lists — follows, mute, pin, bookmarks (M11.9)
View and manage follow (kind:3), mute (kind:10000), pin (kind:10001),
and bookmark (kind:10003) lists. Add/remove items, publish via NIP-07.
Lists sub-tab in Nostr section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:29:26 +00:00
DorianandClaude Opus 4.6 8f2f927ff1 feat(nostr): thread view with nested replies up to 5 levels (M11.8)
Clicking a note opens thread view fetching root + replies via #e tag.
Renders as threaded tree with indentation (max 5 levels). Reply button
opens compose with proper e-tag threading (root + reply markers).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:27:46 +00:00
DorianandClaude Opus 4.6 ffc6524286 feat(nostr): NIP-50 search via relay.nostr.band and nostr.wine (M11.7)
Search input sends REQ with search field to NIP-50 supporting relays.
Results shown as note cards with author, content, timestamp. Filter
by content type. NIP-50 button triggers relay search on Enter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:25:24 +00:00
DorianandClaude Opus 4.6 49c9f71aa2 feat(nostr): NIP-05 verification badge with 24h cache (M11.6)
Verify NIP-05 identifiers by fetching .well-known/nostr.json from
the domain. Results cached in localStorage for 24 hours. Green
checkmark badge shown next to verified NIP-05 on note cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:23:52 +00:00
DorianandClaude Opus 4.6 8ff3d1298e feat(nostr): zap dialog with LNURL-pay and QR (M11.5)
Zap button on notes opens dialog with amount presets, optional message,
LNURL-pay resolution from Lightning address, invoice display with
copy and wallet deep-link. Never holds funds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:21:20 +00:00
DorianandClaude Opus 4.6 bad7506b72 feat(nostr): profile editor with kind:0 publishing (M11.4)
Edit display name, bio, avatar, banner, website, NIP-05, Lightning
address. Live profile card preview. Publish as kind:0 event via NIP-07.
Added Profile sub-tab in Nostr section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:19:07 +00:00
DorianandClaude Opus 4.6 ba6b22bb62 feat(nostr): relay management UI with latency & read/write toggles (M11.3)
Configurable relay list persisted to localStorage. Add/remove relays,
test connection latency, read/write toggle per relay, import from
NIP-65. Relays sub-tab in Nostr section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:16:27 +00:00
DorianandClaude Opus 4.6 3a0d63f2b1 feat(nostr): encrypted DMs with inbox and thread view (M11.2)
NIP-04 encrypted DMs with IDB persistence. DM inbox tab in Nostr
section with contact list, message threads, and new conversation
initiation. Added decodeNpub utility. Sub-tab switcher (Feed/Messages).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:13:53 +00:00
DorianandClaude Opus 4.6 9b4980b669 feat(nostr): publish notes with per-relay status (M11.1)
Compose panel in Nostr tab, sign via NIP-07, broadcast to configured
relays with per-relay send status. Character counter (soft 280 limit).
Removed isDark conditionals from NostrGrid (dark-only app).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:10:53 +00:00
DorianandClaude Opus 4.6 869db87fd5 docs: mark M10 complete and pass TEST:M10 gate (fix lint)
All M10 tasks (M10.1–M10.12) implemented and verified.
74 tests pass, 0 typecheck errors, 0 lint errors.
Fixed CodeRunner.vue script tag string literal lint issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:05:59 +00:00
DorianandClaude Opus 4.6 f4270424f8 feat(renderer): video player with HLS.js & YouTube embed (M10.12)
- VideoPlayer.vue: native <video> with custom glass styling
- HLS.js lazy-loaded for adaptive streaming (.m3u8)
- YouTube detection → youtube-nocookie.com embed
- Fullscreen via native controls, playsinline for mobile

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:03:30 +00:00
DorianandClaude Opus 4.6 7fa06ce500 feat(renderer): sandboxed code runner for HTML/JS/CSS (M10.11)
- CodeRunner.vue: sandboxed iframe with srcdoc, allow-scripts only
- Console capture via postMessage (log + error)
- Run button, clear output, code preview
- Extracts runnable code blocks from markdown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:02:36 +00:00
DorianandClaude Opus 4.6 7f67f0863d feat(renderer): vertical timeline for multiple events (M10.10)
- TimelineRenderer.vue: alternating left/right on desktop, single column mobile
- Animated entries with staggered fade-up
- Auto-switches to timeline when 3+ events detected
- Individual EventCards for 1-2 events

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:01:06 +00:00
DorianandClaude Opus 4.6 04a175171b feat(renderer): interactive table with sort, filter & CSV export (M10.9)
- InteractiveTable.vue: sortable columns, row filter, CSV export
- Extracts markdown tables from AI responses into structured data
- Click column header to sort (asc/desc), numeric-aware
- Filter input for live row filtering
- Renders alongside regular chat markdown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:00:08 +00:00
DorianandClaude Opus 4.6 9404085668 feat(renderer): audio waveform player with WaveSurfer.js (M10.8)
- Lazy-loads WaveSurfer.js on first audio render
- Bitcoin orange waveform on dark background
- Click-to-seek, play/pause, time display
- Responsive width, touch-friendly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:58:39 +00:00
DorianandClaude Opus 4.6 7c6a0565d5 feat(renderer): Mermaid diagram rendering with dark theme (M10.7)
- Lazy-loads Mermaid.js on first ```mermaid block detected
- Dark theme with Bitcoin orange accent colors
- Cached renders to avoid re-rendering on scroll
- Error display inline without crashing app

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:57:41 +00:00
DorianandClaude Opus 4.6 e7961c608b feat(renderer): KaTeX math rendering for $inline$ and $$block$$ (M10.6)
- Lazy-loads KaTeX (~70KB) on first math detected
- Inline $...$ and block $$...$$ LaTeX support
- Falls back to <code> for invalid LaTeX
- Batch render after markdown, not during streaming

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:56:12 +00:00
DorianandClaude Opus 4.6 c070e76515 feat(renderer): recipe & event card renderers (M10.4, M10.5)
- RecipeCard: ingredients checklist, numbered steps, scale slider
- EventCard: date chip, countdown timer, ICS download, Google Calendar
- Extract <recipe_ext> and <event_ext> tags from AI responses
- Both render inline in chat messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:55:02 +00:00
DorianandClaude Opus 4.6 aec87a347c feat(renderer): interactive map with Leaflet + OpenStreetMap (M10.3)
- MapRenderer.vue: lazy-loads Leaflet, renders OSM tiles
- Orange marker pins for all places with coordinates
- Place list sidebar on desktop, popup on click
- "View on map" button in chat messages with places
- Integrated into content panel via openMapView()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:52:16 +00:00
DorianandClaude Opus 4.6 7a3d874f5e feat(renderer): PDF viewer with page nav & zoom controls (M10.2)
- PdfViewer.vue: lazy-loads pdfjs-dist, renders to canvas
- Page navigation (arrows), zoom controls (50%–200%)
- Keyboard navigation (arrow keys)
- Integrated into content panel via openPdfViewer()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:50:07 +00:00
DorianandClaude Opus 4.6 edb4bbadcd feat(renderer): full article reader with TOC, font controls & print (M10.1)
- ArticleReader.vue: sticky TOC sidebar (desktop), mobile dropdown
- Auto-generated heading anchors with IntersectionObserver tracking
- Reading time estimate, font size controls (persisted localStorage)
- Print mode opens clean window with serif typography
- Long-form detection (>800 words + headings) shows "Read as article"
- Removed isDark conditionals from ArticleDetail (dark-only app)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:48:00 +00:00
DorianandClaude Opus 4.6 aeb184b930 docs: mark M9 complete and pass TEST:M9 gate
All M9 tasks (M9.1–M9.10) implemented and verified.
74 tests pass, 0 typecheck errors, 0 lint errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:42:52 +00:00
DorianandClaude Opus 4.6 a9a15a7f3f feat(chat): add temperature/params sliders & stop sequences (M9.9, M9.10)
Collapsible "Advanced" panel with Temperature, Max Tokens, Top-P sliders
and stop sequence tag input. Params persisted per conversation and passed
to Claude API. Reset to defaults button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:40:32 +00:00
DorianandClaude Opus 4.6 a3b80b5548 feat(chat): add model capabilities badges in selector (M9.8)
Model selector shows capability badges per model: Vision, Tools,
Long context. Emoji indicators with tooltips. Capabilities defined
per model in a static map.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:37:48 +00:00
DorianandClaude Opus 4.6 55faf5043a feat(chat): add token & cost estimator badges (M9.6)
Per-message token count badge (e.g. "125 tok", "1.2k tok") next to
timestamp. ContextBar tooltip shows running total and estimated cost
based on model pricing table. Pricing for Claude Haiku/Sonnet/Opus.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:36:31 +00:00
DorianandClaude Opus 4.6 210fa7d3c6 feat(chat): add response feedback thumbs up/down (M9.5)
Thumbs up/down buttons on assistant messages (hover action bar). Feedback
stored per message, persisted in IDB, included in conversation exports.
Toggle to clear feedback. Color-coded: green for up, red for down.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:34:50 +00:00
DorianandClaude Opus 4.6 d595996866 feat(chat): add AI memory panel with persistent facts (M9.7)
Collapsible memory panel below persona selector. Add/edit/delete
persistent facts (max 20) injected into every system prompt. Facts
stored in localStorage, shown as collapsed "Memory (N/20)" toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:32:42 +00:00
DorianandClaude Opus 4.6 751edd2790 feat(chat): add prompt template library with / command palette (M9.3)
Type / in chat input to open glass command palette with prompt templates.
Templates support {{variable}} substitution with a mini form. Built-in
templates for Explain, Compare, Summarise, Translate. Templates stored
in localStorage, importable/exportable as JSON. Arrow key navigation
and Enter to select.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:30:55 +00:00
DorianandClaude Opus 4.6 77b985db9c feat(chat): add system prompt editor & personas (M9.2)
Named personas with system prompt, model preference, and accent colour.
Persona pill selector above chat input, editor modal for create/edit/delete.
Default persona auto-applied to new conversations. Persona system prompt
prepended to AI context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:29:01 +00:00
DorianandClaude Opus 4.6 27aeb2b188 feat(chat): add vision input with drag-and-drop & paste images (M9.4)
Drag-and-drop or paste images into chat input. Thumbnail preview above
input, max 4 images per message. Images encoded as base64 and sent in
Claude vision format (multimodal content arrays). Image attach button
in chat input toolbar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:25:26 +00:00
DorianandClaude Opus 4.6 0ae497f1db feat(chat): add multi-model comparison mode (M9.1)
Split-screen comparison of two AI models streaming simultaneously.
Toggle via header button. Desktop shows side-by-side panes, mobile
shows swipeable tabs. Uses streamWithModel() for provider-agnostic
parallel streaming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:19:46 +00:00
DorianandClaude Opus 4.6 5fdf62520c docs: mark M8 Chat UX Polish complete in plan and progress
All 10 M8 tasks done + TEST:M8 gate passed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:16:53 +00:00
DorianandClaude Opus 4.6 71a249c08a feat(chat): add scroll position memory per conversation (M8.10)
Save scroll position when switching conversations, restore it when
returning. Uses a session-only Map<conversationId, scrollTop>.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:15:57 +00:00
DorianandClaude Opus 4.6 cc5ecf3091 feat(chat): add right-click context menus on messages (M8.9)
Reusable ContextMenu.vue glass-card component positioned at cursor.
Messages get Copy, Reply, Edit (user), Regenerate (assistant), and
Branch from here options. Closes on Escape or outside click.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:15:26 +00:00
DorianandClaude Opus 4.6 37e8aec920 feat(chat): add conversation import from JSON (M8.8)
Import AIUI JSON exports and Claude.ai export format via file picker.
Merges imported conversations into existing data without overwriting.
Shows import summary with count and format detected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:14:11 +00:00
DorianandClaude Opus 4.6 6182adb01c feat(chat): add conversation export & three-dot menu (M8.7)
Export conversations as Markdown, JSON, or plain text via three-dot
menu in chat header. Uses File System Access API with <a download>
fallback. Also adds delete conversation option.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:13:10 +00:00
DorianandClaude Opus 4.6 87cbd8c4ea feat(chat): add context window visualiser bar (M8.6)
Slim progress bar showing estimated token usage (~4 chars/token).
Bitcoin-orange fill turns red when >80% of context window used.
Tooltip shows exact token estimate on hover.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:11:35 +00:00
DorianandClaude Opus 4.6 68e296278c feat(chat): add auto-title generation after first exchange (M8.5)
After the first AI response, sends a background request to generate
a 3-5 word title using Claude Haiku. Silently replaces the default
title derived from the first user message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:10:46 +00:00
DorianandClaude Opus 4.6 5d3114b142 feat(chat): add conversation search with Cmd+F (M8.4)
Glass search panel with real-time filtering, match counter,
up/down navigation to jump between matching messages.
Keyboard shortcuts: Cmd+F to open, Escape to close, Enter/arrows
to navigate between matches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:09:48 +00:00
DorianandClaude Opus 4.6 6454518603 feat(chat): add reply-to threading (M8.3)
Reply button on all messages shows quoted excerpt above input.
On send, quote is prepended as `> excerpt`. Clean up isDark
patterns in ChatInput (dark-only app).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:08:46 +00:00
DorianandClaude Opus 4.6 e439123a61 feat(chat): add conversation branching UI (M8.2)
Wire up branch-from-message handler, add BranchSwitcher glass pill
component showing "Branch X of Y" with prev/next navigation.
Clean up isDark conditionals in chat components (dark-only app).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:06:56 +00:00
DorianandClaude Opus 4.6 61a6e88667 feat(chat): add message editing & regeneration (M8.1)
Pencil icon on hover for user messages to edit inline. Regenerate
icon on assistant messages to re-send from the same prompt. Editing
clears all subsequent messages and triggers a fresh AI response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:01:48 +00:00
DorianandClaude Opus 4.6 9f6158ddd2 chore: prepare automation loop for Plan 2 (M8–M20)
- Add PLAN2.md with 116 tasks across M8–M20 with testing gates
- Populate loop/plan.md with all new tasks (30-attempt retry policy)
- Update loop/prompt.md to enforce glass design system and PLAN2.md
- Fix PassphraseDialog to use glass-card/gradient-button design system
- Fix crypto.randomUUID fallback for non-secure contexts
- Fix dev.sh to skip proxy startup if port 3141 already in use
- Fix PWA manifest for Android A2HS (purpose:any, display_override, id)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:29:18 +00:00
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
DorianandClaude Opus 4.6 20d50d7659 docs: mark M1.5 and all success criteria complete
All tasks in loop/plan.md now checked. All 18 success criteria met:
tests pass, typecheck passes, lint passes, all features implemented.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:13:03 +00:00
DorianandClaude Opus 4.6 6b5d461caf test(app): add 8 E2E tests for chat interactions and content surfaces
Add tests for: streaming response, film cards in panel, detail view
navigation, mobile full-screen overlay, stop button visibility, web
search toggle, new conversation clearing messages, and panel side
layout on desktop viewport.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:12:38 +00:00
DorianandClaude Opus 4.6 297aaf540d docs: mark all milestones M3-M7 complete in plan and progress
Update loop/plan.md with all tasks checked through M7.4. Update
PROGRESS.md with comprehensive session log covering 15 completed
tasks from plugin system through platform features.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:09:28 +00:00
DorianandClaude Opus 4.6 ea5e82bfe3 feat(app): add offline mode composable and enhanced PWA caching
Create useOffline.ts with online/offline detection, sync queue for
pending actions, and auto-process on reconnect. Enhance vite PWA config
with CacheFirst for TMDB and Wikipedia images, StaleWhileRevalidate for
TMDB API responses, with size and TTL limits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:08:50 +00:00
DorianandClaude Opus 4.6 a442fbbe62 feat(app): add Tauri desktop build scaffold
Create src-tauri/ with tauri.conf.json (frameless transparent window,
1200x800 default, system tray), Cargo.toml with tauri v2 dependencies,
and main.rs with tray icon click-to-show and global shortcut
(Cmd+Shift+A) to toggle window visibility. Auto-updater plugin enabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:07:22 +00:00
DorianandClaude Opus 4.6 9c60d18528 feat(app): add multi-provider AI adapter pattern
Create unified AIAdapter interface (types.ts) with streaming chat,
model listing, and capability flags. Implement three adapters:
claude-adapter.ts (Claude proxy + vault key), openrouter-adapter.ts
(OpenAI-compatible SSE), ollama-adapter.ts (local NDJSON streaming).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:06:06 +00:00
DorianandClaude Opus 4.6 3bbcfad43d feat(app): add MCP server plugin with tool definitions and handlers
Create mcp-server.ts exposing AIUI capabilities as MCP tools:
search_films, search_songs, search_podcasts, get_library_stats.
Includes tool call parsing, routing, and result formatting for
Claude tool_use integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:04:10 +00:00
DorianandClaude Opus 4.6 685d357573 feat(app): add Nostr identity login via NIP-07 browser extension
Create useNostrIdentity.ts composable with NIP-07 window.nostr API
integration (getPublicKey, signEvent, extension detection). Add npub
bech32 encoding to bech32.ts. Create NostrLogin.vue settings component
with login/logout, npub display with copy, and extension install guidance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:02:53 +00:00
DorianandClaude Opus 4.6 71125bfde9 feat(app): add Cashu ecash token parsing and inline chat display
Create cashu.ts with token parser (cashuA... base64url decode), amount
extraction, and mint URL formatting. Add CashuToken.vue inline component
with copy-to-clipboard and open-in-wallet deep-link. Integrate detection
in ChatMessage.vue with automatic token stripping from display text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:01:06 +00:00
DorianandClaude Opus 4.6 0f1f576ef5 feat(app): add Lightning wallet deep-links and payment components
Create lightning.ts with BOLT11 parsing, Lightning/BIP21 URI generation,
and LNURL support. Add PaymentButton.vue (Bitcoin orange gradient, opens
wallet via deep-link) and LightningInvoice.vue (QR code, copy, expiry
countdown, open-in-wallet). AIUI is never a wallet — only deep-links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:59:32 +00:00
DorianandClaude Opus 4.6 9e4a2c30e1 feat(app): add encrypted API key vault with IDB storage
Create key-vault.ts with AES-256-GCM encrypted IndexedDB storage for
API keys (Claude, OpenRouter). Add ApiKeyManager.vue settings UI with
masked key display and add/remove functionality. Integrate vault lookups
into useAI.ts streaming functions with graceful fallback when IDB is
unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:57:39 +00:00
DorianandClaude Opus 4.6 621a324859 feat(app): add passphrase dialog and encrypted storage layer
Create PassphraseDialog.vue with create/enter passphrase flows, glass
morphism styling, and skip option. Wire into App.vue to prompt on
startup when crypto is enabled. Salt stored in localStorage, key
derived via PBKDF2 and held in memory for the session only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:52:53 +00:00
DorianandClaude Opus 4.6 ee7f0ede1f feat(app): add AES-256-GCM encryption with PBKDF2 key derivation
Create crypto.ts with Web Crypto API utilities: PBKDF2 key derivation
(100K iterations, SHA-256), AES-256-GCM encrypt/decrypt, session key
management. Modify idb-storage.ts to transparently encrypt/decrypt
conversations when a session key is set. Disabled in dev mode via
VITE_DISABLE_CRYPTO=true.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:51:31 +00:00
DorianandClaude Opus 4.6 258a03d7f1 feat(app): add bookmarks/favorites with IndexedDB persistence
Create favorites Pinia store with IndexedDB backend for persistent
favorites. Add FavoriteButton.vue heart toggle (accent-colored when
active). Create FavoritesGrid.vue with type filtering. Wire favorite
buttons into FilmCard and SongCard. Add Favorites tab to ContentPanel
that appears when items are saved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:49:57 +00:00
DorianandClaude Opus 4.6 1991d75850 feat(app): add federated search across content libraries
Create useFederatedSearch.ts composable that searches films, songs,
and podcasts with 150ms debounce. Add SearchResults.vue overlay with
grouped results and type icons. ChatInput.vue detects /search command
prefix and shows results above the input, inserting content reference
tags on selection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:44:27 +00:00
DorianandClaude Opus 4.6 771e13aaf2 feat(app): add nostr social embeds with bech32 NIP-19 decoding
Create NostrEmbed.vue component that renders nostr:note1, nostr:npub1,
and nostr:nevent1 URIs as rich embedded cards. Add bech32 decoder
utility with NIP-19 TLV support for nevent/nprofile. Extend useNostr
with fetchNote() for single-note relay lookups. ChatMessage.vue now
detects and strips nostr URIs, rendering them as inline embed cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:42:11 +00:00
DorianandClaude Opus 4.6 557f9e6220 feat(app): register film and song renderers via plugin system
Create film-renderer.ts and song-renderer.ts as RendererDefinition
plugins using defineAsyncComponent for lazy loading. ContentPanel.vue
now looks up film/song components via getRendererForContentType()
instead of direct imports, enabling future community renderer plugins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:39:28 +00:00
DorianandClaude Opus 4.6 ad4612a5c2 feat(app): activate plugin registry with Claude provider adapter
Create plugins/index.ts bootstrap and claude-provider.ts implementing
the AIProviderAdapter interface from @aiui/core. The Claude provider
wraps the existing proxy streaming logic as an async generator. Plugin
initialization is called in main.ts before app mount.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:37:00 +00:00
DorianandClaude Opus 4.6 ebaee4c2b9 feat(app): add Nostr feed integration with real relay connections
Create useNostr composable with raw WebSocket connections to public
relays (relay.damus.io, nos.lol, relay.snort.social). Subscribe to
kind:1 notes with limit 50. Update NostrGrid to use real data instead
of mock notes. Lazy-load on tab activation, clean disconnect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:32:15 +00:00
DorianandClaude Opus 4.6 31f2c2b261 feat(app): add virtual scrolling for chat messages
Replace v-for message loop with @tanstack/vue-virtual useVirtualizer.
Dynamic row measurement, estimated sizes (60px user / 200px assistant),
overscan 5 items. Scroll-to-bottom via scrollToIndex during streaming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:24:44 +00:00
DorianandClaude Opus 4.6 79691c9a25 feat(app): add music queue management with next/prev navigation
Add queue (ShallowRef<Song[]>), currentIndex, playNext, playPrevious,
addToQueue, removeFromQueue to usePlayer. Auto-advance on song end.
Add prev/next buttons and queue count to PlayerBar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:22:51 +00:00
DorianandClaude Opus 4.6 81ccd69aae feat(app): add markdown rendering in chat messages
Install markdown-it and render assistant messages through it with safe
defaults (html: false, linkify, breaks). Add CSS for code blocks,
lists, links, blockquotes, headings. Links open in new tab. User
messages remain plain text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:20:13 +00:00
DorianandClaude Opus 4.6 cb3a50a920 test(app): add unit tests for useAI composable
16 tests covering provider selection, context injection, sendMessage
streaming, error handling, stop generation, and web search integration.
Mocks fetch/SSE and IDB for isolated testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:17:57 +00:00
DorianandClaude Opus 4.6 72f5656e02 feat(app): add error boundaries to mobile content and detail views
Wrap mobile ContentGridView and DetailView with ErrorBoundary
components to prevent cascading failures on mobile. Fix prefer-const
lint error in chat store.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:13:08 +00:00
DorianandClaude Opus 4.6 c6843fca1a test(app): add unit tests for contentExtraction composable
49 tests covering all extraction functions:
- extractAllFilms: tag parsing, library lookup, dedup, malformed input
- extractAllSongs: ext tags, library lookup, dedup, false positive filtering
- extractAllPodcasts: ext tags, library lookup, bad content filtering
- extractAllBooks: ext tags, optional fields, query filtering
- extractAllTVSeries: ext tags, creator parsing, query filtering
- extractAllPlaces: full fields, optional fields
- extractAllImages: markdown images, bare URLs, query gating
- extractMagazineSections: headings, numbered lists, hero images
- stripContentTags: all tag types, preservation of non-tag content
- extractBoldDomainLinks / extractMarkdownLinks / mergeNewsResults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:42:24 +00:00
DorianandClaude Opus 4.6 f31e0ba28b feat(app): add error boundaries and fix ESLint config for Vue+TS
- Wrap ChatMessage v-for loop in ChatWindow with ErrorBoundary
- Wrap ContentPanel grid/detail sections with ErrorBoundary
- Fix ESLint flat config: add vue-eslint-parser for TypeScript in SFCs
- Add browser globals to ESLint config
- Fix lint errors in contentExtraction.ts (useless escapes, prefer-const,
  unicode flag for emoji regex)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:40:22 +00:00
Dorian 0863a66ddc Merge branch 'claude/funny-hofstadter' into development
# Conflicts:
#	.claude/hooks/block-risky-bash.sh
#	.claude/hooks/protect-files.sh
#	.claude/settings.json
2026-03-03 18:27:19 +00:00
Dorian 23600da283 Merge branch 'claude/priceless-colden' into development 2026-03-03 18:26:31 +00:00
Dorian 06b6a0e2df Merge branch 'claude/heuristic-raman' into development 2026-03-03 18:26:31 +00:00
Dorian e53aa44ddc Merge branch 'claude/hardcore-beaver' into development 2026-03-03 18:26:31 +00:00
Dorian bec0d885de Merge branch 'claude/happy-colden' into development 2026-03-03 18:26:31 +00:00
Dorian de8faefdc7 Merge branch 'claude/agitated-hofstadter' into development 2026-03-03 18:26:31 +00:00
DorianandClaude Opus 4.6 3f9f650232 chore: prepare overnight automation 2026-03-03
Update loop scripts with rate limit handling, set plan for tonight's
run, and update prompt.md with task instructions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:24:17 +00:00
Dorian 99cb01277a chore(worktrees): update subproject commits for multiple worktrees 2026-03-03 17:48:49 +00:00
Dorian 1c5185a15c fix(app): restore PromptIndex component functionality and update launch configuration 2026-03-03 17:48:38 +00:00
Dorian 666e1232f4 fix(app): restore PromptIndex component functionality and update launch configuration 2026-03-03 17:48:31 +00:00
Dorian a817fa199f fix(app): restore PromptIndex component functionality and update launch configuration 2026-03-03 17:48:19 +00:00
Dorian e8e002debc fix(app): restore PromptIndex component functionality and update launch configuration 2026-03-03 17:48:10 +00:00
Dorian aaaef7d710 fix(app): restore PromptIndex component functionality and update launch configuration 2026-03-03 17:47:50 +00:00
Dorian d63aba788e chore(.gitignore): add loop log file to ignore list 2026-03-03 17:45:21 +00:00
DorianandClaude Opus 4.6 10e12a329f fix(app): harden brief extraction and default to fastest model
- Default chatCollapsed to true so PromptIndex shows on load
- Default AI model to claude-haiku-4.5 for faster responses
- Fix extractMagazineSections to handle numbered/bullet bold lists
  without markdown headings (e.g. "1. **Title** — content")
- Fix "For deeper coverage" stripping for bold-formatted text
- Expand hasMagazine detection with more financial/news keywords

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:53:56 +00:00
DorianandClaude Opus 4.6 721e915a48 fix(app): restore PromptIndex component and update launch config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 13:17:30 +00:00
DorianandClaude Opus 4.6 00bdc055ba feat(app): add design system viewer, nostr feed, stop generation, and content refactor
- Design system browser with grid/detail views for tokens and components
- Nostr feed tab with note/article/zap filtering and relay status
- Stop generation button to abort AI streaming mid-response
- Paste & extract content without sending to AI
- Refactor useContentPanel into contentExtraction.ts and contentFiltering.ts
- Banner fallback composable for 3-stage image loading
- Wikipedia and Google Books as fallback image sources
- Loading skeletons with variant-specific shapes
- Mobile UX: auto-switch to content, back button, detail flow
- Project grid with breadcrumb nav and inline creation
- Filesystem Vite plugin for local project browsing
- Magazine text cleanup and song grid polish

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 13:08:32 +00:00
DorianandClaude Opus 4.6 e8fc54cade feat(content): add places, code mode, mobile context tab, and detail views
- Add Places/Restaurants content type with PlaceCard, PlaceDetail, PlaceGrid
- Add WebsiteDetail and MagazineSectionDetail views for Context panel
- Enhance MagazineGrid hero with background image and 3x taller header
- Add mobile 3-tab layout (Chat, Content, Context) with detail navigation
- Add /code command system: useCodeContext composable, ProjectGrid, FileTreeNode,
  CodeDetail for IDE-style code viewing across all three panels
- Fix /code bubble and prompt index clicks to re-activate code mode
- Fix updatePanelFromText overwriting code tab by skipping command messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:31:18 +00:00
DorianandClaude Opus 4.6 48dd7a9c68 feat(layout): responsive layout with side-by-side detail and mobile tabs
Desktop (>= 1024px):
- Content grid and detail pane shown side-by-side when a detail is selected
- Three-column layout: chat | grid | detail
- Grid takes 45% width, detail fills remaining space

Mobile (< 1024px):
- Bottom tab bar with "Chat" and "Content" tabs
- Only one view visible at a time
- Auto-switches to Content tab when panel opens
- Orange dot indicator on Content tab when content is available

Extracted reusable components:
- ContentGridView: all grid renderers in one component
- DetailView: all detail views in one component
- CloseButton: reusable close/dismiss button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:41:59 +00:00
DorianandClaude Opus 4.6 7f8dc72dd7 feat(magazine): improve brief extraction with heading banners and cleaner content
- Rewrite extractMagazineSections to robustly parse ## and ### headings
  with any emoji (not just a hardcoded list)
- Strip [[podcast:...]], [[film:...]] and other content tags from magazine text
- Strip "For deeper coverage" and "Sources" sections from magazine content
- Use heading titles as banner dividers instead of repeating them on every tile
- Add group field to MagazineSection for heading-based grouping
- Strip ** markdown from both titles and content
- Enlarge "In response to" headline text to text-2xl for editorial prominence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:30:19 +00:00
DorianandClaude Opus 4.6 cddfe93c5c style(content): add distinct placeholder covers per content type
Each content type now has a unique fallback cover with fitting typography:
- Books: Georgia serif italic with spine detail
- Films: Helvetica Neue with separator line
- TV Series: SF Pro Display (new dedicated generator)
- Music: system-ui with vinyl record motif
- Podcasts: system-ui with microphone icon

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:21:45 +00:00
DorianandClaude Opus 4.6 e8ebe56d9e feat(content): add Images content type with masonry grid layout
Extract markdown images and raw image URLs from AI responses into a
browsable image gallery. Masonry (columns) layout in the grid,
full-size detail view with source links. Also fix ContextLoader to
accept all content types for loading state display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:15:02 +00:00
DorianandClaude Opus 4.6 1973b24eaa fix(content): book extraction now works when film tags are present
When the user explicitly asks about books, don't bail on pattern
extraction just because the AI also referenced films from the library.
Also fix book pattern regex to match **Title** — *Author* format,
strip sources section before extraction, and remove duplicate title
below BookGrid cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:03:29 +00:00
DorianandClaude Opus 4.6 ad63a7b1af feat(content): add TV Series content type with TMDB integration
Add complete TV series content surface: extraction from AI responses,
grid/detail views, TMDB TV search endpoint, and panel tab integration.
Includes film_ext-to-TV-series conversion for backward compatibility
and AI prompt instructions for [[tv_ext:...]] tags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:50:03 +00:00
DorianandClaude Opus 4.6 99e380e1b2 feat(content): add Books content type and ContentPanel tab system
Wire up ContentPanel with tab navigation for all existing content types
(films, songs, podcasts, news, websites, magazine). Add complete Books
pipeline: type definition, extraction (tags + pattern matching), cover
fetching from Open Library, BookCard/BookGrid/BookDetail components,
inline chat cards, and prompt index badges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:27:00 +00:00
DorianandClaude Opus 4.6 aab55ef9ca feat(chat): add collapsible prompt index and polish magazine layout
Add a toggle in the chat header to collapse messages into a compact
prompt index showing user queries with content-type badges. Clicking
a prompt surfaces the corresponding content panel. Also fixes magazine
section extraction (### headers, emoji stripping, author regex) and
improves tile layout with editorial rhythm.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:16:02 +00:00
DorianandClaude Opus 4.6 2f27cb86c4 wip: magazine tile layout, film descriptions, dev script fixes
- MagazineGrid: New Yorker tile layout with wide/half/dark/banner tiles
- Extract AI film descriptions into synopsis field
- Fix section extraction to handle ### headers
- Strip emojis from magazine titles and content
- FilmDetail: conditional synopsis with "Why watch" header
- FilmCard: synopsis preview for external films
- Consolidate dev server to single script
- Fix dev.sh for macOS bash 3.2 compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:27:51 +00:00
DorianandClaude Opus 4.6 c82b4ef54f feat(app): redesign magazine grid, improve article views and proxy
- Redesign MagazineGrid with editorial New Yorker-inspired layout
- Simplify ArticleDetail and ArticleOverlay components
- Enhance claude-proxy with improved content extraction
- Add HTML utility for content processing
- Update NewsCard styling and chat message handling
- Clean up worktree references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:50:46 +00:00
DorianandClaude Opus 4.6 b71c88f03b feat(app): update PWA icons to ✦ star design and fix chat panel default
Replace chat-bubble PWA icons with the four-pointed star (✦) used in
the interface. Fix panelSide default so chat appears on the left and
content surface on the right. Add CLAUDE.md project guide and
.claude/launch.json dev server config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:49:31 +00:00
Dorian 43ca3a7837 feat(playwright): integrate Playwright for end-to-end testing and update .gitignore
- Added Playwright as a development dependency for end-to-end testing.
- Updated package.json to include test scripts for Playwright.
- Enhanced .gitignore to exclude Playwright test results and cache files.
- Improved content extraction logic in various components to handle new content types.

Made-with: Cursor
2026-03-02 22:02:19 +00:00
Dorian 5414060225 feat(chat): add support for inline magazine sections and enhance content extraction
- Updated ChatMessage component to display a button for viewing brief magazine sections when available.
- Enhanced useContentPanel to extract and manage magazine sections, improving content organization.
- Refactored extractMagazineSections function to comprehensively handle various content formats, including headings and bullet points.
- Improved formatting of content to preserve bold text and newlines as paragraphs.

Made-with: Cursor
2026-03-02 21:38:01 +00:00
Dorian 2d056f9498 feat(chat): enhance chat functionality with web search and article integration
- Updated ChatMessage and ChatWindow components to support inline web search results and articles.
- Integrated new web search and RSS plugins into the chat system for real-time information retrieval.
- Enhanced useContentPanel to manage web search results alongside existing media types.
- Added ArticleOverlay component for displaying selected articles from search results.
- Improved UI elements and styles for better user interaction with web search features.

Made-with: Cursor
2026-03-02 21:29:50 +00:00
Dorian 49ec6c09b6 feat(chat): integrate podcast support into chat components
- Updated ChatMessage and ChatWindow components to handle podcast content alongside films and songs.
- Enhanced useContentPanel to extract and manage podcasts, allowing for richer media interactions.
- Added PodcastCard and PodcastGrid components for displaying podcast information.
- Improved UI elements to accommodate podcast selection and detail viewing.
- Updated styles for empty state icons and added new CSS for podcast-related elements.

Made-with: Cursor
2026-03-02 19:57:44 +00:00
Dorian 7a0e42bf63 feat(app): integrate Jamendo API and enhance UI components
- Added Jamendo API client ID to the environment configuration for music search.
- Updated pnpm lock file to include new dependencies for enhanced functionality.
- Integrated Plyr library for improved media playback experience.
- Refactored ChatHeader, ChatInput, and ChatMessage components to utilize new styles and improve user interaction.
- Enhanced CSS styles for path-glass elements to align with the new design system.

Made-with: Cursor
2026-03-02 19:37:00 +00:00
Dorian 80aeefa4bf feat(chat): enhance chat functionality with song integration and UI improvements
- Updated ChatHeader and ChatMessage components to support song selection and display.
- Enhanced useContentPanel to handle both film and song content, allowing for richer interactions.
- Improved FilmCard and added SongCard components for better media representation.
- Refactored environment variables for better configuration management.
- Updated .gitignore to include development chat history.

Made-with: Cursor
2026-03-02 18:16:04 +00:00
Dorian 6f79f0860d feat(chat): enhance ChatMessage component with context-aware film interaction
- Updated ChatMessage component to support click interactions for film context.
- Added computed property to determine if a message has associated films.
- Refactored film ID extraction to normalize film IDs for consistency.

Made-with: Cursor
2026-03-02 16:49:47 +00:00
Dorian 464069b2f2 feat(chat): enhance film integration and UI improvements
- Updated ChatMessage component to display inline film cards and added functionality for selecting films.
- Improved ChatWindow to handle film-related content and update the panel with selected films.
- Refactored useAI to streamline film recommendation prompts and context.
- Enhanced ChatPage layout for better film library access and user experience.
- Updated service worker revision for PWA improvements.

Made-with: Cursor
2026-03-02 16:48:17 +00:00
Dorian ece4b8256f feat(app): enhance theme support and improve PWA integration
- Updated the app to support light and dark themes with appropriate CSS classes.
- Enhanced PWA configuration with manifest details and caching strategies.
- Improved the chat UI with dynamic theme adjustments for various components.
- Added new meta tags for better mobile web app experience.
- Refactored environment variables to include new Anthropic token.
- Updated package dependencies for better compatibility and performance.

Made-with: Cursor
2026-03-02 16:34:44 +00:00
Dorian a5a32e3566 feat(ai): add Anthropic Claude adapter as primary AI provider
Wire up Claude Messages API with SSE streaming, supporting the
different event format (content_block_delta) vs OpenRouter's
OpenAI-compatible format. Claude is the default when its API key
is present.

- Dual provider system: Anthropic (direct) and OpenRouter
- Claude streams via content_block_delta events, OpenRouter via
  choices[0].delta.content
- Model picker dropdown in chat header (click model name to switch)
- Available models: Claude Sonnet 4, Opus 4, Haiku 3.5,
  Llama 4 Maverick/Scout (free), Mistral Small 3.1 (free)
- Sidebar status shows active provider name
- anthropic-dangerous-direct-browser-access header for browser use

Made-with: Cursor
2026-03-02 14:30:19 +00:00
Dorian 88e211c87d refactor(ui): port exact Archy glassmorphism system
Replace all glass morphism CSS with exact Archy definitions from
neode-ui/src/style.css. Every container and button class now matches
Archy pixel-for-pixel:

- glass: rgba(0,0,0,0.35), blur(18px), border rgba(255,255,255,0.18)
- glass-strong: same bg, blur(24px)
- glass-card: rgba(0,0,0,0.65), blur(18px), border-radius 1rem
- glass-button: 48px height, rgba(0,0,0,0.6), blur(18px)
- glass-button-sm: compact variant
- gradient-button: primary CTA with gradient intensification on hover
- gradient-card, gradient-card-dark, gradient-border-container
- toast-glass, nav-tab-active with CSS mask gradient border
- Archy inset highlight: inset 0 1px 0 rgba(255,255,255,0.22)
- Archy focus glow: blue box-shadow, no outline
- Archy scrollbar: gradient thumb, hidden variant
- Archy animations: fadeUpIn 900ms cubic-bezier(0.22, 1, 0.36, 1)

Updated design rules (02-tailwind-styling.mdc, 03-design-system.mdc)
to reference Archy as canonical source. Added archy-glass-system.md
to Coding Rules Project.

Made-with: Cursor
2026-03-02 14:26:22 +00:00
Dorian 27864cf92e feat(app): glassmorphism chat UI with widget embed system
- Glassmorphism chat window matching Proux design rules (glass layers,
  gradient backgrounds, glow effects, blur intensities, inner glows)
- Conversation ID displayed in header, side-switching (left/right layout)
- Chat store with Pinia: conversations, messages, streaming state
- OpenRouter AI integration with SSE streaming (Llama 4 Maverick free model)
- Chat components: ChatWindow, ChatHeader, ChatInput, ChatMessage,
  StreamingDots (typing indicator)
- Embeddable widget system: floating action button (FAB) + modal popup
  with scale-in animation, side-aware positioning
- Widget demo page (/widget-demo) showing AIUI embedded in a mock "Acme App"
  with documentation of 4 integration methods: script tag, web component,
  npm package, and iframe
- Theme composable with dark/light mode, system preference detection
- Custom CSS: glass variants (subtle/medium/strong/light/dark), glow effects,
  gradient text, animation keyframes, thin scrollbar
- Responsive: mobile-first, 44px touch targets, dvh viewport

Made-with: Cursor
2026-03-02 14:20:34 +00:00
Dorian c28e6dd811 feat: initialize AIUI monorepo with project rules and core types
Foundation for the next-generation AI content surface UI:
- 16 Cursor rules files covering philosophy, Vue conventions, design system,
  content surfaces, plugin system, AI integration, renderers, security,
  Bitcoin-only policy, dev/prod modes, accessibility, performance, animation,
  mobile UX, and git workflow
- pnpm workspaces + Turborepo monorepo (@aiui/core, @aiui/app)
- Vue 3 + Vite + TypeScript + Tailwind CSS 4
- Core type system: plugins, renderers, messages, content blocks
- Plugin registry with renderer registration
- 50 mock film fixtures with search/filter utilities
- App shell with chat page layout
- Environment config templates

Made-with: Cursor
2026-03-02 14:15:39 +00:00
444 changed files with 69115 additions and 2813 deletions
+6 -188
View File
@@ -156,86 +156,13 @@ The gate is mostly assembly, not invention:
So the new code is: the listener/redirect, the app-identification step (which app is this port?),
the login page render (app name + icon), and per-app scoping on `device_tokens`.
#### Research — StartOS: **DROPPED** (operator, 2026-08-03)
#### Research — StartOS: **NOT YET VERIFIED**
"don't need the startOS research we decided on a approach already." The umbrelOS read
plus the design decision above settled it; no further prior-art work.
### 1b. Manifest declaration of unauthenticated ports — **DONE** (`0c4826f8`, pushed)
`PortMapping` grew `auth` (`session` | `none`, defaulting to **`session`**) and
`auth_rationale`. The default is the protected one, so exposure is now something a
manifest has to ask for rather than something it gets by saying nothing.
Validation is two-sided: `auth: none` without a rationale is rejected, **and** a
rationale without `auth: none` is rejected — that combination means the author wrote an
exemption and did not get one, and shipping it silently would leave them believing
otherwise.
**17 ports across 12 apps are exempt**, each with its reason: Lightning p2p (BOLT-8
noise), LND gRPC 10009 / REST 18080 and CLN gRPC 9835 (macaroon / mutual TLS — this is
what keeps Zeus and remote wallets working), Bitcoin p2p 8333, electrum 50001, the three
Wyoming voice ports, git-over-SSH 2222, and the UDP discovery protocols (mDNS 5353,
SSDP 1900, STUN 3478). **The other 39 published ports now default to gated.**
Bitcoin RPC 8332 is deliberately *not* exempted: it is already `bind: 127.0.0.1`, so the
gate never sees it, and claiming an exemption it does not need would put a meaningless
line in the audit list. If that bind is ever dropped it fails closed.
Two corpus tests pin this: every shipped manifest must parse, and the exempt set is
frozen at 17 so the node's unauthenticated surface cannot grow by accident.
### 1c. The gate itself — **IN PROGRESS**
`core/archipelago/src/appgate/``identity.rs` (port → app id/name/icon, gated vs
exempt, re-read from manifests so a catalog refresh applies without a restart),
`mod.rs` (authorize + login page + TOTP step + reverse proxy), `listener.rs` (binds the
external addresses, sweeps every 60s).
Design points worth not re-deriving:
- **It invents no auth policy.** `verify_password`, `totp::decrypt_secret`,
`verify_code` + used-step replay protection, `SessionStore::create/create_pending/
upgrade_to_full`, and the *same* `LoginRateLimiter` instance as the JSON-RPC path.
Only the transport differs (HTML form vs JSON-RPC), because a browser being redirected
to an app cannot speak JSON-RPC. Sharing the limiter matters: otherwise an attacker
gets a fresh budget of password guesses by moving to an app port.
- **2FA is free.** A session still pending its TOTP step fails `validate()`, so the gate
rejects it without knowing anything about second factors.
- **Cookies ignore port.** The session cookie is host-only with no `Domain`, so one
sign-in covers the dashboard and every app port on the same host. The corollary is
that an app reached on a *different* host — its own onion — is a separate sign-in.
- **401, not a redirect.** A redirect to a login page is indistinguishable from the app
itself redirecting, and machine clients would follow it and parse HTML as their API
response.
- **The gate strips `Cookie` and `Authorization` before proxying.** The app has no use
for the node session and must never be in a position to log or forward it.
- **Machine clients**: `device_tokens` grew `apps: Option<Vec<String>>` and
`verify_for_app`. `None` = node-wide (what every existing companion token is —
migrating them by guessing a scope would silently revoke access nobody asked to
revoke); `Some(list)` restricts to those apps. An empty list is rejected rather than
minted, since it would read as "unrestricted" while authorising nothing.
#### ⚠️ The ordering constraint that shapes the rollout
A published container port is bound `0.0.0.0:<port>`, which claims **every** host
address. While the app holds that, the gate **cannot** bind `<lan-ip>:<port>` at all.
So the gate can only stand in front of an app whose publish has been pinned to loopback
(`bind: 127.0.0.1`) and whose container has been recreated. Gate-first is not possible;
all-apps-at-once would recreate every container on the node simultaneously.
Therefore the rollout is **per app**, and the gate is built to be honest about being
partially deployed: a port it cannot claim is logged at **warn** every sweep and recorded
in `GateStatus::unprotected`. The failure mode this exists to prevent is a gate that
binds nothing, logs at debug, and reports success while every app stays exactly as open
as before — worse than no gate, because it stops anyone looking. (Same reasoning that
killed the nft drop-in: `/etc/fips/fips.nft` is provisioned out-of-band and its absence
is a silent no-op.)
**Still open on this item:** pin the 39 gated ports to loopback app-by-app, repoint
`HiddenServicePort` at the gate (Tor connects *from* loopback, so a loopback-exempt
redirect will not catch it, and the mapping loses the original destination port), gate
the FIPS relay path, surface `GateStatus` in the UI, and verify on a real node.
Their public docs cover the *addressing* model (per-service `.onion` and `.local`
addresses, an explicit "make public" opt-in for clearnet) but do not state whether a
universal auth layer sits in front of service interfaces, and the source could not be
read from this box (`gh` is not installed, and raw GitHub paths 404'd). **Do not assume
they delegate auth to each service — read `Start9Labs/start-os` before designing.**
### 2. Filebrowser ships an insecure default login — **OPEN**
- Change the default credential **without breaking the dashboard's Cloud view**, which
@@ -362,51 +289,6 @@ The whole update *pipeline* is built and is already independent of OTA:
3. **The detail-page affordance** — same treatment as the card.
4. **Button copy**: "See update" rather than "Update".
### 6b. Multiversion for ALL apps + upstream release discovery — **OPEN** (operator, 2026-08-03)
> "we also need a way to provide multiversion support for all apps and it automatically
> pulls the latest versions from the source app repository, safely, and the user can
> choose to update so we aren't always updating manually"
#### Verified 2026-08-03: the schema and runtime already exist
This is much less work than it sounds, because the multiversion machinery built for
Bitcoin generalises as data rather than code:
- `releases/app-catalog.json` entries already support a `versions[]` array of
`{version, image, default?, deprecated?}`.
- Runtime is complete: `catalog_versions(app_id)`, `catalog_default_version`,
`catalog_image_for_version`, `package.versions`, version pinning through
`package.set-config`, and `available_update_for_app` falling back to the
`image-versions.sh` baseline pin.
**It is populated for 2 of 66 apps**`bitcoin-core` (9 versions) and `bitcoin-knots`
(5). Every other app carries a single `version`. So "multiversion for all apps" is
primarily a **catalog-generation and image-mirroring job**, not new runtime plumbing.
#### What has to be built
1. **Populate `versions[]` fleet-wide.** Extend `scripts/generate-app-catalog.py` to emit
a version list per app instead of a single pin. Needs a per-app policy for how many
historical versions to carry and which is `default` (Bitcoin's list shows the shape,
including `deprecated: true` for old-but-installable).
2. **Mirror the images.** A version in the catalog that is not in our registry is a
broken promise — `package.update` would pull and fail. Use the existing skopeo path
(`feedback_skopeo_source_selection`: prefer `.160`, concurrency ≤ 6).
3. **An upstream release-watcher.** 48 manifests already carry a `repo:` URL under
`metadata`, so there is something to poll (GitHub releases / registry tags). It runs
**off-node**, as part of catalog generation.
4. **Keep the signed catalog as the trust boundary.** This is the whole of "safely":
the watcher **proposes** versions, the offline signing ceremony **admits** them, and
nodes only ever install what the signed catalog carries. A node must never pull
straight from an upstream repo — that would put an unsigned third party inside the
supply chain, which is exactly what the signed-registry model exists to prevent.
5. **The user chooses.** Discovery must never auto-apply. `package.check-updates` already
refreshes and hot-reloads without touching the running containers, so "a new version
exists" and "install it" stay separate — which is also what item 6's modal is for.
**Sequencing note:** 6b's step 1 and item 6's UI-vs-app classification want the same
thing — `*-ui` images represented in the catalog. Doing that once unblocks both.
---
## P2 — Carried over from v1.7.120
@@ -443,70 +325,6 @@ below is dead on every path. Pre-existing; spotted in the v1.7.120 build warning
---
## STATUS 2026-08-04 — what shipped in 1.7.121 and what did not
### Shipped (committed + pushed)
| Item | Commit | Verified |
|---|---|---|
| 3. Federation trust escalation | `c0cfc72a` | 42/42 federation tests |
| 3b. Trusted requires node password | `24ce8b39` | 44/44 + 79/79 + vue-tsc |
| 4. lnd-ui OTA pin + host networking | `5088aef5` | — |
| 1b. Manifest `auth:` declarations | `0c4826f8` | 73/73, all 56 manifests parse |
| 1c. App gate (engine + audit) | `0de67ca6` | 23/23 appgate |
| Dashboard backdrop-filter seam | `63d0183d` | 3/3, **live on archi-dev-box** |
| 7. Release refuses unsigned manifest | `cc9e1958` | dry-run: signed/stripped/wrong-signer |
| Gate safety model (`Option<PortAuth>`) | `ab2c8b6e` | 75/75 incl. LND wallet-port case |
| Companion rebuild-loop | `719446c0` | podman behaviour proven first |
| 5. Federated peers messageable | `edc9a172` | predicate pinned across device types |
### The two gate incidents — read before touching the gate again
Both were ONE mistake: a safety decision read an ABSENT manifest field as a
value. A node's installed manifests always lag the binary, so "absent" is the
normal state, and the daemon acted on instructions no manifest ever gave.
1. Gating any `session` port regardless of `bind` **published Bitcoin's
loopback-only RPC 8332 on the LAN/Tailscale/IPv6** within seconds of deploy.
2. The `bind`-keyed replacement looked safe (it protected `bind: 127.0.0.1`)
but LND's gRPC 10009 / REST 18080 carry an EMPTY bind — one container
recreate from pinning them to loopback and **breaking Zeus and every remote
wallet**.
Now structural: `auth_policy()` classifies (undeclared → reported as
unprotected, always safe), `auth_is_declared()` gates action (undeclared →
never acted on). **Silence is not consent.**
### Proven on the node, empirically, not by reasoning
- Gate challenge → login → proxy works end to end over LAN and Tailscale.
- **Daemon-side publish rewriting was removed.** Publishes are built in several
places (`podman_client`, `package::install`, `stacks`); patching one covered
one — the strfry recreate went through another and the pin never fired.
- **Disk manifest edits do not apply to catalog-covered apps.** Even
`bind: 127.0.0.1` written into the node's strfry manifest was overridden by
the signed catalog. The catalog re-sign is REQUIRED; there is no shortcut.
- A loopback-bound host port is **unreachable** from a pasta container, so
loopback-pinning the Wyoming ports would break Home Assistant voice.
### Open for 1.7.122
1. **Catalog re-sign**`bind: 127.0.0.1` + `auth: session` on the ~39 gated
UI ports. This is what turns the gate from auditing into enforcing. Nothing
in code can substitute for it.
2. **Release-root rotation** — branch `rotate-release-root`, key
`did:key:z6Mkfu5LT…DLWT` / `1578adcc…4418`, validated as a real curve point.
**Sign the rotation release with the OLD key**; only the release after it
uses the new one. Re-sign the catalog too.
3. **Wyoming voice ports** (10200/10300/10400) — unauthenticated, and by the
operator's policy they should not be. Correct fix is co-locating Home
Assistant with the pine services on one container network so nothing is
published; needs a node running both.
4. **Item 2** filebrowser default login. **Items 6/6b** app updates +
multiversion (`versions[]` already exists, populated for 2 of 66 apps).
5. **`cargo-test-weekly` times out** at its 1500s cap on a loaded box — raise
the cap or split the stage; it is not a code failure.
## RESUME HERE — next session
**Landed this session (both pushed):**
-11
View File
@@ -1,16 +1,5 @@
# Changelog
## v1.7.121-alpha (2026-08-04)
- **Making another node "Trusted" now asks for your node password.** Trust was being handed out by machines rather than by you: any node able to reach yours could join and mark itself Trusted, because the check proved only that the caller owned the key it had just presented — never that you had approved it. Trust also spread on its own, since every peer a Trusted node advertised was added as Trusted too, so one grant quietly propagated across the whole federation. Uninvited joins are now capped at Observer, advertised peers arrive as Observers, and raising anyone to Trusted — whether by generating an invite or by changing the dropdown on a node — requires your password. Lowering trust deliberately does not, because the safe action must never be the inconvenient one. Existing peers are left exactly as they are rather than silently demoted, and each one now records how its trust was granted so you can review them.
- **Nodes you have peered with can be messaged straight away.** Peering was not enough: you also had to be within LoRa radio range of the other node once before chat would work. The node picked how to send a message based on which radio was plugged in, and only one of those paths knew how to reach a peer over the mesh's internet transports — so on a node with a different radio, or no radio at all, messaging a peer you had just federated with simply failed until a radio contact happened to appear. Peered nodes are reachable without radio by definition, so that choice no longer depends on the hardware. Radio is still preferred when the other node is actually in range and the message fits.
- The dashboard no longer flickers a vertical line across its cards. A rendering seam appeared at random while moving the mouse, because the two large cards used a background-blur effect that this system already disables everywhere else on the dashboard — that browser mis-draws it inside the dashboard's animated container, and these two cards had been missed when the workaround was written. Diagnosed from a single screenshot rather than by trying to reproduce it.
- The Lightning screen will actually update from now on. Its image was set to "latest", and the container system will not re-fetch a label it already holds, so nodes kept the same Lightning screen forever no matter how many updates shipped. A separate copy of the same setting used only by brand-new installs also described the screen incorrectly, so fresh installs got a screen that never answered.
- Apps that provide their own screens stop rebuilding themselves in a loop. On this system's own node one of them rebuilt every thirty-five seconds indefinitely, burning processor time and restarting the app each round. The node decided a rebuild was needed by comparing file dates against the image's creation date, but a rebuild that changes nothing reuses the existing image and leaves that date untouched — so the condition that triggered the rebuild was still true afterwards, forever. Nodes taking this update repair themselves the first time they check.
- Groundwork you can see but that does not change access yet: the node can now tell you which of its app ports answer without a login, and every port that is deliberately open — Bitcoin's peer connections for syncing the chain, Lightning's wallet connections, the Electrum wallet protocol — now has to state in writing why it is safe, so the list of exceptions is something you can read rather than something you have to discover. The login gate that will sit in front of the rest is built and proven working end to end on a real node, but it is not yet closing any ports; that arrives with the signed app catalog that tells each app to hand its address over.
- Releases can no longer ship an unsigned update file. Signing was skippable, and when it was skipped the release was still committed and tagged — producing an update that every node correctly refuses to install. It had been caught by hand every cycle; now the release simply stops.
- Known gaps, disclosed rather than buried: the 5x real-node lifecycle gate was not run for this release. App ports other than the deliberate exceptions above are still reachable without a login — the gate reports them, and closing them needs the next signed catalog. Three voice-assistant ports are open without authentication and should not be; the correct fix puts them on a private network with the assistant instead, which needs testing on a node that runs both. Two nodes on the fleet still share SSH host keys (detection shipped, rotation remains a deliberate operator decision).
## v1.7.120-alpha (2026-08-02)
- **Security, and the reason to take this update: two ports on your node handed anyone who could reach them complete control of your money, with no password.** The Lightning app's port answered a plain web request with the LND admin macaroon, the TLS certificate and the node's onion address — everything needed to drain the wallet remotely, and the onion meant an attacker kept that ability even after losing access to your network. The Bitcoin app's port reached Bitcoin Core's control interface using credentials the node itself supplied on the caller's behalf, with a wallet loaded. Anything on your home network, your Tailscale network or the mesh could use either one. Both now require you to be logged in. If your node has been reachable by anyone you do not fully trust, treat the Lightning macaroon and the Bitcoin RPC password as known to them.
+71
View File
@@ -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
+75
View File
@@ -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))
"
+85
View File
@@ -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
+12
View File
@@ -0,0 +1,12 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "app",
"runtimeExecutable": "bash",
"runtimeArgs": ["packages/app/scripts/dev.sh"],
"port": 5173,
"autoPort": true
}
]
}
+61
View File
@@ -0,0 +1,61 @@
# AIUI Project Memory
## Session Startup
1. Run `preview_start` with name `"app"` immediately — runs both Vite (:5173) + Claude proxy (:3141) via `packages/app/scripts/dev.sh`
2. Always commit work before ending a session
3. Work on `development` branch, merge to `main` only when production ready
## User Preferences
- NO worktrees, NO temporary branches — just `development` and `main`
- Always use combined dev script (proxy + frontend), never bare `vite`
- Commit frequently to avoid losing work
## Current State (2026-03-04)
- Branch: `overnight/2026-03-03`, all committed and pushed to remote (git.tx1138.com)
- Typecheck passes clean
## What's Been Built
- Chat: AI streaming with stop generation, web search, article integration, paste & extract
- Content panel tabs: Films, Music, Magazine, News, Books, TV Series, Images, Places, Code, Design System, Nostr, **Apps**
- Detail views for each content type (side-by-side desktop, overlay mobile)
- **Apps tab**: curated DB of ~30 Nostr/Bitcoin apps, AppsGrid + AppDetail with search/category filtering/how-to
- Design system viewer (grid + detail) for tokens, colors, typography, components
- Nostr feed scaffold with note/article/zap filtering
- Content extraction: contentExtraction.ts + contentFiltering.ts (overhauled classifiers)
- **Bare domain extraction** from AI text (e.g. "check out damus.io")
- Banner fallback composable (primary → API → gradient)
- Image fallbacks: Wikipedia + Google Books sources
- Loading skeletons per content type variant
- Project grid with breadcrumb nav and inline creation
- Filesystem Vite plugin for local project browsing
- PWA with star icon, TMDB proxy, Jamendo for music
- **Slash command palette**: /code, /nostr, /design, /search show in palette with auto-send
- **Chat action buttons**: wrapped in glass container (backdrop blur, border, shadow)
- **Settings modal**: Memory + Advanced Settings via gear icon
- **Chat history**: dedicated clock icon button
- **Web search**: Brave API primary, SearXNG rotation fallback, DuckDuckGo fallback
- iOS HIG mobile UX rules in `.cursor/rules/15-mobile-ux.mdc`
## Key Files
- Dev script: `packages/app/scripts/dev.sh`
- Launch config: `.claude/launch.json` (name: "app")
- Main page: `packages/app/src/pages/ChatPage.vue`
- Content panel: `packages/app/src/components/content/ContentPanel.vue`
- Content grids: `packages/app/src/components/content/*Grid.vue`
- Detail views: `packages/app/src/components/content/*Detail.vue`
- **Apps**: `packages/app/src/data/apps.ts` (curated DB), `AppsGrid.vue`, `AppDetail.vue`
- AI composable: `packages/app/src/composables/useAI.ts`
- Content extraction: `packages/app/src/composables/contentExtraction.ts`
- Content filtering: `packages/app/src/composables/contentFiltering.ts`
- Content panel logic: `packages/app/src/composables/useContentPanel.ts`
- Image fallbacks: `packages/app/src/composables/useImageFallback.ts`
- Banner fallback: `packages/app/src/composables/useBannerFallback.ts`
- Chat input: `packages/app/src/components/chat/ChatInput.vue`
- Prompt palette: `packages/app/src/components/chat/PromptPalette.vue`
- Chat message: `packages/app/src/components/chat/ChatMessage.vue`
- Settings modal: `packages/app/src/components/chat/SettingsModal.vue`
- Web search plugin: `packages/app/vite-web-search.ts`
- Prompt templates store: `packages/app/src/stores/promptTemplates.ts`
## Recent Session Work (2026-03-04)
See `session-2026-03-04.md` for details.
+18
View File
@@ -0,0 +1,18 @@
# Code Mode UI — Future Work
## After content surfacing is complete, implement:
### 1. Code Mode Visual Treatment
- Colour the message container in orange (`#F7931A`) styling when in code mode
- Change header text from "Message AIUI" to "Code"
- Visual signal so user knows they're in coding context
### 2. Design System Context Selection
- All design system items should be selectable with a cursor/pointer icon on hover
- Selecting a design system item provides that UI context to the code generation
- Think of it as "code with this component/token in mind"
### 3. File Browser / Open File Context
- File browser or open file in the content panel
- Selected files provide context for coding
- Pairs with the design system selection — user picks UI + files as coding context
+66
View File
@@ -0,0 +1,66 @@
# Session 2026-03-04
## Completed This Session
### 1. Chat UX Changes
- **History button**: Changed from title-click dropdown to dedicated clock icon in ChatHeader
- **Settings modal**: Created `SettingsModal.vue` — Memory + Advanced Settings behind gear icon, glass-card with backdrop blur
- **PromptIndex fix**: Reverted to original behavior (current conversation only), fixed broken v-if/v-else chain where StreamingDots broke the template chain
- **Chat action buttons**: Wrapped hover icons in proper glass container (`bg-black/60 backdrop-blur-md border border-white/10`) with divider between actions and thumbs
### 2. iOS HIG Integration
- Created `.cursor/rules/15-mobile-ux.mdc` with comprehensive iOS HIG values
- Updated CLAUDE.md Mobile UX section
### 3. Web Search Fix
- All SearXNG instances were returning 429, DuckDuckGo rate-limiting
- Added Brave Search API as primary backend (`BRAVE_SEARCH_API_KEY` env var)
- Expanded SearXNG pool to 8 instances with rotation
- Added HTML response guard for captcha pages
### 4. Content Detection Overhaul (MAJOR)
- **Expanded all classifiers** in `contentFiltering.ts`: isNewsQuery, isMusicQuery, isBookQuery, isTVQuery, isPlaceQuery, isWebsitesQuery + response variants
- **Added Nostr detection**: `isNostrQuery()`, `isNostrLikeResponse()`
- **Added App detection**: `isAppQuery()`, `isAppLikeResponse()`
- **Updated `filterTabsByContext()`**: new `hasNostr` + `hasApps` params, nostr/app query priority
- **Updated `preferredFirstTab()`**: nostr + app checks
### 5. Bare Domain Extraction
- `extractBareDomainLinks(text)` in contentExtraction.ts
- Detects plain domains like "damus.io" not inside markdown/bold/URL patterns
- Known TLDs whitelist, file extension blacklist
### 6. Apps Tab (NEW FEATURE)
- **Database**: `packages/app/src/data/apps.ts` — AppEntry interface, ~30 curated apps
- Nostr clients: Damus, Primal, Snort, Amethyst, Coracle, Iris, noStrudel
- Lightning wallets: Phoenix, Breez, Zeus, Alby, Mutiny, WoS
- Bitcoin wallets: Sparrow, BlueWallet, Nunchuk, Coldcard
- Privacy: SimpleX Chat, Signal, Mullvad VPN
- Node software: Start9, Umbrel, RaspiBlitz, myNode
- Dev tools: NDK, nostr-tools, Nak
- **Extraction**: `extractApps(text, userQuery)` — keyword matching against DB, surfaces with 1+ match for app/nostr/known-app queries, 2+ for general
- **UI**: `AppsGrid.vue` (list with search/category filter), `AppDetail.vue` (gradient header, how-to steps, related apps, external link)
- **Wired in**: useContentPanel.ts (panelApps ref, selectedApp, open/close), ContentPanel.vue (registered), PromptIndex badges
### 7. Slash Command Palette
- `/code`, `/nostr`, `/design`, `/search` appear as commands in PromptPalette
- Commands section above Templates section with `/slash` prefix styling
- Auto-send on select (except `/search` which sets text for query input)
- 8px side margins (`left-2 right-2`), no max-height scroll limit
- `ChatInput.vue`: simplified `isPaletteMode` — no longer excludes command names
### 8. App Detection Fix
- Queries mentioning known app names (e.g. "start9") now match via `queryMatchesApp` check
- Previously required explicit app/nostr query patterns like "what app" or "best wallet"
## Known Issues / TODO for Next Session
- User reported "start9" search shows Brief but Apps tab was empty — FIXED in last commit
- The `/design` command was added to palette and ChatWindow handleSend
- Consider adding more apps to the curated database over time
- The plan file is at `.claude/plans/content-detection-overhaul.md` (all steps complete)
## Git State
- Branch: `overnight/2026-03-03`
- Latest commit: `f346992` — feat(chat): slash command palette, action button containers, app detection fix
- Previous commit: `84ccdc7` — feat(app): content detection overhaul, apps tab, chat UX, web search
- All pushed to origin
@@ -0,0 +1,160 @@
# Plan: Overhaul Content Detection + Add Apps Tab
## Context
The content surfacing system misses many common AI response patterns. Example: AI responds about Nostr (mentioning damus.io, primal.net, snort.social) but the Nostr tab never surfaces. Query/response classifiers use narrow regexes that miss natural language variations. There's no "topic detection" layer, no app detection, and bare domains in AI text aren't extracted as websites.
**Goals:**
1. Fix content detection to handle how AIs actually respond
2. Add Nostr tab surfacing (currently only via `/nostr` command)
3. Add Apps tab with curated Nostr + Bitcoin ecosystem apps (local DB + AI extraction fallback)
4. Extract bare domains from AI text (e.g. "check out damus.io")
---
## Part 1: Expand Query & Response Classifiers
**File:** `packages/app/src/composables/contentFiltering.ts`
### 1A. Add Nostr classifiers (new functions)
- `isNostrQuery(q)` — matches: nostr, npub, nip-\d, damus, primal, snort, amethyst, coracle, zap, relay, note1, nevent, nprofile, fiatjaf, nostrich, "decentralized social"
- `isNostrLikeResponse(text)` — requires literal "nostr" OR 2+ Nostr-specific signals (npub, nip-, client names, relay+wss, zap+lightning)
### 1B. Add App classifiers (new functions)
- `isAppQuery(q)` — matches: app, client, wallet, tool, software, download, install, "what app", "best app for", "recommend.*app"
- `isAppLikeResponse(text)` — matches: "you can use", "popular clients include", "I'd recommend", "available on", "download from"
### 1C. Expand existing classifiers with broader patterns
| Classifier | Add these patterns |
|---|---|
| `isNewsQuery` | "what happened today", "any updates on", "trending", "catch me up", "brief me", "current events" |
| `isMusicQuery` | "genre", "spotify", "bandcamp", "grammys", "billboard", "mixtape", "discography", "banger", "favorite jam" |
| `isBookQuery` | "what should I read", "favorite reads", "reading list", "book club", "memoir", "audiobook", "goodreads", "worth reading" |
| `isTVQuery` | "what's good on netflix", "anything to binge", "hbo", "disney+", "apple tv", "amazon prime", "docuseries", "limited series" |
| `isPlaceQuery` | "hungry", "food near me", "best brunch spot", "happy hour", "speakeasy", "rooftop bar", "food truck" |
| `isWebsitesQuery` | "point me to", "link me", "any good sites", "tools for", "platforms for" |
| `isWebsitesLikeResponse` | "here are some resources", "I'd recommend checking", "you can visit", "useful resources" |
| `isNewsLikeResponse` | "I can't access the web but", "having trouble reaching", "unable to browse but" |
### 1D. Update `preferredFirstTab()` — add nostr + app checks
### 1E. Update `filterTabsByContext()` — add `hasNostr` and `hasApps` params, integrate into tab ordering
---
## Part 2: Bare Domain Extraction
**File:** `packages/app/src/composables/contentExtraction.ts`
Add `extractBareDomainLinks(text)`:
- Detect plain-text domains like "damus.io", "primal.net" not inside markdown links or bold patterns
- Skip positions covered by existing extractors (markdown links, bold-domain, full URLs)
- Require known TLDs (.com, .org, .io, .net, .social, .app, etc.)
- Block file extensions (.js, .ts, .vue, .json, .css)
- Use existing `normUrl()` for dedup
---
## Part 3: Apps Tab — Curated Database + AI Extraction
### 3A. Create app database
**New file:** `packages/app/src/data/apps.ts`
```ts
interface AppEntry {
id: string
name: string
description: string // One-liner
longDescription: string // Why use this, how it works
category: 'nostr-client' | 'lightning-wallet' | 'bitcoin-wallet' | 'privacy' | 'node' | 'dev-tool' | 'relay'
platforms: ('ios' | 'android' | 'web' | 'desktop' | 'cli' | 'nodeos')[]
url: string
icon?: string
keywords: string[] // For matching AI responses
howTo?: string[] // Getting started steps
relatedApps?: string[] // IDs of related apps
}
```
**Initial curated apps (~25-30):**
- Nostr clients: Damus, Primal, Snort, Amethyst, Coracle, Iris, Nostrudel, nos.social
- Lightning wallets: Phoenix, Mutiny, Breez, Zeus, Alby, Wallet of Satoshi
- Bitcoin wallets: Sparrow, Blue Wallet, Nunchuk, Coldcard, Green
- Privacy tools: Tor, SimpleX Chat, Signal, Mullvad VPN
- Node software: Start9, Umbrel, RaspiBlitz, myNode
- Dev tools: NDK, nostr-tools, Nak
### 3B. Add app extraction
**File:** `packages/app/src/composables/contentExtraction.ts`
Add `extractApps(text, userQuery)`:
1. Match AI text against known app names/keywords from database
2. If app query detected OR 2+ known apps mentioned → return matched apps
3. For unknown apps, create basic entries from context (name + URL if bare domain found)
### 3C. Create UI components
**New files:**
- `packages/app/src/components/content/AppsGrid.vue` — Grid of app cards (icon, name, category badge, one-liner)
- `packages/app/src/components/content/AppDetail.vue` — Detail: icon, name, platforms, long description, how-to steps, link, related apps
Follow existing grid/detail patterns (e.g. `BookGrid.vue`/`BookDetail.vue`).
### 3D. Register in ContentPanel.vue
Add rendering for `activeTab === 'app'`, add `'app'` to `ContentTab` type.
---
## Part 4: Wire Everything Together
**File:** `packages/app/src/composables/useContentPanel.ts`
In `updatePanelFromText()`:
- Call `extractBareDomainLinks(text)`, merge with website sources
- Call `extractApps(text, userQuery)`
- Compute `hasNostr = isNostrQuery(userQuery) || isNostrLikeResponse(text)`
- Compute `hasApps = apps.length > 0`
- Pass `hasNostr` and `hasApps` to `filterTabsByContext()`
- Add `panelApps` ref, title logic for apps/nostr tabs
Same changes in `getContextualInlineContent()`.
Broaden magazine detection: add tech/protocol keywords, surface magazine for 3+ sections with no other structured content.
---
## Part 5: PromptIndex badges
**File:** `packages/app/src/components/chat/PromptIndex.vue`
Add 'Nostr' and 'Apps' badge detection.
---
## Implementation Order
1. `contentFiltering.ts` — classifiers + filterTabsByContext signature
2. `contentExtraction.ts``extractBareDomainLinks()` + `extractApps()`
3. `data/apps.ts` — curated app database
4. `useContentPanel.ts` — wire everything
5. `AppsGrid.vue` + `AppDetail.vue` — UI components
6. `ContentPanel.vue` — register tab + components
7. `PromptIndex.vue` — badges
8. Typecheck + manual test
## Verification
1. `pnpm typecheck` passes
2. "tell me about Nostr" → Nostr + magazine tabs surface
3. "best Nostr clients?" → Apps tab with Damus, Primal, Snort
4. "recommend a bitcoin wallet" → Apps tab with Phoenix, Sparrow
5. "what happened with BIP 110?" → Magazine tab (regression)
6. "best movies of 2024" → Films tab (regression)
7. Bare domains in AI text extracted as websites
8. PromptIndex badges show Nostr/Apps
@@ -0,0 +1,74 @@
# Plan: Code Mode UI — Orange Input, Design System Context, File Browser Context
## Context
The user wants three connected features that enhance the coding experience in AIUI:
1. Visual indication when in code mode (orange input container, "Code" label)
2. Ability to select design system items as coding context
3. Ability to select files from file browser as coding context
After this, the user wants to circle back and create a flawless version of content extraction/tab surfacing.
## Changes
### 1. Orange Code Mode Input Container
**Files**: `ChatWindow.vue`, `ChatInput.vue`
**ChatWindow.vue** (line 106-115):
- Pass `activeTab` to ChatInput as a prop: `:active-tab="activeTab"`
- Change placeholder logic: `activeTab === 'code' ? 'Code...' : isStreaming ? 'Waiting for response...' : 'Message AIUI...'`
**ChatInput.vue**:
- Add `activeTab` prop (optional string, default `''`)
- Conditionally style the container div (line 79-81):
- When `activeTab === 'code'`: use `bg-accent/15 border border-accent/25 backdrop-blur-xl` instead of `path-glass-bubble`
- Keep the `rounded-2xl px-4 py-3 flex items-end gap-2 transition-all duration-300` classes
- Conditionally style the send button orange when in code mode
### 2. Design System Item Selection for Coding Context
**Files**: `useCodeContext.ts`, `DesignSystemGrid.vue`
**useCodeContext.ts**:
- Add `selectedDesignTokens: ref<string[]>([])` to module state (stores item IDs)
- Add `toggleDesignToken(id)` — adds/removes from selection array
- Add `clearDesignTokens()` — clears selection
- Add `isDesignTokenSelected(id)` — checks if item is in selection
- Clear on `exitCodeMode()`
- Export all new state/actions
**DesignSystemGrid.vue**:
- Import `useCodeContext`
- When `codeMode` is true, show a selection indicator (accent ring + checkmark) on items
- `selectItem` should call `toggleDesignToken(item.id)` when in code mode (instead of `openDesignSystemItem`)
- When NOT in code mode, keep existing behavior (open detail view)
- Selected items get `ring-2 ring-accent/50 bg-accent/10` styling
### 3. File Browser Selection for Coding Context
**Files**: `useCodeContext.ts`, `ProjectGrid.vue`
**useCodeContext.ts**:
- Add `selectedFiles: ref<string[]>([])` — paths of files selected for context
- Add `toggleFileSelection(path)` — adds/removes from selection
- Add `clearFileSelection()` — clears all
- Add `isFileSelected(path)` — checks if file in selection
- Clear on `exitCodeMode()`
- Export new state/actions
**ProjectGrid.vue**:
- Import `useCodeContext`
- When `codeMode` is true, file clicks toggle selection instead of (or in addition to) opening
- Show visual selection state (accent highlight/checkmark) on selected files in FileTreeNode
## Files to Modify
1. `packages/app/src/components/chat/ChatWindow.vue` — pass activeTab prop
2. `packages/app/src/components/chat/ChatInput.vue` — conditional orange styling + "Code" placeholder
3. `packages/app/src/composables/useCodeContext.ts` — add design token + file selection state
4. `packages/app/src/components/content/DesignSystemGrid.vue` — toggle selection in code mode
5. `packages/app/src/components/content/ProjectGrid.vue` — toggle file selection in code mode
## Verification
1. `pnpm typecheck` — no type errors
2. `pnpm lint` — no new lint errors
3. Manual: `/code` command → input turns orange with "Code..." placeholder
4. Manual: In code mode, design system tab → clicking items toggles selection (accent ring)
5. Manual: In code mode, file browser → clicking files toggles selection
6. Manual: Exiting code mode clears all selections
+35
View File
@@ -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,43 @@
---
name: add-content-type
description: Scaffold a complete new content type (tag, extraction, grid, detail, prompt)
allowed-tools: Bash(*), Read, Edit, Write, Glob, Grep, Agent
---
Add a complete new content type to AIUI. The user will provide the content type name (e.g., "event", "product", "video").
Follow ALL steps — this is the full pipeline for a content type:
1. **Tag format**: Add a new `[[{type}_ext:Field1|Field2|...]]` regex to `packages/app/src/composables/contentExtraction.ts` alongside the existing ones (FILM_EXT_RE, SONG_EXT_RE, etc.)
2. **Type definition**: Add the TypeScript interface to `packages/core/src/types/content.ts` if it doesn't exist
3. **Extraction function**: Add `extractAll{Type}s(text, userQuery)` to `contentExtraction.ts` following the pattern of `extractAllFilms` or `extractAllBooks`
4. **Strip tags function**: Add `strip{Type}Tags()` and include it in `stripContentTags()`
5. **Query classifier**: Add `is{Type}Query()` and optionally `is{Type}LikeResponse()` to `contentFiltering.ts`
6. **ContentTab type**: Add the new tab name to the `ContentTab` union in `contentFiltering.ts`
7. **Tab filtering**: Update `filterTabsByContext()` and `preferredFirstTab()` in `contentFiltering.ts`
8. **Grid component**: Create `packages/app/src/components/content/{Type}Grid.vue` following the glass-morphism pattern of existing grids (BookGrid.vue is a good template)
9. **Detail component**: Create `packages/app/src/components/content/{Type}Detail.vue` following the pattern of BookDetail.vue
10. **Wire into ContentPanel.vue**: Add import, grid render block, detail render block, and panel state refs
11. **Wire into ContentGridView.vue**: Add import, props, and grid render block
12. **Wire into ChatPage.vue**: Pass the new panel data as props to ContentGridView
13. **Wire into useContentPanel.ts**: Add panel ref, selected ref, open/close functions, extraction call in `updatePanelFromText()`
14. **System prompt**: Add tag format instructions to the `SYSTEM_PROMPT` in `useAI.ts`
15. **Tab label**: Add to `TAB_LABELS` in `ContentPanel.vue`
16. **Verify**: Run `pnpm typecheck` and fix any errors
Report what was created and the tag format to use.
+32
View File
@@ -0,0 +1,32 @@
---
name: add-tool
description: Add a new AI tool (function call) to the Claude proxy for the AI to use
allowed-tools: Bash(*), Read, Edit, Write, Glob, Grep
---
Add a new tool that the AI model can call via Claude's tool_use API. The user will describe what the tool should do (e.g., "search local files", "get app status", "browse media").
## Steps
1. **Read the proxy**: Read `packages/app/server/claude-proxy.ts` to understand the existing tool_use loop and `SEARCH_WEB_TOOL` definition.
2. **Define the tool**: Add a new tool definition following the Claude tool_use format:
```ts
const NEW_TOOL = {
name: 'tool_name',
description: 'What this tool does...',
input_schema: {
type: 'object',
properties: { ... },
required: [...]
}
}
```
3. **Add handler**: In the tool_use loop (where `search_web` calls are handled), add a handler for the new tool name.
4. **Implement backend**: If the tool needs a new API endpoint (e.g., `/api/media/scan`), create a Vite plugin or add a route to the proxy.
5. **Update system prompt**: Add instructions in `useAI.ts` SYSTEM_PROMPT telling the AI when and how to use the new tool.
6. **Verify**: Run `pnpm typecheck` and test the proxy starts without errors.
@@ -0,0 +1,37 @@
---
name: audit-prompts
description: Deep audit of AI system prompts — find gaps, test extraction coverage, verify tag formats
allowed-tools: Bash(*), Read, Glob, Grep, Agent
---
Perform a comprehensive audit of AIUI's AI prompt system. Do NOT make changes — report findings only.
## Steps
1. **Read the full system prompt**: Read `packages/app/src/composables/useAI.ts` and reconstruct the complete system prompt including all dynamic sections (persona, Wavlake, memory, web search, Archy context, code context).
2. **Catalog all tag formats**: List every `[[type:...]]` and `[[type_ext:...]]` format defined in the prompt. Cross-reference with regexes in `contentExtraction.ts`.
3. **Check for gaps**: For each content type in `ContentTab` (contentFiltering.ts), verify:
- Is there a tag format in the system prompt?
- Is there a matching extraction regex?
- Is there a query classifier?
- Is there a grid + detail component?
- Is the tab wired in ContentPanel.vue and ContentGridView.vue?
4. **Test extraction coverage**: Read the seed prompts in `src/__tests__/fixtures/seedPrompts.ts`. For each seed, verify:
- Does the extraction function find the expected number of items?
- Are there edge cases that would break extraction?
5. **Analyze prompt quality**: Check for:
- Conflicting instructions
- Missing edge case handling (e.g., "what if the AI can't find a match?")
- Overly vague instructions
- Missing content types that should have tag formats
6. **Check proxy tools**: Read `server/claude-proxy.ts` and verify tool definitions match what the prompt claims.
7. **Report**: Create a structured summary with:
- Content type coverage matrix (tag/extraction/grid/detail/prompt)
- Identified gaps and inconsistencies
- Priority recommendations
+17
View File
@@ -0,0 +1,17 @@
---
name: check
description: Run all quality checks (typecheck, lint, test) and auto-fix errors
allowed-tools: Bash(*), Read, Edit, Glob, Grep, Agent
---
Run all quality checks for the AIUI project and fix any issues found. Execute in order:
1. **TypeScript**: Run `pnpm typecheck`. If errors found, read the failing files and fix the type errors.
2. **Lint**: Run `pnpm lint`. If fixable errors, run `pnpm lint --fix` first, then fix remaining manually.
3. **Tests**: Run `pnpm --filter @aiui/app test -- --run`. For each failure:
- Read the test file and the source file it tests
- Determine if the test is wrong (outdated assertion) or the source has a bug
- Fix whichever is incorrect
4. Report a summary: pass/fail counts, what was fixed.
Important: Do NOT change test expectations just to make them pass — understand WHY they fail first.
+32
View File
@@ -0,0 +1,32 @@
---
name: deploy
description: Build and prepare AIUI for deployment to Archy node
allowed-tools: Bash(*), Read, Edit, Glob, Grep
---
Build AIUI for production deployment. Steps:
1. **Pre-flight checks**:
- `pnpm typecheck` — must pass
- `pnpm lint` — must pass
- `pnpm --filter @aiui/app test -- --run` — report failures but continue
2. **Build**:
- `pnpm build`
- Verify `packages/app/dist/` exists and contains `index.html`
3. **Bundle analysis**:
- Report total dist size and gzip estimate
- List the 5 largest chunks
- Check against 250KB gzipped budget (warn if over)
4. **Verify nginx config**:
- Read `packages/app/server/nginx-archy.conf`
- Verify SPA routing (`try_files $uri $uri/ /aiui/index.html`)
- Verify proxy paths for Claude API
5. **Container build** (if Dockerfile exists):
- `podman build -t aiui:latest packages/app/`
- Report image size
6. **Report**: Build status, bundle size, any warnings.
+33
View File
@@ -0,0 +1,33 @@
---
name: fix-tab
description: Diagnose and fix a broken content panel tab (extraction, routing, rendering)
allowed-tools: Bash(*), Read, Edit, Glob, Grep, Agent
---
Diagnose and fix a broken content panel tab. The user will specify which tab (e.g., "app", "code", "image", "nostr").
## Diagnostic pipeline — check each layer:
1. **System prompt**: Does `useAI.ts` SYSTEM_PROMPT tell the AI to use the tag format for this content type? If not, add instructions.
2. **Tag regex**: Does `contentExtraction.ts` have a regex for this content type's tags? If not, add one.
3. **Extraction function**: Does `extractAll{Type}s()` or `extract{Type}s()` exist and work correctly? Test with sample input.
4. **Query classifier**: Do `is{Type}Query()` and/or `is{Type}LikeResponse()` exist in `contentFiltering.ts`?
5. **Tab filtering**: Is the tab included in `filterTabsByContext()` and `preferredFirstTab()`? Check the boolean flag is wired.
6. **useContentPanel.ts**: Is the extraction called in `updatePanelFromText()`? Is the panel ref populated? Is the tab added to `availableTabs`?
7. **ContentPanel.vue**: Is there a grid render block for `activeTab === '{tab}'`? Are imports present? Is the tab in TAB_LABELS?
8. **ContentGridView.vue**: Same checks for the wide desktop view.
9. **ChatPage.vue**: Are the panel data props passed to ContentGridView?
10. **Grid component**: Does the grid component exist and render correctly?
11. **Detail component**: Does the detail component exist?
Fix each broken layer. Run `pnpm typecheck` after all fixes.
+32
View File
@@ -0,0 +1,32 @@
---
name: mock-archy
description: Enable/configure mock Archy data for standalone dev testing
allowed-tools: Bash(*), Read, Edit, Glob, Grep
---
Set up or modify mock Archy data for testing the Archipelago integration without a real Archy host.
## How it works
Mock data is in `packages/app/src/mocks/archy.ts`. When enabled, `useArchy.ts` loads this data instead of waiting for the Archy bridge.
## Enable mock mode
Two ways:
1. Add `VITE_MOCK_ARCHY=true` to `.env.local`
2. Add `?mockArchy` to the URL: `http://localhost:5173/?mockArchy`
## Customization
The user may ask to:
- Add/remove mock apps from the installed list
- Change wallet balance or channel count
- Add/modify files in the mock file list
- Change system info or network status
- Test specific scenarios (e.g., "node is syncing", "wallet offline", "no files")
Edit `packages/app/src/mocks/archy.ts` accordingly.
## Verify
After changes, check that `buildArchyContext()` in `useArchy.ts` produces the expected system prompt section by reading the function and tracing the mock data through it.
+27
View File
@@ -0,0 +1,27 @@
---
name: new-detail
description: Generate a detail view component following AIUI glass-morphism patterns
allowed-tools: Read, Write, Edit, Glob, Grep
---
Create a new detail view component at `packages/app/src/components/content/{Name}Detail.vue`.
## Requirements
1. **Read a reference**: Read `BookDetail.vue` or `PlaceDetail.vue` as a template.
2. **Follow conventions**:
- `<script setup lang="ts">` with single item prop
- Back button at top (emits 'back' event)
- Hero image/banner area with gradient overlay and fallback
- Title, subtitle, and metadata section
- Description/long text body with proper typography
- Action buttons (external links, share, etc.) with glass-button styling
- Dark/light mode via `useTheme()`
- Smooth scroll, overflow-y-auto
3. **Props**: Accept single item of the content type
4. **Emits**: `back` event for navigation
5. **Responsive**: Full height, works in sidebar and mobile overlay
The user will specify the content type and which fields to display.
+27
View File
@@ -0,0 +1,27 @@
---
name: new-grid
description: Generate a content grid component following AIUI glass-morphism patterns
allowed-tools: Read, Write, Edit, Glob, Grep
---
Create a new content grid component at `packages/app/src/components/content/{Name}Grid.vue`.
## Requirements
1. **Read a reference**: Read `BookGrid.vue` or `PlaceGrid.vue` as a template — they show the standard pattern.
2. **Follow conventions**:
- `<script setup lang="ts">` with props and emits
- Glass morphism styling (bg-white/5, rounded-xl, hover:bg-white/10)
- Dark/light mode support via `useTheme()`
- Search input at top (if the content type has enough items)
- Grid of cards with image fallback, title, subtitle, metadata
- Touch targets min 44x44px
- Empty state message when no items match
- Custom scrollbar class
3. **Props**: Accept array of items + title string
4. **Emits**: `select-{type}` event when a card is clicked
5. **Responsive**: Works on mobile (full width) and desktop (sidebar width)
The user will specify the content type and its fields.
+19
View File
@@ -0,0 +1,19 @@
---
name: overnight
description: Commit, branch, and start the overnight automation loop
disable-model-invocation: true
allowed-tools: Bash(*), Read, Write, Edit, Glob, Grep
---
Prepare and launch the overnight automation loop. Do ALL steps in order, stopping on any failure:
1. Stage and commit all uncommitted changes: `git add -A && git commit -m "chore: pre-overnight snapshot"` (skip if working tree is clean)
2. Push current branch to origin
3. Get today's date as YYYY-MM-DD. Check if `overnight/$DATE` branch exists:
- If yes: `git checkout overnight/$DATE`
- If no: run `./loop/prepare.sh`
4. Verify `loop/plan.md` has unchecked tasks (`grep -c '^\- \[ \]' loop/plan.md`)
5. Commit plan files if modified: `git add loop/plan.md loop/prompt.md && git commit -m "chore: overnight plan $DATE"` (skip if clean)
6. Push: `git push -u origin overnight/$DATE`
7. Start the loop: run `caffeinate -i ./loop/loop.sh` with `run_in_background: true`
8. Report: branch name, number of tasks, and confirm the loop is running in background
@@ -0,0 +1,102 @@
---
name: pwa-icon-cache-fix
description: Use when the user reports a PWA icon not updating, stale PWA icon, wrong icon after install, or any PWA caching issue. Also applies when changing PWA icons in a Vite + vite-plugin-pwa project.
version: 2.0.0
---
# PWA Icon Cache Fix
## Problem
PWA icons are cached at FOUR independent layers:
1. **Service worker cache** (Workbox precache)
2. **Browser HTTP cache**
3. **Browser manifest resources** (Chromium stores resized icons in its profile data, keyed by a permanent extension ID tied to the origin — NEVER re-fetched even after uninstall/reinstall)
4. **macOS .app bundle** (`.icns` file baked into the `.app` in `~/Applications/`)
Query string cache busting (`?v=2`) and uninstall/reinstall do NOT fix this. Chromium reuses the same extension ID for the same origin, so it keeps the old cached icons.
## Fix Steps
### 1. Verify icon files on disk and server are correct
```bash
# Visual check
Read packages/app/public/pwa-192x192.png
Read packages/app/public/pwa-512x512.png
# Hash match check
curl -s http://localhost:5173/pwa-192x192.png | md5
md5 -q packages/app/public/pwa-192x192.png
```
### 2. Find the PWA's Chromium extension ID
Read the installed `.app` bundle's `Info.plist` to get the `CrAppModeShortcutID`:
```bash
plutil -p "~/Applications/Brave Browser Apps.localized/AIUI.app/Contents/Info.plist" | grep CrAppModeShortcutID
```
This returns an ID like `idemibpphagihbobmgmaojhjfidlfpdl`.
### 3. Overwrite the cached icons in browser profile
Chromium stores resized icons at:
`~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/Manifest Resources/{ID}/Icons/`
Overwrite every size using `sips`:
```bash
ICON_DIR="~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/Manifest Resources/{ID}/Icons"
SRC="packages/app/public/pwa-512x512.png"
for size in 32 48 64 96 128 192 256 512; do
sips -z $size $size "$SRC" --out "${ICON_DIR}/${size}.png"
done
```
### 4. Rebuild the macOS .icns in the .app bundle
```bash
ICONSET="/tmp/aiui.iconset"
mkdir -p "$ICONSET"
SRC="packages/app/public/pwa-512x512.png"
sips -z 16 16 "$SRC" --out "$ICONSET/icon_16x16.png"
sips -z 32 32 "$SRC" --out "$ICONSET/icon_16x16@2x.png"
sips -z 32 32 "$SRC" --out "$ICONSET/icon_32x32.png"
sips -z 64 64 "$SRC" --out "$ICONSET/icon_32x32@2x.png"
sips -z 128 128 "$SRC" --out "$ICONSET/icon_128x128.png"
sips -z 256 256 "$SRC" --out "$ICONSET/icon_128x128@2x.png"
sips -z 256 256 "$SRC" --out "$ICONSET/icon_256x256.png"
sips -z 512 512 "$SRC" --out "$ICONSET/icon_256x256@2x.png"
sips -z 512 512 "$SRC" --out "$ICONSET/icon_512x512.png"
cp "$SRC" "$ICONSET/icon_512x512@2x.png"
iconutil -c icns "$ICONSET" -o "~/Applications/Brave Browser Apps.localized/AIUI.app/Contents/Resources/app.icns"
```
### 5. Flush macOS icon cache
```bash
touch "~/Applications/Brave Browser Apps.localized/AIUI.app"
killall Finder
killall Dock
```
### 6. Bump PWA_CACHE_VERSION in main.ts
Increment the `PWA_CACHE_VERSION` constant — this nukes all SW caches on next page load for web-layer caching.
### 7. Delete stale build artifacts
Remove old `dist/` and `dev-dist/` SW/manifest files.
## Browser-Specific Paths
- **Brave**: `~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/`
- **Chrome**: `~/Library/Application Support/Google/Chrome/Default/Web Applications/`
- **PWA apps (Brave)**: `~/Applications/Brave Browser Apps.localized/`
- **PWA apps (Chrome)**: `~/Applications/Chrome Apps.localized/`
## Key Insight
Chromium assigns a permanent extension ID per origin (e.g., `localhost:5173`). This ID persists across uninstall/reinstall. The icon cache in `Manifest Resources/{ID}/Icons/` is populated ONCE and never refreshed from the manifest. The only fix is to overwrite the files directly on disk.
+26
View File
@@ -0,0 +1,26 @@
---
name: test-prompts
description: Test AI prompt quality by simulating queries and checking extraction results
allowed-tools: Bash(*), Read, Edit, Glob, Grep, Agent
---
Test the AIUI AI prompt and content extraction pipeline end-to-end. This skill does NOT call the actual AI — it uses the seed prompts and extraction functions directly.
## Steps
1. **Read seed prompts**: Read `packages/app/src/__tests__/fixtures/seedPrompts.ts` to get all test cases.
2. **Run extraction tests**: For each seed prompt, run the test via `pnpm --filter @aiui/app test -- --run -t "seed"` and report results.
3. **Test edge cases**: Create and test these additional scenarios by calling extraction functions in a test:
- Mixed content response (films + songs + books in one response)
- App recommendation response (should trigger app tab)
- News query with web search results
- Place/restaurant recommendations
- Code response with 3+ code blocks
- Nostr-related query
- Empty/minimal response
4. **Verify tab routing**: For each scenario, check that `filterTabsByContext()` returns the expected tabs in the expected order.
5. **Report**: Summary of what works, what's broken, and what's missing. Include specific test cases that fail.
+28
View File
@@ -0,0 +1,28 @@
---
name: trace
description: End-to-end trace of a query through prompt, extraction, tabs, and rendering
allowed-tools: Bash(*), Read, Glob, Grep, Agent
---
Trace how a specific user query flows through the entire AIUI pipeline. The user will provide a sample query (e.g., "best nostr apps", "recommend some films", "bitcoin news").
## Trace each stage:
1. **Query classifiers**: Run the query through each classifier in `contentFiltering.ts`:
- `isNewsQuery()`, `isMusicQuery()`, `isBookQuery()`, `isTVQuery()`, `isImageQuery()`, `isPlaceQuery()`, `isRecipeQuery()`, `isCodeQuery()`, `isNostrQuery()`, `isAppQuery()`, `isWebsitesQuery()`
- Report which ones return true
2. **Preferred tab**: What does `preferredFirstTab()` return for this query?
3. **System prompt**: What would `buildSystemPrompt()` include? Read `useAI.ts` and trace all dynamic sections.
4. **Expected AI response**: Based on the system prompt instructions, what tags would the AI likely use? Construct a realistic sample response.
5. **Extraction**: Run the sample response through each extraction function and report what gets found:
- `extractAllFilms()`, `extractAllSongs()`, `extractAllPodcasts()`, `extractAllBooks()`, `extractAllTVSeries()`, `extractAllImages()`, `extractAllPlaces()`, `extractApps()`, `extractCodeBlocks()`, `extractRecipes()`
6. **Tab filtering**: What tabs would `filterTabsByContext()` return? In what order?
7. **Rendering**: Which grid component would render? Trace through ContentPanel.vue and/or ContentGridView.vue.
8. **Report**: Complete flow diagram showing: Query -> Classifiers -> Prompt -> Expected Response -> Extraction -> Tabs -> Grid
Submodule aiui/.claude/worktrees/agitated-hofstadter added at 10e12a329f
Submodule aiui/.claude/worktrees/funny-hofstadter added at 1c5185a15c
Submodule aiui/.claude/worktrees/happy-colden added at 666e1232f4
Submodule aiui/.claude/worktrees/hardcore-beaver added at a817fa199f
Submodule aiui/.claude/worktrees/heuristic-raman added at e8e002debc
Submodule aiui/.claude/worktrees/priceless-colden added at aaaef7d710
@@ -0,0 +1,65 @@
---
description: Core development philosophy for AIUI - the foundational rules that govern all code and design decisions
globs: "**/*"
alwaysApply: true
---
# Master Philosophy
## Mission
Build the next-generation AI content surface UI — a paradigm where AI responses are rendered as rich, interactive content, not plain text. Delivered as a reusable component library (@aiui/core) and a reference application (AIUI App).
## Philosophical Pillars
### 1. Open Source Only
Every dependency must be OSS (MIT, Apache-2.0, GPL-compatible). No proprietary SDKs, no vendor-locked services. Before adding any dependency, verify its license.
### 2. Decentralized-First
No hard dependency on any centralized service. AI backends, messaging protocols, storage, search — all connect through pluggable adapter interfaces. Users choose their own providers.
### 3. Bitcoin Only
Bitcoin is the only monetary unit. On-chain, Lightning, ecash (Cashu, Fedimint/Fedi). No fiat payment rails, no altcoins, no stablecoins — anywhere in the UI or codebase. AIUI is never a wallet and never handles funds directly. See `10-bitcoin-only.mdc` for full rules.
### 4. Cryptography for Everything Sensitive
E2E encryption for messages, encrypted local storage, proper key management. Privacy is not a feature — it is a requirement.
### 5. Mobile-First, Everywhere-Perfect
Every component works flawlessly on mobile, tablet, and desktop. Mobile is the foundation, not an afterthought. Touch targets, viewport management, and safe areas are first-class citizens.
### 6. Consistency is Sacred
Mobile and desktop versions show identical content and functionality unless explicitly designed otherwise. Design tokens ensure visual consistency across all breakpoints.
### 7. Theme-First Architecture
Theming is a core architectural decision from day one. Themes are CSS-based with reactive state management. Dark mode and light mode are equals.
### 8. Utility-First, Component-Second
Tailwind CSS utilities in templates for maximum flexibility. Component classes only for truly reusable patterns. Extract components when you repeat, not before.
### 9. Performance as a Feature
Initial load < 250KB gzipped. Lazy load everything that isn't immediately visible. CSS transforms for GPU acceleration. SVG over raster images. Code splitting by default.
### 10. Plugin-Everything
Every external integration connects through a typed plugin interface. AI providers, media sources, messaging protocols, wallets, social embeds — all pluggable.
### 11. Accessibility is Not Optional
WCAG AA compliance minimum. Keyboard navigation everywhere. Screen reader friendly. Color contrast tested and validated.
### 12. MCP-Native
First-class Model Context Protocol support for AI tool interoperability.
## Anti-Patterns to Avoid
- Desktop-first thinking
- Hardcoded values (use design tokens)
- Premature abstraction (build three times before abstracting)
- Magic numbers without comments
- Invisible state (user should always know what's happening)
- Handling funds or private keys
- Loading third-party tracking scripts
- Proprietary dependencies
## The Ultimate Goal
When someone uses AIUI, they should think: "This feels incredibly polished", "Everything just works", "My data is safe", "I control my own setup."
When a developer reads the code: "This is well organized", "I understand exactly what's happening", "Adding a new renderer is straightforward."
+84
View File
@@ -0,0 +1,84 @@
---
description: Vue 3 Composition API conventions and best practices for AIUI
globs: "**/*.vue,**/*.ts"
alwaysApply: false
---
# Vue 3 Conventions
## Composition API with `<script setup>`
Always use `<script setup lang="ts">`. Never use Options API.
## Component Organization Order
1. Imports — external, then internal
2. Props — with TypeScript-style validation
3. Emits — explicitly defined
4. State (refs and reactive)
5. Computed — derived values, always pure
6. Watchers — side effects only
7. Methods — business logic
8. Lifecycle hooks — ordered by execution
9. Expose — public API (if needed)
## File Organization
```
src/
components/
ui/ # Primitives (Button, Card, Badge, Input)
chat/ # Chat window, message list, input
content-panel/ # Side panel for surfaced content
renderers/ # Content type renderers
layout/ # Shell, split-pane, responsive containers
composables/ # Shared composition functions (useTheme, useMedia, useCrypto)
stores/ # Pinia stores
plugins/ # Plugin system
types/ # Shared TypeScript types
styles/ # Global CSS, themes, design tokens
utils/ # Pure utility functions
```
## Naming Conventions
- Components: PascalCase (`ProjectCard.vue`)
- Composables: camelCase, prefixed with "use" (`useTheme.ts`)
- Props: camelCase in JS, kebab-case in templates
- Boolean props: prefix with `is`, `has`, `can`, `should`
- Handler props: prefix with `on` (`onClick`, `onClose`)
- Emits: explicit, kebab-case in templates (`project:updated`)
## Props — Always Validate
```typescript
defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
status: {
type: String as PropType<'pending' | 'active' | 'complete'>,
default: 'pending'
}
})
```
Never use array-style props: `defineProps(['title', 'count'])`
## Reactive State
- `ref` for primitives and single values
- `reactive` for objects with multiple properties
- `computed` for derived state (never side effects in computed)
- `shallowRef` for large objects that change at top level only
## Templates — Keep Clean
Move complex logic to computed properties or methods. No inline logic in templates. Use `v-if` for infrequent toggles, `v-show` for frequent ones.
## Composables
- One responsibility per composable
- Return only what's needed
- Handle cleanup in `onUnmounted`
- Make composables testable
## Performance
- Lazy load heavy components: `defineAsyncComponent(() => import(...))`
- Use `shallowRef` for large lists
- Use `:key` with unique identifiers, never index
- Avoid reactive objects in templates (create in script)
## Error Handling
Use `onErrorCaptured` for component-level error boundaries. Always handle async errors with try/catch/finally pattern (loading, error, data states).
+111
View File
@@ -0,0 +1,111 @@
---
description: Tailwind CSS utility-first styling conventions for AIUI, ported from Archy
globs: "**/*.vue,**/*.css,**/*.ts"
alwaysApply: false
---
# Tailwind CSS Styling
## Source of Truth
All glass morphism, container, and button patterns originate from the Archy project (`/Projects/Archy/neode-ui/src/style.css`). When in doubt, match Archy exactly.
## Utility-First
Use Tailwind utilities directly in templates. Extract to component classes only when a pattern repeats 3+ times.
## 4px Spacing Grid
```
1 = 4px, 2 = 8px, 3 = 12px, 4 = 16px, 5 = 20px, 6 = 24px, 7 = 28px, 8 = 32px
```
## Typography Scale
```
text-xs = 12px (metadata, timestamps)
text-sm = 14px (body text, buttons)
text-base = 16px (default body, inputs)
text-lg = 18px (subtitles)
text-xl = 20px (card titles)
text-2xl = 24px (section headings)
text-3xl = 30px (page headings)
text-4xl = 36px (hero headings)
```
Font weights: `font-normal` (body), `font-medium` (emphasis), `font-semibold` (headings/buttons), `font-bold` (strong emphasis).
## Glass Morphism (from Archy)
### Containers (exact Archy values)
- `.glass` — base: `bg: rgba(0,0,0,0.35)`, `blur(18px)`, `border: 1px solid rgba(255,255,255,0.18)`, `shadow: 0 8px 24px rgba(0,0,0,0.45)`
- `.glass-strong` — stronger blur: same bg but `blur(24px)`
- `.glass-card` — primary card: `bg: rgba(0,0,0,0.65)`, `blur(18px)`, `border-radius: 1rem`, same border/shadow
- `.gradient-card` — gradient: `linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(0,0,0,0.8) 100%)`
- `.gradient-card-dark` — dark gradient: `linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.9) 100%)`
- `.gradient-border-container` — gradient border with inner glass, `border-radius: 1.5rem`
- `.toast-glass` — `border-radius: 0.75rem`, same glass as `.glass-card`
### Buttons (exact Archy values)
- `.glass-button` — 48px height, `bg: rgba(0,0,0,0.6)`, `blur(18px)`, border `rgba(255,255,255,0.18)`, `color: rgba(255,255,255,0.9)`
- `.glass-button-sm` — compact variant (auto height, `py-1.5 px-3`)
### Icon / Ghost buttons (Archy pattern)
```html
<button class="p-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-colors">
```
Touch target: minimum 44x44px via padding.
### Active Navigation
`.nav-tab-active` — `bg: rgba(0,0,0,0.35)`, inset highlight, gradient border via CSS mask `::before`
### Usage Rules
- ✅ Cards, panels, modals, sidebars
- ✅ Navigation bars, headers (fixed positioning)
- ✅ Hover states, buttons
- ❌ Body text containers (readability)
- ❌ Form input fields (confusing UX)
## Inset Highlight
The signature Archy inset glow:
```css
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
```
Apply to headers, selected cards, active nav items.
## Border — No Separators Between Sections
Per Archy theme rules: no borders between sidebar and content, or header and content. Only subtle `rgba(255,255,255,0.06-0.08)` borders for internal dividers.
## Gradient Text
```html
<h1 class="gradient-text">Title</h1>
```
`linear-gradient(to right, #ffffff, #9ca3af)` with `background-clip: text`.
## Focus States — Gamepad/Keyboard Glow
All focusable elements get a blue glow (no outline):
```css
*:focus-visible {
outline: none;
box-shadow: 0 0 16px rgba(120, 180, 255, 0.2), 0 0 32px rgba(100, 160, 255, 0.1);
}
```
## Scrollbar
- `.custom-scrollbar` — gradient thumb (`rgba(255,255,255,0.3)` to `0.1`), dark track
- `.scrollbar-hide` — hidden scrollbar, keeps scroll functionality
## Responsive — Mobile First
Base styles for mobile, enhance with breakpoints:
```html
<div class="text-base md:text-lg lg:text-xl p-4 md:p-6 lg:p-8">
```
Breakpoints: `sm` (640px), `md` (768px), `lg` (1024px), `xl` (1280px), `2xl` (1536px).
## Hover States (from Archy)
```html
<div class="transition-all duration-300 hover:bg-white/10 hover:text-white">
```
Interactive card lift: `hover:translateY(-2px)` with intensified shadow.
## Animations (Archy timings)
- `animate-fade-up` — 900ms `cubic-bezier(0.22, 1, 0.36, 1)` with 120ms delay
- `animate-fade-up-fast` — 400ms, no delay (for chat messages)
- `animate-fade-in` — 500ms ease
- `animate-scale-in` — 250ms for modals/popups
+118
View File
@@ -0,0 +1,118 @@
---
description: Design system foundations - glassmorphism from Archy, colors, typography, spacing
globs: "**/*.vue,**/*.css,**/*.ts"
alwaysApply: false
---
# Design System
All glass morphism, container, and button patterns are ported from the Archy project and must match exactly.
## Glass Morphism Hierarchy (from Archy)
### Glass Intensity Levels
| Class | Background | Blur | Use Case |
|-------|-----------|------|----------|
| `.glass` | `rgba(0,0,0,0.35)` | 18px | Sidebar, panels, inputs |
| `.glass-strong` | `rgba(0,0,0,0.35)` | 24px | Headers, message bubbles (user) |
| `.glass-card` | `rgba(0,0,0,0.65)` | 18px | Primary cards, modals, main containers |
| `.gradient-card` | gradient white→black | 18px | Feature cards |
| `.gradient-card-dark` | gradient black→black | 18px | Dark feature cards |
All share: `border: 1px solid rgba(255,255,255,0.18)`, `box-shadow: 0 8px 24px rgba(0,0,0,0.45)`.
### Button Hierarchy (from Archy)
| Class | Purpose | Details |
|-------|---------|---------|
| `.glass-button` | Default | 48px height, `rgba(0,0,0,0.6)`, blur 18px |
| `.glass-button-sm` | Compact | Auto height, smaller padding |
| Ghost | Icon/text actions | `p-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10` |
### Inset Highlight
Signature Archy top-edge glow on focused/active elements:
```css
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
```
### Gradient Border (CSS mask technique)
For premium-feel borders on selected cards and active nav:
```css
::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), transparent);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
```
## Design Tokens
### Color Palette
Semantic color tokens defined by purpose:
- `primary` — main brand actions (#606060)
- `accent` — highlight, Bitcoin orange (#F7931A)
- `success` — positive states (#10B981)
- `error` — negative states (#EF4444)
- `warning` — caution states (#F59E0B)
- `info` — informational (#3B82F6)
### Glass Tokens (from Archy Tailwind config)
- `glass-dark`: `rgba(0, 0, 0, 0.35)`
- `glass-darker`: `rgba(0, 0, 0, 0.6)`
- `glass-border`: `rgba(255, 255, 255, 0.18)`
- `glass-highlight`: `rgba(255, 255, 255, 0.22)`
### Shadows
- `shadow-glass`: `0 8px 24px rgba(0, 0, 0, 0.45)`
- `shadow-glass-sm`: `0 6px 18px rgba(0, 0, 0, 0.35)`
- `shadow-glass-inset`: `inset 0 1px 0 rgba(255, 255, 255, 0.22)`
### Typography
- Body font: Inter, system-ui (AIUI default)
- Mono font: Menlo, Monaco, Courier New
- Text opacity scale: `text-white/25` (placeholders), `text-white/40` (muted), `text-white/60` (secondary), `text-white/70` (interactive default), `text-white/80` (body), `text-white/90` (emphasis), `text-white/96` (headings), `text-white` (active/selected)
### Spacing
4px grid: `4, 8, 12, 16, 20, 24, 28, 32` px.
### Border Radius
- `rounded-lg` (8px) — buttons, nav items, inputs
- `rounded-xl` (12px) — toasts, small cards
- `rounded-2xl` (16px) — main cards, modals
- `rounded-3xl` (24px) — bottom sheets
- `rounded-full` — pills, avatars, FABs
- `1rem` (16px) — `.glass-card` default
## Component Patterns
### Cards
Use `.glass-card` with additional padding:
```html
<div class="glass-card p-6">Content</div>
```
### Modals
```html
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div class="glass-card p-6 max-w-md w-full">...</div>
</div>
```
### Icons
- SVG, using `currentColor`
- Sizes: 16px, 20px, 24px, 32px
- Icon-only buttons: `p-2 rounded-lg` (reaches 44px touch target with 24px icon)
- Must have `aria-label`
## Theme Architecture
- Base background: `#0a0a0a` (near-black)
- No separator borders between sidebar/header/content
- Header, sidebar, root share same visual weight
- CSS-based themes with reactive Vue state
- `localStorage` persistence
@@ -0,0 +1,91 @@
---
description: Component architecture principles - composition, patterns, and structure
globs: "**/*.vue,**/*.ts"
alwaysApply: false
---
# Component Architecture
## Core Philosophy: Composition Over Configuration
Build complex UIs from simple, focused components that compose well together.
- Single Responsibility: each component does one thing well
- Use slots instead of complex prop APIs
- Provide sensible defaults
- Clear TypeScript interfaces for props
- Keep component state local and minimal
## Anti-Patterns
- God components that do everything
- Prop drilling through many layers (use provide/inject or Pinia)
- Hard-coded values instead of props
- Component logic mixed with layout
- Tight coupling between components
## Compound Component Pattern
Components that work together as a cohesive unit:
```vue
<Card>
<Card.Header>Title</Card.Header>
<Card.Body>Content</Card.Body>
<Card.Footer>Actions</Card.Footer>
</Card>
```
## Container/Presenter Pattern
Separate logic from presentation:
- Container: handles data fetching, state, side effects
- Presenter: pure rendering, receives data via props, emits events
## Slot Pattern (Vue)
Use named slots for flexible content injection:
```vue
<template>
<div class="section">
<slot name="title" />
<slot name="content" />
<slot name="actions" />
</div>
</template>
```
## Prop Interface Design
```typescript
interface BaseComponentProps {
class?: string
testId?: string
}
interface ButtonProps extends BaseComponentProps {
variant?: 'primary' | 'secondary' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
}
```
## Component File Template
```
1. Imports (external, then internal)
2. Types/Interfaces
3. Constants
4. Main component (props, emits, state, computed, methods, lifecycle)
5. Sub-components (if any)
```
## Error Boundaries
Every major section should have error boundary handling via `onErrorCaptured`. Show fallback UI, never a blank screen.
## Responsive Components
Use CSS-based responsive (`hidden md:block`) over JS-based (`useMediaQuery`) when possible. JS-based only when behavior changes (not just visibility).
## Component Checklist
Before shipping any component:
- [ ] TypeScript interface defined
- [ ] Sensible default props
- [ ] Loading and error states handled
- [ ] ARIA attributes added
- [ ] Keyboard navigation works
- [ ] Responsive behavior tested
- [ ] Dark mode styling works
- [ ] Touch interactions verified on mobile
@@ -0,0 +1,91 @@
---
description: The five content surfaces that define how content is rendered in AIUI
globs: "**/renderers/**,**/chat/**,**/content-panel/**"
alwaysApply: false
---
# Content Surfaces
AIUI has five distinct surfaces where content can appear. Every renderer must define how it behaves in each applicable surface.
## Surface 1: Chat Preview
- Location: inline in chat message bubble
- Max height: ~120px
- Purpose: identify content at a glance (thumbnail, title, brief metadata)
- Always tappable/clickable to expand to Panel Preview or Panel Play
- Lightweight rendering only — no heavy libraries loaded
- Examples: film poster thumbnail strip, file icon with name, code snippet (first 5 lines), image thumbnail
## Surface 2: Chat Play
- Location: inline in chat message bubble
- Max height: ~200px
- Purpose: inline playback without leaving the chat
- Must not disrupt chat scrolling
- Has an "expand" button to open in Panel Play
- Examples: voice note waveform with play button, short video player, audio player, small interactive widget
## Surface 3: Panel Preview
- Location: content panel (beside chat on desktop, overlay on mobile)
- No height limit (scrollable within panel)
- Purpose: full browsing/exploration experience
- Supports: filtering, sorting, searching, pagination
- Click items to go to Panel Play or Panel Edit
- Examples: film grid (tiled, filterable), image gallery, search results list, document preview, file tree
## Surface 4: Panel Play
- Location: content panel
- Purpose: full immersive media playback
- Examples: full video player with controls, audio with spectrum visualization, slideshow, trailer playback
## Surface 5: Panel Edit/Interactive
- Location: content panel
- Purpose: full interaction and editing
- Changes can be sent back to chat as new messages
- Examples: code editor (CodeMirror), form filling, approval workflow, spreadsheet editing, diagram creation
## Surface Transitions
```
Chat Preview --tap--> Panel Preview --tap item--> Panel Play
--tap item--> Panel Edit
Chat Play --expand--> Panel Play
Panel Edit --submit--> Chat (new message with result)
```
## Renderer Interface
Every renderer must export:
```typescript
interface RendererDefinition {
id: string
name: string
contentType: string // MIME-like type identifier
surfaces: SurfaceType[] // which surfaces this renderer supports
chatPreview?: Component // Surface 1
chatPlay?: Component // Surface 2
panelPreview?: Component // Surface 3
panelPlay?: Component // Surface 4
panelEdit?: Component // Surface 5
lazyDependencies?: () => Promise<any> // heavy libs loaded on demand
}
```
## Mobile Behavior
- On mobile, there is no side-by-side layout
- Panel surfaces open as a full-screen overlay or bottom sheet
- Chat Preview and Chat Play remain inline
- Transition: tap Chat Preview → full-screen Panel Preview (slide up)
- Back gesture or button returns to chat
## Performance Rules
- Chat Preview and Chat Play must render with zero lazy-loaded dependencies
- Panel surfaces may lazy-load heavy libraries (CodeMirror, pdf.js, etc.)
- Never block the chat scroll with renderer loading
- Use skeleton/placeholder while panel content loads
## Content Type Expert Rules
For extraction, parsing, and surfacing logic, see:
- `20-content-films.mdc` — Films
- `21-content-songs.mdc` — Songs (includes looksLikeSong blocklist)
- `22-content-podcasts.mdc` — Podcasts (includes looksLikePodcast)
- `23-content-news.mdc` — News + RSS, ArticleDetail security
- `24-content-websites.mdc` — Websites vs News, overlay
- `25-content-magazine.mdc` — Magazine/Brief parsing, hero, meme
+98
View File
@@ -0,0 +1,98 @@
---
description: Plugin architecture rules - interfaces, registration, lifecycle, sandboxing
globs: "**/plugins/**,**/*.plugin.ts"
alwaysApply: false
---
# Plugin System
## Philosophy
Every external integration connects through a typed plugin interface. No direct coupling to any service, provider, or protocol.
## Plugin Types
```typescript
type PluginType =
| 'ai-provider' // LLM backends (OpenRouter, Ollama, Claude, etc.)
| 'media-source' // Content sources (Plex, YouTube, Nextcloud, Archive.org)
| 'messaging' // Chat protocols (Nostr, Matrix, local)
| 'storage' // File storage (local FS, IPFS, Nextcloud)
| 'renderer' // Custom content renderers
| 'file-handler' // File open/preview handlers
| 'crypto' // Encryption providers
| 'search' // Search backends (SearXNG, local)
| 'auth' // Authentication (Nostr keys, DID, passkeys)
| 'wallet' // Bitcoin wallet deep-linking (Phoenix, Zeus, Alby, etc.)
| 'social-embed' // Social post fetching (X, Nostr, Mastodon)
| 'mcp' // Model Context Protocol servers
| 'media' // Media processing (ffmpeg.wasm, whisper, TTS)
```
## Base Plugin Interface
```typescript
interface AIUIPlugin {
id: string
name: string
version: string
type: PluginType
description?: string
icon?: string
init(context: PluginContext): Promise<void>
destroy(): Promise<void>
isAvailable(): Promise<boolean>
}
```
## Plugin Context
Plugins receive a context object with access to:
- Settings store (read/write plugin-specific settings)
- Event bus (emit/listen for app events)
- Logger (structured logging)
- Crypto utilities (for encrypting plugin data at rest)
Plugins do NOT receive:
- Direct DOM access (community plugins)
- File system access (without explicit capability grant)
- Network access to arbitrary hosts (without declaration)
## Sandboxing Tiers
### Tier 1: Trusted (built-in, official)
Run in main thread with full API access. AI adapters, core renderers, crypto providers.
### Tier 2: Community
Run in sandboxed iframes with `postMessage` API. Custom renderers, themes, visual extensions. Cannot access host DOM, file system, or network directly.
### Tier 3: External Processes
MCP servers, local AI runners. Run as separate processes (Tauri IPC) or connect via HTTP. Isolated by OS process boundary.
## Plugin Lifecycle
1. `register()` — declare plugin to registry
2. `init()` — plugin sets up, connects to services
3. Active — plugin responds to requests
4. `destroy()` — cleanup on disable/uninstall
## Registration
```typescript
import { registerPlugin } from '@aiui/core'
registerPlugin({
id: 'ai-openrouter',
name: 'OpenRouter',
type: 'ai-provider',
version: '1.0.0',
async init(ctx) { /* setup */ },
async destroy() { /* cleanup */ },
// ... adapter methods
})
```
## Plugin Settings
Each plugin can declare settings schema. Settings are stored encrypted and exposed through a standard settings UI.
## Rules
- Every plugin must declare its type
- Every plugin must implement `init()` and `destroy()`
- Every plugin must implement `isAvailable()` to report its status
- Plugins must handle errors gracefully — never crash the host
- Community plugins must not load external scripts
- All network requests must go through the plugin context (for privacy/proxy control)
+83
View File
@@ -0,0 +1,83 @@
---
description: AI adapter patterns, streaming, tool calling, context injection
globs: "**/ai/**,**/plugins/ai-*/**"
alwaysApply: false
---
# AI Integration
## Universal AI Adapter
All AI providers connect through the `AIProviderAdapter` interface:
```typescript
interface AIProviderAdapter extends AIUIPlugin {
type: 'ai-provider'
chat(messages: Message[], options: ChatOptions): AsyncIterable<ChatChunk>
models(): Promise<Model[]>
supportsStreaming: boolean
supportsVision: boolean
supportsTools: boolean
supportsMultimodal: boolean
}
```
## Provider Hierarchy
1. **OpenAI-Compatible Adapter** — covers OpenRouter, Ollama, vLLM, llama.cpp, LocalAI, Mistral, DeepSeek, xAI, Qwen. Just change `baseURL` + API key.
2. **Anthropic Adapter** — Claude. Different tool_use format (content blocks vs tool_calls).
3. **Gemini Adapter** — Google. Different multimodal format.
4. **MCP Client** — connects to any MCP server for tools, resources, prompts.
## Streaming
- All AI responses use Server-Sent Events (SSE) over HTTP
- Pattern: `data: {"token": "Hello"}\n\n` with `data: [DONE]\n\n` termination
- Client: parse SSE stream, feed tokens to `StreamingTextRenderer`
- Always show a typing indicator while waiting for first token
- Handle connection drops gracefully (show error, offer retry)
## Tool Calling
AI can invoke tools. The adapter normalizes tool call formats:
```typescript
interface ToolCall {
id: string
name: string
arguments: Record<string, unknown>
}
interface ToolResult {
toolCallId: string
content: string | StructuredContent
isError: boolean
}
```
Normalize across providers:
- OpenAI: `tool_calls` in assistant message → `role: "tool"` result
- Claude: `type: "tool_use"` content block → `tool_result` in user message
- Map both to AIUI's unified `ToolCall` / `ToolResult` types
## Context Injection
The system prompt includes context about the user's environment:
- Connected media sources and their capabilities
- Available tools and plugins
- User preferences (language, theme, preferred wallet)
- In dev mode: mock data summaries
Never include sensitive data (API keys, passwords) in system prompts.
## Model Selection
Users can switch models within a conversation. The UI shows:
- Available models from all connected providers
- Model capabilities (vision, tools, streaming)
- Cost per token in sats (if applicable)
## Dev Mode
- `VITE_OPENROUTER_API_KEY` in `.env.local`
- Free models available (Llama, Mistral via OpenRouter)
- Mock tool responses available via dev fixtures
- Debug panel shows: raw messages, token count, latency
## Error Handling
- Rate limits: show user-friendly message, auto-retry with backoff
- Auth errors: prompt to check API key in settings
- Network errors: show offline indicator, queue message for retry
- Model errors: show error in chat, suggest alternative model
@@ -0,0 +1,80 @@
---
description: How to build content renderers - interfaces, lazy loading, accessibility
globs: "**/renderers/**"
alwaysApply: false
---
# Renderer Development
## What is a Renderer?
A renderer is a set of Vue components that know how to display a specific content type across the five content surfaces (chat-preview, chat-play, panel-preview, panel-play, panel-edit).
## Renderer Registration
```typescript
import { registerRenderer } from '@aiui/core'
registerRenderer({
id: 'film',
name: 'Film',
contentType: 'application/x-aiui-film',
surfaces: ['chat-preview', 'panel-preview', 'panel-play'],
chatPreview: () => import('./FilmChatPreview.vue'),
panelPreview: () => import('./FilmGrid.vue'),
panelPlay: () => import('./FilmDetail.vue'),
})
```
## Content Type Detection
Renderers are matched to content by `contentType` field in the message data:
```typescript
interface ContentBlock {
contentType: string // e.g., 'application/x-aiui-film'
data: Record<string, unknown> // renderer-specific data
title?: string // human-readable title for panel tab
}
```
## Performance Rules
1. Chat surfaces (preview, play) must render with ZERO lazy-loaded heavy dependencies
2. Panel surfaces may lazy-load libraries (CodeMirror, pdf.js, etc.)
3. Use `defineAsyncComponent` for panel components
4. Show skeleton/placeholder while loading
5. Never block the main thread — use Web Workers for heavy parsing
## Data Contracts
Each renderer defines its expected data shape as a TypeScript interface:
```typescript
interface FilmRendererData {
films: Film[]
query?: string
filters?: FilmFilters
}
```
Document the interface. Validate incoming data. Show graceful error if data is malformed.
## Accessibility Requirements
- All renderers must be keyboard navigable
- Images need alt text
- Interactive elements need ARIA labels
- Media players need captions/transcripts when available
- Focus management when transitioning between surfaces
## Mobile Behavior
- Chat Preview: constrained to message bubble width
- Chat Play: full message width, max 200px height
- Panel surfaces on mobile: full-screen overlay with back gesture
- Touch targets: minimum 44x44px
- Swipe gestures where appropriate (image gallery, film cards)
## Renderer Checklist
- [ ] TypeScript data interface defined and exported
- [ ] All applicable surfaces implemented
- [ ] Lazy loading for heavy dependencies
- [ ] Skeleton/placeholder states
- [ ] Error state (malformed data)
- [ ] Empty state (no data)
- [ ] Keyboard navigation
- [ ] ARIA labels on interactive elements
- [ ] Mobile responsive
- [ ] Dark mode compatible
- [ ] Transition animations (per motion design rules)
+60
View File
@@ -0,0 +1,60 @@
---
description: Cryptography and security rules - E2E encryption, key management, storage
globs: "**/crypto/**,**/*.ts"
alwaysApply: false
---
# Security & Cryptography
## Principles
- Privacy is a requirement, not a feature
- Zero telemetry, zero analytics unless user explicitly opts in
- Never transmit unencrypted sensitive data
- Never store plaintext credentials
- Minimal data collection — store only what's needed
## Encryption Stack
### E2E Message Encryption
- Library: **tweetnacl.js** (6KB, audited by Cure53)
- Algorithm: XSalsa20-Poly1305 via NaCl `box` (public-key authenticated encryption)
- Each conversation has a shared secret derived from key exchange
### Local Storage Encryption
- Library: **Web Crypto API** (native, zero bundle cost)
- Algorithm: AES-256-GCM for encrypting IndexedDB values
- Key derived from user's master password via PBKDF2 (100K+ iterations)
### Key Management
- **Desktop (Tauri)**: OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service)
- **Web**: Encrypted IndexedDB with user-derived key
- **Nostr compatibility**: secp256k1 keys via @noble/curves, NIP-07 browser extension support
- **Passkeys/WebAuthn**: For passwordless authentication
### Credential Storage
- API keys encrypted at rest using AES-256-GCM
- Never stored in localStorage (use encrypted IndexedDB or OS keychain)
- Never included in logs, error reports, or system prompts
- Display as masked values in settings UI (show last 4 chars only)
## Dev Mode Bypass
When `VITE_DISABLE_CRYPTO=true` (dev only):
- Skip E2E encryption (messages stored in plain text)
- Skip storage encryption (IndexedDB unencrypted)
- API keys stored in `.env.local` (gitignored)
- This flag must NEVER exist in production builds
## Security Rules for Code
- Never log sensitive data (keys, tokens, passwords, message content)
- Never include secrets in error messages
- Sanitize all user input before rendering (XSS prevention)
- Use Content Security Policy headers
- Validate all data from plugins before rendering
- Community plugins run in sandboxed iframes (no direct DOM access)
- Never eval() or innerHTML with untrusted content
## Network Security
- All external requests over HTTPS only
- Certificate pinning for known services (Tauri)
- Proxy social media fetches to avoid leaking user IP
- No third-party tracking scripts, analytics, or telemetry SDKs
+72
View File
@@ -0,0 +1,72 @@
---
description: Bitcoin-only payment and monetary policy - on-chain, Lightning, ecash
globs: "**/*"
alwaysApply: true
---
# Bitcoin Only
## Core Rule
Bitcoin is the only monetary unit in AIUI. This applies everywhere — UI labels, data models, API responses, documentation, and conversation context.
## Supported Payment Protocols
- **On-chain Bitcoin**: BIP21 URI scheme (`bitcoin:bc1q...?amount=0.001`)
- **Lightning Network**: BOLT11 invoices, LNURL-pay, LNURL-withdraw, keysend
- **Cashu ecash**: Cashu tokens, mint interactions (`cashu:` URI, `web+cashu:`)
- **Fedimint/Fedi**: Federation ecash (`fedi:` URI)
- **Nostr Zaps**: NIP-57 Lightning zaps (social tipping)
## AIUI is NEVER a Wallet
### Never Do
- Store private keys or seed phrases
- Sign Bitcoin transactions
- Build or broadcast transactions
- Track wallet balances
- Display transaction history
- Create send/receive screens
- Implement payment processing logic
- Hold funds in custody
### Always Do
- Construct deep-link URIs and hand off to external wallet apps
- Detect installed wallet apps (via URI scheme probing or Tauri app detection)
- Let users configure preferred wallets in settings
- Display payment requests as QR codes with "Open in Wallet" buttons
- Show invoice/address details (amount, memo, expiry) as read-only information
## Wallet Deep-Linking
```typescript
// Construct URI, open external wallet — that's it
const uri = `lightning:${bolt11Invoice}`
window.open(uri) // or Tauri shell.open(uri)
```
Supported wallet URI schemes:
- `bitcoin:` — BIP21 (any on-chain wallet)
- `lightning:` — BOLT11 (any Lightning wallet)
- `cashu:` — Cashu tokens
- `fedi:` — Fedimint
- Wallet-specific: `phoenix://`, `zeus://`, `mutiny://`, `alby://`
## Denomination
- Primary unit: **sats** (1 BTC = 100,000,000 sats)
- Display: `1,234 sats` or `₿0.00001234`
- User preference: sats or BTC (configurable in settings)
- AI cost tracking: show token costs in sats
## Prohibited
- No fiat currencies (USD, EUR, etc.) — not in UI, not in code, not in variable names
- No altcoins or tokens
- No stablecoins (USDT, USDC, etc.)
- No fiat-denominated pricing
- No payment processor integrations (Stripe, PayPal, etc.)
- No KYC/AML flows
## Renderer Components
- `LightningInvoiceRenderer` — BOLT11 QR + amount + memo + "Open in Wallet"
- `BitcoinAddressRenderer` — BIP21 QR + "Open in Wallet"
- `CashuTokenRenderer` — ecash token + mint info + "Redeem in Wallet"
- `FedimintRenderer` — federation ecash + "Open in Fedi"
- `PaymentRequestRenderer` — unified card with payment method options
- `ZapRenderer` — Nostr zap display (NIP-57)
+101
View File
@@ -0,0 +1,101 @@
---
description: Development vs production configuration, feature flags, mock data patterns
globs: "**/*"
alwaysApply: false
---
# Dev & Prod Modes
## Development Mode
### Environment
```env
# .env.local (gitignored)
VITE_OPENROUTER_API_KEY=sk-or-...
VITE_TMDB_API_KEY=...
VITE_DEV_MODE=true
VITE_MOCK_MEDIA_SOURCES=true
VITE_DISABLE_CRYPTO=true
```
### What's Enabled
- Hot reload via Vite HMR
- Debug panel overlay (AI context, plugin status, renderer registry, message data)
- Mock media source plugins (Plex, YouTube, Nextcloud from JSON fixtures)
- OpenRouter AI connection (real API, free models available)
- Component playground (Storybook/Histoire)
- Verbose logging
- TypeScript strict mode
- All renderers available without lazy loading (for dev speed)
### What's Disabled
- E2E encryption (plain text messages for debugging)
- Storage encryption (plain IndexedDB)
- Tauri features (dev runs as pure web app)
- Production optimizations (tree-shaking, minification)
- Service worker / offline mode
### Mock Data
- Film fixtures: 50-100 films with real TMDB poster URLs
- Media source mocks: JSON files returning fake Plex/YouTube/Nextcloud responses
- Located in: `packages/app/src/mocks/`
- Auto-loaded when `VITE_MOCK_MEDIA_SOURCES=true`
- Mock data must match production data interfaces exactly
### Dev Scripts
```
pnpm dev # Web dev server
pnpm dev:desktop # Tauri dev (when needed)
pnpm storybook # Component playground
pnpm test # Vitest
pnpm lint # ESLint + Prettier
pnpm typecheck # TypeScript
pnpm build # Production build
pnpm turbo build # Turborepo cached build
```
## Production Mode
### What's Enabled
- E2E encryption for all messages
- Encrypted local storage
- Key management via OS keychain (Tauri) or encrypted IndexedDB (web)
- User-configured AI providers (settings page)
- Real media source connections (Plex API, YouTube, etc.)
- Optimized builds (tree-shaken, code-split, minified)
- Lazy loading for all heavy renderers
- Service worker for offline support
- Auto-update (Tauri)
### What's Disabled
- Debug panels
- Mock data
- Dev logging
- Source maps (in distributed builds)
- `VITE_DISABLE_CRYPTO` flag (must not exist)
### Build Targets
- Web: Static SPA bundle (< 250KB initial gzipped)
- Desktop: Tauri app (macOS .dmg, Windows .msi, Linux .AppImage)
- Mobile: Tauri mobile (iOS .ipa, Android .apk)
## Feature Flags
Use composable `useFeatureFlags()`:
```typescript
const { isDev, isTauri, isMobile, isCryptoEnabled, isMockData } = useFeatureFlags()
```
Gate platform-specific features:
```typescript
if (isTauri()) {
// Native file system access
} else {
// File System Access API or file picker
}
```
## Environment Variable Rules
- All env vars prefixed with `VITE_` (Vite requirement)
- Secrets only in `.env.local` (gitignored)
- `.env.example` committed with placeholder values
- Never read `process.env` directly — use typed config module
+69
View File
@@ -0,0 +1,69 @@
---
description: Accessibility standards - WCAG AA, keyboard navigation, screen readers
globs: "**/*.vue"
alwaysApply: false
---
# Accessibility
## Standard
WCAG AA compliance minimum. Target AAA where feasible.
## Color Contrast
- Normal text: 4.5:1 minimum ratio
- Large text (18px+ or 14px+ bold): 3:1 minimum
- Interactive elements: 3:1 against adjacent colors
- Test with browser DevTools accessibility panel
## Keyboard Navigation
- All interactive elements focusable via Tab
- Visible focus indicators on every focusable element (`focus:ring-2`)
- Escape closes modals, drawers, dropdowns
- Arrow keys navigate within lists, grids, tabs
- Enter/Space activates buttons and controls
- Focus trap inside modals (Tab cycles within modal)
## Semantic HTML
```html
<header>, <nav>, <main>, <article>, <aside>, <footer>
```
Never `<div class="header">`. Use semantic elements.
## ARIA
- Icon-only buttons: `aria-label="Close modal"`
- Dynamic content: `aria-live="polite"` for updates
- Screen reader only text: `class="sr-only"`
- Expandable sections: `aria-expanded="true/false"`
- Form fields: `aria-describedby` for help text, `aria-invalid` for errors
## Images
- All `<img>` tags need `alt` text
- Decorative images: `alt=""`
- Complex images: `aria-describedby` pointing to description
## Media
- Audio/video players: keyboard-accessible controls
- Provide transcripts/captions when available
- Respect `prefers-reduced-motion` for animations
## Touch Targets
- Minimum: 44x44px (Apple HIG)
- Recommended: 48x48px (Material Design)
- Minimum 8px gap between adjacent targets
## Reduced Motion
```css
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
Check in JS: `window.matchMedia('(prefers-reduced-motion: reduce)').matches`
## Testing
- VoiceOver (macOS), TalkBack (Android), NVDA (Windows)
- Keyboard-only navigation test
- axe DevTools or Lighthouse accessibility audit
- High contrast mode test
+60
View File
@@ -0,0 +1,60 @@
---
description: Performance optimization - bundle budget, lazy loading, virtual scrolling
globs: "**/*"
alwaysApply: false
---
# Performance
## Bundle Budget
- Initial load: **< 250KB gzipped**
- Core (Vue + Tailwind + Pinia + Router + chat UI): ~150KB
- First renderer batch (markdown, streaming text): ~50KB
- Everything else: lazy-loaded on demand
## Lazy Loading Strategy
- Route-based code splitting via Vue Router `() => import(...)`
- Renderer components via `defineAsyncComponent`
- Heavy libraries loaded only when their renderer is activated:
- CodeMirror 6: ~300KB (on code edit)
- Monaco: ~5MB (on IDE panel open)
- pdf.js: ~400KB (on PDF view)
- KaTeX: ~300KB (on math render)
- Mermaid: ~200KB (on diagram render)
- Leaflet: ~40KB (on map render)
- Whisper WASM: ~50MB (on STT activation, cached)
- Piper TTS: ~100MB (on TTS activation, cached)
## Virtual Scrolling
- Chat message list uses TanStack Virtual
- Dynamic row heights (messages vary in size)
- Inverted scroll (newest at bottom, load older on scroll up)
- Buffer: render 5 items above and below viewport
- Recycle DOM nodes for off-screen messages
## GPU Acceleration
Only animate `transform` and `opacity` — never `width`, `height`, `top`, `left`.
Use `will-change` sparingly and remove after animation.
## Image Optimization
- Use `loading="lazy"` on all non-critical images
- Provide `srcset` with multiple sizes
- Use WebP/AVIF where supported
- Skeleton placeholders while loading
## Network
- Preconnect to known API hosts
- Preload critical resources
- Debounce scroll and resize handlers (100ms)
- Batch API requests where possible
## Memory
- Clean up event listeners in `onUnmounted`
- Use `shallowRef` for large data sets
- Dispose heavy library instances when panel closes
- Monitor memory with browser DevTools
## Core Web Vitals Targets
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
@@ -0,0 +1,79 @@
---
description: Animation principles - timing, easing, stagger, reduced motion
globs: "**/*.vue,**/*.css"
alwaysApply: false
---
# Animation & Motion Design
## Philosophy
Every animation serves a purpose: guide attention, provide feedback, show relationships, enhance perceived performance, or add delight. Never animate for decoration alone.
## Duration Scale
```
100ms - Instant: micro-feedback (hover states, button press)
200ms - Fast: small elements (tooltips, dropdowns)
300ms - Moderate: standard UI transitions (modals, cards)
500ms - Normal: page sections, complex components
600ms - Slow: hero animations, page transitions (max for UI)
```
Never exceed 600ms for UI element animations.
## Easing Functions
- **ease-out** (90% of animations): elements entering viewport
- **ease-in**: elements exiting viewport
- **ease-in-out**: elements moving within viewport
- **spring**: playful interactions (button press, drag-and-drop)
- **linear**: progress bars, loading spinners only
Custom smooth deceleration: `cubic-bezier(0.16, 1, 0.3, 1)`
## Common Patterns
### Fade & Slide Up (entrance)
```css
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
```
### Scale & Fade (emphasis)
```css
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.8); }
to { opacity: 1; transform: scale(1); }
}
```
### Hover feedback
```css
.interactive {
transition: transform 0.1s ease, opacity 0.1s ease;
}
.interactive:active {
transform: scale(0.95);
opacity: 0.8;
}
```
## Staggered Animations
When animating multiple elements, stagger by 50-150ms per item:
```css
.card { animation-delay: calc(var(--index) * 0.1s); }
```
Max items in a stagger cascade: 6-8. Total cascade: under 1 second.
## Reduced Motion
Always respect `prefers-reduced-motion`. Provide instant transitions as fallback.
## Performance
- Only animate `transform` and `opacity` (GPU-composited)
- Use `will-change` sparingly, remove after animation
- Limit simultaneous animations
- Use `requestAnimationFrame` for JS animations
## Loading States
- Skeleton shimmer: 2s infinite, `linear-gradient` sweep
- Pulse: 2s infinite, opacity 1 → 0.5 → 1
- Spinner: 1s infinite linear rotation
+173
View File
@@ -0,0 +1,173 @@
---
description: Mobile UX patterns informed by Apple iOS HIG — touch targets, typography, spacing, navigation, animations
globs: "**/*.vue,**/*.css"
alwaysApply: false
---
# Mobile UX (iOS HIG-Informed)
## Philosophy
Design for mobile first, enhance for desktop. Follow Apple iOS Human Interface Guidelines for sizing, spacing, and interaction patterns. Adapt native iOS conventions to our glass morphism dark theme.
## Typography (iOS Dynamic Type Mapped to CSS)
| iOS Text Style | Default Size | CSS Equivalent | AIUI Usage |
|---|---|---|---|
| Large Title | 34pt | `text-[34px]` / `text-3xl` | Page titles (rare) |
| Title 1 | 28pt | `text-[28px]` / `text-2xl` | Section headers |
| Title 2 | 22pt | `text-[22px]` / `text-xl` | Sub-section headers |
| Title 3 | 20pt | `text-[20px]` / `text-lg` | Card titles |
| Headline | 17pt semibold | `text-[17px] font-semibold` | Emphasis labels |
| Body | 17pt | `text-[17px]` / `text-base` | Primary content |
| Callout | 16pt | `text-[16px]` | Secondary content |
| Subheadline | 15pt | `text-[15px]` | Metadata |
| Footnote | 13pt | `text-[13px]` / `text-xs` | Timestamps, captions |
| Caption 1 | 12pt | `text-[12px]` | Badges, small labels |
| Caption 2 | 11pt | `text-[11px]` | Smallest text (tab labels) |
### Key rules
- **Minimum text size**: 11px (Caption 2) — never go smaller
- **Body text on mobile**: 17px (not 14px/16px) for comfortable reading
- Use `text-sm` (14px) sparingly — only for dense UI, not primary reading content
- Chat messages should use at least 15-16px on mobile
- Metadata/timestamps: 11-13px is acceptable
## Touch Targets
| Rule | Value | Tailwind |
|---|---|---|
| Minimum tap target | **44 × 44px** | `min-w-[44px] min-h-[44px]` |
| Minimum gap between targets | **8px** | `gap-2` |
| Comfortable button height | 44-50px | `h-11` to `h-[50px]` |
| iOS nav bar button | 44px | `h-11` |
### Key rules
- The 44px minimum applies to the **tappable area**, not the visual size
- A 24px icon can have a 44px tap target via padding: `p-2.5` on a 24px icon
- Our `w-9 h-9` (36px) header buttons are below 44px — compensate with generous spacing or padding hit areas
- Text buttons must extend touch target beyond text bounds
## Spacing & Layout
| Element | iOS Value | CSS |
|---|---|---|
| Side margins (iPhone) | 16px | `px-4` |
| Nav bar height | 44px | `h-11` |
| Tab bar height | 49px (+34px safe area) | `h-[49px]` + `pb-[env(safe-area-inset-bottom)]` |
| Bottom safe area (notch) | 34px | `env(safe-area-inset-bottom)` |
| Search bar | 36px field + 8px padding | `h-9` + `py-1` |
| Standard content inset | 16px horizontal | `px-4` |
### Safe area insets
```css
/* Always use for full-screen layouts */
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
height: 100dvh; /* Dynamic viewport height — avoids iOS Safari toolbar */
```
## Navigation Patterns
### iOS-native patterns to follow
- **Primary navigation**: Bottom tab bar (persists across screens)
- **Secondary navigation**: Top nav bar with back button (left) and actions (right)
- **Modals**: Sheet sliding up from bottom (half-screen or full)
- **Context menus**: Long-press or action sheets from bottom
### Primary action placement
```
Top 20%: Navigation, info, secondary actions
Middle 60%: Main content (scrollable)
Bottom 20%: Primary actions (thumb zone) — send, approve, play
```
### Sheets & modals on mobile
- Use bottom sheets with three detents: small (~25%), medium (~50%), large (full)
- Always provide a close button — don't rely solely on swipe-to-dismiss
- Content panels: full-screen overlay or bottom sheet, never side-by-side
## Form Inputs
| Rule | Value | Why |
|---|---|---|
| **Minimum input font** | **16px** | Prevents iOS Safari auto-zoom on focus |
| Minimum field height | 44px | Matches tap target |
| Use `inputmode` | `numeric`, `email`, `tel`, `url`, `search` | Shows appropriate keyboard |
| Use `autocomplete` | Standard attributes | Enables autofill |
| Submit button placement | Bottom of form, thumb zone | Easy to reach |
## Animations & Motion (iOS Spring Model)
### Duration guidelines
| Type | Duration | Tailwind |
|---|---|---|
| Micro-interaction (tap, toggle) | 100-200ms | `duration-150` |
| Standard transition (push/pop) | 250-350ms | `duration-300` |
| Modal presentation (sheet) | 300-400ms | `duration-300` |
| Complex transitions | 400-500ms | `duration-500` |
### iOS-style easing
```css
/* Standard iOS-like transition (ease out / decelerate) */
transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.0);
/* Bouncy spring-like (for playful entrances) */
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
/* Quick snap (micro-interactions) */
transition: transform 0.25s cubic-bezier(0.0, 0.0, 0.2, 1.0);
```
### Motion rules
- Entrances: ease-out (decelerate)
- Exits: ease-in (accelerate)
- Only animate `transform` and `opacity`
- **Always** respect `prefers-reduced-motion`:
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
## Gestures
- Swipe left/right: gallery nav, dismiss
- Swipe down: close overlay/bottom sheet, pull-to-refresh
- Long press: context menu, selection
- Pinch: zoom on images
- Minimum swipe distance: 50px before triggering
## Scroll Behavior
- Lock body scroll when modal/drawer is open
- `overscroll-behavior: contain` on modal content
- `touch-action: manipulation` to prevent zoom on double-tap
- `-webkit-overflow-scrolling: touch` for smooth iOS scroll
## Iconography
| Context | Size | Style |
|---|---|---|
| Tab bar | 25px | Filled/solid |
| Nav bar / toolbar | 22px | Outlined, 1.5px stroke |
| Inline with text | Match font size | Outlined |
| Standalone | 28-33px | Filled or outlined |
## AIUI Custom Overrides (Keep These)
These deviate from stock iOS but are intentional for our design language:
- **Dark-only theme**: No light mode. Background `#0a0a0a`, not iOS system colors
- **Glass morphism**: Translucent surfaces with backdrop-blur instead of iOS solid materials
- **Accent color**: Bitcoin orange `#F7931A` instead of iOS systemBlue
- **Text opacity scale**: Our `/25` → `/96` scale instead of iOS label hierarchy
- **No separator borders**: We use spacing and glass layering instead
- **Custom animations**: `animate-fade-up`, `animate-scale-in` per our design system
- **Header buttons**: Currently 36px (`w-9 h-9`), acceptable with generous spacing
## Performance on Mobile
- Test on real devices, not just emulators
- Test on 3G/4G connections
- Debounce scroll handlers
- Lazy load images with `loading="lazy"`
- Critical CSS inlined, rest loaded async
+52
View File
@@ -0,0 +1,52 @@
---
description: Git workflow - commit conventions, branching, PR process
globs: "**/*"
alwaysApply: false
---
# Git Workflow
## Commit Messages
Format: `type(scope): description`
Types:
- `feat`: new feature
- `fix`: bug fix
- `refactor`: code restructuring (no behavior change)
- `style`: formatting, whitespace (no code change)
- `docs`: documentation
- `test`: adding/updating tests
- `chore`: build, dependencies, tooling
- `perf`: performance improvement
Scope: the package or area (`core`, `app`, `plugin-x`, `renderer-film`, etc.)
Examples:
```
feat(core): add renderer registry with lazy loading
fix(chat): prevent scroll jump on new message
refactor(plugin-system): simplify adapter interface
chore(deps): update Vue to 3.6
```
## Branching
- `main`: production-ready, always deployable
- `dev`: integration branch for features
- `feat/description`: feature branches (from dev)
- `fix/description`: bug fix branches
- `release/x.y.z`: release preparation
## Pull Requests
- One feature per PR
- Description: what changed, why, how to test
- All tests pass
- TypeScript strict mode passes
- No linter errors
- Reviewed before merge
## Rules
- Never force push to `main` or `dev`
- Never commit `.env.local` or any secrets
- Never commit `node_modules`
- Squash merge feature branches to keep history clean
- Tag releases with semver: `v1.0.0`
+30
View File
@@ -0,0 +1,30 @@
---
description: Expert rules for Film content extraction, display, and surfacing
globs: "**/useContentPanel.ts,**/FilmCard.vue,**/FilmGrid.vue,**/FilmDetail.vue,**/mocks/films*"
alwaysApply: false
---
# Films Content Surface
## Extraction Patterns
- **Tagged**: `[[film:f123]]` or `[[film:123]]` → resolved from mock library
- **External**: `[[film_ext:Title|YYYY|Director]]` → create external film with fallback poster
## Edge Cases
- `normalizeFilmId`: `f123` and `123` both become `f123`
- Duplicate prevention: key by `title|year` for externals
- Empty/malformed: skip if title < 2 chars, year invalid
- Poster: use `generatePosterFallback(title, year)` for externals
## Strip Rules
- `stripFilmTags` removes `[[film:...]]` and `[[film_ext:...]]` before displaying text
- Preserve `\n{3,}` → `\n\n` to avoid excessive whitespace
## Display
- FilmCard: poster, title, year, director
- FilmDetail: full metadata, sources, cast
- Panel: grid of FilmCards, click opens FilmDetail in panel
+31
View File
@@ -0,0 +1,31 @@
---
description: Expert rules for Song content extraction, display, and surfacing
globs: "**/useContentPanel.ts,**/SongCard.vue,**/SongGrid.vue,**/SongDetail.vue,**/mocks/songs*"
alwaysApply: false
---
# Songs Content Surface
## Extraction Priority
1. Tagged: `[[song:s123]]` or `[[song_ext:Title|Artist|YYYY]]`
2. Library match: title + artist within 120 chars
3. Patterns: `"Title" by Artist`, `Title Artist`, `**Title** by Artist`
## looksLikeSong Rejection
Reject when title/artist contains: news phrases, "BIP", "protocol", "web search", "mailing list", "training cutoff", etc. See `looksLikeSong()` blocklist.
- Max length: title 55 chars, artist 40 chars
## Edge Cases
- If `extractFilmIds` or `extractPodcastIds` found → return [] (don't mix film/podcast with song patterns)
- If `isNewsLikeResponse` → return [] (news bullets often look like "X Y")
- Skip if title/artist is 4-digit year
- Skip if contains `[[film` or `[[song` tags
- Dedupe by `title|artist` lowercase
## Strip Rules
- `stripSongTags` removes song tags before displaying text
@@ -0,0 +1,26 @@
---
description: Expert rules for Podcast content extraction, display, and surfacing
globs: "**/useContentPanel.ts,**/PodcastCard.vue,**/PodcastGrid.vue,**/PodcastDetail.vue,**/mocks/podcasts*"
alwaysApply: false
---
# Podcasts Content Surface
## Extraction Patterns
- **Tagged**: `[[podcast:p123]]` or `[[podcast_ext:Title|Host|YYYY]]`
- No pattern fallback (unlike songs) — only tags
## Edge Cases
- Duplicate prevention: key by `title|host` lowercase
- Empty: skip if title or host < 2 chars
- Year optional in external format
## looksLikePodcast (when added)
Reject when title/host looks like: news source names, documentation sites, "Bitcoin Mailing List", etc. — same philosophy as `looksLikeSong`.
## Strip Rules
- `stripPodcastTags` removes podcast tags before displaying text
+47
View File
@@ -0,0 +1,47 @@
---
description: Expert rules for News content extraction, merge, and surfacing
globs: "**/useContentPanel.ts,**/useRssFetch.ts,**/NewsGrid.vue,**/ArticleDetail.vue,**/vite-rss*"
alwaysApply: false
---
# News Content Surface
## Sources
1. **Web search**: `message.webResults` from AI (with imgSrc, content)
2. **RSS**: Fetched from website URLs only when `newsContext` is true
## newsContext
- `isNewsQuery(userQuery)` — "news", "latest", "what's happening", "what are people saying", etc.
- `isNewsLikeResponse(text)` — "for instant news", "check these sources", "access to web search", etc.
## Merge Rules
- `mergeNewsResults(web, rss)` — dedupe by URL (normalized: lowercase, no trailing slash)
- Web results take precedence when URL collision
## RSS Fetch Guard
- **Only fetch RSS when `newsContext` is true and `mergedWebsites.length > 0`** — avoid surfacing irrelevant RSS from docs/resource links when user asked "websites"
- Max 8 URLs, 15 articles total, 5 sites tried
- Timeout: 15s client, 5s per feed server-side
## Display
- NewsGrid (variant=news): articles open in **ArticleDetail** (in-panel)
- Relevance sort when `query` provided
- Search filter by title, content, url
- imgSrc: validate with `isSafeImgUrl` (https only)
## Known Limitations
- **RSS language**: Feeds return whatever the site publishes; no query/language filtering — may surface non-English articles
- **RSS relevance**: No semantic filtering; articles are shown as published
## ArticleDetail Security
- `sanitizeHtml`: allow only safe tags (p, br, a, strong, em, ul, ol, li, blockquote, h1-h4)
- Strip script, style, iframe, object, embed
- Links: `href` must be `https?://`, reject `javascript:`
- Images: `src` must be `https?://`
@@ -0,0 +1,32 @@
---
description: Expert rules for Websites content extraction and surfacing
globs: "**/useContentPanel.ts,**/NewsGrid.vue,**/articleOverlay*"
alwaysApply: false
---
# Websites Content Surface
## Extraction
1. **Markdown links**: `[Title](https://...)` — extract all with `extractMarkdownLinks`
2. **Bold domains**: `**Name** (domain.tld)` — extract with `extractBoldDomainLinks`
3. Merge with `mergeNewsResults` (dedupe by URL)
## URLs Validation
- Scheme: `https?://` only
- `new URL(raw)` must not throw
- Min length: title 2, url 10 chars
- Normalize for dedupe: lowercase, no trailing slash
## Display
- NewsGrid (variant=websites): card with favicon/globe icon
- Click → **overlay iframe** (not ArticleDetail)
- Use `articleOverlayStore.open(url, title, undefined, imgSrc)`
## Distinction from News
- News = articles (web search + RSS) → ArticleDetail in panel
- Websites = plain links from response → overlay iframe
- Same NewsGrid component, different `variant` and click handler
@@ -0,0 +1,42 @@
---
description: Expert rules for Magazine/Brief content extraction and surfacing
globs: "**/useContentPanel.ts,**/MagazineGrid.vue"
alwaysApply: false
---
# Magazine Content Surface
## Detection
- `hasMagazine` = sections ≥ 1 AND (newsQuery OR newsLikeResponse OR context keywords)
- Context keywords: sentiment, bearish, bull case, macro, %, BTC, bitcoin, BIP, protocol, debate, what's happening
## Section Extraction Order
1. `## Heading` blocks — content until next ## or **Section**
2. `**Pro/Anti camp**` blocks with emoji
3. Bullets: `- **Title**: Content` or `- **Title** — Content` (em/en dash)
4. Attributed: `- **Name** (Role) description`
5. Intro paragraph (before first ##)
6. "Key takeaway" / "This is being called..."
7. "For deeper analysis" / further reading
## Section Rules
- Min: title 2 chars, content 15 chars
- Max content: 2000 chars per section
- Dedupe by title prefix (first 50 chars)
- Skip bullets already inside ## blocks (`blockContents`)
- `addSection` extracts: url, author, imageUrl from content
## Hero Image
1. First markdown image in text
2. First `.jpg|.png|.gif|.webp` URL
3. `webResults[0]?.imgSrc`
4. Picsum fallback seeded by query
## Format & Security
- `formatContent`: escape `&<>`, preserve `**bold**` as `<strong>`, `\n\n` → `</p><p>`
- Meme: imgflip URLs, contextual by topic (bearish, bull, Bitcoin, macro)
+33
View File
@@ -0,0 +1,33 @@
# AIUI Development Environment
# Copy this file to .env.local and fill in your values
# AI Provider (OpenRouter - gives access to many models including free ones)
# Get your key at: https://openrouter.ai/keys
VITE_OPENROUTER_API_KEY=sk-or-your-key-here
# Anthropic Claude — for live web search (Claude invokes search mid-response):
# Option 1: OAuth token from Max subscription (run: claude setup-token, save output)
# → No extra cost; uses your existing Max subscription.
ANTHROPIC_TOKEN=sk-ant-oat01-your-token-here
# Option 2: API key from https://console.anthropic.com/settings/keys
ANTHROPIC_API_KEY=sk-ant-your-key-here
#
# Without either: proxy uses CLI with built-in WebSearch + pre-fetched context.
# TMDB API (free, fetches posters on-demand when images fail)
# Get your key at: https://www.themoviedb.org/settings/api
TMDB_API_KEY=your-tmdb-key-here
# Jamendo API (optional, extends music search - free 35k req/mo)
# Get your client_id at: https://devportal.jamendo.com/
JAMENDO_CLIENT_ID=your-jamendo-client-id
# SearXNG instance for web search (optional)
# Uses public instances by default; falls back to DuckDuckGo when they fail.
# For reliable dev: host your own (https://docs.searxng.org/) or rely on DDG fallback.
# SEARXNG_URL=https://your-searxng.instance
# Development flags
VITE_DEV_MODE=true
VITE_MOCK_MEDIA_SOURCES=true
VITE_DISABLE_CRYPTO=true
+131
View File
@@ -0,0 +1,131 @@
name: CI
on:
push:
branches: [main, development]
pull_request:
branches: [main, development]
jobs:
lint-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test
bundle-size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Check bundle size
run: |
BUNDLE_SIZE=$(find packages/app/dist/assets -name '*.js' -o -name '*.css' | xargs gzip -c | wc -c)
BUNDLE_KB=$((BUNDLE_SIZE / 1024))
echo "Bundle size: ${BUNDLE_KB}KB gzipped"
if [ "$BUNDLE_KB" -gt 250 ]; then
echo "::error::Bundle size ${BUNDLE_KB}KB exceeds 250KB budget"
exit 1
fi
echo "Bundle size ${BUNDLE_KB}KB is within 250KB budget"
e2e:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: cd packages/app && npx playwright install --with-deps ${{ matrix.browser }}
- run: cd packages/app && pnpm test:e2e --project=${{ matrix.browser }}
e2e-mobile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: cd packages/app && npx playwright install --with-deps chromium webkit
- run: cd packages/app && pnpm test:e2e --project=iphone14 --project=galaxy-s21
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v12
with:
configPath: packages/app/lighthouserc.json
uploadArtifacts: true
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Audit dependencies
run: pnpm audit --audit-level=critical || true
- name: Check licenses
run: |
npx license-checker --production --onlyAllow 'MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD;CC0-1.0;Unlicense;CC-BY-4.0;Python-2.0;BlueOak-1.0.0' --excludePrivatePackages || echo "::warning::Non-approved licenses found"
+57
View File
@@ -0,0 +1,57 @@
name: Weekly Dependency Audit
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am UTC
workflow_dispatch:
jobs:
audit:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Audit for vulnerabilities
id: audit
run: |
AUDIT_RESULT=$(pnpm audit --audit-level=moderate 2>&1) || true
echo "$AUDIT_RESULT"
if echo "$AUDIT_RESULT" | grep -q "critical"; then
echo "has_critical=true" >> $GITHUB_OUTPUT
else
echo "has_critical=false" >> $GITHUB_OUTPUT
fi
- name: Check licenses
id: licenses
run: |
LICENSE_RESULT=$(npx license-checker --production --onlyAllow 'MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD;CC0-1.0;Unlicense;CC-BY-4.0;Python-2.0;BlueOak-1.0.0' --excludePrivatePackages 2>&1) || true
echo "$LICENSE_RESULT"
if echo "$LICENSE_RESULT" | grep -q "FAIL"; then
echo "has_violations=true" >> $GITHUB_OUTPUT
else
echo "has_violations=false" >> $GITHUB_OUTPUT
fi
- name: Create issue if violations found
if: steps.audit.outputs.has_critical == 'true' || steps.licenses.outputs.has_violations == 'true'
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '⚠️ Dependency audit: violations found',
body: `The weekly dependency audit found issues:\n\n- Critical vulnerabilities: ${{ steps.audit.outputs.has_critical }}\n- License violations: ${{ steps.licenses.outputs.has_violations }}\n\nRun \`pnpm audit\` and \`npx license-checker\` locally for details.`,
labels: ['security', 'dependencies'],
})
+48
View File
@@ -0,0 +1,48 @@
# Dependencies
node_modules/
.pnpm-store/
# Build output
dist/
*.tsbuildinfo
# Turborepo
.turbo/
# Environment (secrets)
.env.local
.env.*.local
# Tauri
packages/app/src-tauri/target/
# IDE
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Dev chat history
.dev/
# Debug
npm-debug.log*
pnpm-debug.log*
# Test coverage
coverage/
# Overnight loop logs
loop/loop.log
# Playwright
test-results/
playwright-report/
playwright/.cache/
# Storybook
storybook-static/
+345
View File
@@ -0,0 +1,345 @@
# CLAUDE.md — AIUI Project Guide
## Project Overview
AIUI is a next-generation AI content surface UI. It's a **pnpm monorepo** with two packages:
- `@aiui/app` — Reference application (Vite + Vue 3 + Tailwind CSS)
- `@aiui/core` — Reusable component library
**Stack**: Vue 3 (Composition API), TypeScript ~5.8 (strict), Vite, Tailwind CSS, Pinia, Vue Router, Turborepo
**Node**: >=20.0.0 | **pnpm**: >=10.0.0
## Quick Reference
```bash
pnpm dev # Run app dev server + Claude proxy
pnpm dev:core # Watch-build core library
pnpm build # Build all packages (turbo)
pnpm test # Run tests (vitest)
pnpm lint # Lint all packages (eslint)
pnpm typecheck # Type-check all packages (vue-tsc)
pnpm clean # Remove dist/ directories
```
Dev server: `http://localhost:5173` | Claude proxy: `http://localhost:3141`
## Core Philosophy
- **Open source only** — MIT/Apache-2.0 licensed dependencies only
- **Decentralized-first** — Pluggable adapters, no vendor lock-in
- **Bitcoin only** — sats/Lightning/Cashu/Fedimint. Never fiat, never altcoins. AIUI is never a wallet — always deep-link to external wallets
- **Privacy-first** — E2E encryption (tweetnacl.js), encrypted local storage (AES-256-GCM), no tracking/telemetry
- **Mobile-first, everywhere-perfect** — Desktop is an enhancement of the mobile experience
- **Plugin-everything** — All integrations go through typed plugin interfaces
## Vue 3 Conventions
**Always use `<script setup lang="ts">`** — never Options API.
### Script section ordering
Imports → Props (`defineProps`) → Emits (`defineEmits`) → Reactive state → Computed → Watchers → Methods → Lifecycle hooks → `defineExpose`
### Naming
| Thing | Convention | Example |
|-------|-----------|---------|
| Components | PascalCase | `ProjectCard.vue` |
| Composables | camelCase, `use` prefix | `useTheme.ts` |
| Props (JS) | camelCase | `projectName` |
| Props (template) | kebab-case | `project-name` |
| Boolean props | `is`/`has`/`can`/`should` prefix | `isVisible`, `canEdit` |
| Emits (template) | kebab-case with colon namespacing | `project:updated` |
| Stores | camelCase, `use` prefix, `Store` suffix | `useSettingsStore` |
### Reactive state rules
- `ref()` for primitives, `reactive()` for objects
- `computed()` for derived values — no side effects in computed
- `shallowRef()` for large collections/objects not requiring deep reactivity
- Always use unique IDs for `:key` — never array index
### Props
Always use object-style with type annotations, never array-style:
```ts
// Correct
defineProps<{ title: string; count?: number }>()
// Wrong
defineProps(['title', 'count'])
```
### Performance
- Lazy load with `defineAsyncComponent` for non-critical components
- Use `onErrorCaptured` for error boundaries
- Always handle loading/error/data states in async operations
## File Structure
```
packages/app/src/
├── components/
│ ├── ui/ # Generic UI components
│ ├── chat/ # Chat interface components
│ ├── content-panel/ # Content panel components
│ ├── renderers/ # Content type renderers
│ └── layout/ # Layout components
├── composables/ # Shared composition functions
├── stores/ # Pinia stores
├── pages/ # Route-level components
├── styles/ # Global CSS, themes, tokens
├── utils/ # Pure utility functions
├── types/ # TypeScript type definitions
├── plugins/ # Plugin system
└── mocks/ # Dev fixtures & mock data
packages/core/src/
├── plugins/ # Plugin system interfaces
└── types/ # Shared TypeScript types
```
## Tailwind & Design System
### Glass Morphism (Archy-derived)
This project uses a glass morphism design language. Key utility classes:
| Class | Purpose |
|-------|---------|
| `.glass` | Standard glass: `rgba(0,0,0,0.35)`, `blur(18px)`, white border 0.18 opacity |
| `.glass-strong` | Stronger blur: `blur(24px)` |
| `.glass-card` | Card variant: `rgba(0,0,0,0.65)`, `border-radius: 1rem` |
| `.glass-button` | Button: 48px height, `rgba(0,0,0,0.6)`, `blur(18px)` |
| `.glass-button-sm` | Compact button variant |
| `.gradient-card` | Gradient background card |
### Spacing
4px grid system: `1`=4px, `2`=8px, `3`=12px, `4`=16px, etc.
### Colors
- Background: `#0a0a0a` (near-black)
- Accent / Bitcoin orange: `#F7931A`
- Primary: `#606060`
- Text opacity scale: `/25` (placeholder) → `/40` (muted) → `/60` (secondary) → `/70` (interactive) → `/80` (body) → `/90` (emphasis) → `/96` (headings) → `text-white` (active)
- No separator borders between major sections
### Typography
`Inter`/`system-ui` for body, `Menlo`/`Monaco` for monospace.
### Responsive breakpoints (mobile-first)
`sm` 640px → `md` 768px → `lg` 1024px → `xl` 1280px → `2xl` 1536px
### Animations
- `animate-fade-up` (900ms), `animate-fade-up-fast` (400ms), `animate-fade-in` (500ms), `animate-scale-in` (250ms)
- Duration: 100ms micro, 200ms fast, 300ms moderate, 500ms normal, 600ms max
- Easing: `ease-out` for entrances (90% of animations), `ease-in` for exits
- Only animate `transform` and `opacity` — avoid animating layout properties
- Always respect `prefers-reduced-motion`
## Content Surfaces Architecture
Every content renderer supports up to five surfaces:
1. **Chat Preview** (~120px max) — inline bubble, identify content at a glance
2. **Chat Play** (~200px max) — inline playback with expand button
3. **Panel Preview** (unlimited) — full browsing, filtering, sorting
4. **Panel Play** — full immersive playback
5. **Panel Edit** — full interaction, sends changes back to chat
On mobile, Panel surfaces open as full-screen overlays, not side-by-side.
```ts
interface RendererDefinition {
id: string
name: string
contentType: string
surfaces: SurfaceType[]
chatPreview?: Component
chatPlay?: Component
panelPreview?: Component
panelPlay?: Component
panelEdit?: Component
lazyDependencies?: () => Promise<any>
}
```
Chat surfaces must have zero lazy dependencies. Panel surfaces may lazy-load heavy libraries.
## Plugin System
All integrations are plugins. Plugin types: `ai-provider`, `media-source`, `messaging`, `storage`, `renderer`, `file-handler`, `crypto`, `search`, `auth`, `wallet`, `social-embed`, `mcp`, `media`.
```ts
interface AIUIPlugin {
id: string
name: string
version: string
type: PluginType
description?: string
init(context: PluginContext): Promise<void>
destroy(): Promise<void>
isAvailable(): Promise<boolean>
}
```
Sandboxing: Tier 1 (trusted built-in), Tier 2 (community — sandboxed iframes), Tier 3 (external processes). Community plugins get no direct DOM access.
## AI Provider Integration
```ts
interface AIProviderAdapter extends AIUIPlugin {
type: 'ai-provider'
chat(messages: Message[], options: ChatOptions): AsyncIterable<ChatChunk>
models(): Promise<Model[]>
supportsStreaming: boolean
supportsVision: boolean
supportsTools: boolean
}
```
Normalize tool calling across providers (OpenAI `tool_calls` vs Claude `tool_use`). Never include API keys in context injection.
## Security & Crypto
- E2E encryption: tweetnacl.js XSalsa20-Poly1305
- Local storage: Web Crypto API AES-256-GCM + PBKDF2 (100K+ iterations)
- API keys: encrypted at rest, never in localStorage, never logged, masked in UI (last 4 chars)
- No `eval()` or `innerHTML` with untrusted content
- Sanitize all user input against XSS
- HTTPS only, CSP headers in production
- Dev bypass: `VITE_DISABLE_CRYPTO=true` (never in production)
## Accessibility
WCAG AA minimum compliance:
- Color contrast: 4.5:1 normal text, 3:1 large/interactive
- Keyboard: all elements focusable via Tab, visible focus indicators, Escape closes modals
- Semantic HTML: use `<header>`, `<nav>`, `<main>`, `<article>`, `<aside>`, `<footer>` — not div soup
- ARIA: `aria-label` for icon buttons, `aria-live="polite"` for dynamic updates, `sr-only` for screen reader text
- Touch targets: min 44x44px with 8px gaps
- All images need `alt` attributes (decorative: `alt=""`)
- Respect `prefers-reduced-motion`
## Performance Budget
- **Initial load**: < 250KB gzipped
- Core bundle: Vue + Tailwind + Pinia + Router + chat UI (~150KB) + markdown + streaming (~50KB)
- Everything else: lazy-loaded on demand
- Virtual scrolling (TanStack Virtual) for chat lists
- Clean up listeners in `onUnmounted`, use `shallowRef` for large data
- Core Web Vitals: LCP < 2.5s, FID < 100ms, CLS < 0.1
- Preconnect to API hosts, debounce inputs (100ms)
## Mobile UX (iOS HIG-Informed)
Follows Apple iOS Human Interface Guidelines. See `.cursor/rules/15-mobile-ux.mdc` for full reference.
- **Typography**: Body 17px, Footnote 13px, Caption 11px minimum — never smaller than 11px
- **Touch targets**: min 44×44px tappable area, 8px gap between targets
- **Side margins**: 16px (`px-4`)
- **Primary actions**: Bottom thumb zone
- **Viewport**: `height: 100dvh` with `env(safe-area-inset-*)` for notched devices
- **Form inputs**: min 16px font (prevents iOS zoom), appropriate `inputmode`
- **Content panels**: Full-screen overlay or bottom sheet on mobile, never side-by-side
- **Transitions**: 150ms micro, 300ms standard, 400ms modal — ease-out entrances, ease-in exits
- **Sheets**: Bottom sheets with close button, don't rely solely on swipe-to-dismiss
- Support both portrait and landscape
## Environment & Dev Mode
Env vars must be prefixed `VITE_`. Secrets go in `.env.local` (gitignored). See `.env.example` for template.
Feature flags via `useFeatureFlags()`: `isDev`, `isTauri`, `isMobile`, `isCryptoEnabled`, `isMockData`
Dev mode enables: mock data, debug panel, verbose logging, disabled encryption, all renderers without lazy loading.
## Git Conventions
### Commit format
```
type(scope): description
```
**Types**: `feat`, `fix`, `refactor`, `style`, `docs`, `test`, `chore`, `perf`
**Scope**: package or area — `core`, `app`, `chat`, `renderer-film`, `plugin-x`
### Branches
`main` (production), `dev` (integration), `feat/description`, `fix/description`
### Rules
- One feature per PR
- All tests pass, TypeScript strict passes, no lint errors
- No force push to main/dev
- Never commit `.env.local`, secrets, or `node_modules`
- Squash merge features, tag releases `v1.0.0`
## Archipelago (Archy) Integration
AIUI runs inside an iframe in Archipelago's Chat mode. All communication with the host happens via `window.postMessage()` through a strict protocol.
### Architecture
```
AIUI (iframe) ←→ postMessage ←→ Archy ContextBroker ←→ Node data
```
AIUI is **quarantined** — it never directly accesses Archy's APIs, stores, or node data. The Archy ContextBroker fetches and sanitizes data before passing it to AIUI.
### Protocol
Use `archyBridge.ts` (`src/services/archyBridge.ts`) for all Archy communication:
```ts
import { archyBridge } from '@/services/archyBridge'
// Request context (respects user permissions)
const apps = await archyBridge.requestContext('apps')
if (!apps.permitted) {
// Show: "Enable 'Installed Apps' access in Archy Settings"
}
// Request an action
await archyBridge.requestAction('open-app', { appId: 'btcpay-server' })
// Listen for theme/permission updates
archyBridge.onPermissionsUpdate((categories) => { ... })
archyBridge.onThemeUpdate((theme) => { ... })
```
**Context categories** (user toggles each on/off in Archy Settings):
- `apps` — App names, status, health (no credentials)
- `system` — CPU, RAM, disk (no paths or IPs)
- `network` — Connection status, peer count (no IPs)
- `wallet` — Balance, channel count (no keys or seeds)
- `files` — File/folder names (no contents)
### Critical Rules
1. **NEVER** fetch Archy APIs directly — always use `archyBridge`
2. **NEVER** store or log raw user data from context responses
3. **NEVER** make HTTP requests to the host machine
4. Handle `permitted: false` gracefully — tell users what to enable
5. Send `ready` message on mount so Archy knows the iframe loaded
6. Build must output a static SPA servable from any base path
7. All AI provider keys are user-provided and stored locally in AIUI only
### Build & Deploy
AIUI deploys as a Podman container on the Archy node:
- Build: `pnpm build``packages/app/dist/`
- Container: nginx:alpine serving the dist
- Proxied at `/aiui/` via Archy's nginx
- Updates independently of Archy — new container image = new version
Binary file not shown.
+267
View File
@@ -0,0 +1,267 @@
# Claude Code Overnight Automation
Run Claude Code headlessly overnight to execute a full task checklist — with rate-limit resilience, macOS sleep prevention, and a stop hook that prevents Claude from quitting until every task is done.
## How It Works
```
loop.sh (orchestrator)
|
+--> Reads plan.md for unchecked [ ] tasks
+--> Pipes prompt.md into `claude -p` (headless mode)
| |
| +--> Claude reads your plan, specs, and project rules
| +--> Implements tasks one by one
| +--> Runs typecheck/lint/test after each
| +--> Commits, marks [x], moves to next
| |
| +--> Claude tries to stop
| |
| +--> Stop Hook intercepts
| +--> Checks plan.md for remaining [ ] tasks
| +--> If incomplete: BLOCKS the stop (Claude continues)
| +--> If all done: allows stop
|
+--> Detects rate limits in output
| +--> Sleeps 1 hour, retries (up to 5x)
| +--> After 5 retries: schedules macOS launchd job to resume later
|
+--> Loops N iterations (default 10)
+--> Exits when all tasks checked or iterations exhausted
```
### The "Ralph Wiggum" Stop Hook
The secret sauce. Claude Code supports a `Stop` hook — a shell script that runs every time Claude tries to end its session. By returning `{"decision":"block"}`, the hook **prevents Claude from stopping**. Combined with `--dangerously-skip-permissions`, Claude becomes a fully autonomous task executor that won't quit until the job is done.
### Sleep Prevention
On macOS, `caffeinate -i` prevents idle sleep during long runs. A hook starts it when Claude begins and kills it when Claude finishes.
### Rate Limit Resilience
If Claude hits API rate limits:
1. **Inline retry**: Sleep 1 hour, then retry the same iteration
2. **Scheduled retry**: After 5 failed retries, create a macOS `launchd` plist that auto-runs the loop later
3. The plist self-destructs after executing
## Prerequisites
- **Claude Code CLI** (`claude` command available in PATH)
- Install: https://docs.anthropic.com/en/docs/claude-code
- Must be logged in: run `claude login` first
- **macOS** (for `caffeinate` and `launchd` — see Linux notes below)
- **Git** (the script commits after each task)
- A project with `package.json` or similar build tooling
## Quick Start
```bash
# 1. Clone or copy this folder into your project
cp -r "For Others/templates" ~/my-project/loop
# 2. Run the setup script (creates hooks, updates settings)
cd ~/my-project
bash "path/to/For Others/setup.sh"
# 3. Edit your task list
vim loop/plan.md
# 4. Edit your prompt (project-specific rules)
vim loop/prompt.md
# 5. Start the overnight run
./loop/loop.sh
```
Or just run the setup script — it walks you through everything:
```bash
bash "For Others/setup.sh"
```
## File Structure
After setup, your project will have:
```
your-project/
loop/
loop.sh # Main orchestrator (run this)
prompt.md # Instructions piped to Claude each iteration
plan.md # Task checklist ([ ] = todo, [x] = done)
loop.log # Full output log (auto-created)
~/.claude/
hooks/
prevent-sleep.sh # Starts caffeinate on session start
stop-hook-autonomous.sh # Blocks stop until tasks complete
allow-sleep.sh # Kills caffeinate on session end
settings.json # Hook registrations (auto-updated by setup)
```
## Configuration
All config is via environment variables (set before running `loop.sh` or export in your shell):
| Variable | Default | Description |
|----------|---------|-------------|
| `CLAUDE_AUTONOMOUS` | `1` | Set to `0` to disable the stop hook (Claude can quit freely) |
| `ITERATION_COUNT` | `10` | Max loop iterations |
| `ITERATION_DELAY` | `30` | Seconds to pause between iterations |
| `RATE_LIMIT_WAIT` | `3600` | Seconds to sleep when rate limited (1 hour) |
| `MAX_RATE_LIMIT_RETRIES` | `5` | Retries before scheduling launchd |
| `CLAUDE_BIN` | `claude` | Path to Claude CLI binary |
| `PROMPT_FILE` | `loop/prompt.md` | Path to prompt file |
| `LOG_FILE` | `loop/loop.log` | Path to log file |
### Examples
```bash
# Quick test run (2 iterations, 10s delay, no stop hook)
CLAUDE_AUTONOMOUS=0 ITERATION_COUNT=2 ITERATION_DELAY=10 ./loop/loop.sh
# Full overnight run (20 iterations, 1 min between)
ITERATION_COUNT=20 ITERATION_DELAY=60 ./loop/loop.sh
# Use a custom prompt
PROMPT_FILE=my-prompt.md ./loop/loop.sh
```
## Writing Your Plan
`loop/plan.md` is a markdown checklist. Each line starting with `- [ ]` is a pending task:
```markdown
## Phase 1: Core Features
- [ ] **1.1** — Add user authentication (JWT + refresh tokens)
- [ ] **1.2** — Create user profile page with avatar upload
- [ ] **1.3** — Add settings page with theme toggle
## Phase 2: API
- [ ] **2.1** — REST endpoints for CRUD operations
- [ ] **2.2** — WebSocket support for real-time updates
## Final
- [ ] **FINAL** — Run full test suite, fix any failures, tag release
```
Claude will:
1. Find the first `- [ ]` line
2. Read the spec from your prompt or a separate spec file
3. Implement it
4. Mark it `- [x]`
5. Move to the next
### Tips for good plans
- **Be specific**: "Add JWT auth with refresh tokens, store in httpOnly cookies" > "Add auth"
- **Order matters**: Put foundational tasks first (types, utils, config) before features that depend on them
- **Include testing gates**: "Run `pnpm test` and fix failures" as part of each task
- **Keep tasks small**: 30-60 minutes of work each. Large tasks lead to context window exhaustion
- **Add a FINAL task**: A catchall that runs the full test suite
## Writing Your Prompt
`loop/prompt.md` is what Claude reads at the start of every iteration. Include:
1. **What files to read** (your plan, specs, project conventions)
2. **Project-specific rules** (coding style, frameworks, constraints)
3. **Per-task workflow** (implement → test → commit → mark done)
4. **Hard rules** (what to never do, minimum effort before skipping)
See `templates/prompt.md` for a starting template.
## Operating the Loop
### Starting
```bash
# Foreground (see output live)
./loop/loop.sh
# Background with logging
nohup ./loop/loop.sh > /dev/null 2>&1 &
# With caffeinate (prevents sleep even if hooks fail)
caffeinate -i ./loop/loop.sh
```
### Monitoring
```bash
# Watch the log live
tail -f loop/loop.log
# Check progress
grep -c '\- \[x\]' loop/plan.md # completed
grep -c '\- \[ \]' loop/plan.md # remaining
# Check git commits
git log --oneline -20
```
### Stopping
- **Let it finish**: The loop stops automatically when all tasks are checked
- **Kill it**: `Ctrl+C` or `kill %1` — Claude's current task will be interrupted but committed work is preserved
- **Disable stop hook**: Set `CLAUDE_AUTONOMOUS=0` in the environment before the next iteration
### Resuming
Just run `./loop/loop.sh` again. It reads `plan.md` fresh each iteration, so it picks up where it left off (skipping `[x]` tasks).
## Customizing the Prompt
The prompt template has `{{PLACEHOLDER}}` markers. Replace them with your project's specifics:
| Placeholder | What to put |
|-------------|-------------|
| `{{SPEC_FILE}}` | Path to your detailed spec (e.g., `SPEC.md`, `docs/plan.md`) |
| `{{PROJECT_RULES_FILE}}` | Path to your coding conventions file |
| `{{PROJECT_RULES}}` | Inline coding rules (style, frameworks, constraints) |
## Troubleshooting
### Claude exits immediately
- Make sure `claude login` has been run
- Check that `claude -p "hello"` works in your terminal
- Verify `~/.claude/hooks/stop-hook-autonomous.sh` exists and is executable
### Rate limit loop
- Default wait is 1 hour. Increase `RATE_LIMIT_WAIT` if your limits are longer
- Check `loop.log` for the specific rate limit message
- Claude Max subscriptions have higher limits than API keys
### Mac goes to sleep
- Run `caffeinate -i ./loop/loop.sh` as a belt-and-suspenders approach
- Check that `~/.claude/hooks/prevent-sleep.sh` is executable: `chmod +x ~/.claude/hooks/prevent-sleep.sh`
### Tasks not getting marked complete
- Ensure your plan uses exact format: `- [ ]` (dash, space, brackets, space)
- The stop hook matches `^\s*[-*]?\s*\[\s*\]` — standard markdown checkboxes
### Stop hook not working
- Verify `CLAUDE_AUTONOMOUS=1` is set: `echo $CLAUDE_AUTONOMOUS`
- Check hook is registered in `~/.claude/settings.json`
- Test the hook manually: `echo '{}' | bash ~/.claude/hooks/stop-hook-autonomous.sh`
## Linux Notes
The system is macOS-focused but works on Linux with minor changes:
- **Sleep prevention**: Replace `caffeinate` with `systemd-inhibit --what=idle --who=claude-loop --why="Overnight automation" sleep infinity &` or simply disable sleep via `systemctl mask sleep.target`
- **Scheduled retry**: Replace the launchd plist section in `loop.sh` with a `systemd-run --on-calendar` or `at` command
- **Hooks work identically** — they're plain bash scripts
## Security Notes
- `--dangerously-skip-permissions` gives Claude **full system access** within the project. Only run on trusted codebases.
- The loop runs as your user — Claude can read/write anything you can
- API keys in `.env.local` are accessible to Claude during the session
- Review commits after an overnight run before pushing to production
- Consider running in a VM or container for additional isolation
## License
MIT. Use it however you want.
+496
View File
@@ -0,0 +1,496 @@
#!/usr/bin/env bash
# ============================================================================
# Claude Code Overnight Automation — One-File Setup
# ============================================================================
# Run from your project root:
# bash setup.sh
#
# This single script creates everything:
# loop/loop.sh — main orchestrator
# loop/prompt.md — template prompt for Claude
# loop/plan.md — your task checklist
# ~/.claude/hooks/ — sleep prevention + autonomous stop hook
# ~/.claude/settings.json — hook registrations
# ============================================================================
set -euo pipefail
BOLD='\033[1m'
DIM='\033[2m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
RED='\033[0;31m'
NC='\033[0m'
ok() { echo -e " ${GREEN}+${NC} $1"; }
warn() { echo -e " ${YELLOW}!${NC} $1"; }
err() { echo -e " ${RED}x${NC} $1"; }
info() { echo -e " ${DIM}$1${NC}"; }
echo ""
echo -e "${BOLD} Claude Code Overnight Automation${NC}"
echo -e " ${DIM}────────────────────────────────────${NC}"
echo ""
# ── Prerequisites ────────────────────────────────────────────────────────────
echo -e " ${BOLD}Checking prerequisites...${NC}"
echo ""
MISSING=0
if command -v claude &>/dev/null; then
ok "Claude CLI: $(which claude)"
else
err "Claude CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
MISSING=1
fi
if command -v git &>/dev/null; then
ok "Git: $(which git)"
else
err "Git not found."
MISSING=1
fi
if [[ "$(uname)" == "Darwin" ]]; then
ok "macOS (caffeinate + launchd available)"
else
warn "Not macOS — sleep hooks need Linux equivalents (see README)"
fi
if git rev-parse --is-inside-work-tree &>/dev/null; then
PROJECT_DIR="$(git rev-parse --show-toplevel)"
ok "Project: $PROJECT_DIR"
else
PROJECT_DIR="$(pwd)"
warn "Not a git repo — using: $PROJECT_DIR"
fi
[[ "$MISSING" -eq 1 ]] && { echo ""; err "Fix the above and re-run."; exit 1; }
echo ""
# ── Create loop/ directory ───────────────────────────────────────────────────
echo -e " ${BOLD}Creating loop files...${NC}"
echo ""
LOOP_DIR="$PROJECT_DIR/loop"
mkdir -p "$LOOP_DIR"
# ── loop.sh (embedded) ──────────────────────────────────────────────────────
if [[ -f "$LOOP_DIR/loop.sh" ]]; then
warn "loop/loop.sh exists — skipping"
else
cat > "$LOOP_DIR/loop.sh" << 'LOOPEOF'
#!/usr/bin/env sh
# Claude Code Overnight Automation — Loop Script
# Usage: ./loop/loop.sh
# Config via env vars: ITERATION_COUNT, ITERATION_DELAY, CLAUDE_AUTONOMOUS, etc.
set -u
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}"
LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}"
ITERATION_COUNT="${ITERATION_COUNT:-10}"
ITERATION_DELAY="${ITERATION_DELAY:-30}"
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}"
MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}"
CLAUDE_EXIT=0
cd "$PROJECT_DIR"
log() { echo "$1" | tee -a "$LOG_FILE"; }
banner() {
log ""; log "════════════════════════════════════════════════════════════════"
log " $1"; log " $(date '+%Y-%m-%d %H:%M:%S')"
log "════════════════════════════════════════════════════════════════"; log ""
}
section() { log ""; log "────────────────────────────────────────"; log " $1"; log "────────────────────────────────────────"; log ""; }
plan_has_tasks() { grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null; }
remaining_tasks() { grep -c '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null || echo "0"; }
next_task() { grep -m1 '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null | sed 's/^- \[ \] //' || echo "(none)"; }
check_rate_limit() {
[ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1
tail -50 "$LOG_FILE" 2>/dev/null | grep -v "^Rate limit" | grep -v "^Sleeping" | grep -v "^═" | grep -v "^─" \
| grep -qi -e "rate.limit" -e "too.many.requests" -e "429" -e "quota.exceeded" -e "usage.limit" -e "limit.reached" 2>/dev/null
}
banner "OVERNIGHT AUTOMATION STARTED"
log " Project: $PROJECT_DIR"
log " Prompt: $PROMPT_FILE"
log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}"
log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s delay)"
log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry ${MAX_RATE_LIMIT_RETRIES}x"
log " Tasks left: $(remaining_tasks)"
log " Next task: $(next_task)"
log ""
i=1; rate_limit_retries=0
while [ "$i" -le "$ITERATION_COUNT" ]; do
if ! plan_has_tasks; then
banner "ALL TASKS COMPLETE"; log " No remaining [ ] tasks. Stopping."; break
fi
section "ITERATION $i/$ITERATION_COUNT"
log " Remaining: $(remaining_tasks)"; log " Next: $(next_task)"; log ""
export CLAUDE_PROJECT_DIR="$PROJECT_DIR"
export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}"
if [ -f "$PROMPT_FILE" ]; then
log " Starting Claude..."; log ""
"$CLAUDE_BIN" -p --dangerously-skip-permissions < "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE"
CLAUDE_EXIT=$?; log ""; log " Exit code: $CLAUDE_EXIT"
else
log " ERROR: $PROMPT_FILE not found"; exit 1
fi
if check_rate_limit; then
rate_limit_retries=$((rate_limit_retries + 1))
if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then
section "RATE LIMITED — SCHEDULING RETRY"
PLIST_LABEL="com.claude-loop.overnight-retry"
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M')
RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1); RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2)
cat > "$PLIST_PATH" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>${PLIST_LABEL}</string>
<key>ProgramArguments</key><array>
<string>/bin/sh</string><string>-c</string>
<string>cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH}</string>
</array>
<key>StartCalendarInterval</key><dict><key>Hour</key><integer>${RETRY_HOUR}</integer><key>Minute</key><integer>${RETRY_MIN}</integer></dict>
<key>EnvironmentVariables</key><dict>
<key>CLAUDE_AUTONOMOUS</key><string>1</string>
<key>CLAUDE_PROJECT_DIR</key><string>${PROJECT_DIR}</string>
<key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin</string>
</dict>
<key>StandardOutPath</key><string>${LOG_FILE}</string>
<key>StandardErrorPath</key><string>${LOG_FILE}</string>
</dict></plist>
PLIST
launchctl load "$PLIST_PATH" 2>/dev/null || true
log " Scheduled retry at ~${RETRY_TIME}"; exit 0
fi
section "RATE LIMITED — WAITING"
log " Attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES"; log " Sleeping ${RATE_LIMIT_WAIT}s..."
sleep "$RATE_LIMIT_WAIT"
if ! plan_has_tasks; then banner "ALL TASKS COMPLETE"; break; fi
log " Retrying..."; continue
fi
rate_limit_retries=0
section "ITERATION $i COMPLETE"
log " Remaining: $(remaining_tasks)"; log " Next: $(next_task)"
i=$((i + 1))
if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then
log " Pausing ${ITERATION_DELAY}s..."; sleep "$ITERATION_DELAY"
fi
done
banner "LOOP FINISHED"
log " Completed $((i - 1)) iterations"; log " Remaining: $(remaining_tasks)"; log ""
LOOPEOF
chmod +x "$LOOP_DIR/loop.sh"
ok "Created loop/loop.sh"
fi
# ── prompt.md and plan.md are created later after interactive input ────────
echo ""
# ── Install hooks ────────────────────────────────────────────────────────────
echo -e " ${BOLD}Installing hooks...${NC}"
echo ""
HOOKS_DIR="$HOME/.claude/hooks"
mkdir -p "$HOOKS_DIR"
# prevent-sleep.sh
if [[ -f "$HOOKS_DIR/prevent-sleep.sh" ]]; then
warn "prevent-sleep.sh exists — skipping"
else
cat > "$HOOKS_DIR/prevent-sleep.sh" << 'HOOKEOF'
#!/usr/bin/env bash
set -euo pipefail
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
if [[ -f "$PID_FILE" ]]; then
old_pid=$(cat "$PID_FILE")
kill -0 "$old_pid" 2>/dev/null && kill "$old_pid" 2>/dev/null || true
rm -f "$PID_FILE"
fi
caffeinate -i &
echo $! > "$PID_FILE"
exit 0
HOOKEOF
chmod +x "$HOOKS_DIR/prevent-sleep.sh"
ok "Installed ~/.claude/hooks/prevent-sleep.sh"
fi
# stop-hook-autonomous.sh
if [[ -f "$HOOKS_DIR/stop-hook-autonomous.sh" ]]; then
warn "stop-hook-autonomous.sh exists — skipping"
else
cat > "$HOOKS_DIR/stop-hook-autonomous.sh" << 'HOOKEOF'
#!/usr/bin/env bash
# "Ralph Wiggum" — blocks Claude from stopping until all plan tasks are done.
# Requires CLAUDE_AUTONOMOUS=1 to activate.
set -euo pipefail
BASE="${CLAUDE_PROJECT_DIR:-}"
if [[ -z "$BASE" ]] && command -v jq &>/dev/null; then
BASE=$(jq -r '.cwd // empty' 2>/dev/null || true)
fi
[[ -z "$BASE" ]] && BASE="$(pwd)"
PLAN_FILE="${CLAUDE_PLAN_FILE:-plan.md}"
ALT_FILES="loop/plan.md todo.md loop/todo.md"
AUTO_SLEEP_HOOK="$HOME/.claude/hooks/allow-sleep.sh"
plan=""
for f in "$PLAN_FILE" $ALT_FILES; do
[[ -z "$f" ]] && continue
if [[ "$f" == /* ]]; then path="$f"; else path="$BASE/$f"; fi
if [[ -f "$path" ]]; then plan="$path"; break; fi
done
if [[ -z "${CLAUDE_AUTONOMOUS:-}" ]] || [[ "$CLAUDE_AUTONOMOUS" == "0" ]]; then
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true; exit 0
fi
if [[ -z "$plan" ]] || [[ ! -f "$plan" ]]; then
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true; exit 0
fi
incomplete=$(grep -c -E '^\s*[-*]?\s*\[\s*\]' "$plan" 2>/dev/null || echo 0)
if [[ "${incomplete:-0}" -gt 0 ]]; then
echo '{"decision":"block","reason":"Plan has '"$incomplete"' incomplete task(s). Continue with the next item."}'
exit 0
fi
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true
exit 0
HOOKEOF
chmod +x "$HOOKS_DIR/stop-hook-autonomous.sh"
ok "Installed ~/.claude/hooks/stop-hook-autonomous.sh"
fi
# allow-sleep.sh
if [[ -f "$HOOKS_DIR/allow-sleep.sh" ]]; then
warn "allow-sleep.sh exists — skipping"
else
cat > "$HOOKS_DIR/allow-sleep.sh" << 'HOOKEOF'
#!/usr/bin/env bash
set -euo pipefail
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
fi
exit 0
HOOKEOF
chmod +x "$HOOKS_DIR/allow-sleep.sh"
ok "Installed ~/.claude/hooks/allow-sleep.sh"
fi
echo ""
# ── Register hooks in settings.json ─────────────────────────────────────────
echo -e " ${BOLD}Configuring Claude settings...${NC}"
echo ""
SETTINGS_FILE="$HOME/.claude/settings.json"
if [[ -f "$SETTINGS_FILE" ]]; then
cp "$SETTINGS_FILE" "${SETTINGS_FILE}.backup.$(date +%s)"
info "Backed up settings.json"
fi
if [[ -f "$SETTINGS_FILE" ]] && grep -q "stop-hook-autonomous" "$SETTINGS_FILE" 2>/dev/null; then
ok "Hooks already registered"
else
if command -v python3 &>/dev/null; then
python3 << 'PYEOF'
import json, os
p = os.path.expanduser("~/.claude/settings.json")
h = os.path.expanduser("~/.claude/hooks")
s = json.load(open(p)) if os.path.exists(p) else {}
if "hooks" not in s: s["hooks"] = {}
for event, script in [("UserPromptSubmit","prevent-sleep.sh"),("Stop","stop-hook-autonomous.sh"),("SessionEnd","allow-sleep.sh")]:
if event not in s["hooks"]: s["hooks"][event] = []
if not any(script in json.dumps(x) for x in s["hooks"][event]):
s["hooks"][event].append({"matcher":"","hooks":[{"type":"command","command":f"{h}/{script}"}]})
with open(p,"w") as f: json.dump(s,f,indent=2); f.write("\n")
PYEOF
ok "Registered hooks in settings.json"
else
warn "python3 not found — add hooks to ~/.claude/settings.json manually"
fi
fi
echo ""
# ══════════════════════════════════════════════════════════════════════════════
# INTERACTIVE SETUP — collect tasks and project context
# ══════════════════════════════════════════════════════════════════════════════
echo -e " ${DIM}════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}Now let's set up your tasks and project context.${NC}"
echo ""
# ── Collect tasks ────────────────────────────────────────────────────────────
if [[ -f "$LOOP_DIR/plan.md" ]]; then
echo -e " ${YELLOW}loop/plan.md already exists.${NC}"
echo -ne " Overwrite with new tasks? [y/N] "
read -r OVERWRITE_PLAN
[[ "$OVERWRITE_PLAN" =~ ^[Yy] ]] || SKIP_PLAN=1
fi
if [[ "${SKIP_PLAN:-0}" != "1" ]]; then
echo -e " ${BOLD}Enter your tasks${NC} — one per line."
echo -e " ${DIM}Be specific. Claude will execute these literally.${NC}"
echo -e " ${DIM}Example: \"Add JWT authentication with refresh tokens\"${NC}"
echo -e " ${DIM}Press Enter on an empty line when done.${NC}"
echo ""
TASKS=()
TASK_NUM=1
while true; do
echo -ne " ${CYAN}Task $TASK_NUM:${NC} "
read -r TASK_LINE
[[ -z "$TASK_LINE" ]] && break
TASKS+=("$TASK_LINE")
TASK_NUM=$((TASK_NUM + 1))
done
if [[ ${#TASKS[@]} -eq 0 ]]; then
warn "No tasks entered — writing example plan"
cat > "$LOOP_DIR/plan.md" << 'PLANEOF'
# Task Plan
## Phase 1
- [ ] **1.1** — First task description
- [ ] **1.2** — Second task description
## Final
- [ ] **FINAL** — Run full test suite, fix failures, tag release
PLANEOF
else
echo "# Task Plan" > "$LOOP_DIR/plan.md"
echo "" >> "$LOOP_DIR/plan.md"
i=1
for task in "${TASKS[@]}"; do
echo "- [ ] **$i** — $task" >> "$LOOP_DIR/plan.md"
i=$((i + 1))
done
echo "" >> "$LOOP_DIR/plan.md"
echo "- [ ] **FINAL** — Run full test suite, fix any failures" >> "$LOOP_DIR/plan.md"
ok "Wrote ${#TASKS[@]} tasks to loop/plan.md"
fi
echo ""
fi
# ── Collect project context ──────────────────────────────────────────────────
if [[ -f "$LOOP_DIR/prompt.md" ]] && [[ "${SKIP_PLAN:-0}" == "1" ]]; then
SKIP_PROMPT=1
fi
if [[ "${SKIP_PROMPT:-0}" != "1" ]]; then
echo -e " ${BOLD}Project context${NC} — tell Claude about your project."
echo -e " ${DIM}Stack, test commands, coding style, anything important.${NC}"
echo -e " ${DIM}Example: \"TypeScript + React, run 'npm test', use Prettier formatting\"${NC}"
echo -e " ${DIM}Press Enter on an empty line when done (or just Enter to skip).${NC}"
echo ""
RULES=()
while true; do
echo -ne " ${CYAN}>${NC} "
read -r RULE_LINE
[[ -z "$RULE_LINE" ]] && break
RULES+=("$RULE_LINE")
done
# Build prompt.md
cat > "$LOOP_DIR/prompt.md" << 'PROMPTEOF'
You are executing a project roadmap autonomously. Read these files first:
1. `loop/plan.md` — Your task checklist (mark items `- [x]` as you complete them)
2. Read any project documentation (README, CLAUDE.md, etc.) for conventions
PROMPTEOF
if [[ ${#RULES[@]} -gt 0 ]]; then
echo "## Project Rules" >> "$LOOP_DIR/prompt.md"
echo "" >> "$LOOP_DIR/prompt.md"
for rule in "${RULES[@]}"; do
echo "- $rule" >> "$LOOP_DIR/prompt.md"
done
echo "" >> "$LOOP_DIR/prompt.md"
ok "Added ${#RULES[@]} project rules to prompt"
fi
cat >> "$LOOP_DIR/prompt.md" << 'PROMPTEOF'
## For each task in loop/plan.md:
1. Find the first unchecked `- [ ]` item
2. Understand what needs to be done
3. Implement it following the project's existing patterns and conventions
4. Run the project's type checker / linter / tests — fix all errors
5. Commit with a conventional message: `type(scope): description`
6. Mark the task `- [x]` in `loop/plan.md`
7. Move to the next unchecked task immediately
## Rules
- If tests fail, fix them before moving on
- If a task is difficult, make at least 30 genuine attempts before skipping
- Always run linter + type checker after code changes
- Do not stop until all tasks are checked or you are rate limited
PROMPTEOF
ok "Created loop/prompt.md"
echo ""
fi
# ── Summary & launch ─────────────────────────────────────────────────────────
TASK_COUNT=$(grep -c '^\- \[ \]' "$LOOP_DIR/plan.md" 2>/dev/null || echo "0")
echo -e " ${DIM}════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}${GREEN}Ready to go!${NC}"
echo ""
echo -e " ${BOLD}Tasks:${NC} $TASK_COUNT in loop/plan.md"
echo -e " ${BOLD}Prompt:${NC} loop/prompt.md"
echo -e " ${BOLD}Log:${NC} loop/loop.log (created on first run)"
echo ""
echo -e " ${DIM}────────────────────────────────────────────────────────────${NC}"
echo ""
echo -e " ${BOLD}To start:${NC}"
echo -e " ${GREEN}./loop/loop.sh${NC}"
echo ""
echo -e " ${BOLD}To start with sleep prevention (macOS):${NC}"
echo -e " ${GREEN}caffeinate -i ./loop/loop.sh${NC}"
echo ""
echo -e " ${BOLD}Monitor:${NC}"
echo -e " ${DIM}tail -f loop/loop.log${NC}"
echo ""
echo -e " ${BOLD}Config:${NC}"
echo -e " ${DIM}CLAUDE_AUTONOMOUS=0${NC} — let Claude stop freely (testing)"
echo -e " ${DIM}ITERATION_COUNT=20${NC} — more iterations"
echo -e " ${DIM}ITERATION_DELAY=60${NC} — longer pause between rounds"
echo ""
echo -e " ${DIM}────────────────────────────────────────────────────────────${NC}"
echo ""
echo -ne " ${BOLD}Start the loop now?${NC} [y/N] "
read -r START_NOW
if [[ "$START_NOW" =~ ^[Yy] ]]; then
echo ""
echo -e " ${GREEN}Launching...${NC}"
echo ""
exec ./loop/loop.sh
fi
echo ""
echo -e " ${DIM}Run ./loop/loop.sh whenever you're ready.${NC}"
echo ""
+1051
View File
File diff suppressed because it is too large Load Diff
+443
View File
@@ -0,0 +1,443 @@
# AIUI Plan 2 — Extended Roadmap
## Context & Philosophy
This plan continues from M0M7 (all complete). Every item below must honour the core philosophy:
- **Glass morphism only**`glass`, `glass-card`, `glass-button`. No light mode, no gray-900 hacks.
- **Open source / MIT/Apache-2.0** — no proprietary dependencies
- **Decentralised-first** — no vendor lock-in, pluggable everything
- **Bitcoin only** — sats, Lightning, Cashu, Fedimint. AIUI is never a wallet — always deep-link
- **Privacy-first** — no telemetry, no tracking, E2E encryption
- **Mobile-first, everywhere-perfect** — desktop enhances mobile, never replaces it
- **Plugin-everything** — all integrations go through typed plugin interfaces
- **< 250 KB gzipped initial load** — everything else lazy-loaded
---
## M8: Chat UX Polish
### M8.1 — Message Editing & Regeneration
Edit any sent message in place; all messages after it are cleared and AI regenerates from that point. Pencil icon appears on hover. Textarea replaces bubble on click. `Escape` cancels, `Enter` submits.
### M8.2 — Conversation Branching
Fork from any assistant message. Branch indicator in chat header (e.g. "Branch 2 of 3"). Branch switcher as a compact glass pill above the forked message. Each branch stored as a separate conversation in IDB.
### M8.3 — Reply-to Threading
Click any message → "Reply" option. Reply shows a quoted excerpt of the target message above the input. Thread line connects quoted block to source. Visual only — does not send separate context to AI, just prepends `> quote` to the user message.
### M8.4 — Conversation Search
`Cmd+F` / search icon opens a slide-down glass panel above chat. Real-time filtering highlights matching messages. Up/down arrows jump between matches. `Escape` closes.
### M8.5 — Auto-Title Generation
After the first AI response in a new conversation, send a background request: `"Give a 4-word title for this conversation: {first user message}"`. Replace "New Chat" silently. No loading state — title updates smoothly.
### M8.6 — Context Window Visualiser
Slim progress bar at top of chat column. Estimates token count from message lengths (1 token ≈ 4 chars). Shows percentage of model's context window used. Bitcoin-orange fill → red when > 80%. Tooltip: "~12,400 / 200,000 tokens used".
### M8.7 — Conversation Export
Three-dot menu on each conversation → Export. Options: Markdown (download .md), JSON (full data), Plain text. Uses File System Access API when available, falls back to `<a download>`. No server involved.
### M8.8 — Import Conversations
Settings → Import → drag-and-drop or file picker for AIUI JSON export or Claude.ai export JSON. Merges into existing conversations without overwriting. Shows import summary (N conversations added).
### M8.9 — Long-press / Right-click Context Menus
Messages: Copy, Edit, Delete, Reply, Branch from here. Content cards: Favourite, Share, Open detail, Copy title. Uses a reusable `ContextMenu.vue` glass-card component positioned at cursor. Closes on outside click or `Escape`.
### M8.10 — Scroll Position Memory
When switching between conversations, restore the previous scroll position. Store position per conversation ID in a `Map<string, number>` (not persisted — session only). Virtual scroller should seek to the stored offset on mount.
---
## M9: AI Experience
### M9.1 — Multi-Model Comparison Mode
Split-screen: same prompt sent to two models simultaneously. Side-by-side layout on desktop, swipeable tabs on mobile. Model selector per pane. Shows streaming output in both. Useful for comparing Claude vs OpenRouter models.
### M9.2 — System Prompt Editor
Settings → Personas. Create named personas (e.g. "Film Critic", "Bitcoin Analyst"). Each has a system prompt, model preference, and accent colour. Select persona per conversation via a pill menu above the input. Default persona applies to all new conversations.
### M9.3 — Prompt Template Library
`/` in chat input opens a command palette (glass dropdown). Templates listed with title + preview. Variables in templates use `{{variable}}` syntax — on selection, a mini form appears to fill them. Templates stored in IDB, importable/exportable as JSON.
### M9.4 — Vision Input
Drag-and-drop or paste image into chat input. Image preview appears as a thumbnail above the input. On send, image encoded as base64 and included in the message content array (Claude vision format). Only enabled when active model supports vision. Max 4 images per message.
### M9.5 — Response Feedback
Thumbs up / thumbs down on each AI message (appears on hover). Stored locally in IDB per message ID. Shown in conversation export. Future: aggregate across sessions for personal preference tracking. Never sent anywhere.
### M9.6 — Token & Cost Estimator
Settings toggle to show token counts. Each message shows estimated token count in a tiny badge (bottom-right of bubble). Running total shown in context window bar. Cost estimate based on current model's pricing (hardcoded table, updated with model releases).
### M9.7 — AI Memory Panel
Settings → Memory. A list of "always remember" facts injected into every system prompt. e.g. "I live in London", "I prefer sats over fiat". Edit/delete/add. Max 20 items. Stored encrypted in IDB. Shown as a collapsed "Memory" section in the system prompt.
### M9.8 — Model Capabilities Badge
Model selector shows capability badges: Vision 👁, Tools 🔧, Long context 📄. Tooltip explains each. Greys out vision input button when selected model doesn't support it. Updates dynamically when switching providers.
### M9.9 — Temperature & Params Slider
Advanced settings section (collapsed by default) beneath the model selector. Sliders for: Temperature (01), Max tokens (2568192), Top-P. Values persisted per conversation in IDB. Reset to defaults button.
### M9.10 — Stop Sequence Configuration
Advanced settings: configurable stop sequences (comma-separated). Applied to all requests for that conversation. Useful for structured output tasks. Shown as a small tag list below the slider panel.
---
## M10: Advanced Content Renderers
### M10.1 — Full Article Renderer
When AI returns a long-form article (> 800 words with headings), render it in the panel as a paginated article view. Features: auto-generated table of contents (sticky left sidebar on desktop), estimated reading time, font-size control, print mode. Uses existing markdown-it instance.
### M10.2 — PDF Viewer
Content type `pdf` renders via `pdfjs-dist` (lazy loaded, ~400 KB). Page navigation, zoom, text selection, search within PDF. Chat preview: thumbnail of page 1. Panel play: full viewer. Files loaded from URL (no local file upload in v1).
### M10.3 — Map Renderer
Content type `place` upgrades from static card to interactive Leaflet map (lazy loaded). OpenStreetMap tiles (no API key needed). Pins for all places mentioned in conversation. Cluster pins when > 10 places. Panel play: fullscreen map with place list sidebar.
### M10.4 — Recipe Renderer
New content type `recipe`. Tag: `<recipe_ext title="..." servings="..." time="...">`. Structured display: ingredients checklist (tap to strike through), numbered steps, metadata chips (time, servings, calories). "Scale recipe" slider (0.5×–4×) recalculates quantities.
### M10.5 — Event Renderer
New content type `event`. Tag: `<event_ext title="..." date="..." location="..." url="...">`. Shows: date chip, location, countdown. Add to calendar buttons: ICS download, Google Calendar URL, Apple Calendar. Glass card in chat, full detail in panel.
### M10.6 — Math Renderer
Detect `$...$` (inline) and `$$...$$` (block) LaTeX in chat messages. Render using KaTeX (lazy loaded, ~70 KB). Fallback: display raw LaTeX in a code block. No re-renders during streaming — batch render on stream end.
### M10.7 — Mermaid Diagram Renderer
Detect ` ```mermaid ` fenced code blocks. Render using Mermaid.js (lazy loaded, ~500 KB). Support: flowchart, sequence, gantt, entity-relationship. Dark theme matching glass design. Copy SVG button. Pan/zoom on mobile.
### M10.8 — Audio Waveform Player
Upgrade PlayerBar for locally-loaded audio. Use WaveSurfer.js (lazy loaded) to show waveform visualization. Waveform rendered in Bitcoin orange on dark background. Click to seek. Existing queue/next/prev preserved.
### M10.9 — Table Renderer
Markdown tables rendered as interactive tables: column sort (click header), row filter (search input above table), CSV export button. Uses existing markdown-it but overrides the table token renderer. Max 500 rows before virtualisation kicks in.
### M10.10 — Timeline Renderer
New content type `timeline`. AI returns a series of `<event_ext>` tags. Panel renders them as a vertical timeline: date on left, event card on right, connecting line. Animate entries in as they appear during streaming.
### M10.11 — Code Runner
Fenced code blocks with a "Run" button for HTML/CSS/JS. Opens a sandboxed `<iframe srcdoc="...">` in the panel. Output console below. `sandbox="allow-scripts"` only — no network access, no storage. Python: future (Pyodide).
### M10.12 — Video Renderer
New content type `video`. Native `<video>` element with custom glass controls. HLS.js for adaptive streams (lazy loaded). YouTube URL detection → nocookie embed fallback. Panel play: fullscreen. Chat preview: thumbnail + play button.
---
## M11: Nostr Ecosystem
### M11.1 — Publish Nostr Notes
Compose panel in the Nostr tab. Write a note → sign via NIP-07 → broadcast to configured relays. Shows send status per relay. Can attach content card references (film, song, etc.) as URL mentions. Character counter (280 soft limit, no hard cap).
### M11.2 — Nostr DMs (NIP-17)
Encrypted direct messages using NIP-17 sealed gifts. DM inbox tab in Nostr section. Contact list from follows. Message threads per contact. Messages encrypted client-side, stored in IDB. No plaintext ever sent to relay.
### M11.3 — Relay Management UI
Settings → Nostr Relays. Add/remove relay URLs. Health column: latency (ms), status (connected/disconnected/error). Test connection button. Read/write toggle per relay. Import relay list from NIP-65 event.
### M11.4 — Nostr Profile Editor
Settings → Nostr Identity (extends M6.3). Edit: display name, bio, avatar URL, banner URL, website, NIP-05 address, Lightning address. Preview renders as a profile card. Publish as kind:0 event via NIP-07.
### M11.5 — Zaps (NIP-57)
On any Nostr note or profile, show a Zap ⚡ button. Opens a zap dialog: amount input (in sats), optional message. Fetches LNURL-pay from profile's Lightning address. Shows QR + deep-link. Confirms via Lightning payment. Never holds funds.
### M11.6 — NIP-05 Verification Badge
Nostr profiles with NIP-05 show a ✓ badge. Verified by fetching `/.well-known/nostr.json?name=...` from the NIP-05 domain. Cached in IDB for 24 hours. Badge tooltip shows the full NIP-05 identifier.
### M11.7 — Nostr Search (NIP-50)
Search input in Nostr tab. Sends `REQ` with `search` field to NIP-50 supporting relays (nostr.wine, relay.nostr.band). Results show as note cards with author, content, timestamp. Filter by content type.
### M11.8 — Thread View
Clicking a Nostr note opens a thread view in the panel. Fetches root event and all replies (kind:1, `#e` tag). Renders as a threaded tree (indent by depth, max 5 levels). Loads lazily from relays. Reply button opens compose with reply reference.
### M11.9 — Nostr Lists (NIP-51)
View and manage: follow list (kind:3), mute list (kind:10000), pin list (kind:10001), bookmark list (kind:10003). Each as a panel tab in the Nostr section. Add/remove items. Publish via NIP-07.
### M11.10 — Long-Form Content (NIP-23)
Nostr long-form articles (kind:30023) rendered in the article renderer (M10.1). Discovery tab in Nostr section shows recent articles from follows. Clicking opens the full article in panel play. Share as Nostr note button.
---
## M12: Bitcoin Ecosystem
### M12.1 — On-Chain Address Display
Detect Bitcoin addresses in chat (bech32 segwit, legacy). Render as a glass card: address (truncated), QR code, "View on mempool.space" link, copy button. Balance lookup via mempool.space API (lazy, opt-in). Never sends private keys.
### M12.2 — Fedimint Ecash
Detect Fedimint ecash tokens in chat (e-cash token format). Display: federation name, amount in sats, "Receive in Fedi" deep-link button. QR of the token string. Copy button. Same approach as Cashu — AIUI is never a wallet.
### M12.3 — BOLT12 Offers
Detect `lno1...` BOLT12 offer strings. Render as glass card: decoded amount (if fixed), description, "Pay with wallet" deep-link. QR of the offer. BOLT12 is static (reusable), unlike BOLT11 invoices.
### M12.4 — Nostr Wallet Connect (NWC)
Settings → Connect Wallet. Paste NWC connection string (`nostr+walletconnect://...`). AIUI can then: check balance, pay invoices (with user confirmation). Uses NIP-47. All operations require explicit user tap. Stored encrypted in IDB.
### M12.5 — LNURL-auth Login
Settings → LNURL-auth. Generates a LNURL-auth QR code. Scanning with a Lightning wallet proves ownership of the Lightning node. Sets a persistent identity (pubkey) used for local preference sync. No password needed.
### M12.6 — Live Sat/Fiat Price
Settings toggle: show amounts in sats or fiat equivalent. Price fetched from mempool.space `/api/v1/prices` every 60 seconds. Used across: Cashu cards, Lightning invoices, cost estimator, zap dialog. Stored in a `useBitcoinPrice` composable.
### M12.7 — Mempool.space Tx Viewer
Detect txid hashes (64 hex chars) and block heights in chat. Render as a glass card with: confirmations, fee rate, amount, link to mempool.space. Block height renders block summary. Updates live via mempool.space WebSocket.
### M12.8 — BOLT11 Decoder Card
Full BOLT11 invoice decode before paying: show amount, description, expiry countdown, destination node alias (if known). Expiry shown as a red countdown when < 5 minutes. "Pay" button triggers deep-link or NWC payment (M12.4).
---
## M13: Content Discovery
### M13.1 — "For You" Feed
A new "For You" tab in the content panel. Surfaces content types you've interacted with most (from favorites + conversation history). Uses a simple frequency map (no ML). Refreshes on each app open. Fully local, no server.
### M13.2 — Content Tagging
On any content card: "Add tag" (plus icon). Tags are user-defined strings stored in IDB alongside the item. Filter any content grid by tag. Tag cloud view in favorites panel. Export tags with content JSON.
### M13.3 — Smart Playlists
Music tab → Smart Playlists. Auto-generated from: recently played, most played, by genre tag, by decade. Each playlist is a computed view over the song IDB store. Play button queues the whole playlist. No manual curation needed.
### M13.4 — Similar Content
Below any open content detail: "More like this" section. Populated by sending a background AI request: `"List 3 films similar to {title} as film_ext tags"`. Results appear after 23 seconds. Cached in IDB per item for 7 days.
### M13.5 — Recently Viewed
A "Recent" tab in the content panel. Ordered list of the last 50 content items you opened (any type). Each entry: thumbnail, title, type, time ago. Tap to re-open. Stored in IDB, cleared on data wipe.
### M13.6 — Content Collections
User-created collections (like playlists but for any content type). Create collection → name it → add any content card to it via long-press menu. Collections shown as a grid of 4-thumbnail mosaics. Shareable as a Nostr list (NIP-51).
### M13.7 — Trending in Conversations
A "Trending" section: content items referenced most frequently across all your conversations in the last 30 days. Computed on load from IDB. Shows a small "referenced N times" badge. Pure local analytics.
### M13.8 — Content Sharing via Nostr
Any content card: Share → "Post to Nostr". Generates a note with the content title, year, a short AI-generated description, and the content tag as a URL. Signs and broadcasts via NIP-07. Opens compose preview before posting.
---
## M14: Plugin Marketplace
### M14.1 — Plugin Discovery UI
Settings → Plugins → Discover. Fetches a static community registry JSON (hosted on GitHub Pages or IPFS). Lists plugins with: name, description, type, author, version, rating. Install button triggers M14.7 (import by URL).
### M14.2 — Plugin Settings Panel
Each installed plugin has a gear icon → settings panel. Plugin declares its settings schema (JSON Schema). AIUI renders the settings form automatically using a `PluginSettingsForm.vue` component. Settings stored encrypted in IDB under plugin ID.
### M14.3 — Plugin Permissions UI
On install: permissions dialog lists requested capabilities (e.g. "Access chat messages", "Make network requests", "Read favorites"). User grants/denies each. Permissions stored per plugin. Plugin can check granted permissions at runtime via `context.hasPermission()`.
### M14.4 — Plugin Dev Mode
`VITE_PLUGIN_DEV=true` enables: hot-reload of plugins from `src/plugins/dev/`, error inspector panel (shows plugin errors without crashing app), plugin performance profiler (time per `init()` call).
### M14.5 — Built-in Plugin: Wikipedia
Plugin type `search`. `/wiki {query}` in chat input fetches Wikipedia summary via the Wikipedia REST API. Returns a `article` content card inline. No API key needed. Rendered via the article renderer (M10.1).
### M14.6 — Built-in Plugin: OpenLibrary
Plugin type `search`. Searches Open Library (openlibrary.org) for books. Returns `book_ext` tagged results. Cover images from Open Library covers API. Free, no API key.
### M14.7 — Plugin Import by URL
Settings → Plugins → Install from URL. Paste a GitHub raw URL or IPFS CID. AIUI fetches the plugin manifest (`aiui-plugin.json`), validates schema, shows permissions dialog (M14.3), then installs. Plugins are community Tier 2 (sandboxed iframe).
### M14.8 — Plugin Versioning & Auto-Update
Installed plugins store their version. On app start, check registry for newer versions (background fetch). Badge on Plugins settings icon when updates available. Update all button. Changelog shown before updating.
---
## M15: Settings & Personalisation
### M15.1 — Accent Colour Picker
Settings → Appearance. Colour wheel or preset swatches to change the accent colour (default Bitcoin orange #F7931A). Updates `--color-accent` CSS variable in real time. Persisted in IDB. Affects all gradient buttons, badges, active states.
### M15.2 — Glass Intensity Slider
Settings → Appearance. Three presets: Subtle / Default / Strong. Maps to blur(12px)/blur(18px)/blur(28px) and background opacity 0.25/0.35/0.50. Updates glass CSS variables. Live preview as you drag.
### M15.3 — Font Size Settings
Settings → Appearance. Three sizes: Compact (13px base), Default (15px), Large (17px). Sets `--font-size-base` CSS variable. Scales all rem-based text. Persisted in IDB.
### M15.4 — Content Type Visibility
Settings → Content. Toggle visibility of each of the 11 content type tabs in the panel. Hidden types still extract from AI messages but don't show in the panel. Useful for users who only care about music + films.
### M15.5 — Keyboard Shortcut Map
Settings → Shortcuts. Lists all keyboard shortcuts. Each row shows action + current binding. Click to rebind (record next key combo). Conflicts highlighted in red. Stored in IDB. Uses the existing keybindings system.
### M15.6 — Browser Push Notifications
Settings → Notifications. Opt-in for: "Generation complete" (when a long response finishes while tab is backgrounded). Uses the Web Notifications API + Service Worker `showNotification()`. Notification click focuses the tab and scrolls to the response.
### M15.7 — Auto-Archive Old Conversations
Settings → Storage. Slider: archive conversations older than N days (7/30/90/never). Archived conversations move to an "Archive" folder, not deleted. Unarchive individually. Archive stored in a separate IDB object store.
### M15.8 — Full Data Export
Settings → Data → Export All. Creates a JSON archive: all conversations, favorites, settings, tags, collections. Optionally encrypted with the current passphrase. Single file download. Compliant with GDPR right to portability.
### M15.9 — Data Wipe
Settings → Data → Wipe Everything. Two-step confirmation. Clears: all IDB stores, service worker cache, localStorage. Does not clear the API key vault unless explicitly checked. Shows what will be deleted before confirming.
### M15.10 — Default Conversation Settings
Settings → Chat. Set global defaults: default model, default persona, web search on/off, show token counts. These apply to all new conversations. Per-conversation overrides still possible.
---
## M16: Mobile UX Polish
### M16.1 — Bottom Sheet Component
Reusable `BottomSheet.vue`. Gesture-driven: drag down to dismiss, swipe up to expand. Snap points: 40% / 80% / 100% height. Backdrop tap to close. Used by: context menus, share sheets, relay management, plugin settings. Replaces modals on mobile.
### M16.2 — Swipe to Navigate Conversations
On mobile, swipe left/right on the chat area to move between conversations. Animated slide transition. Visual edge indicator (thin line at sides) to hint swipeability. Threshold: 80px swipe distance, 0.3 velocity.
### M16.3 — Pull-to-Refresh on Content Panels
Each content grid supports pull-to-refresh. Custom glass spinner animation. Triggers: re-fetch from AI context, reload Nostr feed, clear image cache for that type. Haptic feedback on release.
### M16.4 — Haptic Feedback
Use `navigator.vibrate()` for: message send (10ms), favourite toggle (15ms), error (pattern: 50ms50ms50ms), pull-to-refresh trigger (20ms). Wrapped in `useHaptics()` composable that checks support before calling. Settings toggle to disable.
### M16.5 — Web Share API
All content cards and conversations: Share button triggers native `navigator.share()` where available. Falls back to a glass share sheet (copy link, copy text, Nostr share). Adapts to iOS (files not supported) vs Android (files supported).
### M16.6 — Pinch-to-Zoom on Images & Maps
Images in the panel support pinch-to-zoom via touch events. Min scale 1×, max 4×. Double-tap resets to 1×. Map renderer uses Leaflet's built-in touch zoom. Implemented with a `usePinchZoom()` composable (no library needed).
### M16.7 — iOS PWA Polish
Meta tags: `apple-mobile-web-app-capable`, `apple-mobile-web-app-status-bar-style: black-translucent`. Safe area insets via `env(safe-area-inset-*)` on all fixed elements (chat input, player bar, nav). Splash screens for common iPhone sizes.
### M16.8 — Long-press Context Menus on Mobile
On mobile, long-press (500ms) on messages or content cards opens the context menu (M8.9) as a bottom sheet (M16.1). Haptic on trigger (20ms). Prevents default browser long-press menu via `@contextmenu.prevent`.
### M16.9 — Scroll Position Memory
Restore scroll position when switching tabs, conversations, or navigating back. Store position per route + conversation ID in a `Map` (session only). Content grids also remember their scroll offset.
### M16.10 — Landscape Mode Optimisation
Detect landscape on mobile. Rearrange layout: chat takes 50% width, content panel 50% (instead of overlay). Player bar becomes minimal (just controls, no waveform). Smooth transition on rotate via CSS transitions on layout classes.
---
## M17: Accessibility & Internationalisation
### M17.1 — Keyboard Navigation Audit
Full Tab order review across all pages. All interactive elements reachable. Focus trap in modals and bottom sheets. `Escape` closes any overlay. Roving tabindex in content card grids. Arrow keys navigate card grids.
### M17.2 — ARIA Audit
All icon buttons: `aria-label`. All dynamic content: `aria-live="polite"`. Dialogs: `role="dialog"`, `aria-modal`, `aria-labelledby`. Content card grids: `role="list"` + `role="listitem"`. Loading states: `aria-busy`.
### M17.3 — High Contrast Mode
`@media (prefers-contrast: more)` stylesheet. Increases border opacity from 0.18 → 0.5. Text opacity: all `/90``100%`. Removes backdrop blur (performance + clarity). Accent remains orange. Toggle also available in Settings.
### M17.4 — Automated Accessibility Tests
Axe-core integrated into Playwright E2E tests. Run `pnpm test:a11y` which opens each page and asserts zero critical axe violations. CI fails on new violations. Reports saved as HTML artefacts.
### M17.5 — i18n Foundation
Add `vue-i18n`. Extract all hardcoded strings into `src/i18n/en.json`. Add `es.json` (Spanish) and `fr.json` (French) with machine-translated initial values (marked as needing review). Language auto-detected from `navigator.language`, overridable in Settings.
### M17.6 — RTL Layout Support
`dir="rtl"` on `<html>` for Arabic/Hebrew locales. Use logical CSS properties (`padding-inline-start` not `padding-left`). Flex row reversal handled by `rtl:flex-row-reverse` Tailwind variant. Test with Arabic locale.
### M17.7 — Dyslexia-Friendly Font Option
Settings → Appearance → Font. Option: "OpenDyslexic". Loaded via self-hosted WOFF2 (MIT licensed). Sets `--font-sans` CSS variable. Letter spacing +0.05em, line height 1.6.
### M17.8 — Skip Navigation Link
Hidden "Skip to main content" link as the first focusable element. Visible on Tab focus. Jumps to `<main>` landmark. Standard accessibility pattern — costs nothing, helps screen reader users significantly.
---
## M18: Performance
### M18.1 — Bundle Analysis & Splitting
Run `vite-bundle-visualizer` in CI. Identify any component loaded eagerly that should be lazy. Target: core bundle stays < 150 KB gzipped. Create per-route chunk boundaries in Vue Router.
### M18.2 — Image Lazy Loading with Blur-up
All content card images: `loading="lazy"` + `decoding="async"`. Low-quality placeholder (16×16 px, base64 inline) shown until full image loads. CSS transition from blurred placeholder to sharp image. `IntersectionObserver`-based (via `useIntersectionObserver`).
### M18.3 — Request Deduplication
`useFetch()` composable wraps all API calls. Identical in-flight requests share a single Promise (keyed by URL + body hash). Cancel via `AbortController` on component unmount. Prevents duplicate AI requests on fast re-renders.
### M18.4 — Web Worker for Heavy Tasks
Move `contentExtraction` parsing and AES-256-GCM encryption/decryption into a Web Worker (`src/workers/heavy.worker.ts`). Main thread posts messages, worker responds. Use `comlink` (MIT, ~1 KB) for typed RPC. Keeps UI thread free.
### M18.5 — Prefetch on Hover
Content cards: on `mouseenter` (desktop) or 100ms touch hold (mobile), prefetch the detail data. E.g. fetch TMDB details for a film card before the user clicks. Store in a short-lived cache (5 min). Makes panel open feel instant.
### M18.6 — Memory Leak Audit
Systematically add `onUnmounted` cleanup to all composables that use: `setInterval`, `setTimeout`, `addEventListener`, WebSocket connections, `IntersectionObserver`, `ResizeObserver`. Add a dev-mode leak detector that logs active listeners on route change.
### M18.7 — Background Sync Queue
If an IDB save fails (e.g. storage quota exceeded), queue the operation in a `SyncQueue`. On next app focus (`visibilitychange`), retry the queue. Show a subtle warning badge in settings if queue is non-empty.
### M18.8 — OPFS Storage Backend (Optional)
Implement an alternative storage backend using Origin Private File System (OPFS) via SQLite WASM (`@sqlite.org/sqlite-wasm`, Apache 2.0). Feature-flagged: `VITE_STORAGE=opfs`. Faster for large datasets (1000+ conversations). Falls back to IDB if OPFS unavailable.
---
## M19: Developer Experience & Quality
### M19.1 — Storybook
Add Storybook 8 to `packages/app`. Stories for all `ui/` components. Glass morphism theme applied to Storybook canvas (`background: #0a0a0a`). Run with `pnpm storybook`. Stories used as visual regression baseline.
### M19.2 — Visual Regression Tests
Playwright screenshot tests for: ChatPage, ContentPanel, each renderer card, PassphraseDialog, BottomSheet. Compare against baseline snapshots on every PR. Fail if pixel diff > 0.5%. Update baseline with `pnpm test:update-snapshots`.
### M19.3 — Bundle Size CI Gate
Add a GitHub Actions step: build → measure gzipped bundle → fail if > 250 KB. Use `bundlesize` (MIT). Track history: post bundle size as a PR comment showing diff from base branch.
### M19.4 — Comprehensive Mock Data
Expand `src/mocks/` with realistic data for all 11 content types (20+ items each). Add a mock Nostr relay (in-process WebSocket server) for E2E tests. Add mock TMDB responses for all test films.
### M19.5 — E2E Cross-Browser Matrix
Playwright config: run tests on Chromium + Firefox + WebKit. CI matrix: macOS (WebKit) + Linux (Chromium + Firefox). Mobile viewports: iPhone 14 (390×844) + Galaxy S21 (360×800).
### M19.6 — Proxy Integration Tests
Test `claude-proxy.ts` with a mock Anthropic API (intercepted by `nock` or `msw`). Assert: SSE streaming format, tool_use round-trips, error handling (401, 429, 500), client disconnect kills child process.
### M19.7 — Performance Benchmarks (Lighthouse CI)
Run Lighthouse in CI on each PR against a built + served app. Track: LCP, FID, CLS, TTI. Fail if LCP > 3s or CLS > 0.15. Post scores as PR comment. Store history in a JSON file committed to `reports/` branch.
### M19.8 — Dependency Audit
Weekly GitHub Actions job: `pnpm audit` for vulnerabilities, `license-checker` to flag non-MIT/Apache dependencies. Auto-create an issue if violations found. Block releases on critical vulnerabilities.
---
## M20: Collaboration & Sharing
### M20.1 — Share Conversation via Nostr
Export a conversation as a Nostr long-form article (kind:30023). Title = conversation title. Content = formatted Markdown. Sign via NIP-07. Optionally encrypt for a specific npub (NIP-44). Shareable via `nostr:naddr1...` link.
### M20.2 — Read-Only Conversation Viewer
A `/view/:nostrAddr` route that renders a shared Nostr conversation (from M20.1) in read-only mode. No auth needed for public conversations. Shows content cards inline. Works as a landing page for shared links.
### M20.3 — Collaborative Playlist (Nostr NIP-51)
Create a shared content list (NIP-51 kind:30004). Invite others by npub to contribute. Each contributor signs their additions. AIUI merges all list events from the relay into a unified view. Useful for collaborative music or film curation.
### M20.4 — Conversation Templates
Pre-built conversation starters: "Bitcoin deep dive", "Film analysis", "Nostr onboarding", "Music discovery". Each is a system prompt + first user message. Shown on the new conversation screen as glass cards. Import/export as JSON. Share via Nostr.
### M20.5 — Export as Audio Podcast
Experimental (M20.5): Text-to-speech for a conversation using Web Speech API (`speechSynthesis`). Reads AI responses only. Controls: voice selector, speed, skip. Export as WAV (Web Audio API). Background music track from the player queue mixed in (opt-in). Pure client-side.
### M20.6 — Community Content Packs
Import a curated set of content (films, songs, books) from a community-maintained JSON file. Hosted on GitHub or IPFS. Registry listed in the plugin marketplace (M14.1). Examples: "2024 Best Films", "Bitcoin Music Playlist", "Essential Nostr Reads".
---
## Automated Session Execution Order
Each session should:
1. Read `PROGRESS.md` — find the next `[ ]` item
2. Read the task spec above
3. Implement the task
4. Run `pnpm typecheck && pnpm lint && pnpm test`
5. Commit: `type(scope): description`
6. Update `PROGRESS.md`
### Priority Queue
**M8 (Chat Polish):** M8.1 → M8.4 → M8.5 → M8.6 → M8.2 → M8.3 → M8.7 → M8.8 → M8.9 → M8.10
**M9 (AI Experience):** M9.1 → M9.4 → M9.2 → M9.3 → M9.7 → M9.5 → M9.6 → M9.8 → M9.9 → M9.10
**M10 (Renderers):** M10.6 → M10.7 → M10.3 → M10.1 → M10.9 → M10.4 → M10.5 → M10.11 → M10.2 → M10.8 → M10.10 → M10.12
**M11 (Nostr):** M11.3 → M11.1 → M11.5 → M11.6 → M11.7 → M11.8 → M11.4 → M11.2 → M11.9 → M11.10
**M12 (Bitcoin):** M12.1 → M12.6 → M12.8 → M12.7 → M12.3 → M12.2 → M12.5 → M12.4
**M13 (Discovery):** M13.5 → M13.1 → M13.2 → M13.3 → M13.4 → M13.6 → M13.7 → M13.8
**M14 (Plugins):** M14.5 → M14.6 → M14.7 → M14.1 → M14.2 → M14.3 → M14.4 → M14.8
**M15 (Settings):** M15.1 → M15.2 → M15.3 → M15.4 → M15.5 → M15.6 → M15.7 → M15.8 → M15.9 → M15.10
**M16 (Mobile):** M16.1 → M16.7 → M16.4 → M16.5 → M16.2 → M16.8 → M16.3 → M16.6 → M16.9 → M16.10
**M17 (a11y/i18n):** M17.8 → M17.1 → M17.2 → M17.3 → M17.4 → M17.5 → M17.6 → M17.7
**M18 (Perf):** M18.2 → M18.6 → M18.3 → M18.5 → M18.1 → M18.4 → M18.7 → M18.8
**M19 (DX):** M19.4 → M19.6 → M19.5 → M19.3 → M19.1 → M19.2 → M19.7 → M19.8
**M20 (Collab):** M20.4 → M20.1 → M20.2 → M20.6 → M20.3 → M20.5
**Total: 116 tasks across 13 milestones**
+157
View File
@@ -0,0 +1,157 @@
# AIUI Progress
## Current Status
**Active Milestone**: COMPLETE
**Overall**: M0M20 all complete. 116 tasks implemented. All tests, typecheck, lint, and build passing.
## 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)
- [x] ESLint flat config (packages/app + packages/core)
- [x] CI baseline (pnpm test, lint, typecheck passing)
- [x] Progress tracking automation
### M1: Stability & Polish ✅
- [x] ErrorBoundary.vue component created
- [x] Error boundaries wrapping all major sections
- [x] Unit tests — contentExtraction composable (10 extraction functions, 49 tests)
- [x] IndexedDB persistent storage (conversations survive refresh)
- [x] Unit tests — useAI composable (16 tests, mocked fetch/SSE)
- [x] E2E test expansion (8 new tests: streaming, content cards, mobile, etc.)
### M2: Content Experience ✅
- [x] Markdown rendering in chat (markdown-it, XSS safe)
- [x] Music source resolution + queue management (next/prev, queue panel)
- [x] Virtual scrolling for chat (@tanstack/vue-virtual)
- [x] Nostr feed integration (relay WebSocket, kind:1 notes)
### M3: Plugin System ✅
- [x] Activate plugin registry at runtime (claude-provider adapter)
- [x] Renderer plugin registration (film/song as plugins)
### M4: Social & Discovery ✅
- [x] Social embeds (Nostr notes inline via nostr: URI)
- [x] Federated search across content types (/search command)
- [x] Bookmarks/favorites (Pinia + IndexedDB, heart toggle)
### M5: Security & Privacy ✅
- [x] E2E encryption (Web Crypto API, AES-256-GCM, PBKDF2)
- [x] Encrypted storage layer (PassphraseDialog, all stores encrypted)
- [x] API key vault (encrypted at rest, masked UI)
### M6: Payments & Identity ✅
- [x] Lightning wallet deep-links (LNURL-pay, BIP21, QR code)
- [x] Cashu token support (parse + display inline, never a wallet)
- [x] Nostr identity NIP-07 (browser extension login, event signing)
### M7: Platform ✅
- [x] MCP server integration (content surfaces as MCP tools)
- [x] Multi-provider AI normalization (Claude/OpenRouter/Ollama adapters)
- [x] Tauri desktop build (transparent window, system tray, global shortcut)
- [x] Offline mode (cache strategies, offline banner, cached content browsing)
## Session Log
<!-- Entries below are auto-populated by the post-push Claude Code hook -->
<!-- Format: ### YYYY-MM-DD HH:MM — branch-name -->
### 2026-03-03 — overnight/2026-03-03
**Completed M3M7 (15 tasks)**:
- M3.1: Plugin registry + Claude provider adapter
- M3.2: Film/song renderer plugins with lazy loading
- M4.1: Nostr social embeds (bech32 NIP-19 decoder, NostrEmbed.vue)
- M4.2: Federated search (/search command, SearchResults overlay)
- M4.3: Bookmarks/favorites (Pinia + IDB, FavoriteButton, FavoritesGrid)
- M5.1: E2E encryption (AES-256-GCM, PBKDF2 100K iterations)
- M5.2: Encrypted storage layer (PassphraseDialog, transparent encrypt/decrypt)
- M5.3: API key vault (encrypted IDB, ApiKeyManager.vue, vault integration in useAI)
- M6.1: Lightning wallet deep-links (BOLT11 parser, PaymentButton, LightningInvoice)
- M6.2: Cashu token support (cashu.ts parser, CashuToken.vue inline in chat)
- M6.3: Nostr identity NIP-07 (useNostrIdentity.ts, NostrLogin.vue, bech32 encode)
- M7.1: MCP server integration (tool definitions + handlers for library search)
- M7.2: Multi-provider AI normalization (adapter pattern: Claude/OpenRouter/Ollama)
- M7.3: Tauri desktop build scaffold (frameless window, tray, global shortcut)
- M7.4: Offline mode (useOffline.ts, enhanced PWA image/API caching)
### 2026-03-03 (cont.) — overnight/2026-03-03
**Completed M8 Chat UX Polish (10 tasks)**:
- M8.1: Message editing & regeneration (pencil icon, re-send clears subsequent)
- M8.2: Conversation branching (BranchSwitcher.vue, fork from any assistant msg)
- M8.3: Reply-to threading (quoted excerpt in input, `> quote` prepend)
- M8.4: Conversation search (Cmd+F glass panel, match nav, jump to message)
- M8.5: Auto-title generation (background Haiku call after first exchange)
- M8.6: Context window visualiser (ContextBar.vue, token estimate, orange→red)
- M8.7: Conversation export (Markdown/JSON/text, File System Access API)
- M8.8: Import conversations (AIUI JSON + Claude.ai format parser)
- M8.9: Context menus (ContextMenu.vue + ContextMenuItem.vue, right-click)
- M8.10: Scroll position memory (Map per conversation, restore on switch)
- TEST:M8: All 74 tests pass, typecheck + lint clean
### 2026-03-03 (cont.) — overnight/2026-03-03
**Completed M9M20 (all remaining milestones)**:
**M9: AI Experience (10 tasks)**
- Multi-model comparison, system prompt editor/personas, prompt templates
- Vision input, response feedback, token/cost estimator
- AI memory panel, model capabilities badges, temperature sliders, stop sequences
**M10: Advanced Content Renderers (12 tasks)**
- Full article renderer, PDF viewer (pdfjs-dist), map renderer (Leaflet)
- Recipe, event, math (KaTeX), Mermaid diagram renderers
- Audio waveform (WaveSurfer.js), table, timeline, code runner, video (HLS.js)
**M11: Nostr Ecosystem (10 tasks)**
- Publish notes, DMs NIP-17, relay management, profile editor
- Zaps NIP-57, NIP-05 verification, NIP-50 search, thread view
- NIP-51 lists, long-form content NIP-23
**M12: Bitcoin Ecosystem (8 tasks)**
- On-chain address display, Fedimint ecash, BOLT12 offers
- NWC (NIP-47), LNURL-auth, live sat/fiat price, mempool viewer, BOLT11 decoder
**M13: Content Discovery (8 tasks)**
- "For You" feed, content tagging, smart playlists, similar content
- Recently viewed history, content collections, trending, share to Nostr
**M14: Plugin Marketplace (8 tasks)**
- Plugin discovery UI, settings panel, permissions, dev mode
- Wikipedia + OpenLibrary built-in plugins, import by URL, versioning
**M15: Settings & Personalisation (10 tasks)**
- Accent colour picker, glass intensity slider, font size settings
- Content visibility toggles, keyboard shortcut map, push notifications
- Auto-archive, full data export, data wipe, default conversation settings
**M16: Mobile UX Polish (10 tasks)**
- Bottom sheet component, swipe navigation, pull-to-refresh, haptic feedback
- Web Share API, pinch-to-zoom, iOS PWA polish, long-press context menus
- Scroll position memory per route, landscape optimisation
**M17: Accessibility & Internationalisation (8 tasks)**
- Keyboard nav audit, ARIA audit, high contrast mode, axe-core tests
- i18n foundation (vue-i18n, en/es/fr), RTL support, dyslexia-friendly font, skip nav
**M18: Performance (8 tasks)**
- Bundle analysis/splitting, image lazy loading, request deduplication
- Web Worker for heavy tasks, prefetch on hover, memory leak audit
- Background sync queue, OPFS storage backend (SQLite WASM)
**M19: Developer Experience & Quality (8 tasks)**
- Storybook 8, visual regression tests, bundle size CI gate
- Comprehensive mock data (20+ items per type), E2E cross-browser matrix
- Proxy integration tests, Lighthouse CI, dependency audit
**M20: Collaboration & Sharing (6 tasks)**
- Share conversation via Nostr (kind:30023, NIP-44 encryption)
- Read-only conversation viewer (/view/:nostrAddr)
- Collaborative playlists (NIP-51), conversation templates
- Audio podcast export (Web Speech API), community content packs
**FINAL**: All gates passed — 101 tests, 0 typecheck errors, 0 lint errors, build succeeds
+113
View File
@@ -0,0 +1,113 @@
# iOS App Research — AIUI
## Overview
Three approaches for shipping AIUI (Vue 3 + Vite SPA) as an iOS app.
## Approach 1: Capacitor (Recommended)
Capacitor wraps the Vite build output (`dist/`) in a native iOS Xcode project. The web app runs inside WKWebView with a JavaScript bridge to native device APIs.
```bash
pnpm add @capacitor/core @capacitor/cli @capacitor/ios
npx cap init && npx cap add ios
pnpm build && npx cap sync
npx cap open ios # opens Xcode
```
**Pros:**
- Near-zero code changes to existing Vue 3 app — one codebase for web + iOS + Android
- Large, mature plugin ecosystem (camera, biometrics, push, geolocation, haptics)
- Hot reload during dev via `npx cap run ios --livereload`
- OTA live updates possible via Capgo, bypassing App Store review for JS changes
- `@capacitor/push-notifications` wraps APNs natively
**Cons:**
- Service workers do NOT work in WKWebView on iOS (capacitor:// protocol breaks SW registration)
- Performance ceiling is WebKit JS engine (not V8)
- Each iOS SDK bump requires Capacitor + plugin updates
**Push Notifications:** Full support via `@capacitor/push-notifications` (APNs). Production-grade.
**Offline:** Entire app bundle ships inside .ipa — available offline. Dynamic data must use `@capacitor/preferences` or local SQLite. Workbox/SW caching does not work.
**Performance:** Modern WKWebView uses Nitro JS engine (same as Safari). For a chat UI like AIUI, indistinguishable from Safari. GPU-accelerated CSS transforms work well.
## Approach 2: Custom WKWebView Swift Wrapper
Write a native Swift/SwiftUI app embedding WKWebView. Use `WKScriptMessageHandler` for JS↔Swift communication.
**Pros:**
- Maximum native control — own the shell, native navigation, gestures
- Can implement App Clips, Share Extensions, Widgets alongside web content
- Full access to all iOS APIs at the native layer
**Cons:**
- Requires Swift knowledge — adds second language + build system
- JS↔Swift bridge must be hand-written for every integration
- No structured plugin community; each integration is bespoke
- More setup friction vs Capacitor
**Push/Offline/Performance:** Same as Capacitor (all use WKWebView). More manual setup.
## Approach 3: React Native WebView
Create a React Native app with `react-native-webview` rendering the Vite build output.
**Pros:**
- RN has deep native API access and large ecosystem
- Surrounding shell can be fully native
**Cons:**
- Two separate tech stacks (Vue + RN) — highest maintenance burden
- No code sharing between Vue app and RN shell
- Performance often worse (full RN runtime + WebView engine)
- RN's own breaking changes cadence adds risk
**Verdict:** Only justified if an existing RN app is already in production.
## App Store Risk: Guideline 4.2
Apple's Guideline 4.2 (Minimum Functionality) is the primary risk for all webview-based apps. Apps that pass share these traits:
- Native tab bar or navigation (not web-based menus)
- At least one native API integration (push, biometrics, camera, Apple Pay)
- Offline functionality beyond what a browser bookmark offers
- UI formatted for iOS, not a desktop website in a phone frame
For AIUI: the chat interface, push notifications, and offline message history constitute sufficient native functionality.
## Service Workers in WKWebView
**SWs do not run inside WKWebView** — this is a fundamental WebKit limitation, not framework-specific. The correct offline strategy for all three approaches: ship assets in app bundle + implement dynamic caching via native storage APIs.
## Deep Linking
All three support iOS Universal Links via AASA file + Associated Domains capability:
- **Capacitor:** `@capacitor/app` `appUrlOpen` event → Vue Router
- **Custom WKWebView:** `AppDelegate.application(_:continue:...)` → JS evaluation
- **RN:** React Navigation linking config → WebView `postMessage`
## Comparison
| Dimension | Capacitor | Custom WKWebView | RN WebView |
|---|---|---|---|
| Vue code reuse | 100% | 100% | 100% |
| Native shell effort | Low | High | Very high |
| Push notifications | First-class | Manual APNs | Via RN layer |
| App Store risk | Moderate* | Moderate* | Moderate* |
| Performance | Good | Good | Adequate |
| Maintenance burden | Low-moderate | High | Very high |
| Team fit (web-first) | Best | Poor | Poor |
*All face identical Guideline 4.2 scrutiny — framework choice is irrelevant to reviewers.
## Concrete Next Steps
1. Add `@capacitor/core`, `@capacitor/cli`, `@capacitor/ios` to `packages/app`
2. Set Vite `base: './'` for the Capacitor build config
3. Disable PWA service worker for native builds (partially done already)
4. Add `@capacitor/push-notifications` for APNs
5. Implement native splash screen and app icon
6. Test on iOS Simulator via `npx cap run ios`
7. Set up Apple Developer account + code signing
8. Submit TestFlight build for internal testing
+119
View File
@@ -0,0 +1,119 @@
# Mac Desktop App Research — AIUI
## Overview
Two approaches for shipping AIUI as a Mac desktop app: Tauri v2 (Rust-based, system WebView) vs Electron (Chromium-based).
## Tauri v2 (Recommended)
Released stable October 2024. Uses OS-native WebView (WKWebView on macOS). The Vue 3 + Vite frontend runs inside the WebView unchanged. JS calls into Rust via typed IPC bridge.
**Binary Size:** 28 MB installer (no bundled runtime)
**Memory Usage:** ~3040 MB idle
**Startup Time:** < 500ms
### Menu Bar App Pattern (Raycast-style)
Fully supported via `tauri-plugin-positioner` + tray + window APIs. Frameless popover window anchored to tray icon with `decorations: false`, `skip_taskbar: true`. Community examples exist (`ahkohd/tauri-macos-menubar-app-example` v2-popover branch).
### Global Hotkey
Built-in via `@tauri-apps/plugin-global-shortcut`. Register accelerators (e.g., `CmdOrCtrl+Space`) that fire even when background/minimized. First-class plugin.
### System Tray
First-class support. `AppHandle::tray()` with native menus and click event handling from Rust or frontend.
### Auto-Update
`@tauri-apps/plugin-updater` — signed updates required (Ed25519 keypair). Host a static JSON endpoint with version metadata and signed artifact URLs.
### macOS Code Signing / Notarization
Automated via Tauri CLI environment variables (`APPLE_CERTIFICATE`, `APPLE_SIGNING_IDENTITY`, `APPLE_ID`, `APPLE_TEAM_ID`). Notarization adds ~25 min per build.
### Build Pipeline
- Prerequisites: Rust toolchain + Xcode CLI tools
- First build: 515 min (Cargo compiles Rust deps)
- Incremental builds: Fast with caching
- Config: `tauri.conf.json` + `Cargo.toml`
- Complexity: Medium-High (Rust requirement is the barrier)
### Mobile Support
Tauri v2 has **first-class iOS/Android support** in the same codebase (WKWebView on iOS, Android System WebView on Android). HMR extends to physical devices. This is a genuine differentiator — Electron is desktop-only.
## Electron
Mature since 2013. Bundles full Chromium + Node.js runtime. Used by VS Code, Slack, Discord, Obsidian.
**Binary Size:** 80150 MB installer
**Memory Usage:** 200350 MB idle
**Startup Time:** 12s
### Menu Bar App
Well-established via `menubar` npm package. Creates BrowserWindow positioned below tray icon, manages show/hide on tray click. Very mature.
### Global Hotkey
`globalShortcut` module in Electron core. System-wide even when hidden.
### System Tray
`Tray` class in Electron core with context menus and click events.
### Auto-Update
`electron-updater` (S3/GitHub Releases) or `update.electronjs.org` (free for open-source).
### macOS Code Signing / Notarization
Via `@electron/osx-sign` + `@electron/notarize`, integrated into `electron-builder` / Electron Forge.
### Build Pipeline
- Prerequisites: Node.js only — no additional runtimes
- Build tools: `electron-vite` for Vue 3 + Vite integration
- Build times: 25 min (no Rust compilation) + 25 min notarization
- Complexity: Medium (main/renderer process split requires understanding)
## Comparison
| Dimension | Tauri v2 | Electron |
|---|---|---|
| Installer size | 28 MB | 80150 MB |
| Idle RAM | 3040 MB | 200350 MB |
| Startup time | < 500ms | 12s |
| Menu bar app | Supported | Supported |
| Global hotkey | Built-in plugin | Built-in API |
| System tray | Built-in | Built-in |
| Auto-update | Built-in (signed) | electron-updater |
| New language | Rust | None (JS/TS) |
| iOS/Android | Yes (same codebase) | No |
| WebView | WKWebView (varies by OS) | Chromium (pinned, consistent) |
| Ecosystem maturity | Growing fast | Very mature |
| Security model | Capability-based, opt-in | Opt-out, manual discipline |
| Debug tools | Safari Web Inspector | Chrome DevTools |
## Recommendation
**Tauri v2 is the stronger choice for AIUI:**
1. **Memory advantage is decisive.** Users running local LLMs or managing API streaming need resources for the AI workload, not the shell. 30 MB vs 300 MB matters.
2. **Menu bar pattern fits naturally** for a chat/AI assistant (Raycast-style quick invoke).
3. **iOS/Android support** from the same codebase aligns with AIUI's multi-surface vision.
4. **Capability-based security** is appropriate for handling API keys and sensitive chat data.
5. **Binary size matters** — 5 MB download vs 120 MB affects distribution trust.
## Concrete Next Steps
1. Scaffold Tauri v2 project: `npm create tauri-app@latest` with Vite template
2. Point dev server to existing `packages/app` Vite config
3. Implement tray icon + menu bar popover window
4. Register global hotkey (e.g., `Cmd+Shift+Space`) to invoke chat
5. Write Rust commands for: file I/O, tray management, updater config
6. Set up macOS code signing + notarization pipeline
7. Distribute via Homebrew cask or direct download
8. Evaluate Tauri mobile targets for iOS/Android convergence
+209
View File
@@ -0,0 +1,209 @@
# Plugin System Hardening Research — AIUI
## Current State
The plugin system has these existing components:
- `packages/core/src/types/plugin.ts``AIUIPlugin` interface, `PluginContext`, `PluginType`
- `packages/core/src/plugins/registry.ts` — in-memory Vue ref-based registry
- `packages/app/src/stores/pluginMarketplace.ts``InstalledPlugin`, `PluginPermission`, `installPlugin`, `hasPermission`
- `packages/app/src/components/settings/PluginMarketplace.vue` — permissions dialog
- `packages/app/src/components/renderers/CodeRunner.vue` — existing `<iframe sandbox="allow-scripts">` + postMessage pattern
**Gaps:** No cryptographic signature verification, no runtime permission enforcement in sandbox, no CSP on plugin iframes.
## 1. Signature Validation for Community Plugins
### Problem
`importFromUrl` fetches arbitrary `aiui-plugin.json` from any URL with no integrity check. A compromised URL grants arbitrary code execution.
### Recommended: Ed25519 Detached Signatures
Aligns with existing crypto posture (tweetnacl.js for E2E, Web Crypto for storage).
**Manifest format:**
```json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.2.0",
"signature": {
"algorithm": "ed25519",
"publicKey": "base64-public-key",
"value": "base64-signature-over-canonical-manifest"
}
}
```
**Verification:** Canonical payload = manifest JSON minus `signature` field, sorted keys. Verify via `crypto.subtle.verify('Ed25519', ...)` (Chrome 113+, Firefox 130+, Safari 17+) with `tweetnacl.js` fallback.
**Integration point:** Gate `installPlugin()` on signature verification for Tier 2+ plugins.
### Secondary: SRI Hash
`bundleUrl` + `integrity` field enables browser-native subresource integrity enforcement at load time.
### Trust Model
| Tier | Who | Signing | Sandbox | Verification |
|---|---|---|---|---|
| 1 — Built-in | AIUI maintainers | Bundled in app | None (trusted) | None needed |
| 2 — Verified | Reviewed by AIUI team | Ed25519 by author | Full iframe sandbox | Signature + hash |
| 3 — Sideloaded | User-imported URL | Optional | Full iframe sandbox, stricter CSP | Warn prominently |
## 2. Sandboxed iframe Execution
### Architecture
Each community plugin runs in an isolated iframe. AIUI manages a `PluginBridge` service:
```
AIUI App (parent)
│ postMessage (structured protocol)
└── Plugin Sandbox iframe (origin: null, sandbox="allow-scripts")
└── Plugin code (no DOM, no storage, no cross-origin network)
```
### Sandbox Configuration
```html
<!-- Tier 2 plugin (headless, no UI) -->
<iframe sandbox="allow-scripts" srcdoc="..." style="display: none" />
<!-- Tier 2 plugin with UI panel -->
<iframe sandbox="allow-scripts allow-popups-to-escape-sandbox" srcdoc="..." />
```
**Never grant:** `allow-same-origin` (breaks isolation), `allow-forms`, `allow-top-navigation`, `allow-modals` unless explicitly user-approved.
### postMessage Protocol
Typed message envelopes following the archyBridge pattern:
```typescript
// Plugin → Host
interface PluginRequest {
type: 'plugin:request'
id: string // correlation ID
pluginId: string
capability: string // e.g. 'storage:get', 'network:fetch'
payload: unknown
}
// Host → Plugin
interface PluginResponse {
type: 'plugin:response'
id: string
success: boolean
data?: unknown
error?: string
}
```
### Host-side Validation
1. `event.origin` must be `null` (sandboxed srcdoc iframes)
2. `pluginId` must match the iframe→plugin mapping
3. Requested capability must be in `grantedPermissions`
4. Rate-limit: reject if > N requests/second (DoS prevention)
### Network Restriction
With `sandbox="allow-scripts"` alone, iframes can still `fetch()`. Block direct network via CSP in srcdoc:
```html
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'unsafe-inline'; connect-src 'none'">
```
Plugins requiring network use the `network:fetch` capability — host proxies the request after validating the URL.
### Storage Isolation
Sandboxed iframes without `allow-same-origin` cannot access parent's localStorage/IndexedDB. Plugin storage goes through the `storage` capability, namespaced under `plugin::{id}::`.
## 3. Permission System Per Plugin
### Expanded Permissions
```typescript
export type PluginPermission =
| 'chat-read' // read chat history
| 'chat-inject' // inject messages (high risk)
| 'chat-messages' // read + inject (legacy, maps to both)
| 'network' // outbound HTTPS via host proxy
| 'favorites' // read/write favorites
| 'storage' // namespaced plugin storage
| 'nostr' // access Nostr identity (high risk)
| 'wallet' // deep-link to wallet (high risk)
| 'clipboard' // read/write clipboard
| 'notifications' // send notifications
| 'media-playback' // control media player
| 'renderer' // register content renderer
| 'settings-read' // read AIUI settings
```
### Risk Classification
```typescript
const permissionRisk: Record<PluginPermission, 'low' | 'medium' | 'high'> = {
'network': 'low',
'storage': 'low',
'chat-read': 'low',
'favorites': 'low',
'notifications': 'medium',
'clipboard': 'medium',
'media-playback': 'medium',
'renderer': 'medium',
'settings-read': 'medium',
'chat-messages': 'high',
'chat-inject': 'high',
'nostr': 'high',
'wallet': 'high',
}
```
### User Consent Flow
1. High-risk permissions default to unchecked in consent dialog
2. Show risk badges (low/medium/high) next to each permission
3. Unverified plugins (Tier 3) show prominent warning before permissions dialog
4. Progressive disclosure: summary before detailed checkboxes
### Permission Revocation
1. Terminate plugin iframe immediately (`iframe.remove()`)
2. Host handler rechecks `hasPermission()` on every capability call
3. Emit `plugin:permission-revoked` event so plugin can react gracefully
### Least Privilege
- Plugins declare minimum permissions in manifest
- All capabilities gated on `hasPermission()` — no admin override
- Storage namespaced under `plugin::{id}::`
- Optional `allowedOrigins[]` in manifest restricts network targets
- Audit log: capability invocations logged with plugin ID
## Integration Points
### Files to modify:
**`packages/core/src/types/plugin.ts`** — Add `tier`, `signature`, `sandbox` fields to manifest type.
**`packages/app/src/stores/pluginMarketplace.ts`** — Add `verifyManifestSignature()` in `installPlugin()`. Add `revokePermission()`. Move `grantedPermissions` to encrypted storage.
**`packages/core/src/plugins/registry.ts`** — Evolve into `PluginHost` service that manages sandbox iframe lifecycle and routes postMessage capability requests.
**`packages/app/src/plugins/index.ts`** — Tier 1 plugins use `registerPlugin()` directly. Tier 2+ load through `PluginHost.loadSandboxed(manifest)`.
## Concrete Next Steps
1. Define `PluginSignature` type and `verifyManifestSignature()` using tweetnacl.js
2. Create `PluginSandbox` service to manage iframe lifecycle + postMessage routing
3. Add CSP meta tag to plugin srcdoc template
4. Implement capability handlers (storage, network, chat) with permission checks
5. Update consent dialog with risk classification badges
6. Add `revokePermission()` with live iframe termination
7. Create CLI tool for plugin authors to sign manifests
+150
View File
@@ -0,0 +1,150 @@
# Overnight Claude Automation
Run Claude Code autonomously while you're away. Combines sleep prevention, task-based execution, the Ralph Wiggum Technique (Stop hook blocks until plan is complete), and security hooks that restrict AI to project files and block destructive commands.
## Prerequisites
- **Claude Code CLI** ([claude.ai/code](https://claude.ai/code)) — installed at `~/.local/bin/claude` or in PATH
- **Hooks** — user-level hooks in `~/.claude/` (sleep, Ralph Wiggum)
- **jq** — for security hook scripts (`brew install jq`)
## Flow
### Pre-run (before 56pm)
1. **Commit and push** — Snap current work and back up to remote.
2. **Run prepare script** — Creates date-stamped branch and verifies clean state:
```bash
./loop/prepare.sh
```
3. **Edit plan** — Update `loop/plan.md` with evening scope and tasks (see template below).
4. **Commit plan** — Version the plan so you can revert if needed:
```bash
git add loop/plan.md && git commit -m "chore: overnight plan $(date +%Y-%m-%d)"
```
5. **Push** (optional but recommended): `git push -u origin overnight/YYYY-MM-DD`
### Overnight
```bash
tmux new -s overnight
caffeinate -i ./loop/loop.sh
# Detach: Ctrl+B, then D
```
### Post-run (next morning)
1. `git status` and `git diff` to review changes.
2. Run `pnpm test && pnpm lint && pnpm typecheck`.
3. Merge branch or revert if needed.
## Quick Start
1. **Edit your plan** — Add tasks to `loop/plan.md` using the evening run format:
```markdown
# Evening Run — YYYY-MM-DD
## Scope
Add tests to chat components.
## Tasks
- [ ] Add unit tests for useAI composable
- [ ] Fix linter errors in packages/app
```
2. **Run overnight** — From project root:
```bash
caffeinate -i ./loop/loop.sh
```
## How It Works
| Component | Purpose |
|-----------|---------|
| **UserPromptSubmit hook** | Starts `caffeinate` to prevent Mac sleep when you submit a prompt |
| **Stop hook** | Checks `plan.md` for unchecked tasks; blocks Claude from stopping until all are done (Ralph Wiggum) |
| **SessionEnd hook** | Kills `caffeinate` so Mac can sleep again |
| **PreToolUse (Bash)** | Blocks dangerous commands (rm -rf, git reset --hard, etc.) |
| **PreToolUse (Edit\|Write)** | Blocks edits outside project and to protected paths |
| **loop.sh** | Runs Claude with `--dangerously-skip-permissions` and feeds the prompt from `loop/prompt.md` |
## Security Model
Project-scoped hooks in `.claude/hooks/` restrict the AI during overnight runs:
### Bash guard (`block-risky-bash.sh`)
Blocks: `rm -rf`, `git reset --hard`, `git push --force`, `git clean -fd`, `chmod -R 777`, fork bombs, block device overwrites, `mkfs`, and path traversal with destructive commands.
### File edit guard (`protect-files.sh`)
Blocks Edit/Write when:
- Path is **outside project directory**
- Path contains **`.git/`**
- Path is **`.env`**, **`.env.local`**, **`.env.*.local`**
- Path is **`package-lock.json`** or **`pnpm-lock.yaml`**
- Path contains **`node_modules/`**
Read, Glob, and Grep remain unrestricted.
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `CLAUDE_AUTONOMOUS` | `1` | Set to `1` to enable Ralph Wiggum (Stop hook checks plan). `0` disables. |
| `CLAUDE_PLAN_FILE` | `plan.md` | Plan file path (relative to project). |
| `ITERATION_COUNT` | `1` | Number of loop iterations (use >1 for multi-run without Ralph Wiggum). |
| `ITERATION_DELAY` | `600` | Seconds between iterations when `ITERATION_COUNT` > 1. |
| `PROMPT_FILE` | `loop/prompt.md` | Prompt content for Claude. |
| `LOG_FILE` | `loop/loop.log` | Log output (gitignored). |
| `RATE_LIMIT_WAIT` | `3600` | Seconds to wait when rate limited (default 1 hour). |
| `MAX_RATE_LIMIT_RETRIES` | `5` | Max rate limit retries before scheduling launchd job. |
## Rate Limit Handling
The loop script automatically detects rate limits (429, quota exceeded, etc.) and handles them:
1. **Inline retry** — On first rate limit hit, sleeps for `RATE_LIMIT_WAIT` seconds (default 1 hour) and retries.
2. **Escalating retries** — Retries up to `MAX_RATE_LIMIT_RETRIES` times with the same wait.
3. **launchd fallback** — After max retries, creates a self-cleaning launchd plist at `~/Library/LaunchAgents/com.aiui.overnight-retry.plist` that restarts the loop at the estimated reset time. The plist auto-removes after running.
This means you can walk away knowing the automation will survive rate limits overnight.
## Scheduling (Optional)
Install [claude-code-schedule](https://github.com/macalinao/claude-code-schedule) for time-based runs:
```bash
cargo install claude-code-schedule
ccschedule --time 05:30 --message "Review plan.md and complete next task"
```
## continuous-claude (Optional)
For full PR-based workflow (branches, PRs, CI):
```bash
# Install from https://github.com/AnandChowdhary/continuous-claude
continuous-claude -p "Work through loop/plan.md" -m 10 --max-duration 8h
```
## Remote Monitoring
- **Tmux + SSH**: Attach from another machine: `ssh host 'tmux attach -t overnight'`
- **Tailscale**: Use Tailscale for easy remote SSH when away from home network
- **Log tail**: `tail -f loop/loop.log` to watch progress
## Safety
- **Start small** — Test with 12 tasks before overnight runs
- **prepare.sh** — Run before starting; fails if working tree is dirty or branch exists
- **Git** — Loop does not auto-commit; you review and merge in the morning
- **`--dangerously-skip-permissions`** — Security hooks still run and block dangerous actions
- **Project-scoped hooks** — Only apply when Claude runs in AIUI; other projects unaffected
+286
View File
@@ -0,0 +1,286 @@
# AIUI Debug, Fix & Hardening Plan
## Execution Rules
- Run on `development` branch
- Each `- [ ]` task is one agent iteration — complete fully before moving on
- After each phase: `pnpm typecheck && pnpm lint && pnpm test -- --run`
- If checks fail, fix before proceeding
- Commit at end of each phase with `type(scope): description` format
- Do NOT push — human will review and push
---
## PHASE 0: Baseline Verification
- [ ] **P0.1 — Baseline check**
- Run: `pnpm install && pnpm typecheck && pnpm lint && pnpm test -- --run`
- Record output. Note existing failures. Fix any blockers before proceeding.
- Commit: `chore(app): verify baseline before hardening`
---
## PHASE 1: Critical Security Fixes
- [ ] **P1.1 — Fix CORS wildcard in claude-proxy.ts**
- File: `packages/app/server/claude-proxy.ts`
- Replace all `'Access-Control-Allow-Origin': '*'` (lines ~415, 473, 531) with the correct localhost origin or import `ALLOWED_ORIGIN` from `dev-auth.ts`. Match the pattern already used on lines 294/303.
- Verify: `grep -n "Allow-Origin.*\*" packages/app/server/claude-proxy.ts` returns zero matches.
- [ ] **P1.2 — Fix dev auth token bypass**
- File: `packages/app/server/dev-auth.ts`
- Line 11: `if (!token) return true` skips auth entirely when token empty.
- Fix: Only skip in non-production. `if (!token) { if (process.env.NODE_ENV === 'production') { res.writeHead(401); res.end('Unauthorized'); return false; } return true; }`
- Add console.warn when auth disabled.
- [ ] **P1.3 — Symlink traversal protection in vite-fs.ts**
- File: `packages/app/vite-fs.ts`
- In `walk` function: after constructing `fullPath`, add `if (lstatSync(fullPath).isSymbolicLink()) continue;`
- In `handleRead`: before `statSync`, check `lstatSync(filePath).isSymbolicLink()` → return 403.
- Import `lstatSync` from `fs`.
- [ ] **P1.4 — Body size limit on vite-fs.ts mkdir**
- File: `packages/app/vite-fs.ts`
- `handleMkdir` reads body with no size cap (lines 185-209).
- Add `const MAX_BODY_SIZE = 1024`. Track `let size = 0` on `data` events. Return 413 if exceeded.
- [ ] **P1.5 — JSON schema validation in vite-dev-chats.ts**
- File: `packages/app/vite-dev-chats.ts`
- After `JSON.parse(body)` on line 66, validate shape: must be object with optional `conversations` (object) and `activeConversationId` (string|null). Reject with 400 if invalid.
- [ ] **P1.6 — CSP headers in nginx-archy.conf**
- File: `packages/app/server/nginx-archy.conf`
- Add inside `/aiui/` location block:
```
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https://api.anthropic.com https://openrouter.ai https://wavlake.com https://itunes.apple.com https://openlibrary.org https://covers.openlibrary.org https://en.wikipedia.org https://www.googleapis.com https://image.tmdb.org; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
```
- [ ] **P1.7 — OpenRouter validation + streaming timeout**
- File: `packages/app/server/claude-proxy.ts`
- OpenRouter handler: parse `reqBody`, validate has `model` (string), `messages` (array), `stream` (boolean). Return 400 if invalid.
- Both streaming reader loops: add idle timeout (120s). `let idleTimer = setTimeout(() => reader.cancel(), 120000)`. Reset on each chunk. Clear on completion.
- Verify: `pnpm typecheck`
- **Commit**: `fix(app): critical security — CORS, auth, symlink, CSP, body validation`
---
## PHASE 2: Proxy & Server Hardening
- [ ] **P2.1 — Fix tool use loop in claude-proxy.ts**
- File: `packages/app/server/claude-proxy.ts` (lines 164-222)
- Unknown tool names: push error tool_result `{ type: 'tool_result', tool_use_id: tu.id, content: 'Error: unknown tool', is_error: true }`.
- Add turnMessages size guard: `if (JSON.stringify(turnMessages).length > 500_000)` break loop.
- Add `AbortSignal.timeout(30000)` on each API call within loop.
- [ ] **P2.2 — SSRF mitigation in vite-rss.ts**
- File: `packages/app/vite-rss.ts`
- After `tryParseFeed` returns, validate returned article URLs with `isPrivateUrl`. Filter out any private URLs.
- Add comment documenting residual TOCTOU risk (dev-only middleware).
- [ ] **P2.3 — Sanitize web search context in useAI.ts**
- File: `packages/app/src/composables/useAI.ts` (lines 399-408)
- Create helper: `function sanitizeSearchText(s: string): string { return s.replace(/[[\]()#*_~` >]/g, '\\$&').replace(/\n/g, ' ').slice(0, 500) }`
- Apply to `r.title` and `r.content` in `formatWebSearchContext`.
- **Commit**: `fix(app): proxy hardening — tool loop, SSRF, prompt injection`
---
## PHASE 3: State & Logic Bugs
- [ ] **P3.1 — Fix contentType assignment in useContentPanel.ts**
- File: `packages/app/src/composables/useContentPanel.ts` (lines 219-225)
- The `contentType` ref type is `'film' | 'song' | 'podcast'`. Expand to include all content types or use `ContentTab`. Fix assignments: books→'film' is acceptable if type can't expand, but news/TV should map correctly. If the type is only used for player logic, keep narrow type but fix assignments to be semantically correct.
- [ ] **P3.2 — Fix apiFetching race in useBannerFallback.ts**
- File: `packages/app/src/composables/useBannerFallback.ts`
- Change `let apiFetching = false` to `const apiFetching = ref(false)`. Update all references to `.value`.
- [ ] **P3.3 — Fix event listener cleanup in chat.ts**
- File: `packages/app/src/stores/chat.ts` (lines 111-118)
- Extract handlers to named functions. Add `import.meta.hot?.dispose()` cleanup for HMR.
- [ ] **P3.4 — Fix news RSS race condition in useContentPanel.ts**
- File: `packages/app/src/composables/useContentPanel.ts` (lines 134-154)
- Capture `const tabAtStart = activeTab.value` before RSS fetch. In `.then()`, only set `activeTab.value = 'news'` if `activeTab.value === tabAtStart` (user hasn't manually switched).
- [ ] **P3.5 — Add error logging to silent catch blocks**
- Files: `useImageFallback.ts`, `useAI.ts`, `stores/chat.ts`
- Replace all `catch { }` and `catch { /* ignore */ }` with `catch(e) { console.debug('[module] op failed:', e) }`.
- Keep `console.debug` (not `error`) for expected failures.
- **Commit**: `fix(app): state bugs — contentType, race conditions, error logging`
---
## PHASE 4: Content Extraction & Filtering
- [ ] **P4.1 — Add recipe instructions to system prompt**
- File: `packages/app/src/composables/useAI.ts` (SYSTEM_PROMPT)
- Add after Apps section:
```
**Recipes:** When sharing recipes, use the <recipe_ext> XML tag:
<recipe_ext title="Name" servings="4" time="30 min" calories="450">
- ingredient 1
1. Step one
</recipe_ext>
```
- [ ] **P4.2 — Fix app category validation**
- File: `packages/app/src/composables/contentExtraction.ts` (line ~1330)
- Add `const VALID_CATEGORIES = new Set(['nostr-client','lightning-wallet','bitcoin-wallet','privacy','node','dev-tool','relay'])`. Validate before cast, fallback to `'dev-tool'`.
- [ ] **P4.3 — Fix song deduplication**
- File: `packages/app/src/composables/contentExtraction.ts` (lines 606-638)
- After combining library + external songs, deduplicate by normalized `title|artist` key. Library songs (with real IDs) take priority.
- [ ] **P4.4 — Fix film/TV tag ambiguity**
- File: `packages/app/src/composables/contentExtraction.ts` (lines 914-916)
- Only convert `film_ext` to TV when: query is TV-like AND no explicit `tv_ext` tags present AND response reads TV-like. If AI used both `film_ext` and `tv_ext`, keep both as-is.
- [ ] **P4.5 — Fix isBookLikeResponse false positives**
- File: `packages/app/src/composables/contentFiltering.ts` (lines 60-62)
- Increase `by [Author]` threshold from `>= 1` to `>= 2`.
- [ ] **P4.6 — Fix isRecipeLikeResponse for prose recipes**
- File: `packages/app/src/composables/contentFiltering.ts` (lines 92-94)
- Add prose detection: recipe keywords + list structure pattern.
- [ ] **P4.7 — Fix looksLikeSong false positives**
- File: `packages/app/src/composables/contentExtraction.ts` (lines 488-506)
- Add financial terms to blocklist: `'market cap','etf','price','trading','volume','earnings','valuation','stock','portfolio','investment','yield','inflation','interest rate'`.
- **Commit**: `fix(app): extraction — dedup, tag ambiguity, false positives, recipes`
---
## PHASE 5: Memory & Cache Hardening
- [ ] **P5.1 — Bound caches in useImageFallback.ts**
- File: `packages/app/src/composables/useImageFallback.ts`
- Create helper: `function boundedSet<K,V>(map: Map<K,V>, key: K, val: V, max=500) { if (map.size >= max) { const first = map.keys().next().value; if (first !== undefined) map.delete(first); } map.set(key, val); }`
- Replace all `.set()` calls on memory caches with `boundedSet()`.
- Also bound `failedUrls` Set to 1000 entries.
- [ ] **P5.2 — Bound caches in usePlayer.ts + cleanup**
- File: `packages/app/src/composables/usePlayer.ts`
- Bound `resultCache` and `nullCacheTimestamps` to 200 entries.
- Add `destroy()` method that calls `destroyPlayer()`, resets DOM refs, cancels active search controller.
- **Commit**: `fix(app): bound caches, player cleanup`
---
## PHASE 6: Accessibility
- [ ] **P6.1 — Fix touch targets and aria attributes**
- Audit all `*Grid.vue` and `*Detail.vue` for: interactive elements < 44px, images without alt, SVG fallbacks without aria-label.
- ImageGrid.vue: add `role="img"` `aria-label="Image unavailable"` to fallback SVG wrapper.
- Ensure all interactive genre/tag pills in grids have adequate touch targets (min 44x44 including padding).
- Non-interactive info pills are exempt.
- **Commit**: `fix(app): accessibility — touch targets, alt text, aria`
---
## PHASE 7: Security Tests
- [ ] **P7.1 — Write dev-auth tests**
- New file: `packages/app/src/__tests__/dev-auth.test.ts`
- Tests: auth bypass when no token (dev), auth required when token set, rate limiting 429, CORS headers correct.
- [ ] **P7.2 — Extend proxy tests**
- File: `packages/app/src/__tests__/proxy.test.ts`
- Tests: unknown tool error result, max rounds termination, turnMessages size guard, streaming timeout.
- [ ] **P7.3 — Write vite-fs security tests**
- New file: `packages/app/src/__tests__/vite-fs.test.ts`
- Tests: path traversal rejected, symlink blocked, sensitive files blocked, body size limit enforced.
- [ ] **P7.4 — Write SSRF tests for vite-rss**
- New file: `packages/app/src/__tests__/vite-rss.test.ts`
- Tests: private IP detection, localhost blocked, non-http blocked, private URLs filtered from results.
- **Commit**: `test(app): security tests — auth, proxy, fs, SSRF`
---
## PHASE 8: Content Extraction Tests
- [ ] **P8.1 — Write content filtering tests**
- New file: `packages/app/src/composables/__tests__/contentFiltering.test.ts`
- Tests: isBookLikeResponse false positive fix, isRecipeLikeResponse prose detection, isMusicQuery rejects financial terms, filterTabsByContext ordering.
- [ ] **P8.2 — Extend content extraction tests**
- File: `packages/app/src/__tests__/contentExtraction.test.ts`
- Tests: song dedup, looksLikeSong rejects financial terms, app category fallback, recipe parsing.
- [ ] **P8.3 — Write TV/film resolution tests**
- Same file as P8.2.
- Tests: film_ext→TV conversion rules, book_ext in news context, explicit tags respected.
- **Commit**: `test(app): content extraction — filtering, dedup, tag resolution`
---
## PHASE 9: State & Composable Tests
- [ ] **P9.1 — Write useContentPanel tests**
- File: `packages/app/src/composables/__tests__/useContentPanel.test.ts`
- Tests: contentType assignment per content type, RSS tab override prevention, closePanel resets, empty text → no tabs.
- [ ] **P9.2 — Write useBannerFallback tests**
- New file: `packages/app/src/composables/__tests__/useBannerFallback.test.ts`
- Tests: primary URL fallthrough, API fetch on exhaustion, apiFetching guard, gradient fallback.
- [ ] **P9.3 — Write chat store tests**
- New file: `packages/app/src/__tests__/chat-store.test.ts`
- Tests: createConversation, addMessage, deleteConversation, branchFromMessage, debouncedIDBSave.
- **Commit**: `test(app): state tests — contentPanel, bannerFallback, chat store`
---
## PHASE 10: AI Integration Tests
- [ ] **P10.1 — Extend useAI tests**
- File: `packages/app/src/__tests__/useAI.test.ts`
- Tests: system prompt includes recipe instructions, web search results sanitized, editAndResend truncates, sanitizeHistory merges consecutive roles.
- [ ] **P10.2 — Write useImageFallback cache tests**
- New file: `packages/app/src/composables/__tests__/useImageFallback.test.ts`
- Tests: boundedSet evicts at max, generatePosterFallback valid SVG, escapeXml handles specials.
- **Commit**: `test(app): AI integration, image fallback cache tests`
---
## PHASE 11: Final Verification
- [ ] **P11.1 — Full integration check**
- Run: `pnpm typecheck && pnpm lint && pnpm test -- --run`
- Run: `pnpm build` — verify production build succeeds
- Fix any regressions.
- [ ] **P11.2 — Final commit**
- Run coverage report if configured: `pnpm test -- --run --coverage`
- Ensure all changes committed on `development`.
- **Commit**: `chore(app): hardening complete — all checks pass`
---
## Phase Dependencies
```
P0 → P1 → P2 → P3 → P4 → P5 ─┐
├→ P7 (tests P1,P2)
P6 ──┤
├→ P8 (tests P4)
├→ P9 (tests P3,P5)
├→ P10 (tests P2.3,P4.1)
└→ P11 (final)
```
P5 and P6 can run in parallel. P7-P10 can run in any order after their fix phases.
## Summary
- **12 phases**, **42 tasks**
- Phases 0-6: fixes (security → proxy → state → extraction → caches → a11y)
- Phases 7-10: tests (security → extraction → state → AI)
- Phase 11: final verification
- Target: all checks pass, 40%+ test coverage on critical paths, secure for Archy deployment
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env sh
# Headless loop script for overnight Claude Code automation.
# Set CLAUDE_AUTONOMOUS=1 for Ralph Wiggum (Stop hook blocks until plan is complete).
# Rate-limit aware: detects limits, sleeps until reset, and retries automatically.
set -u
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}"
LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}"
ITERATION_COUNT="${ITERATION_COUNT:-10}"
ITERATION_DELAY="${ITERATION_DELAY:-30}"
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}" # Default: wait 1 hour on rate limit
MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}" # Max retries before giving up
CLAUDE_EXIT=0
cd "$PROJECT_DIR"
# Human-readable log with visual separators
log() {
echo "$1" | tee -a "$LOG_FILE"
}
banner() {
log ""
log "════════════════════════════════════════════════════════════════"
log " $1"
log " $(date '+%Y-%m-%d %H:%M:%S')"
log "════════════════════════════════════════════════════════════════"
log ""
}
section() {
log ""
log "────────────────────────────────────────"
log " $1"
log "────────────────────────────────────────"
log ""
}
# Check if plan has remaining tasks
plan_has_tasks() {
grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null
}
# Show remaining task count
remaining_tasks() {
grep -c '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null || echo "0"
}
# Show next task
next_task() {
grep -m1 '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null | sed 's/^- \[ \] //' || echo "(none)"
}
# Detect rate limit from Claude output (only when Claude exited non-zero)
check_rate_limit() {
[ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1
# Check last 50 lines for rate limit indicators, excluding our own log lines
tail -50 "$LOG_FILE" 2>/dev/null | grep -v "^Rate limit detected" | grep -v "^Sleeping" | grep -v "^═" | grep -v "^─" | grep -qi \
-e "rate.limit" \
-e "too.many.requests" \
-e "429" \
-e "quota.exceeded" \
-e "usage.limit" \
-e "limit.reached" 2>/dev/null
}
banner "OVERNIGHT AUTOMATION STARTED"
log " Project: $PROJECT_DIR"
log " Prompt: $PROMPT_FILE"
log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}"
log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s between each)"
log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry up to ${MAX_RATE_LIMIT_RETRIES}x"
log " Tasks left: $(remaining_tasks)"
log " Next task: $(next_task)"
log ""
i=1
rate_limit_retries=0
while [ "$i" -le "$ITERATION_COUNT" ]; do
# Check if there are tasks remaining before starting
if ! plan_has_tasks; then
banner "ALL TASKS COMPLETE"
log " No remaining tasks in plan.md. Stopping."
break
fi
section "ITERATION $i/$ITERATION_COUNT"
log " Tasks remaining: $(remaining_tasks)"
log " Next task: $(next_task)"
log ""
export CLAUDE_PROJECT_DIR="$PROJECT_DIR"
export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}"
# Run Claude with autonomous permissions; prompt from file
if [ -f "$PROMPT_FILE" ]; then
log " Starting Claude session..."
log ""
"$CLAUDE_BIN" -p --dangerously-skip-permissions \
< "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE"
CLAUDE_EXIT=$?
log ""
log " Claude exited with code: $CLAUDE_EXIT"
else
log " ERROR: $PROMPT_FILE not found"
exit 1
fi
# Check for rate limit after Claude exits
if check_rate_limit; then
rate_limit_retries=$((rate_limit_retries + 1))
if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then
section "RATE LIMITED — SCHEDULING LAUNCHD RETRY"
log " Hit rate limit $rate_limit_retries times. Creating launchd job to retry later."
# Schedule a retry using launchd for after rate limit resets
PLIST_LABEL="com.aiui.overnight-retry"
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M')
RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1)
RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2)
cat > "$PLIST_PATH" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${PLIST_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH}</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>${RETRY_HOUR}</integer>
<key>Minute</key>
<integer>${RETRY_MIN}</integer>
</dict>
<key>EnvironmentVariables</key>
<dict>
<key>CLAUDE_AUTONOMOUS</key>
<string>1</string>
<key>CLAUDE_PROJECT_DIR</key>
<string>${PROJECT_DIR}</string>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin</string>
</dict>
<key>StandardOutPath</key>
<string>${LOG_FILE}</string>
<key>StandardErrorPath</key>
<string>${LOG_FILE}</string>
</dict>
</plist>
PLIST
launchctl load "$PLIST_PATH" 2>/dev/null || true
log " Scheduled retry at ~${RETRY_TIME}"
log " Plist: $PLIST_PATH (auto-removes after running)"
exit 0
fi
section "RATE LIMITED — WAITING"
log " Attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES"
log " Sleeping ${RATE_LIMIT_WAIT}s until $(date -v+${RATE_LIMIT_WAIT}S '+%H:%M:%S' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M:%S')..."
sleep "$RATE_LIMIT_WAIT"
# Check if plan still has tasks before retrying
if ! plan_has_tasks; then
banner "ALL TASKS COMPLETE (during rate limit wait)"
break
fi
log " Retrying..."
continue # Retry same iteration
fi
# Reset rate limit counter on successful run
rate_limit_retries=0
section "ITERATION $i COMPLETE"
log " Tasks remaining: $(remaining_tasks)"
log " Next task: $(next_task)"
i=$((i + 1))
if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then
log " Pausing ${ITERATION_DELAY}s before next iteration..."
sleep "$ITERATION_DELAY"
fi
done
banner "LOOP FINISHED"
log " Completed $((i - 1)) iterations"
log " Tasks remaining: $(remaining_tasks)"
log ""
+59
View File
@@ -0,0 +1,59 @@
# Overnight Plan — 2026-03-04
## Phase 1: Critical Fixes
- [x] P1-1: Brighten SVG fallbacks — increase background lightness from 18% to 28% across all 8 generators in `useImageFallback.ts` (generateSongCoverFallback, generatePodcastCoverFallback, generateNewsFallback, generateImageFallback, generatePosterFallback, generateTVSeriesFallback, generateBookCoverFallback, generatePlaceFallback). Proportionally increase all inner element lightness by +10%. TEST: run `pnpm typecheck` and visually confirm SVGs generate valid data URIs.
- [x] P1-2: Add `.catch(() => {})` to all cover fetch promise chains in grid components — SongGrid.vue (line 157), FilmGrid.vue (line 143), TVSeriesGrid.vue (line 160), BookGrid.vue (line 141), PodcastGrid.vue (line 130). Prevents unhandled rejection if fetch throws unexpectedly. TEST: `pnpm typecheck && pnpm lint`.
- [x] P1-3: Refine mobile keyboard handling — In `useVisualViewport.ts`, add debounce to viewport change handler (50ms) to prevent jittery resizing. In `App.vue`, ensure the `rootStyle` computed applies `overflow: hidden` when keyboard is open. TEST: `pnpm typecheck`.
- [x] P1-4: Verify service worker cleanup — Confirm `dev-dist/sw.js` contains the self-destructing SW and `vite.config.ts` has `devOptions.enabled: false`. If not, fix. TEST: read both files and verify.
## Phase 2: Error Handling Hardening
- [x] P2-1: Wrap JSON.parse calls in try/catch — `useContentDiscovery.ts` sessionStorage parse, all sessionStorage/localStorage reads in composables. Search for `JSON.parse` across all `.ts` and `.vue` files, wrap any unprotected calls. TEST: `pnpm typecheck && pnpm lint`.
- [x] P2-2: Add `.ok` checks before `.json()` on fetch calls — `useBitcoinPrice.ts` (Mempool API), `MempoolTxCard.vue` (tip height), `useNip05Verification.ts` (NIP-05 lookup), `ZapDialog.vue` (Lightning address). Search for `fetch(``.json()` patterns without `.ok` check. TEST: `pnpm typecheck && pnpm lint`.
- [x] P2-3: Harden SSE streaming — In `useAI.ts` `readSSE()`, wrap `reader.read()` in try/catch, close reader on error. In `openrouter-adapter.ts`, add same pattern. TEST: `pnpm typecheck`.
- [x] P2-4: Add error handling to async watchers — `PdfViewer.vue` watch calling `renderPage()`, `VideoPlayer.vue` `initHls()` in onMounted. Wrap in try/catch with user-friendly error state. TEST: `pnpm typecheck`.
## Phase 3: Security Hardening
- [x] P3-1: postMessage origin validation — In `archyBridge.ts`, replace `'*'` targetOrigin with configurable origin. Add origin check on incoming message handler. TEST: `pnpm typecheck`.
- [x] P3-2: URL validation — In `contentExtraction.ts`, add URL length limit (2048 chars) to `extractUrlFromText()`. Validate URLs before fetch. TEST: `pnpm typecheck && pnpm lint`.
- [x] P3-3: Content sanitization — Review `html.ts` for innerHTML usage, ensure SVG injection is covered. Replace `innerHTML = ''` with `textContent = ''` in `usePlayer.ts`. TEST: `pnpm typecheck`.
- [x] P3-4: Add CSP meta tag — Add `<meta http-equiv="Content-Security-Policy" ...>` to `index.html` with appropriate directives for the app (allow self, API hosts, image CDNs). TEST: `pnpm typecheck`.
## Phase 4: Test Suite
- [x] P4-1: Unit tests for usePlayer — Create `packages/app/src/composables/__tests__/usePlayer.test.ts`. Test playback state, queue management, play/pause/next/prev. Minimum 8 test cases. TEST: `pnpm test`.
- [x] P4-2: Unit tests for useContentPanel — Create `packages/app/src/composables/__tests__/useContentPanel.test.ts`. Test tab switching, detail opening, panel state management. Minimum 6 test cases. TEST: `pnpm test`.
- [x] P4-3: Unit tests for useVisualViewport — Create `packages/app/src/composables/__tests__/useVisualViewport.test.ts`. Mock visualViewport API, test keyboard detection, viewport height calculation. Minimum 5 test cases. TEST: `pnpm test`.
- [x] P4-4: Content extraction edge case tests — Create `packages/app/src/composables/__tests__/contentExtraction.test.ts`. Test interleaved tags, malformed tags, unicode content, missing fields. Minimum 10 test cases. TEST: `pnpm test`.
- [x] P4-5: Seeded prompt regression tests — Create `packages/app/src/__tests__/seed-conversations.test.ts`. Import all seed conversations from mocks, run content extraction on each, verify expected content types are produced. Minimum 1 test per seed. TEST: `pnpm test`.
## Phase 5: Feature Work
- [x] P5-1: File browser page — Create `packages/app/src/pages/BrowsePage.vue` with file tree navigation. Add route `/browse` to router. Use the existing `vite-fs.ts` plugin for file reading. Show files/folders with icons, breadcrumb nav. TEST: `pnpm typecheck && pnpm lint`.
- [x] P5-2: File tree component — Create `packages/app/src/components/browse/FileTree.vue`. Recursive tree with expand/collapse, file type icons (folder, code, image, document). Use glass morphism styling. TEST: `pnpm typecheck`.
- [x] P5-3: File preview component — Create `packages/app/src/components/browse/FilePreview.vue`. Preview text files with syntax highlighting (reuse code viewer), images inline, show file metadata. TEST: `pnpm typecheck`.
- [x] P5-4: Allow .claude folder in code viewer — Update `vite-fs.ts` to allow `.claude/` path. Update any path validation that blocks dotfiles. Show CLAUDE.md, settings, hooks, memory files. TEST: `pnpm typecheck`.
- [x] P5-5: Archy local search guide — Create `packages/app/src/docs/archy-local-search.md` documenting how file types map to content surfaces (images→ImageGrid, music→SongGrid, etc.), how ContextBroker filtering works. Also add a help section component that can display this in-app. TEST: file exists and is valid markdown.
## Phase 6: Accessibility
- [x] P6-1: Add aria-labels to icon buttons — Audit all icon-only buttons across chat components (ChatHeader.vue, ChatMessage.vue, ChatInput.vue, ChatSearch.vue). Add descriptive `aria-label` to each. TEST: `pnpm lint`.
- [x] P6-2: Add aria-labels to content grids — All Grid components (SongGrid, FilmGrid, TVSeriesGrid, PlaceGrid, BookGrid, PodcastGrid, NewsGrid, ImageGrid). Each card button needs `aria-label` with content title. TEST: `pnpm lint`.
- [x] P6-3: Focus management for dialogs — `ZapDialog.vue`: add focus trap, auto-focus close button, `aria-modal="true"`, `role="dialog"`. Same for `SettingsModal.vue`. Ensure Escape key closes. TEST: `pnpm typecheck`.
- [x] P6-4: Color contrast audit — Check `text-white/40` against dark backgrounds for WCAG AA (4.5:1). Verify `#F7931A` accent contrast. Fix any failing ratios by increasing opacity. Document findings in comments. TEST: `pnpm lint`.
- [x] P6-5: Alt text improvements — `ImageGrid.vue`: use `img.title || img.alt` instead of generic. All content grids: ensure img alt includes meaningful content (title + artist/director/author). TEST: `pnpm lint`.
## Phase 7: Performance & Compatibility
- [x] P7-1: Lazy load heavy renderers — Use `defineAsyncComponent` for PdfViewer, VideoPlayer, MapView. Add loading skeleton components for each. TEST: `pnpm typecheck`.
- [x] P7-2: Add in-memory caching — `useNip05Verification.ts`: cache results with 5-min TTL. `useBitcoinPrice.ts`: cache price with 30s TTL. TEST: `pnpm typecheck`.
- [x] P7-3: Error boundaries for grid items — Create `packages/app/src/components/ui/ErrorBoundary.vue` using `onErrorCaptured`. Wrap each grid item renderer to prevent cascade failures. Show fallback UI on component crash. TEST: `pnpm typecheck`.
- [x] P7-4: Code file size limits — In `useCodeContext.ts` `openFile()`, add file size check before reading (reject > 1MB). Add loading indicator for large files. TEST: `pnpm typecheck`.
## Phase 8: Research & Documentation
- [x] P8-1: iOS app research — Research Capacitor vs WKWebView wrapper vs React Native WebView for shipping AIUI as iOS app. Document in `docs/research/ios-app.md`: pros/cons, App Store requirements, push notification integration, offline capability. Include concrete next steps.
- [x] P8-2: Mac desktop app research — Research Tauri v2 vs Electron for Mac desktop app. Document in `docs/research/mac-desktop.md`: binary size, memory usage, menu bar app pattern (like Raycast), global hotkey/command invocation, tray API. Include concrete next steps.
- [x] P8-3: Plugin system hardening research — Document in `docs/research/plugin-security.md`: signature validation for community plugins, sandboxed iframe execution, permission system per plugin. Reference existing plugin interfaces in `packages/core/src/plugins/`.
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env sh
# Pre-run script: verify repo state and create overnight branch.
# Run before 5-6pm to ensure you can safely start the overnight loop.
set -eu
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
cd "$PROJECT_DIR"
DATE=$(date '+%Y-%m-%d')
BRANCH="overnight/${DATE}"
echo "=== Overnight pre-run check @ $(date '+%Y-%m-%dT%H:%M:%S') ==="
# 1. Check git status is clean
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: Working tree not clean. Commit or stash changes first." >&2
echo " git status" >&2
git status --short >&2
exit 1
fi
# 2. Check we're not already on an overnight branch
current=$(git branch --show-current 2>/dev/null || true)
if [ -n "$current" ] && [ "$current" = "$BRANCH" ]; then
echo "Already on $BRANCH. Ready to run." >&2
exit 0
fi
# 3. Create date-stamped branch
if git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then
echo "Branch $BRANCH already exists. Checkout or use a different date." >&2
exit 1
fi
git checkout -b "$BRANCH"
echo "Created branch $BRANCH"
# 4. Remind to push
echo ""
echo "Reminder: Push before starting overnight run: git push -u origin $BRANCH"
echo "Then run: caffeinate -i ./loop/loop.sh"
echo "=== Ready ==="
+73
View File
@@ -0,0 +1,73 @@
You are working through an overnight automation plan for the AIUI app. Read these files first:
1. `loop/plan.md` — Your task checklist (mark items `- [x]` as you complete them)
2. `CLAUDE.md` — Project conventions, design system rules, and coding standards
## Project Context
AIUI is an AI content surface UI — a Vue 3 + TypeScript + Tailwind CSS app with chat, content panels (films, music, books, TV, places, news, images, podcasts), and a plugin system. It runs as a PWA and inside Archy (an iframe host).
Key directories:
- `packages/app/src/` — Main application source
- `packages/app/src/composables/` — Shared composition functions
- `packages/app/src/components/content/` — Content grid and detail components
- `packages/app/src/components/chat/` — Chat interface components
- `packages/app/src/styles/main.css` — Glass morphism design system
- `packages/core/src/` — Core library and types
## Working Process
For each task in `loop/plan.md`:
1. Find the first unchecked `- [ ]` item
2. Read the task description carefully — it tells you what to change and where
3. Read the relevant source files before making changes
4. Make the change following CLAUDE.md conventions
5. Run the TEST command specified in the task
6. Fix any errors from the test command before proceeding
7. Commit with conventional commit format: `type(scope): description`
8. Mark the task done: change `- [ ]` to `- [x]` in `loop/plan.md`
9. Move to the next unchecked task immediately
## Testing Gates
Every task specifies a TEST command. You MUST run it and pass before committing:
- `pnpm typecheck` — TypeScript strict mode compilation
- `pnpm lint` — ESLint checks
- `pnpm test` — Vitest unit tests
- Multiple commands joined with `&&` must ALL pass
If a test fails, fix the issue and re-run. Do not skip tests. Do not mark a task as done if tests fail.
## Coding Rules
- **Vue 3 Composition API only**`<script setup lang="ts">`, never Options API
- **Glass morphism design** — use `.glass`, `.glass-card`, `.glass-button` from `main.css`
- **Dark theme**`bg-white/5`, `text-white/80`, never `bg-gray-*` or plain `bg-white`
- **Text opacity scale**`text-white/25``/40``/60``/70``/80``/90``/96`
- **Accent**`text-accent` (`#F7931A`)
- **Touch targets** — minimum 44x44px for all interactive elements
- **Font minimums** — never smaller than 11px
- **No over-engineering** — only change what the task asks for
- **Keep existing patterns** — match the style of surrounding code
## Commit Format
```
type(scope): description
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
```
Types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `perf`
Scope: `app`, `core`, or specific area like `chat`, `content`, `player`
## Rules
- Never skip a testing gate — if tests fail, fix them before moving on
- If a task is proving difficult, make at least 10 genuine attempts before moving on
- Always read source files before editing them
- Do not stop until all tasks are checked or you are rate limited
- Commit after each completed task
- For research tasks (Phase 8), create the docs directory if needed: `mkdir -p docs/research`
- For test tasks (Phase 4), create the test directory if needed: `mkdir -p packages/app/src/composables/__tests__`
+33
View File
@@ -0,0 +1,33 @@
{
"name": "aiui",
"version": "0.1.0",
"private": true,
"description": "The next-generation AI content surface UI",
"license": "MIT",
"scripts": {
"dev": "pnpm --filter @aiui/app dev",
"dev:core": "pnpm --filter @aiui/core dev",
"build": "turbo build",
"test": "turbo test",
"lint": "turbo lint",
"typecheck": "turbo typecheck",
"clean": "turbo clean"
},
"devDependencies": {
"turbo": "^2.8.12",
"typescript": "~5.8.0"
},
"packageManager": "pnpm@10.30.3",
"engines": {
"node": ">=20.0.0",
"pnpm": ">=10.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
},
"dependencies": {
"pdfjs-dist": "^5.5.207"
}
}
+21
View File
@@ -0,0 +1,21 @@
import type { StorybookConfig } from '@storybook/vue3-vite'
const config: StorybookConfig = {
stories: ['../src/**/__stories__/*.stories.ts'],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
addons: ['@storybook/addon-essentials'],
viteFinal(config) {
config.resolve ??= {}
config.resolve.alias ??= {}
// Match app aliases
const alias = config.resolve.alias as Record<string, string>
alias['@'] = new URL('../src', import.meta.url).pathname
alias['@aiui/core'] = new URL('../../core/src', import.meta.url).pathname
return config
},
}
export default config
+16
View File
@@ -0,0 +1,16 @@
import type { Preview } from '@storybook/vue3'
import '../src/styles/main.css'
const preview: Preview = {
parameters: {
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#0a0a0a' },
],
},
layout: 'centered',
},
}
export default preview
+1
View File
@@ -0,0 +1 @@
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
+124
View File
@@ -0,0 +1,124 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-f97094b3'], (function (workbox) { 'use strict';
self.skipWaiting();
workbox.clientsClaim();
/**
* The precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
workbox.precacheAndRoute([{
"url": "registerSW.js",
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.rat89nkoims"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
workbox.registerRoute(/^https:\/\/api\.anthropic\.com\/.*/i, new workbox.NetworkOnly(), 'GET');
workbox.registerRoute(/^https:\/\/openrouter\.ai\/.*/i, new workbox.NetworkOnly(), 'GET');
workbox.registerRoute(/\/api\/web-search\?.*/i, new workbox.NetworkOnly(), 'GET');
workbox.registerRoute(/\/api\/rss-articles\?.*/i, new workbox.NetworkOnly(), 'GET');
workbox.registerRoute(/\/api\/tmdb\/.*/i, new workbox.StaleWhileRevalidate({
"cacheName": "tmdb-cache",
plugins: [new workbox.ExpirationPlugin({
maxEntries: 200,
maxAgeSeconds: 86400
})]
}), 'GET');
workbox.registerRoute(/^https:\/\/image\.tmdb\.org\/.*/i, new workbox.CacheFirst({
"cacheName": "tmdb-images",
plugins: [new workbox.ExpirationPlugin({
maxEntries: 500,
maxAgeSeconds: 604800
})]
}), 'GET');
workbox.registerRoute(/^https:\/\/upload\.wikimedia\.org\/.*/i, new workbox.CacheFirst({
"cacheName": "wiki-images",
plugins: [new workbox.ExpirationPlugin({
maxEntries: 200,
maxAgeSeconds: 604800
})]
}), 'GET');
workbox.registerRoute(/^https:\/\/d12wklypp119aj\.cloudfront\.net\/image\/.*/i, new workbox.CacheFirst({
"cacheName": "wavlake-images",
plugins: [new workbox.ExpirationPlugin({
maxEntries: 300,
maxAgeSeconds: 604800
})]
}), 'GET');
}));
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,208 @@
import { test, expect } from '@playwright/test'
test.describe('Content surfaces', () => {
test('empty state shows when no conversation selected', async ({ page }) => {
await page.goto('/')
const main = page.locator('main.path-glass-card')
await expect(main).toBeVisible()
})
test('chat can receive input', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
const input = page.getByPlaceholder(/Message AIUI/)
await input.click()
await input.pressSequentially('Recommend some films')
await expect(input).toHaveValue('Recommend some films', { timeout: 3000 })
})
test('films surface: films conversation loads and shows film cards', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
await page.getByRole('button', { name: /View all \d+ films/i }).click()
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
})
test('films surface: clicking assistant bubble opens panel', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
})
test('magazine surface: BIP brief shows sections', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'BIP 110 brief' }).click()
await expect(page.getByText(/BIP 110|Pro camp|Summary/i).first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: 'View brief' }).click()
await expect(page.getByText(/AI Brief|Summary|Pro camp/i).first()).toBeVisible({ timeout: 5000 })
})
test('songs surface: songs conversation shows song cards', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'Music recommendations' }).click()
await expect(page.getByText('Never Meant').first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: /View all \d+ songs/i }).click()
await expect(page.locator('main').getByText('Never Meant').first()).toBeVisible({ timeout: 5000 })
})
test('podcasts surface: podcasts conversation shows podcast cards', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'Bitcoin podcasts' }).click()
await expect(page.getByText('What Bitcoin Did').first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: /View all \d+ podcasts/i }).click()
await expect(page.locator('main').getByText('What Bitcoin Did').first()).toBeVisible({ timeout: 5000 })
})
test('websites surface: websites tab shows link cards', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'Bitcoin resources' }).click()
await expect(page.getByText('Bitcoin Magazine').first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: /View all \d+ websites/i }).click()
await expect(page.locator('main').getByText('Bitcoin Magazine').first()).toBeVisible({ timeout: 5000 })
})
test('news surface: news conversation shows articles', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
await page.getByRole('button', { name: 'Latest Bitcoin news' }).click()
await expect(page.getByText(/Bitcoin hits|ETF inflows/i).first()).toBeVisible({ timeout: 8000 })
await page.getByRole('button', { name: /View all \d+ articles/i }).click()
await expect(page.locator('main').getByText(/Bitcoin hits|ETF inflows/i).first()).toBeVisible({ timeout: 5000 })
})
})
test.describe('Chat interactions', () => {
test('sends a message and receives streaming response', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await page.waitForLoadState('networkidle')
const input = page.getByPlaceholder(/Message AIUI/)
await input.click()
await input.fill('Hello')
await input.press('Enter')
// User message should appear in chat
await expect(page.getByText('Hello').first()).toBeVisible({ timeout: 5000 })
})
test('content panel shows film cards when AI mentions films', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Film cards should be visible inline in assistant message
await expect(page.getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
// Open panel via "View all" button
await page.getByRole('button', { name: /View all \d+ films/i }).click()
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
})
test('clicking a film card opens detail view', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Open panel
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
const filmCard = page.locator('main').getByText('Blade Runner 2049').first()
await expect(filmCard).toBeVisible({ timeout: 5000 })
// Click film card to open detail
await filmCard.click()
// Detail view should show film metadata
await expect(page.getByText(/Denis Villeneuve|2017|Sci-Fi/i).first()).toBeVisible({ timeout: 5000 })
})
test('mobile viewport shows full-screen overlay for content', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 })
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Open panel on mobile
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
// Content should be visible as overlay on mobile
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
})
test('stop button halts generation', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
// When not streaming, stop button should not be visible
const stopButton = page.getByRole('button', { name: 'Stop generation' })
await expect(stopButton).toBeHidden({ timeout: 3000 })
// Chat input should be available instead
const input = page.getByPlaceholder(/Message AIUI/)
await expect(input).toBeVisible({ timeout: 3000 })
})
test('web search toggle works', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
const toggle = page.getByRole('button', { name: 'Toggle web search' })
await expect(toggle).toBeVisible({ timeout: 5000 })
// Click to toggle web search on
await toggle.click()
// The button styling should change (it gains accent color when active)
await expect(toggle).toBeVisible()
// Click again to toggle off
await toggle.click()
await expect(toggle).toBeVisible()
})
test('new conversation clears messages', async ({ page }) => {
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Click "New conversation" button
await page.getByRole('button', { name: 'New conversation' }).click()
// Previous messages should be cleared
await expect(page.getByText('Recommend some sci-fi films')).toBeHidden({ timeout: 5000 })
})
test('panel side toggle switches layout', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 })
await Promise.all([
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
page.goto('/'),
])
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
// Open the panel
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
// Both chat and panel sections should be visible on desktop
const chatSection = page.locator('section').first()
await expect(chatSection).toBeVisible()
})
})
@@ -0,0 +1,148 @@
import type { Conversation } from '@aiui/core/types/message'
const now = Date.now()
/** Films: user asks for films, assistant responds with [[film:f1]] etc */
export const filmsConversation: Conversation = {
id: 'e2e-films',
title: 'Film recommendations',
messages: [
{
id: 'm1',
role: 'user',
content: 'Recommend some sci-fi films',
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Here are some great sci-fi films:\n\n- [[film:f1]] - Blade Runner 2049\n- [[film:f2]] - Arrival\n- [[film:f3]] - Dune\n\nAll from Denis Villeneuve.`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** Magazine: news-like query + bullet sections (BIP/debate context) */
export const magazineConversation: Conversation = {
id: 'e2e-magazine',
title: 'BIP 110 brief',
messages: [
{
id: 'm1',
role: 'user',
content: "What's the latest on BIP 110? What are people saying?",
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `## Summary\n\nBIP 110 is being debated. Macro sentiment is bearish. BTC holding.\n\n- **Pro camp** — Technical improvement, faster.\n- **Anti camp** — Too risky, prefer status quo.\n\n**Henrik Zeberg** (analyst) says this could be bullish long-term.\n\nFor deeper analysis: check **Bitcoin Mailing List** (gnusha.org).`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** Websites: user asks for resources, assistant gives markdown links */
export const websitesConversation: Conversation = {
id: 'e2e-websites',
title: 'Bitcoin resources',
messages: [
{
id: 'm1',
role: 'user',
content: 'Best websites to check for Bitcoin news?',
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Here are the best places to check:\n\n- [Bitcoin Magazine](https://bitcoinmagazine.com)\n- [Bitcoin.org](https://bitcoin.org)\n- [Mempool.space](https://mempool.space)`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** News: web search results + news-like response */
export const newsConversation: Conversation = {
id: 'e2e-news',
title: 'Latest Bitcoin news',
messages: [
{
id: 'm1',
role: 'user',
content: "What's the latest Bitcoin news?",
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Here's what's happening. For the latest news check these sources:\n\n- [Bitcoin hits new high](https://example.com/btc-high)\n- [ETF inflows surge](https://example.com/etf-inflows)`,
timestamp: now - 30000,
webResults: [
{ title: 'Bitcoin hits new high', url: 'https://example.com/btc-high', content: 'BTC reached...' },
{ title: 'ETF inflows surge', url: 'https://example.com/etf-inflows', content: 'Spot ETF...' },
],
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** Songs: user asks for music, assistant responds with [[song:s1]] */
export const songsConversation: Conversation = {
id: 'e2e-songs',
title: 'Music recommendations',
messages: [
{
id: 'm1',
role: 'user',
content: 'Recommend some math rock',
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Here are great math rock tracks:\n\n- [[song:s1]] Never Meant by American Football\n- [[song:s2]] The Kill by Toe`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
/** Podcasts */
export const podcastsConversation: Conversation = {
id: 'e2e-podcasts',
title: 'Bitcoin podcasts',
messages: [
{
id: 'm1',
role: 'user',
content: 'Best Bitcoin podcasts?',
timestamp: now - 60000,
},
{
id: 'm2',
role: 'assistant',
content: `Check these:\n\n- [[podcast:p1]] What Bitcoin Did\n- [[podcast:p2]] The Audacity to Podcast`,
timestamp: now - 30000,
},
],
createdAt: now - 120000,
updatedAt: now,
}
export const allTestConversations = {
[filmsConversation.id]: filmsConversation,
[magazineConversation.id]: magazineConversation,
[websitesConversation.id]: websitesConversation,
[newsConversation.id]: newsConversation,
[songsConversation.id]: songsConversation,
[podcastsConversation.id]: podcastsConversation,
}
+27
View File
@@ -0,0 +1,27 @@
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs'
import { resolve } from 'path'
import { allTestConversations } from './fixtures/test-chats'
const CHATS_PATH = resolve(process.cwd(), '.dev', 'chats.json')
export default async function globalSetup() {
const dir = resolve(process.cwd(), '.dev')
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
// Backup existing chats if present (for local dev)
let backup: string | null = null
if (existsSync(CHATS_PATH)) {
backup = readFileSync(CHATS_PATH, 'utf-8')
}
const payload = {
conversations: allTestConversations,
activeConversationId: 'e2e-films',
}
writeFileSync(CHATS_PATH, JSON.stringify(payload, null, 2), 'utf-8')
// Store backup path for teardown (we pass via env since globalSetup/Teardown don't share scope easily)
if (backup) {
process.env.AIUI_E2E_CHATS_BACKUP = backup
}
}
+11
View File
@@ -0,0 +1,11 @@
import { writeFileSync } from 'fs'
import { resolve } from 'path'
const CHATS_PATH = resolve(process.cwd(), '.dev', 'chats.json')
export default async function globalTeardown() {
const backup = process.env.AIUI_E2E_CHATS_BACKUP
if (backup) {
writeFileSync(CHATS_PATH, backup, 'utf-8')
}
}
+20
View File
@@ -0,0 +1,20 @@
import { test, expect } from '@playwright/test'
test.describe('AIUI smoke tests', () => {
test('app loads and shows chat interface', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/AIUI/)
})
test('chat input is visible and focusable', async ({ page }) => {
await page.goto('/')
const input = page.getByPlaceholder(/Message AIUI|Waiting for/)
await expect(input).toBeVisible()
})
test('content panel area exists', async ({ page }) => {
await page.goto('/')
const main = page.locator('main.path-glass-card')
await expect(main).toBeVisible()
})
})
@@ -0,0 +1,48 @@
import { test, expect } from '@playwright/test'
test.describe('Visual Regression', () => {
test('ChatPage renders correctly', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
await expect(page).toHaveScreenshot('chat-page.png', {
maxDiffPixelRatio: 0.005,
fullPage: true,
})
})
test('ContentPanel renders correctly', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
// Open content panel by clicking a content tab
const filmTab = page.getByRole('button', { name: /film/i }).first()
if (await filmTab.isVisible()) {
await filmTab.click()
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot('content-panel-films.png', {
maxDiffPixelRatio: 0.005,
})
}
})
test('PassphraseDialog renders correctly', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
// PassphraseDialog shows on first load if crypto is enabled
const dialog = page.locator('.glass-card').filter({ hasText: 'Unlock AIUI' })
if (await dialog.isVisible()) {
await expect(dialog).toHaveScreenshot('passphrase-dialog.png', {
maxDiffPixelRatio: 0.005,
})
}
})
test('BottomSheet renders correctly on mobile', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/')
await page.waitForLoadState('networkidle')
await expect(page).toHaveScreenshot('mobile-view.png', {
maxDiffPixelRatio: 0.005,
fullPage: true,
})
})
})
+46
View File
@@ -0,0 +1,46 @@
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginVue from 'eslint-plugin-vue'
import vueParser from 'vue-eslint-parser'
import globals from 'globals'
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/recommended'],
{
files: ['src/**/*.vue'],
languageOptions: {
parser: vueParser,
parserOptions: {
parser: tseslint.parser,
extraFileExtensions: ['.vue'],
sourceType: 'module',
},
},
},
{
files: ['src/**/*.{ts,vue}'],
languageOptions: {
globals: {
...globals.browser,
},
},
rules: {
// TypeScript
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
// Vue
'vue/multi-word-component-names': 'off',
'vue/require-default-prop': 'off',
'vue/no-v-html': 'warn',
// General
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
{
ignores: ['dist/', 'node_modules/', 'e2e/', 'server/'],
},
)
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en" class="h-full overflow-hidden">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#faf9f6" media="(prefers-color-scheme: light)" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="AIUI" />
<meta name="description" content="AI chat interface with rich content surfaces" />
<!-- CSP set via HTTP headers in production nginx — not in HTML meta to avoid breaking Vite HMR -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
<title>AIUI</title>
</head>
<body class="antialiased h-full overflow-hidden fixed w-full">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
{
"ci": {
"collect": {
"staticDistDir": "./dist",
"numberOfRuns": 3,
"settings": {
"preset": "desktop"
}
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.7 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 3000 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.15 }],
"categories:accessibility": ["warn", { "minScore": 0.8 }],
"categories:best-practices": ["warn", { "minScore": 0.8 }]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
+67
View File
@@ -0,0 +1,67 @@
{
"name": "@aiui/app",
"version": "0.1.0",
"private": true,
"description": "AIUI reference application",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "bash scripts/dev.sh",
"dev:vite": "vite",
"dev:proxy": "tsx server/claude-proxy.ts",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"lint": "eslint src/",
"typecheck": "vue-tsc --noEmit",
"clean": "rm -rf dist",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@aiui/core": "workspace:*",
"@tanstack/vue-virtual": "^3.13.19",
"hls.js": "^1.6.15",
"katex": "^0.16.33",
"leaflet": "^1.9.4",
"markdown-it": "^14.1.1",
"mermaid": "^11.12.3",
"pdfjs-dist": "^5.5.207",
"pinia": "^3.0.4",
"plyr": "^3.8.4",
"vue": "^3.5.29",
"vue-router": "^5.0.3",
"wavesurfer.js": "^7.12.1",
"dompurify": "^3.2.6"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.49.0",
"@storybook/addon-essentials": "^8.6.0",
"@storybook/vue3": "^8.6.0",
"@storybook/vue3-vite": "^8.6.0",
"@tailwindcss/vite": "^4.2.1",
"@types/leaflet": "^1.9.21",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-basic-ssl": "^2.1.4",
"@vitejs/plugin-vue": "^6.0.4",
"duck-duck-scrape": "^2.2.7",
"eslint": "^10.0.2",
"eslint-plugin-vue": "^10.8.0",
"globals": "^17.4.0",
"happy-dom": "^20.8.3",
"rss-parser": "^3.13.0",
"storybook": "^8.6.0",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"typescript": "~5.8.0",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1",
"vite-plugin-pwa": "^1.2.0",
"vitest": "^4.0.18",
"vue-eslint-parser": "^10.4.0",
"vue-tsc": "^3.2.5"
}
}
+49
View File
@@ -0,0 +1,49 @@
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
globalSetup: './e2e/global-setup.ts',
globalTeardown: './e2e/global-teardown.ts',
reporter: 'html',
snapshotPathTemplate: '{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}',
expect: {
toHaveScreenshot: { maxDiffPixelRatio: 0.005 },
},
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
projects: [
// Desktop browsers
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
// Mobile viewports
{
name: 'iphone14',
use: {
...devices['iPhone 14'],
viewport: { width: 390, height: 844 },
},
},
{
name: 'galaxy-s21',
use: {
userAgent: 'Mozilla/5.0 (Linux; Android 12; SM-G991B) AppleWebKit/537.36',
viewport: { width: 360, height: 800 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
},
},
],
webServer: {
command: 'pnpm run dev:vite',
url: 'http://localhost:5173',
reuseExistingServer: true,
},
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 83.44 122.88">
<path fill="currentColor" d="M45.04,95.45v24.11c0,1.83-1.49,3.32-3.32,3.32c-1.83,0-3.32-1.49-3.32-3.32V95.45c-10.16-0.81-19.32-5.3-26.14-12.12C4.69,75.77,0,65.34,0,53.87c0-1.83,1.49-3.32,3.32-3.32s3.32,1.49,3.32,3.32c0,9.64,3.95,18.41,10.31,24.77c6.36,6.36,15.13,10.31,24.77,10.31h0c9.64,0,18.41-3.95,24.77-10.31c6.36-6.36,10.31-15.13,10.31-24.77c0-1.83,1.49-3.32,3.32-3.32s3.32,1.49,3.32,3.32c0,11.48-4.69,21.91-12.25,29.47C64.36,90.16,55.2,94.64,45.04,95.45z M41.94,0c6.38,0,12.18,2.61,16.38,6.81c4.2,4.2,6.81,10,6.81,16.38v30c0,6.38-2.61,12.18-6.81,16.38c-4.2,4.2-10,6.81-16.38,6.81s-12.18-2.61-16.38-6.81c-4.2-4.2-6.81-10-6.81-16.38v-30c0-6.38,2.61-12.18,6.81-16.38C29.76,2.61,35.56,0,41.94,0z M53.62,11.51c-3-3-7.14-4.86-11.68-4.86c-4.55,0-8.68,1.86-11.68,4.86c-3,3-4.86,7.14-4.86,11.68v30c0,4.55,1.86,8.68,4.86,11.68c3,3,7.14,4.86,11.68,4.86c4.55,0,8.68-1.86,11.68-4.86c3-3,4.86-7.14,4.86-11.68v-30C58.49,18.64,56.62,14.51,53.62,11.51z"/>
</svg>

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#111111"/>
<path d="M 16,5.5 Q 16,16 26.5,16 Q 16,16 16,26.5 Q 16,16 5.5,16 Q 16,16 16,5.5 Z" fill="#fafafa"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#1a1a1a"/>
<stop offset="100%" stop-color="#0a0a0a"/>
</linearGradient>
<linearGradient id="border" x1="0" y1="0" x2="0" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="rgba(255,255,255,0.18)"/>
<stop offset="100%" stop-color="rgba(255,255,255,0.04)"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="108" fill="url(#bg)"/>
<rect x="2" y="2" width="508" height="508" rx="106" fill="none" stroke="url(#border)" stroke-width="2"/>
<circle cx="256" cy="256" r="120" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="2.5"/>
<path d="M 256,106 Q 256,256 406,256 Q 256,256 256,406 Q 256,256 106,256 Q 256,256 256,106 Z" fill="#fafafa"/>
</svg>

After

Width:  |  Height:  |  Size: 922 B

@@ -0,0 +1,24 @@
<svg width="200" height="300" viewBox="0 0 200 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="clip-poster">
<rect width="200" height="300" rx="12" fill="white"/>
</clipPath>
</defs>
<g clip-path="url(#clip-poster)">
<rect width="200" height="300" fill="currentColor" fill-opacity="0.15"/>
<g fill="currentColor" fill-opacity="0.2">
<rect x="8" y="12" width="16" height="24" rx="2"/>
<rect x="40" y="12" width="16" height="24" rx="2"/>
<rect x="72" y="12" width="16" height="24" rx="2"/>
<rect x="104" y="12" width="16" height="24" rx="2"/>
<rect x="136" y="12" width="16" height="24" rx="2"/>
<rect x="168" y="12" width="16" height="24" rx="2"/>
<rect x="8" y="48" width="16" height="24" rx="2"/>
<rect x="40" y="48" width="16" height="24" rx="2"/>
<rect x="72" y="48" width="16" height="24" rx="2"/>
<rect x="104" y="48" width="16" height="24" rx="2"/>
<rect x="136" y="48" width="16" height="24" rx="2"/>
<rect x="168" y="48" width="16" height="24" rx="2"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Some files were not shown because too many files have changed in this diff Show More