Compare commits

..
Author SHA1 Message Date
Archipelago 170a12b99d Archipelago — open-source initial import 2026-08-12 10:55:49 +00:00
972 changed files with 22679 additions and 128678 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ name: Demo images
# code (see demo-deploy/ and docs/demo-deployment-design.md).
#
# Required repo configuration:
# vars.DEMO_REGISTRY e.g. source.archipelago-foundation.org/lfg2025
# vars.DEMO_REGISTRY e.g. 146.59.87.168:3000/lfg2025
# vars.DEMO_REGISTRY_HOST registry host for docker login (no org suffix)
# secrets.DEMO_REGISTRY_USER
# secrets.DEMO_REGISTRY_TOKEN
+21 -32
View File
@@ -4,11 +4,13 @@ on:
workflow_dispatch:
inputs:
target:
description: 'Target node IP or hostname'
description: 'Target node IP (e.g. 192.168.1.198)'
required: true
default: '192.168.1.198'
password:
description: 'Node UI password (leave blank to use the NODE_UI_PASSWORD secret)'
description: 'Node password (or "auto" for fresh install)'
required: false
default: 'auto'
jobs:
post-install-tests:
@@ -20,46 +22,33 @@ jobs:
with:
fetch-depth: 1
- name: Install SSH key
env:
SSH_KEY: ${{ secrets.NODE_SSH_KEY }}
run: |
if [ -z "$SSH_KEY" ]; then
echo "ERROR: repository secret NODE_SSH_KEY is not configured."
echo "Post-install tests authenticate by key; password auth is not supported."
exit 1
fi
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
- name: Run post-install tests on target
env:
TARGET: ${{ github.event.inputs.target }}
NODE_PASSWORD: ${{ github.event.inputs.password }}
NODE_UI_PASSWORD: ${{ secrets.NODE_UI_PASSWORD }}
SSH_USER: ${{ vars.NODE_SSH_USER }}
run: |
PASSWORD="${NODE_PASSWORD:-$NODE_UI_PASSWORD}"
if [ -z "$PASSWORD" ]; then
echo "ERROR: no node password supplied (input or NODE_UI_PASSWORD secret)."
exit 1
TARGET="${{ github.event.inputs.target }}"
PASSWORD="${{ github.event.inputs.password }}"
if [ "$PASSWORD" = "auto" ]; then
PASSWORD="testpass123!"
fi
USER_NAME="${SSH_USER:-archipelago}"
echo "══════════════════════════════════════════"
echo "Running post-install tests on $TARGET"
echo "══════════════════════════════════════════"
scp -o StrictHostKeyChecking=accept-new \
# Copy test script to target and run
sshpass -p 'archipelago' scp -o StrictHostKeyChecking=no \
scripts/run-post-install-tests.sh \
"${USER_NAME}@${TARGET}:/tmp/run-post-install-tests.sh"
archipelago@${TARGET}:/tmp/run-post-install-tests.sh 2>/dev/null || \
scp -o StrictHostKeyChecking=no \
scripts/run-post-install-tests.sh \
archipelago@${TARGET}:/tmp/run-post-install-tests.sh
# Password is passed over stdin, never as an argv the node's process
# list (or this job's log) would expose.
printf '%s' "$PASSWORD" | ssh -o StrictHostKeyChecking=accept-new \
"${USER_NAME}@${TARGET}" \
"sudo bash /tmp/run-post-install-tests.sh --password-stdin"
# Run tests (with sudo for service checks)
sshpass -p 'archipelago' ssh -o StrictHostKeyChecking=no \
archipelago@${TARGET} \
"sudo bash /tmp/run-post-install-tests.sh '$PASSWORD'" 2>/dev/null || \
ssh -o StrictHostKeyChecking=no \
archipelago@${TARGET} \
"sudo bash /tmp/run-post-install-tests.sh '$PASSWORD'"
frontend-tests:
runs-on: ubuntu-latest
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Keep the served companion APK in sync with main on every push.
#
# When a push to main includes Android changes, rebuild the APK, refresh
# neode-ui/public/packages/archipelago-companion.apk, commit it, and ask
# you to push again (so the refreshed APK rides along in the same push).
#
# Enable once per clone: git config core.hooksPath .githooks
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
# ship-companion.sh already (re)published the APK for this push — don't redo it.
[ -n "${SHIP_COMPANION:-}" ] && exit 0
PUSH_MAIN=0; RANGE_OLD=""; RANGE_NEW=""
while read -r _local_ref local_sha remote_ref remote_sha; do
if [ "${remote_ref##*/}" = "main" ]; then
PUSH_MAIN=1; RANGE_OLD="$remote_sha"; RANGE_NEW="$local_sha"
fi
done
[ "$PUSH_MAIN" = "1" ] || exit 0
# Loop-break: if the tip is already the auto APK commit, let the push proceed.
case "$(git log -1 --pretty=%s)" in
*"companion APK"*) exit 0 ;;
esac
# Only rebuild when this push actually touches the Android app.
ZEROS="0000000000000000000000000000000000000000"
if [ -z "$RANGE_OLD" ] || [ "$RANGE_OLD" = "$ZEROS" ]; then
ANDROID_CHANGED=1
elif git diff --quiet "$RANGE_OLD" "$RANGE_NEW" -- Android/ 2>/dev/null; then
ANDROID_CHANGED=0
else
ANDROID_CHANGED=1
fi
[ "$ANDROID_CHANGED" = "1" ] || exit 0
bash scripts/publish-companion-apk.sh || exit 0
DEST="neode-ui/public/packages/archipelago-companion.apk"
if git diff --cached --quiet -- "$DEST"; then
exit 0 # APK unchanged — nothing to do
fi
git commit -q -m "chore(android): update companion APK download [skip ci]"
echo "" >&2
echo "▶ Companion APK rebuilt and committed. Run your push again to include it." >&2
exit 1
-41
View File
@@ -31,27 +31,9 @@ jobs:
- name: Format
run: cargo fmt --all -- --check
# KEY-05 layer (b) is enforced HERE, with no step of its own: core/clippy.toml
# bans the defaulted RNG entry points, and `-D warnings` already turns a
# `disallowed_methods` hit into a build failure. `--all-targets` covers tests
# too, deliberately. See docs/security/KEY-05-ENTROPY-ENFORCEMENT.md
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
# KEY-05 layer (c) — see core/deny.toml for the policy and its rationale.
#
# The version is pinned deliberately. EmbarkStudios/cargo-deny-action exposes
# no input to pin the cargo-deny version, and an unpinned supply-chain checker
# is a contradiction in terms, so the tool is installed from crates.io — the
# source actually vetted at the 10-06 Task 5 legitimacy checkpoint — rather
# than by adding another unvetted action to this workflow.
#
# `check bans` ONLY: the advisories gate is not enabled (bans-only policy).
- name: Supply chain (cargo-deny)
run: |
cargo install --locked cargo-deny --version 0.20.2
cargo deny check bans
- name: Test
run: cargo test --all-features
@@ -93,31 +75,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install YAML parser
run: python3 -m pip install --quiet pyyaml
- name: Validate manifests
run: |
for manifest in apps/*/manifest.yml; do
./scripts/validate-app-manifest.sh --repo-audit "$manifest"
done
# The signed catalog overrides on-disk manifests on every node, so a
# catalog naming a registry host the deployed fleet does not trust breaks
# every install fleet-wide. Blocking, and cheap.
- name: Catalog registry trust floor
run: python3 scripts/check-catalog-registry-trust.py
# A stale image literal on the fallback install path deploys an old
# image after the manifest has moved on — how a withdrawn, vulnerable
# release gets installed post-fix. Blocking.
- name: Installer image pins
run: python3 scripts/check-installer-image-pins.py
# Advisory: shows where the release catalog has fallen behind the
# manifests in this repo. Not blocking, because the catalog can only be
# updated through the signing ceremony, so drift is expected between a
# manifest landing and the next signed release.
- name: Catalog drift (advisory)
continue-on-error: true
run: python3 scripts/check-app-catalog-drift.py --catalog releases/app-catalog.json --release
+1 -1
View File
@@ -5,7 +5,7 @@ name: Demo images
# code (see demo-deploy/ and docs/demo-deployment-design.md).
#
# Required repo configuration:
# vars.DEMO_REGISTRY e.g. source.archipelago-foundation.org/lfg2025
# vars.DEMO_REGISTRY e.g. 146.59.87.168:3000/lfg2025
# vars.DEMO_REGISTRY_HOST registry host for docker login (no org suffix)
# secrets.DEMO_REGISTRY_USER
# secrets.DEMO_REGISTRY_TOKEN
-73
View File
@@ -19,9 +19,6 @@ dist-ssr/
build/
*.local
# Vite build cache
neode-ui/.vite/
# IDE / editor
.idea/
.vscode/
@@ -62,13 +59,6 @@ coverage/
releases/**
!releases/
!releases/manifest.json
# The signed app catalog and the registry trust floor are source, not build
# output: nodes fetch the catalog from this path on main, and the floor is what
# scripts/check-catalog-registry-trust.py checks it against. Both were being
# swallowed by the rule above — app-catalog.json only stayed tracked because it
# predates it.
!releases/app-catalog.json
!releases/registry-trust-floor.json
# Image recipe output
image-recipe/output/
@@ -92,23 +82,6 @@ scripts/resilience/reports/
.codex-tmp/
.claude/
.pnpm-store/
# Key material and local databases — belt-and-braces so a stray key or a
# copied node database can never be committed. Open-source readiness plan,
# Phase 1 item 5: `.claude/settings.local.json` was previously only caught by
# a machine-global ignore rule, which protects one machine and no contributor.
*.key
*.pem
id_rsa*
*.sqlite
*.sqlite3
*.db
# ...except the throwaway TLS fixtures the appgate tests compile in via
# include_bytes!. They are documented non-identity material (see that
# directory's README) and are already tracked; the negation stops the rule
# above from silently dropping them if they are ever regenerated.
!core/archipelago/src/appgate/testdata/*.key
**/__pycache__/
*.bak
@@ -116,49 +89,3 @@ id_rsa*
# app/docs asset path with a descriptive filename.
Screenshot *.png
uploads/
# ── Local-only material ─────────────────────────────────────────────────────
# Present on disk, never tracked: everything describing Archipelago's own
# infrastructure or internal development process. The repo is source code and
# guidelines only. Inventory: .local-only/manifest.txt — wipe: .local-only/wipe.sh
/.local-only/
/.planning/
/loop/
/docs/operations-runbook.md
/docs/hotfix-process.md
/docs/PRODUCTION-MASTER-PLAN.md
/docs/UNIFIED-TASK-TRACKER.md
/docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md
/docs/HANDOFF-2026-07-20-fips-peer-files.md
/docs/HANDOFF-2026-07-23-companion-apk-deploy.md
/docs/qr-scanner-snappiness-handover.md
/docs/RETICULUM-TRANSPORT-PROGRESS.md
/docs/combined-test-plan-2026-07-22.md
/docs/pine-voice-release-test-plan.md
/docs/OPEN-SOURCE-READINESS-PLAN.md
/docs/archive/HANDOVER-2026-07-02-iso-feedback.md
/docs/archive/SESSION-1.8.0-OTA-PROGRESS.md
/docs/security/KEY-02-FLEET-ROTATION.md
/docs/security/KEY-03-SIGNING-POSTURE.md
/tests/production-quality/TRACKER.md
/scripts/deploy-config-defaults.sh
/scripts/deploy-tailscale.sh
/scripts/deploy-to-target.sh
/scripts/setup-target-dev.sh
/scripts/setup-aiui-server.sh
/scripts/setup-https-dev.sh
/scripts/debug-frontend.sh
/scripts/node-profile.sh
/scripts/fleet-fips-pair.sh
/scripts/fleet-fips-unpair.sh
/image-recipe/sync-from-live.sh
/docs/security/PHASE-10-VERIFICATION-GUIDE.md
/docs/security/KEY-01-ON-NODE-VERIFICATION.md
/docs/security/KEY-02-ROOTFS-EVIDENCE.md
/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md
/image-recipe/INTEGRATION-GUIDE.md
/docs/multinode-testing-plan.md
/docs/bitcoin-version-bulletproof-rollout.md
# Generated PWA dev output (vite-plugin-pwa) — never a source artifact
neode-ui/dev-dist/
+3
View File
@@ -0,0 +1,3 @@
[submodule "indeedhub"]
path = indeedhub
url = http://146.59.87.168:3000/lfg2025/indeehub.git
+1 -1
View File
@@ -92,7 +92,7 @@ built and signed:
```bash
SERVED=neode-ui/public/packages/archipelago-companion.apk
GITEA_URL=https://source.archipelago-foundation.org/lfg2025/archy/raw/branch/main/$SERVED
GITEA_URL=http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/$SERVED
QR_URL=http://146.59.87.168:2100/packages/archipelago-companion.apk
curl -sS -o /tmp/live-gitea.apk "$GITEA_URL"
curl -sS -o /tmp/live-qr.apk "$QR_URL"
@@ -150,7 +150,7 @@ class ArchyVpnService : VpnService() {
* Pre-warm + keep-warm mesh sessions to every known node ULA.
*
* Discovery + first session through the public tree can take 15s+
* (from node diagnosis) — paying that cost here, the
* (HANDOFF-2026-07-23 node diagnosis) — paying that cost here, the
* moment the tunnel is up, means the connect probe and WebView hit an
* established session instead of timing out on a cold one. The periodic
* touch afterwards keeps the session from idling out. Failed connects
@@ -331,7 +331,7 @@ class FipsPreferences(private val context: Context) {
/**
* Peer aliases feed the fips host map as `<alias>.fips` hostnames; anything
* that isn't a valid DNS label ("Test Node" — the space) gets rejected and
* that isn't a valid DNS label ("Framework PT" — the space) gets rejected and
* silently drops the peer from name resolution. Slug it instead of losing it.
*/
internal fun hostSafeAlias(alias: String): String =
@@ -330,7 +330,7 @@ object FlareServer {
/** Outbound flares: plain HTTP to the peer's ULA — FIPS encrypts underneath. */
object FlareClient {
// Connect timeout must outlive cold mesh-session establishment (~15s via
// the public tree); the attempt itself drives
// the public tree per HANDOFF-2026-07-23); the attempt itself drives
// session setup, same trick as the VPN service's session warmer.
private val http = OkHttpClient.Builder()
.connectTimeout(25, TimeUnit.SECONDS)
@@ -187,7 +187,7 @@ fun ServerConnectScreen(
port = "",
)
// Mesh discovery + first session can take 15s+ through the
// public tree (per node diagnosis), and on a
// public tree (HANDOFF-2026-07-23 node diagnosis), and on a
// first-ever pairing the VPN consent dialog is on screen at
// the same time — so probe patiently inside a 60s budget with
// per-attempt timeouts wide enough to ride out TCP
Binary file not shown.
+2 -2
View File
@@ -258,8 +258,8 @@ mod tests {
"npub": "npub1abc",
"alias": "My Archipelago",
"addresses": [
{"transport": "udp", "addr": "192.0.2.10:2121", "priority": 10},
{"transport": "tcp", "addr": "192.0.2.10:8443", "priority": 20}
{"transport": "udp", "addr": "192.168.1.228:2121", "priority": 10},
{"transport": "tcp", "addr": "192.168.1.228:8443", "priority": 20}
]
}]"#,
)
+20 -134
View File
@@ -1,126 +1,12 @@
# Changelog
## Unreleased
## v1.7.117-alpha (2026-07-27)
- **You can now replace your Lightning connection keys from Settings, without touching a terminal.** The tokens wallet apps like Zeus use to reach your node are bearer keys: anything that has ever seen one can spend from your node until they are replaced, and there is no way to cancel one individually. Replacing them was previously a script you had to SSH in and run, which in practice meant it never happened. Settings → Lightning credentials now shows when yours were issued, which node they belong to and how many channels must survive, then does the whole job behind your node password — with a step-by-step progress list, and a refusal to call it a success unless it has confirmed your node identity and every channel came back. Your coins and channels are not touched: nothing is closed, and the wallet is never re-created. Afterwards you re-pair Zeus by scanning the Lightning app's QR code again.
- **Replacing those keys no longer silently breaks BTCPay Server.** BTCPay holds its own copy of the key, and that copy cannot repair itself — so a node that replaced its keys ended up with BTCPay running, healthy, and unable to take a single Lightning payment, with nothing anywhere saying why. The dashboard now updates BTCPay's copy as part of the run and restarts it around its existing data, and the Settings screen warns you if it finds a node already stuck in that state. The command-line script fixes the same gap.
- **Lightning stops getting stuck locked on a busy node.** Lightning opens its databases before it will accept the password that unlocks the wallet, and on a loaded node that took nearly three minutes — longer than the node was willing to wait. Giving up restarted Lightning, which started the slow open again, so the wallet stayed locked forever and everything depending on it stayed broken. The node now waits as long as it takes. A genuinely wrong password still fails immediately.
## v1.7.126-alpha (2026-08-07)
- **The most important fix in this release: the update button could take you backwards onto a version withdrawn for a security hole.** BTCPay Server published 2.4.2 to close a flaw that was being actively exploited — a way past two-factor authentication. Nodes that had already moved to 2.4.2 were then shown an "Update" button offering 2.3.9, the very release being withdrawn, and taking it would have rolled the node back onto the vulnerable version. The cause was that the node only asked whether the two version numbers differed, never which was newer, so any stale record anywhere could present a rollback as an upgrade. It now refuses to offer a lower version as an update, so a stale record fails safe instead of becoming a trap. BTCPay itself is on 2.4.2, and every place that still named the old version — including the fallback installer, which would have installed it outright — has been corrected.
- **An app now reports its own version, not a helper's.** Where an app is made of several parts, the node could read the version of the wrong part: BTCPay showed as "15.17", which is the version of its database, while offering an update to 2.4.2. That is the number update decisions are made from, so a nonsensical pair was being presented as a legitimate upgrade. When the node cannot identify an app's own container it now says so rather than guessing at a neighbour.
- **Your node issues its own certificate, so apps stop being flagged as insecure.** Each node now has its own certificate authority, with a one-step install from Settings, and app screens are served over the same secure connection as the dashboard rather than dropping back to an unprotected one. Apps answer on both the secure and plain address on the same port, so nothing that worked before stops working.
- **An app that is still starting says "starting".** It previously reported "App not reachable", which reads as a failure when the app is simply warming up.
- **Updates and app downloads now come from a proper domain name.** They previously used a bare numeric address over an unprotected connection. Downloads are now encrypted in transit, and the old address is kept as an automatic fallback for nodes whose clock or name lookup is off — the signature, not the address, is what makes either source safe.
- Also in this release: the tool app developers run to check their app description no longer rejects every valid file (it needed a program most machines do not have, and reported the missing program as a broken file); and the node's own security audit, which had been reporting all-clear, now actually inspects the files where credentials had been sitting.
- Housekeeping, disclosed rather than buried: this release removes Archipelago's own infrastructure details from the published source — machine names, addresses and internal working notes — ahead of the code being opened to the public. No behaviour changes for your node.
- Known gaps, unchanged from the last release: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release.
## v1.7.125-alpha (2026-08-06)
- **The Lightning, Bitcoin, Electrum and mesh screens work again behind the login gate.** Since the gate went up, those screens loaded their frame and then showed every number as unreachable. The gate was deliberately hiding your login from the apps it protects — right for third-party apps, wrong for the node's own screens, which need that login to fetch your data. The gate now removes only its own credential, and the node's own screens explicitly receive yours. The same mistake was also quietly signing you out of apps with their own logins — Vaultwarden, Nextcloud, Gitea — on every single request; that stops too.
- **IndeeHub heals itself.** Three separate faults fixed: its database helper was recreated with permissions too tight to read its own files (it had crashed and restarted roughly ten thousand times on one node); on another node two of its seven parts could never be recreated at all because of how the node asked for their storage — it would remove the old part and then fail to build its replacement, leaving the app half-missing forever; and a regenerated password could lock the app out of a database that keeps the original. The storage fault fixes the same trap for every future multi-part app.
- **A missing piece of a running app now gets put back automatically.** If one container of a multi-part app disappears while its siblings are still running, the node treats that as a hole to repair rather than a choice to respect, and rebuilds the missing piece. An app you actually uninstalled stays uninstalled.
- **Send and Receive open clean every time.** Whatever you typed last — an address, an amount, and above all an armed "send all funds" toggle — no longer quietly carries over into the next payment. Choosing "send all funds" also shows the amount being swept instead of a confusing 0.
- **A sweep that cannot happen now says why.** Trying to sweep a balance that is below Bitcoin's dust minimum (about 546 sats) or not yet confirmed used to fail with "check server logs"; it now explains that no transaction can be built from those coins.
- **The camera scanner option no longer vanishes on desktop.** Browsers only allow the live camera on secure (HTTPS) pages, and the scan window silently hid the camera choice on plain connections — which read as "the scanner is gone". The option now stays visible and explains itself, and the photo and paste routes always work. The companion app's built-in scanner is untouched.
- **App data folders can no longer be "repaired" into a state the app cannot use.** When the node fixed a folder's ownership through its fallback path, it wrote the container's raw user number instead of the translated one, so the fix reported success while the app still could not open its own files — one node's BotFights restarted every ten seconds over exactly this. The translation is now applied.
- Also: the app login page uses the Archipelago mark and stays centred on phones with the keyboard open, app icons in the install window are no longer cropped, and when the node fails to build a container it now records the actual reason instead of a one-line stub that hid the cause of the IndeeHub fault for days.
- Known gaps, disclosed rather than buried: three voice-assistant ports remain open without authentication. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release.
## v1.7.124-alpha (2026-08-05)
- **The most important fix in this release: some nodes were left switched off by their own update, and could not switch themselves back on.** The node replaces its program and then exits, expecting the system to start it again — but nodes installed from older images carried a setting that only restarts the program if it *crashes*. A clean, deliberate exit looked like success, so nothing restarted it, and the node sat dead showing "server starting" with nothing able to start it. One of ours was down for over two hours this way, and three of four checked had the same setting waiting to bite. Your node now repairs that setting itself the first time it starts, so it survives every future update.
- **Portainer opens again.** Its screen reported the app as not responding because the app was quietly refusing to start: nodes have been running Portainer 2.39.1, their stored data was written by that version, and the app list pinned a version from two years earlier — so when the container was rebuilt it landed on the old one, which will not read newer data. The correct version is now pinned, older installs upgrade cleanly, and no data was touched.
- **Bitcoin starts reliably again.** A leftover settings file in the Bitcoin folder — one the node itself kept rewriting and Bitcoin no longer reads — is treated as fatal by Bitcoin, so affected nodes restarted every few seconds forever. The node no longer writes that file, removes stale copies, and treats any that remain as harmless.
- **Every app screen opens from My Apps again.** The login gate refused to be displayed inside another page at all, which is exactly how My Apps opens an app, so protected apps appeared broken. It now allows only your own node to display it, and refuses everyone else — a distinction the old setting could not express.
- **The app login screen now looks like the node's own.** Same rotating artwork, the same panel, the Archipelago mark, and the app's real icon shown as a tile the way My Apps shows it, instead of a plain box with a letter.
- **The Mesh screen uses wide displays properly.** On very large screens it stacked all five panels on top of each other, clipping three of the headings to a sliver and squeezing the map into a letterbox — more screen producing a worse view. It now shows one panel at a time, filling the space, with the map running edge to edge.
- **You can choose how long you stay signed in.** Settings → Account now offers an inactivity timeout and a hard limit, plus an option to re-enter your password before sending funds. TV and kiosk screens are never signed out for sitting idle, because there is nobody there to sign them back in.
- Updates now come from `source.archipelago-foundation.org` rather than a bare address, with the old one kept as an automatic fallback for nodes whose clock or name lookup is off. Also included: clearer wallet errors from ecash mints, and mesh peers reconnecting via their last known address before falling back to the wider network.
- Known gaps, disclosed rather than buried: three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release.
## v1.7.123-alpha (2026-08-05)
- **Five more screens on your node were readable by anyone who could reach it, and the previous release's own check said they were fine.** The Bitcoin, Lightning, Electrum, FIPS mesh and Fedimint Guardian screens each answered on their port with no login. They were missed because they work differently from ordinary apps: they run directly on the node's network rather than behind its container plumbing, so there was no address to pin and their descriptions listed no port at all — and the node builds its list of what to protect from exactly those descriptions. It therefore neither protected them nor listed them as unprotected. A check that reports success while five screens are open is worse than no check, and this was found by scanning the node from another machine rather than asking the node about itself.
- **What was actually readable was the page, not your money.** Every request on those ports that could have returned a credential — the Lightning connection details, the wallet passthrough, container logs, and every node command — already required a login and still refused without one. The Lightning macaroon fix from v1.7.120 was verified directly rather than assumed. What leaked was the screen itself: layout and code, no wallet data, no keys.
- All five now serve only to the node itself, with the login gate in front of them, exactly like the twenty app screens closed in the previous release.
- **Every port on the node now has a stated policy — there are no undecided ones left.** Eleven ports previously had no instruction either way and stayed open by default. The BotFights arena, the router screen and the Pine voice screen now require the node password. The ones that genuinely cannot take a login page stay open with a written reason: Fedimint's guardian and gateway connections (federation members authenticate to the federation), NetBird's management and dashboard ports (your VPN devices carry their own credentials and cannot hold a browser session, and its dashboard needs its own certificate), Pine's secure listener, and the Lightning REST port, which wallets reach with a macaroon exactly as before.
- Fresh installs are covered too, not just existing nodes. The five screens are delivered as prebuilt images, so a newly flashed node would have come up open even after this fix. All five were rebuilt, published, and then pulled back and inspected to confirm the fix is really inside them.
- Two delivery faults fixed alongside, either of which would have silently undone the above: two of the five screens were reaching nodes through no update path at all, so edits to them never arrived; and a fourth copy of the Bitcoin screen's configuration was being rewritten on every health check, which would have re-opened that port after everything else was corrected.
- Known gaps, disclosed rather than buried: non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — meet the login page and need an access token. Three voice-assistant ports remain open without authentication; the correct fix puts them on a private network with the assistant. The 5x real-node lifecycle gate was not run for this release.
## v1.7.122-alpha (2026-08-04)
- **Your apps now ask for your node password before they open — over your home network, Tailscale, the mesh and Tor alike.** Until now anyone who could reach your node could open Immich, Nextcloud, Vaultwarden, Jellyfin, Grafana and the rest simply by typing the address and port, with no login at all. Twenty app screens now sit behind the same login you use for the node, showing you which app you are opening, and honouring two-factor if you have it switched on. Logging in at an app address logs you into the dashboard too, so it is one password, not one per app. This completes the groundwork disclosed in v1.7.121.
- **The things that must stay open stayed open.** Zeus and other remote wallets still reach your Lightning node directly, Electrum wallets still connect, and Bitcoin still talks to its peers — those connections carry their own proof of identity and a login page would simply break them. Every one of these seventeen exceptions now has to state in writing why it is safe to leave open, so the list is something you can read rather than something you have to discover.
- **A private address on your node was answering the mesh without a password.** One app's port was marked as being for this machine only, and the part of the node that carries mesh traffic did not know that — it forwarded requests from the whole mesh straight to it. Found while verifying the work above on a real node, not in testing. That path now refuses anything marked machine-only, and the app is reachable only from the node itself, as intended.
- **Tor addresses no longer skip the login.** An app published as a .onion address was handed straight to the app, because a Tor visitor carries no session cookie to check. The login gate now takes those addresses first, closing the last of the four routes that went around it.
- Nodes fix themselves after this update. Apps installed before this system used its current container setup kept their old wide-open address even after the signed list told them to move, and each would otherwise have needed hand-holding on every node. Your node now notices the difference and rebuilds those apps itself, keeping their data, within about half a minute of starting. Verified by putting a node back into the old state deliberately and watching it repair.
- The node had been reading two different sets of instructions about its own apps — the signed list it downloads, and older copies on disk — which is how a port meant to stay private was briefly opened on a test node. Both now come from the signed list, and a port withdrawn from the login gate is released without needing a restart.
- **The key that signs these updates has been replaced.** The previous signing key was exposed where it should not have been, so it is treated as compromised and this release installs its replacement. This update is the last one signed with the old key, by necessity — it is the one that teaches your node the new one.
- Known gaps, disclosed rather than buried: eleven app ports still have no stated policy — BotFights, the Fedimint gateway, NetBird, the voice assistant's own screens and the router screen — and remain reachable without a login until each is decided deliberately; the node reports them rather than guessing, because guessing at an unstated setting caused both incidents behind this work. Three voice-assistant ports are still open without authentication; the correct fix puts them on a private network with the assistant. Non-browser clients — phone apps for Vaultwarden, Home Assistant or Jellyfin, and git over the web — will now meet the login page and need an access token. The 5x real-node lifecycle gate was not run for this release.
## 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.
- The Bitcoin and Lightning app screens can no longer be published as public Tor addresses automatically. They were one app-id away from being handed a worldwide, permanent address as a silent side effect of being installed — which would have re-opened the hole above to the entire internet. Turning Tor on for them deliberately still works; it just never happens on its own.
- **Fixes shipped inside the program now actually reach apps that your system keeps running.** A container the node had been told to uninstall, but that the system service manager kept alive anyway, was quietly skipped by the part of the node that applies configuration — so it never received updates that shipped with the program. This was found the hard way: the Bitcoin control-interface fix above appeared to be installed and silently was not, while the Lightning half applied correctly, which is the most misleading way for a security fix to fail. Both halves are now proven to land on a real node.
- The Lightning and Bitcoin node screens have been rebuilt to match what umbrelOS offers. Lightning gains Overview, Channels, Activity, Insights, Connect and Settings tabs with a sats/BTC switch; Bitcoin gains Insights, Peers, Connect and Sharing. Along the way: every copy button on those screens silently did nothing (the browser blocks clipboard access inside an embedded page) and now works; the channels link led to a dead page; and Node ID showed a bare key instead of the full address someone can actually connect to.
- Updates to the Bitcoin screen show up without a hard refresh. The page was being cached by the browser, so a freshly updated screen kept rendering the previous one.
- The AI sidebar loads again. It was asking for its program files at an address that pointed at the main app's files, where they do not exist, so it silently loaded nothing.
- The navigation above the bottom bar no longer follows you between screens. Back buttons and the mesh tab bar stayed pinned over every other page once you had visited the screen that owns them. Keeping tabs loaded in the background — the change that made switching between them instant — means leaving a screen hides it rather than destroying it, and this floating navigation sits outside the screen it belongs to, so it was never being hidden with it. It is now tied to whether its own screen is on display. The speed is unchanged: the screens are still kept loaded, so returning to one is still instant.
- Wallet: Lightning actions are now offered based on whether you actually have a usable channel rather than just a running node, sending is gated the same way, and an invoice you cannot yet receive offers to install a Lightning node instead of simply failing.
- Onboarding and viewing fixes: the "I have written down my recovery words" tickbox is findable on short screens, paid pictures and videos open in the app's own viewer with a visible loading state instead of a blank browser tab, picture-in-picture survives changing tabs, and the FIPS/Tor labels on peer cards stay put instead of wrapping into the card below.
- Key-material hardening across the node: a node that is already set up refuses to have its identity replaced by an unauthenticated request; first-boot secret generation now fails loudly instead of silently continuing with shared keys; the node proves its TLS certificate and key are actually a matching pair; the Bitcoin Core wallet path that kept a second copy of your spending key outside the encrypted store has been removed; and every place the node generates a key, token or nonce now names its source of randomness explicitly, enforced at build time.
- Federation and mesh: a rotated gateway credential now reaches the already-running container instead of leaving the old one in place, sync failures are surfaced to you instead of being swallowed, and nodes can share their Lightning connection details with a chosen peer over the mesh — the groundwork for opening channels with nodes you already talk to.
- Known gaps, disclosed rather than buried: the 5x real-node lifecycle gate was not run for this release. Two nodes on the fleet still share SSH host keys with each other (detection shipped, rotation is a deliberate operator decision and has not been performed). Bitcoin Core can now reach Tor from its container, but is not yet routed through it — the network mode is becoming a setting you choose, and until then Core's peers remain on the clear internet.
## v1.7.119-alpha (2026-07-31)
- Wallet payments now work on nodes whose channels are private/unannounced. Every invoice-creation call site — the wallet's own Receive flow, and the seller-side paid-content/peer-files flow — only ever sent LND the amount and memo, so LND defaulted private to false and returned invoices with no route hints. Any node whose only usable channel is private or unannounced (the common shape for a channel someone opened to you) was silently unpayable through the wallet, and unpayable through paid file/content sales too. Both call sites now set LND's private flag correctly; this was broken in the field and is the main reason for this release.
- Tor and the mesh's Tor fallback are reliable again. The node's background "doctor" health-checker was fighting Tor over the permission bits on its own hidden-service directory: it compared the directory's mode against the literal string "700", but Tor's own setgid hidden-service mode is 2700 — a value the doctor's check never recognized as correct. Every ~5 minutes it "corrected" the mode back to 700 and restarted Tor to apply it, and Tor immediately reasserted 2700 — a permanent restart loop that meant Tor could never hold onto its consensus/HSDir cache long enough to be useful, breaking the mesh's Tor fallback path entirely. The check now compares only the owner/group/other bits that actually matter (both 700 and 2700 pass; genuinely wrong modes like 750 or 2755 are still corrected and restart Tor), plus a 30-minute restart backoff so no future condition can reproduce the storm.
- Wallet balances and your node's own FIPS identity key (npub) are no longer written to the browser's sessionStorage — caught by an audit of the page-caching work below. Every cache call site in the app now makes an explicit, reviewed decision about whether its data is allowed to persist across a reload, and a one-time migration purges any legacy, unaudited snapshot left behind by an older build.
- Server, Home, Mesh, Chat/AI chat, and the secondary screens (app details, marketplace, cloud, federation, monitoring, router/OpenWrt) now load instantly from cache when you revisit them and refresh quietly in the background, instead of blanking and re-fetching everything on every tab switch — this closes out the page-performance work started back in v1.7.116/117.
- App updates (including this one) now apply automatically in the background instead of waiting on a tap-to-update prompt, matching how kiosk/TV installs already behaved — the reload still waits for any in-progress splash/dashboard animation to finish first, so it won't land mid-motion. This was a direct, explicit decision made with the mid-payment-reload risk spelled out in advance; reverting to a confirmation prompt for beta is a one-line change if wanted later.
- Known gap, disclosed rather than buried: the project's 5x production lifecycle gate (install/UI/stop/start/restart/reinstall/reboot-survive/archipelago-restart-survive/uninstall, run on a real node — CLAUDE.md's own definition of done before a release tag) was NOT run for this release, because its target node was unreachable and running it here would have required rebooting a shared, live build machine out from under other active work. This release's own automated gates (release-gate harness, strict catalog-drift check, the full cargo test suite, a mount-level ISO smoke test, and a headless QEMU boot test) all still ran and passed — this is specifically about the separate 5x real-node lifecycle gate, which is still outstanding and should be run as soon as the node is reachable again.
## v1.7.118-alpha (2026-07-29)
- Fixes mesh radios dropping off on nodes that took the v1.7.117 update. Updates only ever replaced the main program, never the packaged radio helpers — so updated nodes were left running an older radio daemon that didn't understand a new option and quietly gave up, showing "device not connected" with a Connect button that did nothing. The node now checks what its radio daemon supports before using new options, and updates finally carry the radio helpers themselves, so every node gets current radio support with the update instead of only from a fresh install.
- The in-app "Flash LoRa" flow works on updated nodes. The RNode flashing tool was only ever included on freshly installed nodes; everywhere else flashing failed with a cryptic "No such file or directory". The tool now ships with updates and is included on new install images, and if it's somehow still missing the error says exactly what to do instead.
- Message notification badges finally remember what you've read. Unread counts were only kept in memory, so every visit re-counted old messages as new — including a phantom badge for chats with nothing new in them. Read-state is now saved on the device, opening a chat marks all its linked conversations read, and history no longer re-badges after a reload.
- Every mesh message now has a visible "⋯" button that opens the route view: watch the path your message took animate — sender and receiver appear, a pulse travels the link, and each relay hop lights up in order — with signal quality for radio links and delivery status. (Tapping the transport pill still works too.)
## v1.7.117-alpha (2026-07-29)
- Flash your LoRa radio from inside the app. The Mesh page now has a "Flash LoRa" button that opens a guided flow: pick the firmware family (MeshCore, Meshtastic, or Reticulum RNode) and your board, and the node downloads the latest release and flashes it with live progress — no external flasher website, no cables to a computer. The same flow appears when a freshly plugged-in radio is detected, and a long list of flashing pitfalls was fixed along the way: radios no longer boot-loop after a flash, failures show the real error instead of silently bouncing back, wedged flash jobs can't get stuck forever, and board auto-detection no longer misidentifies Heltec boards.
- Every Archipelago node now acts as a Reticulum relay. Nodes forward mesh traffic and re-broadcast peer announcements, so two radios that can't hear each other directly can still discover and message each other through any Archipelago node in between — your nodes become infrastructure for the whole neighbourhood mesh, including non-Archipelago apps like Sideband.
- Reticulum (RNode) radios are now first-class mesh citizens. Radios are reliably detected on node startup (a boot-timing race used to leave them unclaimed), settings changes apply live without a restart, your node's name propagates over the Reticulum network so other apps like Sideband see it properly, and a crashed Reticulum daemon is detected and restarted automatically. Photo and file attachments sent over Reticulum now actually arrive — four separate delivery bugs were found and fixed, verified end-to-end over real radio hardware.
- Messages to contacts that exist on both the internet mesh and a LoRa radio now prefer the radio when it's live, and attachments follow the same path — so co-located nodes talk over the air even when the internet path exists.
- Mesh chat polish: each message in the image viewer shows which transport carried it, a new hop-route view shows the path a message took, reactions moved into a tidy dropdown, and read-tracking now reflects what you've actually seen. The Refresh and Broadcast buttons give real feedback, and the radio-setup modal shows honest probe progress instead of freezing.
- The wallet transactions list works properly on phones now: it scrolls (it silently couldn't on touch screens before), and the All / On-chain / Lightning / Ecash filter tabs stay pinned at the top with a subtle blur while the list scrolls underneath.
- Backend services no longer masquerade as launchable apps. Anything without a real web interface — databases, APIs, background workers, including stacks you deploy by hand for testing — now files under Services with no Launch button. Apps declare their interface in their manifest; for everything else the node checks the port itself to see whether a browser page actually lives there.
- Lightning payments that take a while (slow multi-hop routes) are no longer reported as failed while they're still in flight. The wallet now waits properly, shows an honest "pending" state, and reports the true final outcome.
- Server pages feel instant: Server, Federation, Lightning channels, Monitoring, wallet, Cloud, and Credentials screens now render immediately from a shared cache and refresh live in the background (including push updates over the node's websocket), instead of blanking while every panel refetches.
- The node stays responsive under heavy load: the connection handler now sheds excess load instead of stalling everything behind it, and companion-app probes no longer trigger container builds during routine checks.
- FIPS mesh uptime hardening continues: the node's peer port is opened explicitly everywhere, LAN anchors use the right port, direct peering between co-located nodes works again, dials fail fast instead of hanging, and a connectivity watcher re-applies anchors immediately when the network comes back.
- FIPS startup is more reliable on nodes that have the packaged `fips.service` instead of Archipelago's `archipelago-fips.service`. Startup self-heal, onboarding, dashboard Start, and reconnect now use the systemd unit the node actually has, so FIPS no longer looks like it needs to be installed when it only needs to be started.
- App screens over the FIPS mesh now bind their relay only to the node's FIPS address instead of reserving the same host ports Podman needs. This keeps apps such as FileBrowser and Botfights from restart-looping because the backend was already holding their published ports.
- Companion app 0.5.25: a redesigned settings hub (three-finger tap opens it over the dashboard), seamless transport handoff with FIPS mesh settings, the wallet scanner reads dense invoice QR codes, app webviews clear the phone status bar with an HTTPS toggle on add/edit, and off-LAN loads fall back to the mesh URL instead of a dead LAN address.
- Companion WebView safe-area handling now also moves fixed and sticky top bars below the phone status bar, including headers mounted after page load by single-page apps.
- Public-source preparation now includes a Nostr Git hosting plan using `ngit`, NIP-34, and GRASP: anyone can clone, fork, review, and propose changes from their Archipelago node, while canonical merge authority stays with a small signed maintainer set in the style of Bitcoin Core.
- Node OS release notes for v1.7.117 are still open; add any installer, service, kernel, firewall, or package changes here before cutting the release.
## v1.7.116-alpha (2026-07-27)
@@ -466,14 +352,14 @@
- Saleor storefront proxying now forwards `X-Forwarded-Host`, fixing Next.js Server Actions requests that compared the browser origin with the internal `storefront-app:3000` upstream host.
- Saleor storefront media now routes `/thumbnail/` and `/media/` through the same `9011` proxy to the Saleor API, fixing product image optimizer failures caused by `localhost:8000` media URLs.
- The Saleor storefront container receives an explicit internal media origin so rewritten media URLs resolve inside the Podman network without exposing private API ports to browsers.
- Validation passed with `cargo fmt --all --check --manifest-path core/Cargo.toml`, `cargo check -p archipelago --manifest-path core/Cargo.toml`, and live checks on the staging node for storefront HTML, static assets, GraphQL, media redirects, and optimized product images.
- Validation passed with `cargo fmt --all --check --manifest-path core/Cargo.toml`, `cargo check -p archipelago --manifest-path core/Cargo.toml`, and live checks on `100.114.134.21` for storefront HTML, static assets, GraphQL, media redirects, and optimized product images.
## v1.7.81-alpha (2026-05-21)
- Saleor storefront installs now use the prebuilt registry image instead of building the Next.js app on-device, avoiding Podman build failures during stack installation.
- Existing Saleor stacks are repaired on adoption by recreating missing storefront containers, forcing the storefront app to bind `0.0.0.0:3000`, and resolving nginx upstreams dynamically after container restarts.
- The shipped Saleor storefront image now includes public assets and omits Vercel-only Speed Insights injection, fixing broken static asset responses and the local `/_vercel/speed-insights/script.js` browser warning.
- Validation passed with `cargo fmt --all --check --manifest-path core/Cargo.toml`, `cargo check -p archipelago --manifest-path core/Cargo.toml`, and live checks on the staging node for `9011` storefront, static assets, and proxied GraphQL.
- Validation passed with `cargo fmt --all --check --manifest-path core/Cargo.toml`, `cargo check -p archipelago --manifest-path core/Cargo.toml`, and live checks on `100.114.134.21` for `9011` storefront, static assets, and proxied GraphQL.
## v1.7.80-alpha (2026-05-21)
@@ -504,7 +390,7 @@
- Saleor installs now create or repair the `admin@example.com` staff account idempotently after sample data loads, use the correct dashboard mount path, and re-check stack containers after startup so stopped containers are caught.
- NetBird embedded login now uses the upstream-compatible IdP signing-key behavior and sends ID tokens from the dashboard to the management API, fixing the post-signup `Unauthenticated` state while preserving the unified local proxy/logout routes.
- Transient unnamed Podman helper containers created during app install tasks are hidden from My Apps, so generated names like `eager_keldysh` no longer appear as user applications.
- Validation passed with catalog/release JSON checks, `npm run type-check`, and `cargo fmt --all --check --manifest-path core/Cargo.toml`; live checks on the staging node confirmed Saleor dashboard/API availability, generated Saleor admin login, NetBird OAuth availability, and NetBird logout redirects.
- Validation passed with catalog/release JSON checks, `npm run type-check`, and `cargo fmt --all --check --manifest-path core/Cargo.toml`; live checks on `100.114.134.21` confirmed Saleor dashboard/API availability, generated Saleor admin login, NetBird OAuth availability, and NetBird logout redirects.
## v1.7.76-alpha (2026-05-20)
@@ -513,7 +399,7 @@
- NetBird's browser proxy now sends API, OAuth, relay, WebSocket, and management traffic through the stable host-published server port at `169.254.1.2:8086`, avoiding stale rootless Podman DNS/IPs after `netbird-server` restarts.
- Mobile App Store category chips now stay visible above the tab bar, Discover is available on mobile, and category selection updates the page route/query so the selected category is actually shown.
- Apps that require a real browser tab now open directly from the app icon tap instead of first entering an in-shell app-session route, including BTCPay, Grafana, Home Assistant, Vaultwarden, Nextcloud, Portainer, OnlyOffice, Tailscale, Uptime Kuma, Gitea, and Nginx Proxy Manager.
- Validation passed with catalog JSON checks, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`; live checks on a fleet node confirmed Saleor dashboard `9010`/API `8000` and NetBird API/OAuth routes survive `netbird-server` restart.
- Validation passed with catalog JSON checks, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`; live checks on `100.70.96.88` confirmed Saleor dashboard `9010`/API `8000` and NetBird API/OAuth routes survive `netbird-server` restart.
## v1.7.75-alpha (2026-05-19)
@@ -535,7 +421,7 @@
- Mobile app launches for iframe-blocked apps now open the direct app URL in a new browser tab immediately instead of landing in a broken in-shell webview that requires a second tap.
- Mobile My Apps/Websites tabs now react to route query changes, App Store pages label the mobile view as Discover, mobile filters have safe bottom spacing, and App Store search ignores the current category so searches cover all available apps.
- My Apps search now surfaces matching App Store entries when the app is not installed, making it possible to jump directly from a failed My Apps search to the installable app details.
- NetBird self-host installs now prefer a `100.x` tailnet/CGNAT address for dashboard, management, relay, STUN, and auth redirect origins when one is present; live repair on a fleet node updated the existing stack from LAN origins to its tailnet address and restored `netbird-server`.
- NetBird self-host installs now prefer a `100.x` tailnet/CGNAT address for dashboard, management, relay, STUN, and auth redirect origins when one is present; live repair on `100.89.209.89` updated the existing stack from LAN origins to `100.89.209.89` and restored `netbird-server`.
- App-session iframe frames now focus automatically and wrap the iframe in a scroll host so wheel/touch scrolling works in the active right frame without requiring an initial click.
## v1.7.72-alpha (2026-05-19)
@@ -546,7 +432,7 @@
## v1.7.71-alpha (2026-05-19)
- NetBird stack installs now pre-create `/var/lib/archipelago/netbird/data` before binding it into `netbird-server`, fixing the failed install/start path seen on a fleet node where Podman rejected the missing host directory.
- NetBird stack installs now pre-create `/var/lib/archipelago/netbird/data` before binding it into `netbird-server`, fixing the failed install/start path seen on `100.70.96.88` where Podman rejected the missing host directory.
- NetBird start/restart ordering now starts `netbird-server` before the dashboard container so lifecycle actions bring the control plane up before the UI.
- App-session invalid IDs and panel-mode fallbacks now return to `/dashboard/apps`, avoiding the stale `/apps` route that could render a 404.
- Mobile launches for apps that block iframes now stay inside the Archipelago app-session fallback instead of automatically opening an external browser tab.
@@ -564,7 +450,7 @@
## v1.7.69-alpha (2026-05-19)
- App installs now allow up to 10 minutes for the initial `package.install` RPC to return, matching slow container image pulls and preventing apps from disappearing from My Apps while the backend is still pulling or retrying mirrors.
- Live diagnostics on a fleet node confirmed the Gitea install did not fail; the primary registry pull timed out after 300 seconds, the fallback mirror succeeded, and Gitea came up healthy on `3001` while the frontend had already timed out at 15 seconds.
- Live diagnostics on `100.70.96.88` confirmed the Gitea install did not fail; the primary registry pull timed out after 300 seconds, the fallback mirror succeeded, and Gitea came up healthy on `3001` while the frontend had already timed out at 15 seconds.
- Gitea and other Docker-image app installs now stay visible during slow registry pulls instead of being marked as failed by the browser before backend install progress can complete.
- Gitea is now categorized as a known Data app in My Apps, so a running Gitea container appears with installed apps instead of being filtered into the Websites/Services split.
- NetBird `0.71.2` is now available in the app catalog and fallback marketplace data as a recommended networking app using the official `docker.io/netbirdio/netbird:0.71.2` image.
@@ -582,8 +468,8 @@
- App session close buttons now return to the previous dashboard screen when possible and otherwise fall back to My Apps, avoiding the 404 page after closing an app launched from an invalid or stale history entry.
- System Update confirmation and mirror modals now teleport to the document body with a full-screen overlay, so they cover the whole app instead of only the right-hand dashboard panel.
- Mobile app launches stay inside Archipelago's app-session webview and hide desktop-only new-tab launch affordances, including apps such as Home Assistant that previously looked like they would leave the mobile shell.
- Live recovery on a fleet node upgraded only the `btcpay-server` container to `docker.io/btcpayserver/btcpayserver:2.3.9`, preserved the existing datadir and Postgres database, and confirmed the container is healthy after a pre-upgrade backup.
- Public validation confirmed ``the BTCPay host`/`www` redirect to BTCPay login over HTTPS and `the L484 host`/`www` serve the L484 page over HTTPS using the issued Let's Encrypt certificates.
- Live recovery on `100.70.96.88` upgraded only the `btcpay-server` container to `docker.io/btcpayserver/btcpayserver:2.3.9`, preserved the existing datadir and Postgres database, and confirmed the container is healthy after a pre-upgrade backup.
- Public validation confirmed `spay.tx1138.com`/`www` redirect to BTCPay login over HTTPS and `sapien.tx1138.com`/`www` serve the L484 page over HTTPS using the issued Let's Encrypt certificates.
## v1.7.67-alpha (2026-05-18)
@@ -592,18 +478,18 @@
- Settings What's New is filled through `v1.7.67-alpha`, including the missing historical `v1.7.44-alpha` through `v1.7.66-alpha` entries.
- Bitcoin/Knots/Core shell lifecycle specs now match the Rust app config memory policy: 8 GiB on normal hosts, 4 GiB on low-memory hosts, and pruned Knots uses a larger dbcache on hosts with enough RAM to improve IBD throughput.
- ElectrumX/electrs shell lifecycle specs now match the 4 GiB memory policy used by the Rust app config, reducing drift between first boot, reconcile, and app lifecycle paths.
- Live assessment of a fleet node identified the current IBD bottlenecks as CPU/thermal/I/O pressure rather than RAM exhaustion, with follow-up work planned for existing-node swap repair, kiosk Chromium CPU reduction, and reconcile failure cleanup.
- Live assessment of `100.70.96.88` identified the current IBD bottlenecks as CPU/thermal/I/O pressure rather than RAM exhaustion, with follow-up work planned for existing-node swap repair, kiosk Chromium CPU reduction, and reconcile failure cleanup.
## v1.7.66-alpha (2026-05-18)
- Nginx Proxy Manager stale-port repair now detects stopped or `Created` Podman records by inspecting `podman ps -a` port metadata, covering records where `podman port nginx-proxy-manager` returns no mapping until start.
- Live recovery on a fleet node removed only the stale Nginx Proxy Manager container record and recreated it with `8081:81`, `8084:80`, and `8444:443`, preserving `/var/lib/archipelago/nginx-proxy-manager` data.
- Live recovery on `100.70.96.88` removed only the stale Nginx Proxy Manager container record and recreated it with `8081:81`, `8084:80`, and `8444:443`, preserving `/var/lib/archipelago/nginx-proxy-manager` data.
- Validation confirmed Nginx Proxy Manager recovered as healthy and responds through direct admin port `8081`, host compatibility port `81`, and `/app/nginx-proxy-manager/`.
## v1.7.65-alpha (2026-05-18)
- Orchestrator-backed app starts now run the same pre-start repairs as the legacy Podman path, so Nginx Proxy Manager stale `81:81` container metadata is removed and recreated before the orchestrator tries to start it.
- Live diagnostics on a fleet node confirmed host nginx is healthy while Nginx Proxy Manager has no listeners on `8081`, `8084`, or `8444`, causing host nginx `502` responses for NPM proxy paths.
- Live diagnostics on `100.70.96.88` confirmed host nginx is healthy while Nginx Proxy Manager has no listeners on `8081`, `8084`, or `8444`, causing host nginx `502` responses for NPM proxy paths.
## v1.7.64-alpha (2026-05-18)
@@ -628,7 +514,7 @@
- Multi-container stack installs now keep their app card in the `Installing` state for up to 20 minutes while dependency containers are being pulled and prepared.
- BTCPay Server installs no longer appear to vanish or fail after two minutes while Postgres and NBXplorer are still being created before the primary `btcpay-server` container exists.
- The stale-transition escape hatch remains short for start, stop, restart, update, and removal operations, so genuinely wedged lifecycle actions still recover quickly.
- Live validation on a fleet node confirmed BTCPay Server completed installation and responds on port `23000` with the expected HTTP redirect.
- Live validation on `100.70.96.88` confirmed BTCPay Server completed installation and responds on port `23000` with the expected HTTP redirect.
## v1.7.60-alpha (2026-05-18)
@@ -636,7 +522,7 @@
- Mesh radio auto-detection now skips known non-mesh serial devices such as Sierra Wireless LTE modems and Zooz/Z-Wave sticks, avoiding interference with production peripherals.
- Meshtastic config sync now sends `want_config_id` with the correct protobuf wire type, fixing radio-side `ignore malformed toradio` errors and allowing node-info/contact ingestion.
- The stable `/dev/mesh-radio` udev rule no longer claims every `ttyACM*` device; it only matches known mesh USB serial adapters and known USB CDC ACM radio vendors.
- Live validation on a fleet node confirmed Archipelago selects `/dev/ttyUSB0`, identifies the Meshtastic node, and refreshes 103 mesh contacts.
- Live validation on `100.70.96.88` confirmed Archipelago selects `/dev/ttyUSB0`, identifies the Meshtastic node, and refreshes 103 mesh contacts.
## v1.7.59-alpha (2026-05-17)
@@ -660,7 +546,7 @@
- Host nginx now serves `/assets/*` hashed frontend chunks as immutable static files with a hard 404 on misses instead of falling back to `index.html`, preventing strict MIME errors when a browser has a stale pre-update HTML shell.
- The SPA HTML shell and service-worker files now revalidate on every load, reducing stale frontend references after OTA updates.
- OTA runtime promotion now installs the bundled `nginx-archipelago.conf` into `/etc/nginx/sites-available/archipelago` and reloads nginx after a successful config test, so frontend cache/fallback fixes reach existing nodes without a manual deploy.
- Local validation passed with `cargo check -p archipelago`; live SSH testing against a fleet node was not completed because temporary public-key authentication was rejected on the target.
- Local validation passed with `cargo check -p archipelago`; live SSH testing against `100.70.96.88` was not completed because temporary public-key authentication was rejected on the target.
## v1.7.57-alpha (2026-05-17)
@@ -741,7 +627,7 @@
- Health monitor no longer pages "Auto-restart failed" for orphaned containers. After a variant switch (bitcoin-core ↔ bitcoin-knots) the previous variant's container could survive uninstall and the health monitor would try restarting it forever. Now skipped silently with a debug log.
- Apps no longer disappear from My Apps when an install fails. The card stays visible with state=Stopped so the user can retry or uninstall, with the failure reason surfaced via the new install_progress.message field.
- "Downloading…" progress now actually advances during multi-image stack pulls. Was sticking at 20% until all pulls finished; now interpolates 20%→70% based on which image of N has landed.
- Pulled four docker.io images (bitcoin, gitea, nextcloud, valkey) into the lfg2025 registries on the registry mirrors. Removes a docker.io dependency from first-boot installs.
- Pulled four docker.io images (bitcoin, gitea, nextcloud, valkey) into the lfg2025 registries on OVH and tx1138. Removes a docker.io dependency from first-boot installs.
- Resilience harness improvements: install-fail entries no longer vanish, install/uninstall/probe cells are timing-tolerant (60s retry on ui_probe and auth_probe), dep snapshots no longer leak companion containers into the dependent app's "new containers" set.
## v1.7.45-alpha (2026-04-29)
@@ -787,7 +673,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Infrastructure
- CI pipeline added (.github/workflows/ci.yml) — cargo fmt, clippy, tests + frontend type-check, build
- Update system now fetches from the release Gitea instance (configurable via ARCHIPELAGO_UPDATE_URL)
- Update system now fetches from git.tx1138.com Gitea instance (configurable via ARCHIPELAGO_UPDATE_URL)
- Cleaned up stale git branches (app-store, overnight/2026-03-12, overnight/2026-03-13)
## [1.3.0] - 2026-03-19
+56 -44
View File
@@ -1,72 +1,84 @@
# Archipelago — contributor guide
# Archipelago — agent guide
This file orients anyone (human or AI) working in this repository: the
invariants that must hold, how to build and verify, and where the deeper
design docs live. The authoritative behaviour is always the code in `core/`.
## ✅ Single-node production gate is GREEN (2026-06-23)
**Read [`docs/ROADMAP.md`](docs/ROADMAP.md) for where the project is going** and
[`docs/README.md`](docs/README.md) for the full documentation index.
`tests/lifecycle/run-gate.sh` is **5/5 on .228, 0 failures** — the single-node exit
criterion is met and the priority banner is demoted. Next exit-criteria: the
**multinode pass** (`docs/multinode-testing-plan.md`) and workstreams B/C/D.
The north star: a world-class, **developer-ready app platform**every app
manifest-driven, rootless, secure, and 100%-uptime-capable, with third-party
developers publishing via an external/decentralized registry.
**For day-to-day work, use `docs/UNIFIED-TASK-TRACKER.md`** — the consolidated,
priority-ordered "what's left" list across the 1.8.0 OTA and master-plan docs
(fastest/simplest tasks first). It supersedes hunting through the two source docs
below for open items; those remain the narrative/history.
Detailed sub-plans:
- App platform / packaging phases + security model → [`docs/APP-PACKAGING-MIGRATION-PLAN.md`](docs/APP-PACKAGING-MIGRATION-PLAN.md)
- Registry-distributed manifests → [`docs/registry-manifest-design.md`](docs/registry-manifest-design.md)
- External/decentralized marketplace for devs → [`docs/marketplace-protocol.md`](docs/marketplace-protocol.md)
- App manifest schema → [`docs/app-manifest-spec.md`](docs/app-manifest-spec.md)
- Production test gate → [`tests/lifecycle/TESTING.md`](tests/lifecycle/TESTING.md)
**Read `docs/PRODUCTION-MASTER-PLAN.md` first** — it is still the authoritative plan
for the north star: a world-class, **developer-ready app platform** where every app
is manifest-driven, manifests ship via the **signed registry** (not OTA disk files),
and **third-party developers publish apps via an external/decentralized registry**
all rootless, secure, robust, and 100%-uptime-capable. It no longer overrides all
ad-hoc direction now that the gate is green, but it remains the source of truth for
sequencing the remaining workstreams.
## Commit & push every unit of work
Detailed sub-plans (all linked from the master):
- App platform / packaging phases + security model → `docs/APP-PACKAGING-MIGRATION-PLAN.md`
- Registry-distributed manifests (in progress) → `docs/registry-manifest-design.md`
- External/decentralized marketplace for devs → `docs/marketplace-protocol.md`
- Current per-app state → `docs/archive/app-registry-status-2026-06-21.md`
- Production test gate (exit criterion) → `tests/lifecycle/TESTING.md`
Work is not "done" until it is committed **and** pushed. Finished work has been
lost by sitting uncommitted in a shared tree across sessions. To prevent that:
## Commit & push every unit of work (never violate)
**The #1 process rule: work is not "done" until it is committed AND pushed.** This
exists because finished work has been lost/clobbered by sitting uncommitted in the
shared tree across agents and sessions. To prevent that:
- **Commit each feature/fix the moment it works** — one focused, self-contained
commit per logical change (it compiles and its targeted tests pass). Don't let
commit per logical change (it compiles and its targeted tests pass). Do not let
unrelated changes accumulate uncommitted.
- **Push immediately after committing** so nothing lives only on one machine.
- **Never leave a stack of finished work uncommitted** overnight or when handing
off — if you must pause mid-change, commit a clearly-labelled WIP checkpoint
rather than leaving the tree dirty.
- **Stage explicitly by path** (`git add <paths>`) when another contributor's
uncommitted work shares the tree — never `git add -A` / `git commit -a`, which
clobbers or entangles their changes.
- **Never commit secrets** (mnemonics, private keys, API tokens). Signing is done
offline; artifacts (catalog/manifest) are signed, not the keys.
- **Push immediately after committing** so nothing lives only on one machine. `main`
is protected → push via `git push gitea-ai main` (account `ai`, see the memory
note); feature branches push to their own remote.
- **Never leave a stack of finished work uncommitted** overnight or when handing off
between agents — if you must pause mid-change, commit a clearly-labelled WIP
checkpoint rather than leaving it dirty.
- **Stage explicitly by path** (`git add <paths>`) when another agent's uncommitted
work shares the tree — never `git add -A` / `git commit -a`, which clobbers or
entangles their changes.
- **Never commit or push secrets** (mnemonics, private keys, API tokens). Signing is
done offline; artifacts (catalog/manifest) are signed, not the keys.
- Commit messages end with the `Co-Authored-By: Claude …` trailer.
## Invariants (never violate)
- **Rootless Podman only.** No rootful, no Docker-socket mounts, no privileged
containers unless explicitly approved.
- **No per-app Rust installers / no OS-level reliance.** Apps are declarative;
the orchestrator owns the lifecycle. A hardcoded `podman run` + `sudo chown`
installer is the anti-pattern being deleted, not a template.
the orchestrator owns the lifecycle. `install_immich_stack` (hardcoded
`podman run` + `sudo chown`) is the anti-pattern being deleted, not a template.
- **Secrets are manifest-declared** (`generated_secrets`, materialised by
`container::secrets`, 0600/rootless) — never hardcoded, per-app, or logged.
- **Migrations never destroy data** — preserve `/var/lib/archipelago/<app>`,
secrets, credentials, ports, and adoption container names; keep a rollback path.
- **Verify on a real node before any release tag.**
- **Verify on the real node .228 before any tag.** (Fleet-wide multinode
verification is a separate plan: `docs/multinode-testing-plan.md`.)
## Build / verify
- Rust workspace root is `core/` (no Cargo.toml at repo root). Run `cargo` from `core/`.
- Rust workspace root is `core/` (no Cargo.toml at repo root). `cargo` from `core/`.
- If a `cargo test`/build hits `rust-lld: undefined hidden symbol`, it's
incremental-cache corruption — rebuild with `CARGO_INCREMENTAL=0`.
- Frontend: `neode-ui/``npm run build` outputs to `web/dist/neode-ui/`.
Grep the built bundle for new strings before shipping (the build can silently
no-op).
- App manifests are delivered inside the **signed catalog** (`releases/app-catalog.json`),
whose entry overrides the on-disk `/opt/archipelago/apps/*/manifest.yml`
(origin-wins; disk is the fallback). Editing a disk manifest alone does **not**
change a catalog-covered app — regenerate and re-sign the catalog.
Grep the built bundle for new strings before shipping (build can silently no-op).
- App manifests load from disk on nodes at `/opt/archipelago/apps/*/manifest.yml`
(today); the goal is to distribute them via the signed catalog instead.
## Production test gate (definition of done)
`tests/lifecycle/run-gate.sh` must be green across install / UI / stop / start /
restart / reinstall / reboot-survive / archipelago-restart-survive / uninstall.
**Run the gate on the node** (it uses local podman/systemctl/bitcoin probes), not
via RPC from another host, and re-run it after any orchestrator/lifecycle change.
Multinode / fleet testing is a separate pass. See
[`tests/lifecycle/TESTING.md`](tests/lifecycle/TESTING.md).
`tests/lifecycle/run-gate.sh` green across install / UI / stop / start / restart /
reinstall / reboot-survive / archipelago-restart-survive / uninstall — **5× on
.228** (`ARCHY_ITERATIONS=5`). **Run the gate ON the node** (it uses local podman/systemctl/bitcoin
probes), not via RPC from another host. **✅ GREEN 2026-06-23 (5/5, 0 not-ok)** — keep it
green (re-run after orchestrator/lifecycle changes); regressions are top priority again.
**Multinode testing (.198 + the rest of the fleet) is a SEPARATE plan** —
`docs/multinode-testing-plan.md` — not part of this single-node gate criterion, and is
the next exit criterion now that single-node is green.
+4 -3
View File
@@ -45,6 +45,7 @@ Start with:
- [App Developer Guide](docs/app-developer-guide.md)
- [App Manifest Spec](docs/app-manifest-spec.md)
- [Nostr Git Source Hosting Plan](docs/nostr-git-source-hosting.md)
- [Operations Runbook](docs/operations-runbook.md)
- [Troubleshooting](docs/troubleshooting.md)
## Quick start
@@ -83,9 +84,6 @@ python3 scripts/check-app-catalog-drift.py --release --strict
## Documentation map
The full, grouped index lives at **[docs/README.md](docs/README.md)**. The most
common entry points:
| Doc | Purpose |
|-----|---------|
| [Architecture](docs/architecture.md) | System layers, crates, data paths, security model |
@@ -96,7 +94,10 @@ common entry points:
| [Nostr Git Source Hosting Plan](docs/nostr-git-source-hosting.md) | ngit/NIP-34 contribution workflow and maintainer model |
| [Apps README](apps/README.md) | Packaged app catalog overview |
| [Image Recipe](image-recipe/README.md) | Bootable image build flow |
| [Operations Runbook](docs/operations-runbook.md) | Production operations and recovery |
| [Open Source Readiness](docs/OPEN_SOURCE_READINESS.md) | Public-release cleanup checklist |
| [Roadmap](docs/ROADMAP.md) | Shipped, in-progress, and planned work |
| [Unified Task Tracker](docs/UNIFIED-TASK-TRACKER.md) | Launch hardening task list |
| [Archive](docs/archive/) | Historical plans, audits, and handoffs |
## Contributing
+112
View File
@@ -0,0 +1,112 @@
# Archipelago v1.0.0 Release Notes
**Release Date**: March 2026
**Target Platform**: Debian 13 (Trixie) — x86_64 and ARM64
## What is Archipelago?
Archipelago is a self-sovereign Bitcoin Node OS. Flash it to a USB drive, install on any x86_64 or ARM64 machine, and manage your personal server through a modern web interface. Run Bitcoin infrastructure, self-hosted apps, and Web5 identity — all from hardware you control.
## Key Features
### Bitcoin Infrastructure
- **Bitcoin Knots** full node with pruning support
- **LND** Lightning Network daemon with channel management UI
- **Electrs** Electrum server for wallet connectivity
- **BTCPay Server** for accepting Bitcoin payments
- **Mempool** block explorer and fee estimator
- **Fedimint** federation guardian and gateway
### Self-Hosted Apps (20+)
- **Storage**: File Browser, Immich, PhotoPrism, Nextcloud
- **Productivity**: Penpot, OnlyOffice, Vaultwarden
- **Media**: Jellyfin
- **Search**: SearXNG (private search)
- **AI**: Ollama (local LLMs with Claude, GPT, and open models)
- **Network**: Tailscale VPN, Nginx Proxy Manager, Uptime Kuma
- **Home**: Home Assistant
- **Platform**: IndeedHub, Grafana monitoring
### Web5 Identity
- DID-based digital identity (Ed25519 + secp256k1 dual key)
- Verifiable Credentials issuance and verification
- Decentralized Web Node (DWN) for data sync
- Nostr relay integration for node discovery
### Federation
- DID-authenticated peer-to-peer federation
- Remote node monitoring and management
- Bilateral trust with single-use invite codes
- Tor hidden services for private communication
### Security
- AES-256-GCM encrypted secrets at rest
- Container isolation: read-only root, capability dropping, non-root user
- TOTP two-factor authentication with backup codes
- Session management: HttpOnly cookies, SameSite=Strict, CSRF tokens
- Rate limiting on sensitive endpoints
- AppArmor profiles for container confinement
- Per-endpoint input validation
### System
- Rust backend with JSON-RPC API (<1ms response time)
- Vue 3 frontend with glassmorphism design
- WebSocket real-time updates
- Automated OTA updates with rollback
- Tor hidden services for all apps
- Goal-based onboarding wizard
- Kiosk mode for dedicated hardware
## Supported Hardware
- **x86_64**: Any 64-bit PC, Intel NUC, mini PCs
- **ARM64**: Raspberry Pi 5, other ARM64 SBCs
- **Minimum**: 4GB RAM, 32GB storage (500GB+ recommended for Bitcoin)
- **Recommended**: 8GB+ RAM, 1TB+ NVMe SSD
## Installation
1. Download the ISO for your architecture
2. Flash to USB drive (use Balena Etcher or `dd`)
3. Boot from USB on target hardware
4. Follow the automated installer
5. Access the web UI at `http://<device-ip>`
6. Set your password and start the onboarding wizard
## Known Limitations
- Bitcoin initial block download takes 3-7 days depending on hardware
- Some apps (BTCPay Server, Home Assistant) open in new tab due to X-Frame-Options
- ARM64 builds may have slower container pulls due to less cached registry content
- Tor hidden service generation takes 1-2 minutes on first boot
## Upgrade from Beta
If upgrading from v0.5.0-beta:
1. Back up your data via Settings > Backup
2. The OTA update system will handle the upgrade automatically
3. If OTA fails, reflash with the v1.0.0 ISO (app data is preserved on separate partition)
## Security Model
Archipelago follows defense-in-depth:
- **Network**: Nginx reverse proxy, Tor hidden services, VPN support
- **Application**: Container isolation with Podman (rootless)
- **Data**: AES-256-GCM encryption for secrets, 0600 file permissions
- **Auth**: Argon2 password hashing, TOTP 2FA, session rotation
- **Updates**: SHA-256 verified downloads with rollback capability
See `docs/adr/` for architectural decision records on security choices.
## Contributing
Archipelago is open source. To contribute:
1. Fork the repository
2. Create a feature branch (`feature/description`)
3. Follow the coding standards in `CLAUDE.md`
4. Submit a pull request with tests
## License
MIT License. See `LICENSE` for details.
# 2026-04-18 ISO build trigger
-71
View File
@@ -1,71 +0,0 @@
#!/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
@@ -1,75 +0,0 @@
#!/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
@@ -1,85 +0,0 @@
#!/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
@@ -1,12 +0,0 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "app",
"runtimeExecutable": "bash",
"runtimeArgs": ["packages/app/scripts/dev.sh"],
"port": 5173,
"autoPort": true
}
]
}
-61
View File
@@ -1,61 +0,0 @@
# 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
@@ -1,18 +0,0 @@
# 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
@@ -1,66 +0,0 @@
# 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
@@ -1,160 +0,0 @@
# 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
@@ -1,74 +0,0 @@
# 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
@@ -1,35 +0,0 @@
{
"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"
}
]
}
]
}
}
@@ -1,43 +0,0 @@
---
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
@@ -1,32 +0,0 @@
---
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.
@@ -1,37 +0,0 @@
---
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
@@ -1,17 +0,0 @@
---
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
@@ -1,32 +0,0 @@
---
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
@@ -1,33 +0,0 @@
---
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
@@ -1,32 +0,0 @@
---
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
@@ -1,27 +0,0 @@
---
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
@@ -1,27 +0,0 @@
---
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
@@ -1,19 +0,0 @@
---
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
@@ -1,102 +0,0 @@
---
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
@@ -1,26 +0,0 @@
---
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
@@ -1,28 +0,0 @@
---
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 deleted from 10e12a329f
Submodule aiui/.claude/worktrees/funny-hofstadter deleted from 1c5185a15c
Submodule aiui/.claude/worktrees/happy-colden deleted from 666e1232f4
Submodule aiui/.claude/worktrees/hardcore-beaver deleted from a817fa199f
Submodule aiui/.claude/worktrees/heuristic-raman deleted from e8e002debc
Submodule aiui/.claude/worktrees/priceless-colden deleted from aaaef7d710
@@ -1,65 +0,0 @@
---
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
@@ -1,84 +0,0 @@
---
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
@@ -1,111 +0,0 @@
---
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
@@ -1,118 +0,0 @@
---
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
@@ -1,91 +0,0 @@
---
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
@@ -1,91 +0,0 @@
---
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
@@ -1,98 +0,0 @@
---
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
@@ -1,83 +0,0 @@
---
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
@@ -1,80 +0,0 @@
---
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
@@ -1,60 +0,0 @@
---
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
@@ -1,72 +0,0 @@
---
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
@@ -1,101 +0,0 @@
---
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
@@ -1,69 +0,0 @@
---
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
@@ -1,60 +0,0 @@
---
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
@@ -1,79 +0,0 @@
---
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
@@ -1,173 +0,0 @@
---
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
@@ -1,52 +0,0 @@
---
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
@@ -1,30 +0,0 @@
---
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
@@ -1,31 +0,0 @@
---
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
@@ -1,26 +0,0 @@
---
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
@@ -1,47 +0,0 @@
---
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?://`
@@ -1,32 +0,0 @@
---
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
@@ -1,42 +0,0 @@
---
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
@@ -1,33 +0,0 @@
# 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
@@ -1,131 +0,0 @@
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
@@ -1,57 +0,0 @@
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'],
})
-54
View File
@@ -1,54 +0,0 @@
# Dependencies
node_modules/
.pnpm-store/
# Build output
dist/
*.tsbuildinfo
# scripts/build-aiui.sh's staleness-detection cache (13-09) — a local,
# best-effort marker so the script can tell "source changed but the
# emitted asset filenames didn't" from a plain rebuild with no changes.
.build-aiui-last-src-hash
.build-aiui-last-assets
# 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
@@ -1,345 +0,0 @@
# 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
@@ -1,267 +0,0 @@
# 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
@@ -1,496 +0,0 @@
#!/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
@@ -1,443 +0,0 @@
# 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
@@ -1,157 +0,0 @@
# 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
@@ -1,113 +0,0 @@
# 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
@@ -1,119 +0,0 @@
# 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
@@ -1,209 +0,0 @@
# 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
@@ -1,150 +0,0 @@
# 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
@@ -1,286 +0,0 @@
# 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
-59
View File
@@ -1,59 +0,0 @@
# 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/`.
-73
View File
@@ -1,73 +0,0 @@
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
@@ -1,33 +0,0 @@
{
"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
@@ -1,21 +0,0 @@
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
@@ -1,16 +0,0 @@
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
File diff suppressed because it is too large Load Diff
@@ -1,208 +0,0 @@
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()
})
})
@@ -1,148 +0,0 @@
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
@@ -1,27 +0,0 @@
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
@@ -1,11 +0,0 @@
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
@@ -1,20 +0,0 @@
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()
})
})
@@ -1,48 +0,0 @@
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
@@ -1,46 +0,0 @@
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/'],
},
)
-30
View File
@@ -1,30 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="h-full overflow-hidden">
<head>
<meta charset="UTF-8" />
<!-- interactive-widget=resizes-content: when the soft keyboard opens,
Chrome (108+) resizes the LAYOUT viewport instead of only the visual
one, so window.innerHeight shrinks and full-height layouts scale to
the space above the keyboard rather than scrolling under it.
neode-ui's index.html already carries this; this adds parity for
AIUI's standalone mode. Ignored by iOS Safari and by Android WebView —
the companion app controls keyboard resize itself via
windowSoftInputMode/IME insets (see docs/companion-keyboard-viewport.md). -->
<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, interactive-widget=resizes-content" />
<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>

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