Add 'aiui/' from commit 'e30ac1d1069532fb6d652d87e2d4a2fe9d1b4773'

git-subtree-dir: aiui
git-subtree-mainline: 0c4826f8cc
git-subtree-split: e30ac1d106
This commit is contained in:
archipelago
2026-08-03 15:07:11 -04:00
384 changed files with 68967 additions and 0 deletions
+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