Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ba3109b6d | ||
|
|
e30ac1d106 | ||
|
|
6e8b96d135 | ||
|
|
900c0b9060 | ||
|
|
91763246c3 | ||
|
|
cb22131909 | ||
|
|
cc875d1c43 | ||
|
|
dc4c80a08b | ||
|
|
2304e5904e | ||
|
|
1eaf30ae12 | ||
|
|
e97c8f36ac | ||
|
|
9adeab9420 | ||
|
|
fa3c446baf | ||
|
|
6d6408e2a4 | ||
|
|
6720711302 | ||
|
|
5c4afe00e5 | ||
|
|
6425b2f53b | ||
|
|
4053eb46af | ||
|
|
f7f8c140c3 | ||
|
|
c3e58fdab8 | ||
|
|
956e98b041 | ||
|
|
c21939b1f8 | ||
|
|
4dc9588c8a | ||
|
|
cc7d9fc19e | ||
|
|
b77c93607a | ||
|
|
c84c0fb424 | ||
|
|
3620b1f4d5 | ||
|
|
4ac18cedb6 | ||
|
|
290aa047d7 | ||
|
|
04db18c35a | ||
|
|
9032e8cd60 | ||
|
|
31241d8d12 | ||
|
|
91dbaee38c | ||
|
|
3258c047d9 | ||
|
|
3c709c143f | ||
|
|
2011fdcae0 | ||
|
|
b4b1f8faf5 | ||
|
|
a60faedc48 | ||
|
|
e33cb359a0 | ||
|
|
cfec1dcc13 | ||
|
|
5d8ce56b1b | ||
|
|
5c82a09e65 | ||
|
|
c6e384a288 | ||
|
|
1a9b769e45 | ||
|
|
7c8315f362 | ||
|
|
ece4540388 | ||
|
|
e7496883c4 | ||
|
|
2581f20ebf | ||
|
|
2ced4830f6 | ||
|
|
f84e68ad06 | ||
|
|
9b3fd5d973 | ||
|
|
82f1e99324 | ||
|
|
9ba4854024 | ||
|
|
378b60820a | ||
|
|
6e671faa3c | ||
|
|
cba448e32a | ||
|
|
9dbc9c24a6 | ||
|
|
8a1c3135dd | ||
|
|
e21d13ab3f | ||
|
|
0fa78cbf1d | ||
|
|
a0a9cf8e54 | ||
|
|
799ca131d9 | ||
|
|
6a1ce2e185 | ||
|
|
c88e837cc9 | ||
|
|
982a0c6aa6 | ||
|
|
f0d4ae5acc | ||
|
|
2963bca7fe | ||
|
|
dfce63de66 | ||
|
|
1a76efce4a | ||
|
|
493657549e | ||
|
|
6c29e9e41d | ||
|
|
43fa1bac73 | ||
|
|
a0edbe7d20 | ||
|
|
c554b0d280 | ||
|
|
a8fc4ab204 | ||
|
|
723dd6e4d3 | ||
|
|
b2fcc23623 | ||
|
|
dde3859a2e | ||
|
|
bda670d8d9 | ||
|
|
21b43214ac | ||
|
|
ba0320fa8d | ||
|
|
decee9163b | ||
|
|
b8f1e9301b | ||
|
|
20ef35c8e3 | ||
|
|
fe94693ef6 | ||
|
|
705e184a5b | ||
|
|
89885269f0 | ||
|
|
f0eb4383bd | ||
|
|
75e9274582 | ||
|
|
eda9e48965 | ||
|
|
b96d96b654 | ||
|
|
82718a3453 | ||
|
|
b08fd86f41 | ||
|
|
9263d7bb26 | ||
|
|
f1db573f10 | ||
|
|
f346992924 | ||
|
|
84ccdc7048 | ||
|
|
5d4367eaff | ||
|
|
b63a1250bb | ||
|
|
77bad97377 | ||
|
|
6581b057ac | ||
|
|
18b972b351 | ||
|
|
a01f43c95f | ||
|
|
1664fbbb6f | ||
|
|
dd7b694dee | ||
|
|
725f629f69 | ||
|
|
85936d3005 | ||
|
|
6be4fbe081 | ||
|
|
72f93e4e94 | ||
|
|
1b5debee33 | ||
|
|
dd559502ae | ||
|
|
df5b4e04ae | ||
|
|
2a836b1395 | ||
|
|
52e62e33cd | ||
|
|
d5d8a1f8b2 | ||
|
|
c328d6498c | ||
|
|
3a718f44de | ||
|
|
d7d1f1df37 | ||
|
|
091a1ce4d1 | ||
|
|
f4e8614730 | ||
|
|
8f2f927ff1 | ||
|
|
ffc6524286 | ||
|
|
49c9f71aa2 | ||
|
|
8ff3d1298e | ||
|
|
bad7506b72 | ||
|
|
ba6b22bb62 | ||
|
|
3a0d63f2b1 | ||
|
|
9b4980b669 | ||
|
|
869db87fd5 | ||
|
|
f4270424f8 | ||
|
|
7fa06ce500 | ||
|
|
7f67f0863d | ||
|
|
04a175171b | ||
|
|
9404085668 | ||
|
|
7c6a0565d5 | ||
|
|
e7961c608b | ||
|
|
c070e76515 | ||
|
|
aec87a347c | ||
|
|
7a3d874f5e | ||
|
|
edb4bbadcd | ||
|
|
aeb184b930 | ||
|
|
a9a15a7f3f | ||
|
|
a3b80b5548 | ||
|
|
55faf5043a | ||
|
|
210fa7d3c6 | ||
|
|
d595996866 | ||
|
|
751edd2790 | ||
|
|
77b985db9c | ||
|
|
27aeb2b188 | ||
|
|
0ae497f1db | ||
|
|
5fdf62520c | ||
|
|
71a249c08a | ||
|
|
cc5ecf3091 | ||
|
|
37e8aec920 | ||
|
|
6182adb01c | ||
|
|
87cbd8c4ea | ||
|
|
68e296278c | ||
|
|
5d3114b142 | ||
|
|
6454518603 | ||
|
|
e439123a61 | ||
|
|
61a6e88667 | ||
|
|
9f6158ddd2 | ||
|
|
69c32dce1c | ||
|
|
20d50d7659 | ||
|
|
6b5d461caf | ||
|
|
297aaf540d | ||
|
|
ea5e82bfe3 | ||
|
|
a442fbbe62 | ||
|
|
9c60d18528 | ||
|
|
3bbcfad43d | ||
|
|
685d357573 | ||
|
|
71125bfde9 | ||
|
|
0f1f576ef5 | ||
|
|
9e4a2c30e1 | ||
|
|
621a324859 | ||
|
|
ee7f0ede1f | ||
|
|
258a03d7f1 | ||
|
|
1991d75850 | ||
|
|
771e13aaf2 | ||
|
|
557f9e6220 | ||
|
|
ad4612a5c2 | ||
|
|
ebaee4c2b9 | ||
|
|
31f2c2b261 | ||
|
|
79691c9a25 | ||
|
|
81ccd69aae | ||
|
|
cb3a50a920 | ||
|
|
72f5656e02 | ||
|
|
c6843fca1a | ||
|
|
f31e0ba28b | ||
|
|
0863a66ddc | ||
|
|
23600da283 | ||
|
|
06b6a0e2df | ||
|
|
e53aa44ddc | ||
|
|
bec0d885de | ||
|
|
de8faefdc7 | ||
|
|
3f9f650232 | ||
|
|
99cb01277a | ||
|
|
1c5185a15c | ||
|
|
666e1232f4 | ||
|
|
a817fa199f | ||
|
|
e8e002debc | ||
|
|
aaaef7d710 | ||
|
|
d63aba788e | ||
|
|
10e12a329f | ||
|
|
721e915a48 | ||
|
|
00bdc055ba | ||
|
|
e8fc54cade | ||
|
|
48dd7a9c68 | ||
|
|
7f8dc72dd7 | ||
|
|
cddfe93c5c | ||
|
|
e8ebe56d9e | ||
|
|
1973b24eaa | ||
|
|
ad63a7b1af | ||
|
|
99e380e1b2 | ||
|
|
aab55ef9ca | ||
|
|
2f27cb86c4 | ||
|
|
c82b4ef54f | ||
|
|
b71c88f03b | ||
|
|
43ca3a7837 | ||
|
|
5414060225 | ||
|
|
2d056f9498 | ||
|
|
49ec6c09b6 | ||
|
|
7a0e42bf63 | ||
|
|
80aeefa4bf | ||
|
|
6f79f0860d | ||
|
|
464069b2f2 | ||
|
|
ece4b8256f | ||
|
|
a5a32e3566 | ||
|
|
88e211c87d | ||
|
|
27864cf92e | ||
|
|
c28e6dd811 |
@@ -156,86 +156,13 @@ The gate is mostly assembly, not invention:
|
||||
So the new code is: the listener/redirect, the app-identification step (which app is this port?),
|
||||
the login page render (app name + icon), and per-app scoping on `device_tokens`.
|
||||
|
||||
#### Research — StartOS: **DROPPED** (operator, 2026-08-03)
|
||||
#### Research — StartOS: **NOT YET VERIFIED**
|
||||
|
||||
"don't need the startOS research we decided on a approach already." The umbrelOS read
|
||||
plus the design decision above settled it; no further prior-art work.
|
||||
|
||||
### 1b. Manifest declaration of unauthenticated ports — **DONE** (`0c4826f8`, pushed)
|
||||
|
||||
`PortMapping` grew `auth` (`session` | `none`, defaulting to **`session`**) and
|
||||
`auth_rationale`. The default is the protected one, so exposure is now something a
|
||||
manifest has to ask for rather than something it gets by saying nothing.
|
||||
|
||||
Validation is two-sided: `auth: none` without a rationale is rejected, **and** a
|
||||
rationale without `auth: none` is rejected — that combination means the author wrote an
|
||||
exemption and did not get one, and shipping it silently would leave them believing
|
||||
otherwise.
|
||||
|
||||
**17 ports across 12 apps are exempt**, each with its reason: Lightning p2p (BOLT-8
|
||||
noise), LND gRPC 10009 / REST 18080 and CLN gRPC 9835 (macaroon / mutual TLS — this is
|
||||
what keeps Zeus and remote wallets working), Bitcoin p2p 8333, electrum 50001, the three
|
||||
Wyoming voice ports, git-over-SSH 2222, and the UDP discovery protocols (mDNS 5353,
|
||||
SSDP 1900, STUN 3478). **The other 39 published ports now default to gated.**
|
||||
|
||||
Bitcoin RPC 8332 is deliberately *not* exempted: it is already `bind: 127.0.0.1`, so the
|
||||
gate never sees it, and claiming an exemption it does not need would put a meaningless
|
||||
line in the audit list. If that bind is ever dropped it fails closed.
|
||||
|
||||
Two corpus tests pin this: every shipped manifest must parse, and the exempt set is
|
||||
frozen at 17 so the node's unauthenticated surface cannot grow by accident.
|
||||
|
||||
### 1c. The gate itself — **IN PROGRESS**
|
||||
|
||||
`core/archipelago/src/appgate/` — `identity.rs` (port → app id/name/icon, gated vs
|
||||
exempt, re-read from manifests so a catalog refresh applies without a restart),
|
||||
`mod.rs` (authorize + login page + TOTP step + reverse proxy), `listener.rs` (binds the
|
||||
external addresses, sweeps every 60s).
|
||||
|
||||
Design points worth not re-deriving:
|
||||
|
||||
- **It invents no auth policy.** `verify_password`, `totp::decrypt_secret`,
|
||||
`verify_code` + used-step replay protection, `SessionStore::create/create_pending/
|
||||
upgrade_to_full`, and the *same* `LoginRateLimiter` instance as the JSON-RPC path.
|
||||
Only the transport differs (HTML form vs JSON-RPC), because a browser being redirected
|
||||
to an app cannot speak JSON-RPC. Sharing the limiter matters: otherwise an attacker
|
||||
gets a fresh budget of password guesses by moving to an app port.
|
||||
- **2FA is free.** A session still pending its TOTP step fails `validate()`, so the gate
|
||||
rejects it without knowing anything about second factors.
|
||||
- **Cookies ignore port.** The session cookie is host-only with no `Domain`, so one
|
||||
sign-in covers the dashboard and every app port on the same host. The corollary is
|
||||
that an app reached on a *different* host — its own onion — is a separate sign-in.
|
||||
- **401, not a redirect.** A redirect to a login page is indistinguishable from the app
|
||||
itself redirecting, and machine clients would follow it and parse HTML as their API
|
||||
response.
|
||||
- **The gate strips `Cookie` and `Authorization` before proxying.** The app has no use
|
||||
for the node session and must never be in a position to log or forward it.
|
||||
- **Machine clients**: `device_tokens` grew `apps: Option<Vec<String>>` and
|
||||
`verify_for_app`. `None` = node-wide (what every existing companion token is —
|
||||
migrating them by guessing a scope would silently revoke access nobody asked to
|
||||
revoke); `Some(list)` restricts to those apps. An empty list is rejected rather than
|
||||
minted, since it would read as "unrestricted" while authorising nothing.
|
||||
|
||||
#### ⚠️ The ordering constraint that shapes the rollout
|
||||
|
||||
A published container port is bound `0.0.0.0:<port>`, which claims **every** host
|
||||
address. While the app holds that, the gate **cannot** bind `<lan-ip>:<port>` at all.
|
||||
So the gate can only stand in front of an app whose publish has been pinned to loopback
|
||||
(`bind: 127.0.0.1`) and whose container has been recreated. Gate-first is not possible;
|
||||
all-apps-at-once would recreate every container on the node simultaneously.
|
||||
|
||||
Therefore the rollout is **per app**, and the gate is built to be honest about being
|
||||
partially deployed: a port it cannot claim is logged at **warn** every sweep and recorded
|
||||
in `GateStatus::unprotected`. The failure mode this exists to prevent is a gate that
|
||||
binds nothing, logs at debug, and reports success while every app stays exactly as open
|
||||
as before — worse than no gate, because it stops anyone looking. (Same reasoning that
|
||||
killed the nft drop-in: `/etc/fips/fips.nft` is provisioned out-of-band and its absence
|
||||
is a silent no-op.)
|
||||
|
||||
**Still open on this item:** pin the 39 gated ports to loopback app-by-app, repoint
|
||||
`HiddenServicePort` at the gate (Tor connects *from* loopback, so a loopback-exempt
|
||||
redirect will not catch it, and the mapping loses the original destination port), gate
|
||||
the FIPS relay path, surface `GateStatus` in the UI, and verify on a real node.
|
||||
Their public docs cover the *addressing* model (per-service `.onion` and `.local`
|
||||
addresses, an explicit "make public" opt-in for clearnet) but do not state whether a
|
||||
universal auth layer sits in front of service interfaces, and the source could not be
|
||||
read from this box (`gh` is not installed, and raw GitHub paths 404'd). **Do not assume
|
||||
they delegate auth to each service — read `Start9Labs/start-os` before designing.**
|
||||
|
||||
### 2. Filebrowser ships an insecure default login — **OPEN**
|
||||
- Change the default credential **without breaking the dashboard's Cloud view**, which
|
||||
@@ -362,51 +289,6 @@ The whole update *pipeline* is built and is already independent of OTA:
|
||||
3. **The detail-page affordance** — same treatment as the card.
|
||||
4. **Button copy**: "See update" rather than "Update".
|
||||
|
||||
### 6b. Multiversion for ALL apps + upstream release discovery — **OPEN** (operator, 2026-08-03)
|
||||
> "we also need a way to provide multiversion support for all apps and it automatically
|
||||
> pulls the latest versions from the source app repository, safely, and the user can
|
||||
> choose to update so we aren't always updating manually"
|
||||
|
||||
#### Verified 2026-08-03: the schema and runtime already exist
|
||||
|
||||
This is much less work than it sounds, because the multiversion machinery built for
|
||||
Bitcoin generalises as data rather than code:
|
||||
|
||||
- `releases/app-catalog.json` entries already support a `versions[]` array of
|
||||
`{version, image, default?, deprecated?}`.
|
||||
- Runtime is complete: `catalog_versions(app_id)`, `catalog_default_version`,
|
||||
`catalog_image_for_version`, `package.versions`, version pinning through
|
||||
`package.set-config`, and `available_update_for_app` falling back to the
|
||||
`image-versions.sh` baseline pin.
|
||||
|
||||
**It is populated for 2 of 66 apps** — `bitcoin-core` (9 versions) and `bitcoin-knots`
|
||||
(5). Every other app carries a single `version`. So "multiversion for all apps" is
|
||||
primarily a **catalog-generation and image-mirroring job**, not new runtime plumbing.
|
||||
|
||||
#### What has to be built
|
||||
|
||||
1. **Populate `versions[]` fleet-wide.** Extend `scripts/generate-app-catalog.py` to emit
|
||||
a version list per app instead of a single pin. Needs a per-app policy for how many
|
||||
historical versions to carry and which is `default` (Bitcoin's list shows the shape,
|
||||
including `deprecated: true` for old-but-installable).
|
||||
2. **Mirror the images.** A version in the catalog that is not in our registry is a
|
||||
broken promise — `package.update` would pull and fail. Use the existing skopeo path
|
||||
(`feedback_skopeo_source_selection`: prefer `.160`, concurrency ≤ 6).
|
||||
3. **An upstream release-watcher.** 48 manifests already carry a `repo:` URL under
|
||||
`metadata`, so there is something to poll (GitHub releases / registry tags). It runs
|
||||
**off-node**, as part of catalog generation.
|
||||
4. **Keep the signed catalog as the trust boundary.** This is the whole of "safely":
|
||||
the watcher **proposes** versions, the offline signing ceremony **admits** them, and
|
||||
nodes only ever install what the signed catalog carries. A node must never pull
|
||||
straight from an upstream repo — that would put an unsigned third party inside the
|
||||
supply chain, which is exactly what the signed-registry model exists to prevent.
|
||||
5. **The user chooses.** Discovery must never auto-apply. `package.check-updates` already
|
||||
refreshes and hot-reloads without touching the running containers, so "a new version
|
||||
exists" and "install it" stay separate — which is also what item 6's modal is for.
|
||||
|
||||
**Sequencing note:** 6b's step 1 and item 6's UI-vs-app classification want the same
|
||||
thing — `*-ui` images represented in the catalog. Doing that once unblocks both.
|
||||
|
||||
---
|
||||
|
||||
## P2 — Carried over from v1.7.120
|
||||
@@ -443,70 +325,6 @@ below is dead on every path. Pre-existing; spotted in the v1.7.120 build warning
|
||||
|
||||
---
|
||||
|
||||
## STATUS 2026-08-04 — what shipped in 1.7.121 and what did not
|
||||
|
||||
### Shipped (committed + pushed)
|
||||
|
||||
| Item | Commit | Verified |
|
||||
|---|---|---|
|
||||
| 3. Federation trust escalation | `c0cfc72a` | 42/42 federation tests |
|
||||
| 3b. Trusted requires node password | `24ce8b39` | 44/44 + 79/79 + vue-tsc |
|
||||
| 4. lnd-ui OTA pin + host networking | `5088aef5` | — |
|
||||
| 1b. Manifest `auth:` declarations | `0c4826f8` | 73/73, all 56 manifests parse |
|
||||
| 1c. App gate (engine + audit) | `0de67ca6` | 23/23 appgate |
|
||||
| Dashboard backdrop-filter seam | `63d0183d` | 3/3, **live on archi-dev-box** |
|
||||
| 7. Release refuses unsigned manifest | `cc9e1958` | dry-run: signed/stripped/wrong-signer |
|
||||
| Gate safety model (`Option<PortAuth>`) | `ab2c8b6e` | 75/75 incl. LND wallet-port case |
|
||||
| Companion rebuild-loop | `719446c0` | podman behaviour proven first |
|
||||
| 5. Federated peers messageable | `edc9a172` | predicate pinned across device types |
|
||||
|
||||
### The two gate incidents — read before touching the gate again
|
||||
|
||||
Both were ONE mistake: a safety decision read an ABSENT manifest field as a
|
||||
value. A node's installed manifests always lag the binary, so "absent" is the
|
||||
normal state, and the daemon acted on instructions no manifest ever gave.
|
||||
|
||||
1. Gating any `session` port regardless of `bind` **published Bitcoin's
|
||||
loopback-only RPC 8332 on the LAN/Tailscale/IPv6** within seconds of deploy.
|
||||
2. The `bind`-keyed replacement looked safe (it protected `bind: 127.0.0.1`)
|
||||
but LND's gRPC 10009 / REST 18080 carry an EMPTY bind — one container
|
||||
recreate from pinning them to loopback and **breaking Zeus and every remote
|
||||
wallet**.
|
||||
|
||||
Now structural: `auth_policy()` classifies (undeclared → reported as
|
||||
unprotected, always safe), `auth_is_declared()` gates action (undeclared →
|
||||
never acted on). **Silence is not consent.**
|
||||
|
||||
### Proven on the node, empirically, not by reasoning
|
||||
|
||||
- Gate challenge → login → proxy works end to end over LAN and Tailscale.
|
||||
- **Daemon-side publish rewriting was removed.** Publishes are built in several
|
||||
places (`podman_client`, `package::install`, `stacks`); patching one covered
|
||||
one — the strfry recreate went through another and the pin never fired.
|
||||
- **Disk manifest edits do not apply to catalog-covered apps.** Even
|
||||
`bind: 127.0.0.1` written into the node's strfry manifest was overridden by
|
||||
the signed catalog. The catalog re-sign is REQUIRED; there is no shortcut.
|
||||
- A loopback-bound host port is **unreachable** from a pasta container, so
|
||||
loopback-pinning the Wyoming ports would break Home Assistant voice.
|
||||
|
||||
### Open for 1.7.122
|
||||
|
||||
1. **Catalog re-sign** — `bind: 127.0.0.1` + `auth: session` on the ~39 gated
|
||||
UI ports. This is what turns the gate from auditing into enforcing. Nothing
|
||||
in code can substitute for it.
|
||||
2. **Release-root rotation** — branch `rotate-release-root`, key
|
||||
`did:key:z6Mkfu5LT…DLWT` / `1578adcc…4418`, validated as a real curve point.
|
||||
**Sign the rotation release with the OLD key**; only the release after it
|
||||
uses the new one. Re-sign the catalog too.
|
||||
3. **Wyoming voice ports** (10200/10300/10400) — unauthenticated, and by the
|
||||
operator's policy they should not be. Correct fix is co-locating Home
|
||||
Assistant with the pine services on one container network so nothing is
|
||||
published; needs a node running both.
|
||||
4. **Item 2** filebrowser default login. **Items 6/6b** app updates +
|
||||
multiversion (`versions[]` already exists, populated for 2 of 66 apps).
|
||||
5. **`cargo-test-weekly` times out** at its 1500s cap on a loaded box — raise
|
||||
the cap or split the stage; it is not a code failure.
|
||||
|
||||
## RESUME HERE — next session
|
||||
|
||||
**Landed this session (both pushed):**
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v1.7.121-alpha (2026-08-04)
|
||||
|
||||
- **Making another node "Trusted" now asks for your node password.** Trust was being handed out by machines rather than by you: any node able to reach yours could join and mark itself Trusted, because the check proved only that the caller owned the key it had just presented — never that you had approved it. Trust also spread on its own, since every peer a Trusted node advertised was added as Trusted too, so one grant quietly propagated across the whole federation. Uninvited joins are now capped at Observer, advertised peers arrive as Observers, and raising anyone to Trusted — whether by generating an invite or by changing the dropdown on a node — requires your password. Lowering trust deliberately does not, because the safe action must never be the inconvenient one. Existing peers are left exactly as they are rather than silently demoted, and each one now records how its trust was granted so you can review them.
|
||||
- **Nodes you have peered with can be messaged straight away.** Peering was not enough: you also had to be within LoRa radio range of the other node once before chat would work. The node picked how to send a message based on which radio was plugged in, and only one of those paths knew how to reach a peer over the mesh's internet transports — so on a node with a different radio, or no radio at all, messaging a peer you had just federated with simply failed until a radio contact happened to appear. Peered nodes are reachable without radio by definition, so that choice no longer depends on the hardware. Radio is still preferred when the other node is actually in range and the message fits.
|
||||
- The dashboard no longer flickers a vertical line across its cards. A rendering seam appeared at random while moving the mouse, because the two large cards used a background-blur effect that this system already disables everywhere else on the dashboard — that browser mis-draws it inside the dashboard's animated container, and these two cards had been missed when the workaround was written. Diagnosed from a single screenshot rather than by trying to reproduce it.
|
||||
- The Lightning screen will actually update from now on. Its image was set to "latest", and the container system will not re-fetch a label it already holds, so nodes kept the same Lightning screen forever no matter how many updates shipped. A separate copy of the same setting used only by brand-new installs also described the screen incorrectly, so fresh installs got a screen that never answered.
|
||||
- Apps that provide their own screens stop rebuilding themselves in a loop. On this system's own node one of them rebuilt every thirty-five seconds indefinitely, burning processor time and restarting the app each round. The node decided a rebuild was needed by comparing file dates against the image's creation date, but a rebuild that changes nothing reuses the existing image and leaves that date untouched — so the condition that triggered the rebuild was still true afterwards, forever. Nodes taking this update repair themselves the first time they check.
|
||||
- Groundwork you can see but that does not change access yet: the node can now tell you which of its app ports answer without a login, and every port that is deliberately open — Bitcoin's peer connections for syncing the chain, Lightning's wallet connections, the Electrum wallet protocol — now has to state in writing why it is safe, so the list of exceptions is something you can read rather than something you have to discover. The login gate that will sit in front of the rest is built and proven working end to end on a real node, but it is not yet closing any ports; that arrives with the signed app catalog that tells each app to hand its address over.
|
||||
- Releases can no longer ship an unsigned update file. Signing was skippable, and when it was skipped the release was still committed and tagged — producing an update that every node correctly refuses to install. It had been caught by hand every cycle; now the release simply stops.
|
||||
- Known gaps, disclosed rather than buried: the 5x real-node lifecycle gate was not run for this release. App ports other than the deliberate exceptions above are still reachable without a login — the gate reports them, and closing them needs the next signed catalog. Three voice-assistant ports are open without authentication and should not be; the correct fix puts them on a private network with the assistant instead, which needs testing on a node that runs both. Two nodes on the fleet still share SSH host keys (detection shipped, rotation remains a deliberate operator decision).
|
||||
|
||||
## v1.7.120-alpha (2026-08-02)
|
||||
|
||||
- **Security, and the reason to take this update: two ports on your node handed anyone who could reach them complete control of your money, with no password.** The Lightning app's port answered a plain web request with the LND admin macaroon, the TLS certificate and the node's onion address — everything needed to drain the wallet remotely, and the onion meant an attacker kept that ability even after losing access to your network. The Bitcoin app's port reached Bitcoin Core's control interface using credentials the node itself supplied on the caller's behalf, with a wallet loaded. Anything on your home network, your Tailscale network or the mesh could use either one. Both now require you to be logged in. If your node has been reachable by anyone you do not fully trust, treat the Lightning macaroon and the Bitcoin RPC password as known to them.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse Bash guard: block dangerous shell commands.
|
||||
# Denies: rm -rf, git reset --hard, git push -f, git clean -fd, chmod -R 777,
|
||||
# fork bombs, block device overwrites, mkfs, paths escaping project root.
|
||||
# Uses python3 instead of jq for JSON (guaranteed on macOS).
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
CMD=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('tool_input', {}).get('command', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
BASE="${CLAUDE_PROJECT_DIR:-}"
|
||||
[[ -z "$BASE" ]] && BASE=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('cwd', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
[[ -z "$BASE" ]] && BASE="$(pwd)"
|
||||
|
||||
# Normalize: collapse whitespace, strip leading/trailing
|
||||
CMD_NORM=$(echo "$CMD" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
|
||||
deny() {
|
||||
local reason="$1"
|
||||
python3 -c "
|
||||
import json
|
||||
print(json.dumps({
|
||||
'hookSpecificOutput': {
|
||||
'hookEventName': 'PreToolUse',
|
||||
'permissionDecision': 'deny',
|
||||
'permissionDecisionReason': '$reason'
|
||||
}
|
||||
}))
|
||||
"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Dangerous patterns (case-insensitive where sensible)
|
||||
case "$CMD_NORM" in
|
||||
*"rm -rf"*|*"rm -fr"*|*"rm -f -r"*|*"rm -r -f"*) deny "Destructive rm -rf blocked by security hook" ;;
|
||||
*"git reset --hard"*) deny "git reset --hard would lose uncommitted work" ;;
|
||||
*"git push --force"*|*"git push -f"*|*"git push -f "*) deny "git push --force would rewrite history" ;;
|
||||
*"git clean -fd"*|*"git clean -f -d"*) deny "git clean -fd deletes untracked files" ;;
|
||||
*"chmod -R 777"*|*"chmod -R 0777"*) deny "chmod -R 777 is a security risk" ;;
|
||||
*":(){ :"*"};:"*) deny "Fork bomb pattern blocked" ;;
|
||||
*"> /dev/sd"*|*">/dev/sd"*) deny "Block device overwrite blocked" ;;
|
||||
*"mkfs "*|*"mkfs."*) deny "Disk format command blocked" ;;
|
||||
esac
|
||||
|
||||
# Check for path traversal escaping project root (../ outside project)
|
||||
# Only if we have a sensible base
|
||||
if [[ -n "$BASE" ]] && [[ -d "$BASE" ]]; then
|
||||
# Simple heuristic: command contains .. and would resolve outside project
|
||||
if echo "$CMD_NORM" | grep -qE '\.\./|/\.\.'; then
|
||||
# Extract plausible paths and check - allow ../ within project
|
||||
if echo "$CMD_NORM" | grep -qE '(rm|mv|cp|cat|chmod|chown)\s+.*\.\.'; then
|
||||
# Could be risky; be conservative for rm/mv/cp
|
||||
if echo "$CMD_NORM" | grep -qE '\brm\b.*\.\.'; then
|
||||
deny "Path traversal with rm blocked"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse Bash hook: detect git push/commit and prompt Claude to update PROGRESS.md.
|
||||
# Returns structured feedback with recent commits so Claude can write a session log entry.
|
||||
# Uses python3 instead of jq for JSON (guaranteed on macOS).
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# Extract command from JSON using python3
|
||||
CMD=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('tool_input', {}).get('command', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
|
||||
# Only trigger on git push or git commit commands
|
||||
if ! echo "$CMD" | grep -qE '\bgit\s+(push|commit)\b'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Gather context for the progress update
|
||||
BASE="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
||||
BRANCH=$(git -C "$BASE" branch --show-current 2>/dev/null || echo "unknown")
|
||||
PROGRESS_FILE="$BASE/PROGRESS.md"
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M')
|
||||
|
||||
# Get recent commits (branch vs main, or last 10)
|
||||
if git -C "$BASE" rev-parse --verify main &>/dev/null; then
|
||||
COMMITS=$(git -C "$BASE" log --oneline main..HEAD 2>/dev/null | head -15)
|
||||
if [ -z "$COMMITS" ]; then
|
||||
COMMITS=$(git -C "$BASE" log --oneline -10 2>/dev/null)
|
||||
fi
|
||||
else
|
||||
COMMITS=$(git -C "$BASE" log --oneline -10 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Get changed files in recent commits
|
||||
CHANGED_FILES=$(git -C "$BASE" diff --name-only main..HEAD 2>/dev/null | head -20 || \
|
||||
git -C "$BASE" diff --name-only HEAD~5..HEAD 2>/dev/null | head -20 || \
|
||||
echo "unknown")
|
||||
|
||||
# Build the feedback message and output as JSON using python3
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
message = '''Progress Update Needed
|
||||
|
||||
A git push/commit was detected on branch \`$BRANCH\` at $TIMESTAMP.
|
||||
|
||||
Recent commits:
|
||||
\`\`\`
|
||||
$COMMITS
|
||||
\`\`\`
|
||||
|
||||
Changed files:
|
||||
\`\`\`
|
||||
$CHANGED_FILES
|
||||
\`\`\`
|
||||
|
||||
Please update PROGRESS.md:
|
||||
1. Add a session log entry under '## Session Log' with format: ### $TIMESTAMP — $BRANCH
|
||||
2. Summarize what was accomplished (2-4 bullet points based on the commits above)
|
||||
3. Update any roadmap checkboxes if tasks were completed
|
||||
4. Commit the PROGRESS.md update'''
|
||||
|
||||
output = {
|
||||
'hookSpecificOutput': {
|
||||
'hookEventName': 'PostToolUse',
|
||||
'progressUpdate': message
|
||||
}
|
||||
}
|
||||
print(json.dumps(output))
|
||||
"
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse Edit|Write guard: block edits outside project and to protected paths.
|
||||
# Denies: paths outside project, .git/, .env*, lockfiles, node_modules/
|
||||
# Uses python3 instead of jq for JSON (guaranteed on macOS).
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('tool_input', {}).get('file_path', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
BASE="${CLAUDE_PROJECT_DIR:-}"
|
||||
[[ -z "$BASE" ]] && BASE=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('cwd', ''))
|
||||
except: pass
|
||||
" <<< "$INPUT")
|
||||
[[ -z "$BASE" ]] && BASE="$(pwd)"
|
||||
|
||||
# Resolve to absolute path
|
||||
if [[ -z "$FILE_PATH" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
ABS_BASE=$(cd "$BASE" 2>/dev/null && pwd) || true
|
||||
[[ -z "$ABS_BASE" ]] && ABS_BASE=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$BASE" 2>/dev/null) || true
|
||||
[[ -z "$ABS_BASE" ]] && ABS_BASE="$BASE"
|
||||
# Ensure base has trailing slash for prefix check
|
||||
[[ "$ABS_BASE" != */ ]] && ABS_BASE="${ABS_BASE}/"
|
||||
if [[ "$FILE_PATH" != /* ]]; then
|
||||
ABS_PATH="$ABS_BASE${FILE_PATH#./}"
|
||||
else
|
||||
ABS_PATH="$FILE_PATH"
|
||||
fi
|
||||
# Normalize path (collapse .. and ., no symlink resolution needed)
|
||||
ABS_PATH=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$ABS_PATH" 2>/dev/null) || true
|
||||
[[ -z "$ABS_PATH" ]] && ABS_PATH="$ABS_BASE${FILE_PATH#./}"
|
||||
|
||||
deny() {
|
||||
local reason="$1"
|
||||
echo "Blocked: $ABS_PATH — $reason" >&2
|
||||
python3 -c "
|
||||
import json
|
||||
print(json.dumps({
|
||||
'hookSpecificOutput': {
|
||||
'hookEventName': 'PreToolUse',
|
||||
'permissionDecision': 'deny',
|
||||
'permissionDecisionReason': '$reason'
|
||||
}
|
||||
}))
|
||||
"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Protected patterns (path contains or equals)
|
||||
PROTECTED_PATTERNS=(
|
||||
".git/"
|
||||
".env"
|
||||
".env.local"
|
||||
"node_modules/"
|
||||
"package-lock.json"
|
||||
"pnpm-lock.yaml"
|
||||
)
|
||||
|
||||
for pattern in "${PROTECTED_PATTERNS[@]}"; do
|
||||
if [[ "$ABS_PATH" == *"$pattern"* ]] || [[ "$ABS_PATH" == *"/$pattern" ]]; then
|
||||
deny "Edit blocked: path matches protected pattern ($pattern)"
|
||||
fi
|
||||
done
|
||||
|
||||
# .env.*.local
|
||||
if [[ "$ABS_PATH" =~ \.env\..*\.local$ ]]; then
|
||||
deny "Edit blocked: .env.*.local files contain secrets"
|
||||
fi
|
||||
|
||||
# Ensure path is under project root (ABS_BASE has trailing /)
|
||||
if [[ "$ABS_PATH" != "$ABS_BASE"* ]] && [[ "$ABS_PATH" != "$BASE"* ]]; then
|
||||
deny "Edit blocked: path is outside project directory"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "app",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": ["packages/app/scripts/dev.sh"],
|
||||
"port": 5173,
|
||||
"autoPort": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# AIUI Project Memory
|
||||
|
||||
## Session Startup
|
||||
1. Run `preview_start` with name `"app"` immediately — runs both Vite (:5173) + Claude proxy (:3141) via `packages/app/scripts/dev.sh`
|
||||
2. Always commit work before ending a session
|
||||
3. Work on `development` branch, merge to `main` only when production ready
|
||||
|
||||
## User Preferences
|
||||
- NO worktrees, NO temporary branches — just `development` and `main`
|
||||
- Always use combined dev script (proxy + frontend), never bare `vite`
|
||||
- Commit frequently to avoid losing work
|
||||
|
||||
## Current State (2026-03-04)
|
||||
- Branch: `overnight/2026-03-03`, all committed and pushed to remote (git.tx1138.com)
|
||||
- Typecheck passes clean
|
||||
|
||||
## What's Been Built
|
||||
- Chat: AI streaming with stop generation, web search, article integration, paste & extract
|
||||
- Content panel tabs: Films, Music, Magazine, News, Books, TV Series, Images, Places, Code, Design System, Nostr, **Apps**
|
||||
- Detail views for each content type (side-by-side desktop, overlay mobile)
|
||||
- **Apps tab**: curated DB of ~30 Nostr/Bitcoin apps, AppsGrid + AppDetail with search/category filtering/how-to
|
||||
- Design system viewer (grid + detail) for tokens, colors, typography, components
|
||||
- Nostr feed scaffold with note/article/zap filtering
|
||||
- Content extraction: contentExtraction.ts + contentFiltering.ts (overhauled classifiers)
|
||||
- **Bare domain extraction** from AI text (e.g. "check out damus.io")
|
||||
- Banner fallback composable (primary → API → gradient)
|
||||
- Image fallbacks: Wikipedia + Google Books sources
|
||||
- Loading skeletons per content type variant
|
||||
- Project grid with breadcrumb nav and inline creation
|
||||
- Filesystem Vite plugin for local project browsing
|
||||
- PWA with star icon, TMDB proxy, Jamendo for music
|
||||
- **Slash command palette**: /code, /nostr, /design, /search show in palette with auto-send
|
||||
- **Chat action buttons**: wrapped in glass container (backdrop blur, border, shadow)
|
||||
- **Settings modal**: Memory + Advanced Settings via gear icon
|
||||
- **Chat history**: dedicated clock icon button
|
||||
- **Web search**: Brave API primary, SearXNG rotation fallback, DuckDuckGo fallback
|
||||
- iOS HIG mobile UX rules in `.cursor/rules/15-mobile-ux.mdc`
|
||||
|
||||
## Key Files
|
||||
- Dev script: `packages/app/scripts/dev.sh`
|
||||
- Launch config: `.claude/launch.json` (name: "app")
|
||||
- Main page: `packages/app/src/pages/ChatPage.vue`
|
||||
- Content panel: `packages/app/src/components/content/ContentPanel.vue`
|
||||
- Content grids: `packages/app/src/components/content/*Grid.vue`
|
||||
- Detail views: `packages/app/src/components/content/*Detail.vue`
|
||||
- **Apps**: `packages/app/src/data/apps.ts` (curated DB), `AppsGrid.vue`, `AppDetail.vue`
|
||||
- AI composable: `packages/app/src/composables/useAI.ts`
|
||||
- Content extraction: `packages/app/src/composables/contentExtraction.ts`
|
||||
- Content filtering: `packages/app/src/composables/contentFiltering.ts`
|
||||
- Content panel logic: `packages/app/src/composables/useContentPanel.ts`
|
||||
- Image fallbacks: `packages/app/src/composables/useImageFallback.ts`
|
||||
- Banner fallback: `packages/app/src/composables/useBannerFallback.ts`
|
||||
- Chat input: `packages/app/src/components/chat/ChatInput.vue`
|
||||
- Prompt palette: `packages/app/src/components/chat/PromptPalette.vue`
|
||||
- Chat message: `packages/app/src/components/chat/ChatMessage.vue`
|
||||
- Settings modal: `packages/app/src/components/chat/SettingsModal.vue`
|
||||
- Web search plugin: `packages/app/vite-web-search.ts`
|
||||
- Prompt templates store: `packages/app/src/stores/promptTemplates.ts`
|
||||
|
||||
## Recent Session Work (2026-03-04)
|
||||
See `session-2026-03-04.md` for details.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Code Mode UI — Future Work
|
||||
|
||||
## After content surfacing is complete, implement:
|
||||
|
||||
### 1. Code Mode Visual Treatment
|
||||
- Colour the message container in orange (`#F7931A`) styling when in code mode
|
||||
- Change header text from "Message AIUI" to "Code"
|
||||
- Visual signal so user knows they're in coding context
|
||||
|
||||
### 2. Design System Context Selection
|
||||
- All design system items should be selectable with a cursor/pointer icon on hover
|
||||
- Selecting a design system item provides that UI context to the code generation
|
||||
- Think of it as "code with this component/token in mind"
|
||||
|
||||
### 3. File Browser / Open File Context
|
||||
- File browser or open file in the content panel
|
||||
- Selected files provide context for coding
|
||||
- Pairs with the design system selection — user picks UI + files as coding context
|
||||
@@ -0,0 +1,66 @@
|
||||
# Session 2026-03-04
|
||||
|
||||
## Completed This Session
|
||||
|
||||
### 1. Chat UX Changes
|
||||
- **History button**: Changed from title-click dropdown to dedicated clock icon in ChatHeader
|
||||
- **Settings modal**: Created `SettingsModal.vue` — Memory + Advanced Settings behind gear icon, glass-card with backdrop blur
|
||||
- **PromptIndex fix**: Reverted to original behavior (current conversation only), fixed broken v-if/v-else chain where StreamingDots broke the template chain
|
||||
- **Chat action buttons**: Wrapped hover icons in proper glass container (`bg-black/60 backdrop-blur-md border border-white/10`) with divider between actions and thumbs
|
||||
|
||||
### 2. iOS HIG Integration
|
||||
- Created `.cursor/rules/15-mobile-ux.mdc` with comprehensive iOS HIG values
|
||||
- Updated CLAUDE.md Mobile UX section
|
||||
|
||||
### 3. Web Search Fix
|
||||
- All SearXNG instances were returning 429, DuckDuckGo rate-limiting
|
||||
- Added Brave Search API as primary backend (`BRAVE_SEARCH_API_KEY` env var)
|
||||
- Expanded SearXNG pool to 8 instances with rotation
|
||||
- Added HTML response guard for captcha pages
|
||||
|
||||
### 4. Content Detection Overhaul (MAJOR)
|
||||
- **Expanded all classifiers** in `contentFiltering.ts`: isNewsQuery, isMusicQuery, isBookQuery, isTVQuery, isPlaceQuery, isWebsitesQuery + response variants
|
||||
- **Added Nostr detection**: `isNostrQuery()`, `isNostrLikeResponse()`
|
||||
- **Added App detection**: `isAppQuery()`, `isAppLikeResponse()`
|
||||
- **Updated `filterTabsByContext()`**: new `hasNostr` + `hasApps` params, nostr/app query priority
|
||||
- **Updated `preferredFirstTab()`**: nostr + app checks
|
||||
|
||||
### 5. Bare Domain Extraction
|
||||
- `extractBareDomainLinks(text)` in contentExtraction.ts
|
||||
- Detects plain domains like "damus.io" not inside markdown/bold/URL patterns
|
||||
- Known TLDs whitelist, file extension blacklist
|
||||
|
||||
### 6. Apps Tab (NEW FEATURE)
|
||||
- **Database**: `packages/app/src/data/apps.ts` — AppEntry interface, ~30 curated apps
|
||||
- Nostr clients: Damus, Primal, Snort, Amethyst, Coracle, Iris, noStrudel
|
||||
- Lightning wallets: Phoenix, Breez, Zeus, Alby, Mutiny, WoS
|
||||
- Bitcoin wallets: Sparrow, BlueWallet, Nunchuk, Coldcard
|
||||
- Privacy: SimpleX Chat, Signal, Mullvad VPN
|
||||
- Node software: Start9, Umbrel, RaspiBlitz, myNode
|
||||
- Dev tools: NDK, nostr-tools, Nak
|
||||
- **Extraction**: `extractApps(text, userQuery)` — keyword matching against DB, surfaces with 1+ match for app/nostr/known-app queries, 2+ for general
|
||||
- **UI**: `AppsGrid.vue` (list with search/category filter), `AppDetail.vue` (gradient header, how-to steps, related apps, external link)
|
||||
- **Wired in**: useContentPanel.ts (panelApps ref, selectedApp, open/close), ContentPanel.vue (registered), PromptIndex badges
|
||||
|
||||
### 7. Slash Command Palette
|
||||
- `/code`, `/nostr`, `/design`, `/search` appear as commands in PromptPalette
|
||||
- Commands section above Templates section with `/slash` prefix styling
|
||||
- Auto-send on select (except `/search` which sets text for query input)
|
||||
- 8px side margins (`left-2 right-2`), no max-height scroll limit
|
||||
- `ChatInput.vue`: simplified `isPaletteMode` — no longer excludes command names
|
||||
|
||||
### 8. App Detection Fix
|
||||
- Queries mentioning known app names (e.g. "start9") now match via `queryMatchesApp` check
|
||||
- Previously required explicit app/nostr query patterns like "what app" or "best wallet"
|
||||
|
||||
## Known Issues / TODO for Next Session
|
||||
- User reported "start9" search shows Brief but Apps tab was empty — FIXED in last commit
|
||||
- The `/design` command was added to palette and ChatWindow handleSend
|
||||
- Consider adding more apps to the curated database over time
|
||||
- The plan file is at `.claude/plans/content-detection-overhaul.md` (all steps complete)
|
||||
|
||||
## Git State
|
||||
- Branch: `overnight/2026-03-03`
|
||||
- Latest commit: `f346992` — feat(chat): slash command palette, action button containers, app detection fix
|
||||
- Previous commit: `84ccdc7` — feat(app): content detection overhaul, apps tab, chat UX, web search
|
||||
- All pushed to origin
|
||||
@@ -0,0 +1,160 @@
|
||||
# Plan: Overhaul Content Detection + Add Apps Tab
|
||||
|
||||
## Context
|
||||
|
||||
The content surfacing system misses many common AI response patterns. Example: AI responds about Nostr (mentioning damus.io, primal.net, snort.social) but the Nostr tab never surfaces. Query/response classifiers use narrow regexes that miss natural language variations. There's no "topic detection" layer, no app detection, and bare domains in AI text aren't extracted as websites.
|
||||
|
||||
**Goals:**
|
||||
1. Fix content detection to handle how AIs actually respond
|
||||
2. Add Nostr tab surfacing (currently only via `/nostr` command)
|
||||
3. Add Apps tab with curated Nostr + Bitcoin ecosystem apps (local DB + AI extraction fallback)
|
||||
4. Extract bare domains from AI text (e.g. "check out damus.io")
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Expand Query & Response Classifiers
|
||||
|
||||
**File:** `packages/app/src/composables/contentFiltering.ts`
|
||||
|
||||
### 1A. Add Nostr classifiers (new functions)
|
||||
|
||||
- `isNostrQuery(q)` — matches: nostr, npub, nip-\d, damus, primal, snort, amethyst, coracle, zap, relay, note1, nevent, nprofile, fiatjaf, nostrich, "decentralized social"
|
||||
- `isNostrLikeResponse(text)` — requires literal "nostr" OR 2+ Nostr-specific signals (npub, nip-, client names, relay+wss, zap+lightning)
|
||||
|
||||
### 1B. Add App classifiers (new functions)
|
||||
|
||||
- `isAppQuery(q)` — matches: app, client, wallet, tool, software, download, install, "what app", "best app for", "recommend.*app"
|
||||
- `isAppLikeResponse(text)` — matches: "you can use", "popular clients include", "I'd recommend", "available on", "download from"
|
||||
|
||||
### 1C. Expand existing classifiers with broader patterns
|
||||
|
||||
| Classifier | Add these patterns |
|
||||
|---|---|
|
||||
| `isNewsQuery` | "what happened today", "any updates on", "trending", "catch me up", "brief me", "current events" |
|
||||
| `isMusicQuery` | "genre", "spotify", "bandcamp", "grammys", "billboard", "mixtape", "discography", "banger", "favorite jam" |
|
||||
| `isBookQuery` | "what should I read", "favorite reads", "reading list", "book club", "memoir", "audiobook", "goodreads", "worth reading" |
|
||||
| `isTVQuery` | "what's good on netflix", "anything to binge", "hbo", "disney+", "apple tv", "amazon prime", "docuseries", "limited series" |
|
||||
| `isPlaceQuery` | "hungry", "food near me", "best brunch spot", "happy hour", "speakeasy", "rooftop bar", "food truck" |
|
||||
| `isWebsitesQuery` | "point me to", "link me", "any good sites", "tools for", "platforms for" |
|
||||
| `isWebsitesLikeResponse` | "here are some resources", "I'd recommend checking", "you can visit", "useful resources" |
|
||||
| `isNewsLikeResponse` | "I can't access the web but", "having trouble reaching", "unable to browse but" |
|
||||
|
||||
### 1D. Update `preferredFirstTab()` — add nostr + app checks
|
||||
|
||||
### 1E. Update `filterTabsByContext()` — add `hasNostr` and `hasApps` params, integrate into tab ordering
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Bare Domain Extraction
|
||||
|
||||
**File:** `packages/app/src/composables/contentExtraction.ts`
|
||||
|
||||
Add `extractBareDomainLinks(text)`:
|
||||
- Detect plain-text domains like "damus.io", "primal.net" not inside markdown links or bold patterns
|
||||
- Skip positions covered by existing extractors (markdown links, bold-domain, full URLs)
|
||||
- Require known TLDs (.com, .org, .io, .net, .social, .app, etc.)
|
||||
- Block file extensions (.js, .ts, .vue, .json, .css)
|
||||
- Use existing `normUrl()` for dedup
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Apps Tab — Curated Database + AI Extraction
|
||||
|
||||
### 3A. Create app database
|
||||
|
||||
**New file:** `packages/app/src/data/apps.ts`
|
||||
|
||||
```ts
|
||||
interface AppEntry {
|
||||
id: string
|
||||
name: string
|
||||
description: string // One-liner
|
||||
longDescription: string // Why use this, how it works
|
||||
category: 'nostr-client' | 'lightning-wallet' | 'bitcoin-wallet' | 'privacy' | 'node' | 'dev-tool' | 'relay'
|
||||
platforms: ('ios' | 'android' | 'web' | 'desktop' | 'cli' | 'nodeos')[]
|
||||
url: string
|
||||
icon?: string
|
||||
keywords: string[] // For matching AI responses
|
||||
howTo?: string[] // Getting started steps
|
||||
relatedApps?: string[] // IDs of related apps
|
||||
}
|
||||
```
|
||||
|
||||
**Initial curated apps (~25-30):**
|
||||
- Nostr clients: Damus, Primal, Snort, Amethyst, Coracle, Iris, Nostrudel, nos.social
|
||||
- Lightning wallets: Phoenix, Mutiny, Breez, Zeus, Alby, Wallet of Satoshi
|
||||
- Bitcoin wallets: Sparrow, Blue Wallet, Nunchuk, Coldcard, Green
|
||||
- Privacy tools: Tor, SimpleX Chat, Signal, Mullvad VPN
|
||||
- Node software: Start9, Umbrel, RaspiBlitz, myNode
|
||||
- Dev tools: NDK, nostr-tools, Nak
|
||||
|
||||
### 3B. Add app extraction
|
||||
|
||||
**File:** `packages/app/src/composables/contentExtraction.ts`
|
||||
|
||||
Add `extractApps(text, userQuery)`:
|
||||
1. Match AI text against known app names/keywords from database
|
||||
2. If app query detected OR 2+ known apps mentioned → return matched apps
|
||||
3. For unknown apps, create basic entries from context (name + URL if bare domain found)
|
||||
|
||||
### 3C. Create UI components
|
||||
|
||||
**New files:**
|
||||
- `packages/app/src/components/content/AppsGrid.vue` — Grid of app cards (icon, name, category badge, one-liner)
|
||||
- `packages/app/src/components/content/AppDetail.vue` — Detail: icon, name, platforms, long description, how-to steps, link, related apps
|
||||
|
||||
Follow existing grid/detail patterns (e.g. `BookGrid.vue`/`BookDetail.vue`).
|
||||
|
||||
### 3D. Register in ContentPanel.vue
|
||||
|
||||
Add rendering for `activeTab === 'app'`, add `'app'` to `ContentTab` type.
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Wire Everything Together
|
||||
|
||||
**File:** `packages/app/src/composables/useContentPanel.ts`
|
||||
|
||||
In `updatePanelFromText()`:
|
||||
- Call `extractBareDomainLinks(text)`, merge with website sources
|
||||
- Call `extractApps(text, userQuery)`
|
||||
- Compute `hasNostr = isNostrQuery(userQuery) || isNostrLikeResponse(text)`
|
||||
- Compute `hasApps = apps.length > 0`
|
||||
- Pass `hasNostr` and `hasApps` to `filterTabsByContext()`
|
||||
- Add `panelApps` ref, title logic for apps/nostr tabs
|
||||
|
||||
Same changes in `getContextualInlineContent()`.
|
||||
|
||||
Broaden magazine detection: add tech/protocol keywords, surface magazine for 3+ sections with no other structured content.
|
||||
|
||||
---
|
||||
|
||||
## Part 5: PromptIndex badges
|
||||
|
||||
**File:** `packages/app/src/components/chat/PromptIndex.vue`
|
||||
|
||||
Add 'Nostr' and 'Apps' badge detection.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. `contentFiltering.ts` — classifiers + filterTabsByContext signature
|
||||
2. `contentExtraction.ts` — `extractBareDomainLinks()` + `extractApps()`
|
||||
3. `data/apps.ts` — curated app database
|
||||
4. `useContentPanel.ts` — wire everything
|
||||
5. `AppsGrid.vue` + `AppDetail.vue` — UI components
|
||||
6. `ContentPanel.vue` — register tab + components
|
||||
7. `PromptIndex.vue` — badges
|
||||
8. Typecheck + manual test
|
||||
|
||||
## Verification
|
||||
|
||||
1. `pnpm typecheck` passes
|
||||
2. "tell me about Nostr" → Nostr + magazine tabs surface
|
||||
3. "best Nostr clients?" → Apps tab with Damus, Primal, Snort
|
||||
4. "recommend a bitcoin wallet" → Apps tab with Phoenix, Sparrow
|
||||
5. "what happened with BIP 110?" → Magazine tab (regression)
|
||||
6. "best movies of 2024" → Films tab (regression)
|
||||
7. Bare domains in AI text extracted as websites
|
||||
8. PromptIndex badges show Nostr/Apps
|
||||
@@ -0,0 +1,74 @@
|
||||
# Plan: Code Mode UI — Orange Input, Design System Context, File Browser Context
|
||||
|
||||
## Context
|
||||
The user wants three connected features that enhance the coding experience in AIUI:
|
||||
1. Visual indication when in code mode (orange input container, "Code" label)
|
||||
2. Ability to select design system items as coding context
|
||||
3. Ability to select files from file browser as coding context
|
||||
|
||||
After this, the user wants to circle back and create a flawless version of content extraction/tab surfacing.
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Orange Code Mode Input Container
|
||||
**Files**: `ChatWindow.vue`, `ChatInput.vue`
|
||||
|
||||
**ChatWindow.vue** (line 106-115):
|
||||
- Pass `activeTab` to ChatInput as a prop: `:active-tab="activeTab"`
|
||||
- Change placeholder logic: `activeTab === 'code' ? 'Code...' : isStreaming ? 'Waiting for response...' : 'Message AIUI...'`
|
||||
|
||||
**ChatInput.vue**:
|
||||
- Add `activeTab` prop (optional string, default `''`)
|
||||
- Conditionally style the container div (line 79-81):
|
||||
- When `activeTab === 'code'`: use `bg-accent/15 border border-accent/25 backdrop-blur-xl` instead of `path-glass-bubble`
|
||||
- Keep the `rounded-2xl px-4 py-3 flex items-end gap-2 transition-all duration-300` classes
|
||||
- Conditionally style the send button orange when in code mode
|
||||
|
||||
### 2. Design System Item Selection for Coding Context
|
||||
**Files**: `useCodeContext.ts`, `DesignSystemGrid.vue`
|
||||
|
||||
**useCodeContext.ts**:
|
||||
- Add `selectedDesignTokens: ref<string[]>([])` to module state (stores item IDs)
|
||||
- Add `toggleDesignToken(id)` — adds/removes from selection array
|
||||
- Add `clearDesignTokens()` — clears selection
|
||||
- Add `isDesignTokenSelected(id)` — checks if item is in selection
|
||||
- Clear on `exitCodeMode()`
|
||||
- Export all new state/actions
|
||||
|
||||
**DesignSystemGrid.vue**:
|
||||
- Import `useCodeContext`
|
||||
- When `codeMode` is true, show a selection indicator (accent ring + checkmark) on items
|
||||
- `selectItem` should call `toggleDesignToken(item.id)` when in code mode (instead of `openDesignSystemItem`)
|
||||
- When NOT in code mode, keep existing behavior (open detail view)
|
||||
- Selected items get `ring-2 ring-accent/50 bg-accent/10` styling
|
||||
|
||||
### 3. File Browser Selection for Coding Context
|
||||
**Files**: `useCodeContext.ts`, `ProjectGrid.vue`
|
||||
|
||||
**useCodeContext.ts**:
|
||||
- Add `selectedFiles: ref<string[]>([])` — paths of files selected for context
|
||||
- Add `toggleFileSelection(path)` — adds/removes from selection
|
||||
- Add `clearFileSelection()` — clears all
|
||||
- Add `isFileSelected(path)` — checks if file in selection
|
||||
- Clear on `exitCodeMode()`
|
||||
- Export new state/actions
|
||||
|
||||
**ProjectGrid.vue**:
|
||||
- Import `useCodeContext`
|
||||
- When `codeMode` is true, file clicks toggle selection instead of (or in addition to) opening
|
||||
- Show visual selection state (accent highlight/checkmark) on selected files in FileTreeNode
|
||||
|
||||
## Files to Modify
|
||||
1. `packages/app/src/components/chat/ChatWindow.vue` — pass activeTab prop
|
||||
2. `packages/app/src/components/chat/ChatInput.vue` — conditional orange styling + "Code" placeholder
|
||||
3. `packages/app/src/composables/useCodeContext.ts` — add design token + file selection state
|
||||
4. `packages/app/src/components/content/DesignSystemGrid.vue` — toggle selection in code mode
|
||||
5. `packages/app/src/components/content/ProjectGrid.vue` — toggle file selection in code mode
|
||||
|
||||
## Verification
|
||||
1. `pnpm typecheck` — no type errors
|
||||
2. `pnpm lint` — no new lint errors
|
||||
3. Manual: `/code` command → input turns orange with "Code..." placeholder
|
||||
4. Manual: In code mode, design system tab → clicking items toggles selection (accent ring)
|
||||
5. Manual: In code mode, file browser → clicking files toggles selection
|
||||
6. Manual: Exiting code mode clears all selections
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-risky-bash.sh"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-push-progress.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: add-content-type
|
||||
description: Scaffold a complete new content type (tag, extraction, grid, detail, prompt)
|
||||
allowed-tools: Bash(*), Read, Edit, Write, Glob, Grep, Agent
|
||||
---
|
||||
|
||||
Add a complete new content type to AIUI. The user will provide the content type name (e.g., "event", "product", "video").
|
||||
|
||||
Follow ALL steps — this is the full pipeline for a content type:
|
||||
|
||||
1. **Tag format**: Add a new `[[{type}_ext:Field1|Field2|...]]` regex to `packages/app/src/composables/contentExtraction.ts` alongside the existing ones (FILM_EXT_RE, SONG_EXT_RE, etc.)
|
||||
|
||||
2. **Type definition**: Add the TypeScript interface to `packages/core/src/types/content.ts` if it doesn't exist
|
||||
|
||||
3. **Extraction function**: Add `extractAll{Type}s(text, userQuery)` to `contentExtraction.ts` following the pattern of `extractAllFilms` or `extractAllBooks`
|
||||
|
||||
4. **Strip tags function**: Add `strip{Type}Tags()` and include it in `stripContentTags()`
|
||||
|
||||
5. **Query classifier**: Add `is{Type}Query()` and optionally `is{Type}LikeResponse()` to `contentFiltering.ts`
|
||||
|
||||
6. **ContentTab type**: Add the new tab name to the `ContentTab` union in `contentFiltering.ts`
|
||||
|
||||
7. **Tab filtering**: Update `filterTabsByContext()` and `preferredFirstTab()` in `contentFiltering.ts`
|
||||
|
||||
8. **Grid component**: Create `packages/app/src/components/content/{Type}Grid.vue` following the glass-morphism pattern of existing grids (BookGrid.vue is a good template)
|
||||
|
||||
9. **Detail component**: Create `packages/app/src/components/content/{Type}Detail.vue` following the pattern of BookDetail.vue
|
||||
|
||||
10. **Wire into ContentPanel.vue**: Add import, grid render block, detail render block, and panel state refs
|
||||
|
||||
11. **Wire into ContentGridView.vue**: Add import, props, and grid render block
|
||||
|
||||
12. **Wire into ChatPage.vue**: Pass the new panel data as props to ContentGridView
|
||||
|
||||
13. **Wire into useContentPanel.ts**: Add panel ref, selected ref, open/close functions, extraction call in `updatePanelFromText()`
|
||||
|
||||
14. **System prompt**: Add tag format instructions to the `SYSTEM_PROMPT` in `useAI.ts`
|
||||
|
||||
15. **Tab label**: Add to `TAB_LABELS` in `ContentPanel.vue`
|
||||
|
||||
16. **Verify**: Run `pnpm typecheck` and fix any errors
|
||||
|
||||
Report what was created and the tag format to use.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: add-tool
|
||||
description: Add a new AI tool (function call) to the Claude proxy for the AI to use
|
||||
allowed-tools: Bash(*), Read, Edit, Write, Glob, Grep
|
||||
---
|
||||
|
||||
Add a new tool that the AI model can call via Claude's tool_use API. The user will describe what the tool should do (e.g., "search local files", "get app status", "browse media").
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Read the proxy**: Read `packages/app/server/claude-proxy.ts` to understand the existing tool_use loop and `SEARCH_WEB_TOOL` definition.
|
||||
|
||||
2. **Define the tool**: Add a new tool definition following the Claude tool_use format:
|
||||
```ts
|
||||
const NEW_TOOL = {
|
||||
name: 'tool_name',
|
||||
description: 'What this tool does...',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: { ... },
|
||||
required: [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add handler**: In the tool_use loop (where `search_web` calls are handled), add a handler for the new tool name.
|
||||
|
||||
4. **Implement backend**: If the tool needs a new API endpoint (e.g., `/api/media/scan`), create a Vite plugin or add a route to the proxy.
|
||||
|
||||
5. **Update system prompt**: Add instructions in `useAI.ts` SYSTEM_PROMPT telling the AI when and how to use the new tool.
|
||||
|
||||
6. **Verify**: Run `pnpm typecheck` and test the proxy starts without errors.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: audit-prompts
|
||||
description: Deep audit of AI system prompts — find gaps, test extraction coverage, verify tag formats
|
||||
allowed-tools: Bash(*), Read, Glob, Grep, Agent
|
||||
---
|
||||
|
||||
Perform a comprehensive audit of AIUI's AI prompt system. Do NOT make changes — report findings only.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Read the full system prompt**: Read `packages/app/src/composables/useAI.ts` and reconstruct the complete system prompt including all dynamic sections (persona, Wavlake, memory, web search, Archy context, code context).
|
||||
|
||||
2. **Catalog all tag formats**: List every `[[type:...]]` and `[[type_ext:...]]` format defined in the prompt. Cross-reference with regexes in `contentExtraction.ts`.
|
||||
|
||||
3. **Check for gaps**: For each content type in `ContentTab` (contentFiltering.ts), verify:
|
||||
- Is there a tag format in the system prompt?
|
||||
- Is there a matching extraction regex?
|
||||
- Is there a query classifier?
|
||||
- Is there a grid + detail component?
|
||||
- Is the tab wired in ContentPanel.vue and ContentGridView.vue?
|
||||
|
||||
4. **Test extraction coverage**: Read the seed prompts in `src/__tests__/fixtures/seedPrompts.ts`. For each seed, verify:
|
||||
- Does the extraction function find the expected number of items?
|
||||
- Are there edge cases that would break extraction?
|
||||
|
||||
5. **Analyze prompt quality**: Check for:
|
||||
- Conflicting instructions
|
||||
- Missing edge case handling (e.g., "what if the AI can't find a match?")
|
||||
- Overly vague instructions
|
||||
- Missing content types that should have tag formats
|
||||
|
||||
6. **Check proxy tools**: Read `server/claude-proxy.ts` and verify tool definitions match what the prompt claims.
|
||||
|
||||
7. **Report**: Create a structured summary with:
|
||||
- Content type coverage matrix (tag/extraction/grid/detail/prompt)
|
||||
- Identified gaps and inconsistencies
|
||||
- Priority recommendations
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: check
|
||||
description: Run all quality checks (typecheck, lint, test) and auto-fix errors
|
||||
allowed-tools: Bash(*), Read, Edit, Glob, Grep, Agent
|
||||
---
|
||||
|
||||
Run all quality checks for the AIUI project and fix any issues found. Execute in order:
|
||||
|
||||
1. **TypeScript**: Run `pnpm typecheck`. If errors found, read the failing files and fix the type errors.
|
||||
2. **Lint**: Run `pnpm lint`. If fixable errors, run `pnpm lint --fix` first, then fix remaining manually.
|
||||
3. **Tests**: Run `pnpm --filter @aiui/app test -- --run`. For each failure:
|
||||
- Read the test file and the source file it tests
|
||||
- Determine if the test is wrong (outdated assertion) or the source has a bug
|
||||
- Fix whichever is incorrect
|
||||
4. Report a summary: pass/fail counts, what was fixed.
|
||||
|
||||
Important: Do NOT change test expectations just to make them pass — understand WHY they fail first.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: deploy
|
||||
description: Build and prepare AIUI for deployment to Archy node
|
||||
allowed-tools: Bash(*), Read, Edit, Glob, Grep
|
||||
---
|
||||
|
||||
Build AIUI for production deployment. Steps:
|
||||
|
||||
1. **Pre-flight checks**:
|
||||
- `pnpm typecheck` — must pass
|
||||
- `pnpm lint` — must pass
|
||||
- `pnpm --filter @aiui/app test -- --run` — report failures but continue
|
||||
|
||||
2. **Build**:
|
||||
- `pnpm build`
|
||||
- Verify `packages/app/dist/` exists and contains `index.html`
|
||||
|
||||
3. **Bundle analysis**:
|
||||
- Report total dist size and gzip estimate
|
||||
- List the 5 largest chunks
|
||||
- Check against 250KB gzipped budget (warn if over)
|
||||
|
||||
4. **Verify nginx config**:
|
||||
- Read `packages/app/server/nginx-archy.conf`
|
||||
- Verify SPA routing (`try_files $uri $uri/ /aiui/index.html`)
|
||||
- Verify proxy paths for Claude API
|
||||
|
||||
5. **Container build** (if Dockerfile exists):
|
||||
- `podman build -t aiui:latest packages/app/`
|
||||
- Report image size
|
||||
|
||||
6. **Report**: Build status, bundle size, any warnings.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: fix-tab
|
||||
description: Diagnose and fix a broken content panel tab (extraction, routing, rendering)
|
||||
allowed-tools: Bash(*), Read, Edit, Glob, Grep, Agent
|
||||
---
|
||||
|
||||
Diagnose and fix a broken content panel tab. The user will specify which tab (e.g., "app", "code", "image", "nostr").
|
||||
|
||||
## Diagnostic pipeline — check each layer:
|
||||
|
||||
1. **System prompt**: Does `useAI.ts` SYSTEM_PROMPT tell the AI to use the tag format for this content type? If not, add instructions.
|
||||
|
||||
2. **Tag regex**: Does `contentExtraction.ts` have a regex for this content type's tags? If not, add one.
|
||||
|
||||
3. **Extraction function**: Does `extractAll{Type}s()` or `extract{Type}s()` exist and work correctly? Test with sample input.
|
||||
|
||||
4. **Query classifier**: Do `is{Type}Query()` and/or `is{Type}LikeResponse()` exist in `contentFiltering.ts`?
|
||||
|
||||
5. **Tab filtering**: Is the tab included in `filterTabsByContext()` and `preferredFirstTab()`? Check the boolean flag is wired.
|
||||
|
||||
6. **useContentPanel.ts**: Is the extraction called in `updatePanelFromText()`? Is the panel ref populated? Is the tab added to `availableTabs`?
|
||||
|
||||
7. **ContentPanel.vue**: Is there a grid render block for `activeTab === '{tab}'`? Are imports present? Is the tab in TAB_LABELS?
|
||||
|
||||
8. **ContentGridView.vue**: Same checks for the wide desktop view.
|
||||
|
||||
9. **ChatPage.vue**: Are the panel data props passed to ContentGridView?
|
||||
|
||||
10. **Grid component**: Does the grid component exist and render correctly?
|
||||
|
||||
11. **Detail component**: Does the detail component exist?
|
||||
|
||||
Fix each broken layer. Run `pnpm typecheck` after all fixes.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: mock-archy
|
||||
description: Enable/configure mock Archy data for standalone dev testing
|
||||
allowed-tools: Bash(*), Read, Edit, Glob, Grep
|
||||
---
|
||||
|
||||
Set up or modify mock Archy data for testing the Archipelago integration without a real Archy host.
|
||||
|
||||
## How it works
|
||||
|
||||
Mock data is in `packages/app/src/mocks/archy.ts`. When enabled, `useArchy.ts` loads this data instead of waiting for the Archy bridge.
|
||||
|
||||
## Enable mock mode
|
||||
|
||||
Two ways:
|
||||
1. Add `VITE_MOCK_ARCHY=true` to `.env.local`
|
||||
2. Add `?mockArchy` to the URL: `http://localhost:5173/?mockArchy`
|
||||
|
||||
## Customization
|
||||
|
||||
The user may ask to:
|
||||
- Add/remove mock apps from the installed list
|
||||
- Change wallet balance or channel count
|
||||
- Add/modify files in the mock file list
|
||||
- Change system info or network status
|
||||
- Test specific scenarios (e.g., "node is syncing", "wallet offline", "no files")
|
||||
|
||||
Edit `packages/app/src/mocks/archy.ts` accordingly.
|
||||
|
||||
## Verify
|
||||
|
||||
After changes, check that `buildArchyContext()` in `useArchy.ts` produces the expected system prompt section by reading the function and tracing the mock data through it.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: new-detail
|
||||
description: Generate a detail view component following AIUI glass-morphism patterns
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep
|
||||
---
|
||||
|
||||
Create a new detail view component at `packages/app/src/components/content/{Name}Detail.vue`.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. **Read a reference**: Read `BookDetail.vue` or `PlaceDetail.vue` as a template.
|
||||
|
||||
2. **Follow conventions**:
|
||||
- `<script setup lang="ts">` with single item prop
|
||||
- Back button at top (emits 'back' event)
|
||||
- Hero image/banner area with gradient overlay and fallback
|
||||
- Title, subtitle, and metadata section
|
||||
- Description/long text body with proper typography
|
||||
- Action buttons (external links, share, etc.) with glass-button styling
|
||||
- Dark/light mode via `useTheme()`
|
||||
- Smooth scroll, overflow-y-auto
|
||||
|
||||
3. **Props**: Accept single item of the content type
|
||||
4. **Emits**: `back` event for navigation
|
||||
5. **Responsive**: Full height, works in sidebar and mobile overlay
|
||||
|
||||
The user will specify the content type and which fields to display.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: new-grid
|
||||
description: Generate a content grid component following AIUI glass-morphism patterns
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep
|
||||
---
|
||||
|
||||
Create a new content grid component at `packages/app/src/components/content/{Name}Grid.vue`.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. **Read a reference**: Read `BookGrid.vue` or `PlaceGrid.vue` as a template — they show the standard pattern.
|
||||
|
||||
2. **Follow conventions**:
|
||||
- `<script setup lang="ts">` with props and emits
|
||||
- Glass morphism styling (bg-white/5, rounded-xl, hover:bg-white/10)
|
||||
- Dark/light mode support via `useTheme()`
|
||||
- Search input at top (if the content type has enough items)
|
||||
- Grid of cards with image fallback, title, subtitle, metadata
|
||||
- Touch targets min 44x44px
|
||||
- Empty state message when no items match
|
||||
- Custom scrollbar class
|
||||
|
||||
3. **Props**: Accept array of items + title string
|
||||
4. **Emits**: `select-{type}` event when a card is clicked
|
||||
5. **Responsive**: Works on mobile (full width) and desktop (sidebar width)
|
||||
|
||||
The user will specify the content type and its fields.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: overnight
|
||||
description: Commit, branch, and start the overnight automation loop
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Bash(*), Read, Write, Edit, Glob, Grep
|
||||
---
|
||||
|
||||
Prepare and launch the overnight automation loop. Do ALL steps in order, stopping on any failure:
|
||||
|
||||
1. Stage and commit all uncommitted changes: `git add -A && git commit -m "chore: pre-overnight snapshot"` (skip if working tree is clean)
|
||||
2. Push current branch to origin
|
||||
3. Get today's date as YYYY-MM-DD. Check if `overnight/$DATE` branch exists:
|
||||
- If yes: `git checkout overnight/$DATE`
|
||||
- If no: run `./loop/prepare.sh`
|
||||
4. Verify `loop/plan.md` has unchecked tasks (`grep -c '^\- \[ \]' loop/plan.md`)
|
||||
5. Commit plan files if modified: `git add loop/plan.md loop/prompt.md && git commit -m "chore: overnight plan $DATE"` (skip if clean)
|
||||
6. Push: `git push -u origin overnight/$DATE`
|
||||
7. Start the loop: run `caffeinate -i ./loop/loop.sh` with `run_in_background: true`
|
||||
8. Report: branch name, number of tasks, and confirm the loop is running in background
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: pwa-icon-cache-fix
|
||||
description: Use when the user reports a PWA icon not updating, stale PWA icon, wrong icon after install, or any PWA caching issue. Also applies when changing PWA icons in a Vite + vite-plugin-pwa project.
|
||||
version: 2.0.0
|
||||
---
|
||||
|
||||
# PWA Icon Cache Fix
|
||||
|
||||
## Problem
|
||||
|
||||
PWA icons are cached at FOUR independent layers:
|
||||
1. **Service worker cache** (Workbox precache)
|
||||
2. **Browser HTTP cache**
|
||||
3. **Browser manifest resources** (Chromium stores resized icons in its profile data, keyed by a permanent extension ID tied to the origin — NEVER re-fetched even after uninstall/reinstall)
|
||||
4. **macOS .app bundle** (`.icns` file baked into the `.app` in `~/Applications/`)
|
||||
|
||||
Query string cache busting (`?v=2`) and uninstall/reinstall do NOT fix this. Chromium reuses the same extension ID for the same origin, so it keeps the old cached icons.
|
||||
|
||||
## Fix Steps
|
||||
|
||||
### 1. Verify icon files on disk and server are correct
|
||||
|
||||
```bash
|
||||
# Visual check
|
||||
Read packages/app/public/pwa-192x192.png
|
||||
Read packages/app/public/pwa-512x512.png
|
||||
|
||||
# Hash match check
|
||||
curl -s http://localhost:5173/pwa-192x192.png | md5
|
||||
md5 -q packages/app/public/pwa-192x192.png
|
||||
```
|
||||
|
||||
### 2. Find the PWA's Chromium extension ID
|
||||
|
||||
Read the installed `.app` bundle's `Info.plist` to get the `CrAppModeShortcutID`:
|
||||
|
||||
```bash
|
||||
plutil -p "~/Applications/Brave Browser Apps.localized/AIUI.app/Contents/Info.plist" | grep CrAppModeShortcutID
|
||||
```
|
||||
|
||||
This returns an ID like `idemibpphagihbobmgmaojhjfidlfpdl`.
|
||||
|
||||
### 3. Overwrite the cached icons in browser profile
|
||||
|
||||
Chromium stores resized icons at:
|
||||
`~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/Manifest Resources/{ID}/Icons/`
|
||||
|
||||
Overwrite every size using `sips`:
|
||||
|
||||
```bash
|
||||
ICON_DIR="~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/Manifest Resources/{ID}/Icons"
|
||||
SRC="packages/app/public/pwa-512x512.png"
|
||||
for size in 32 48 64 96 128 192 256 512; do
|
||||
sips -z $size $size "$SRC" --out "${ICON_DIR}/${size}.png"
|
||||
done
|
||||
```
|
||||
|
||||
### 4. Rebuild the macOS .icns in the .app bundle
|
||||
|
||||
```bash
|
||||
ICONSET="/tmp/aiui.iconset"
|
||||
mkdir -p "$ICONSET"
|
||||
SRC="packages/app/public/pwa-512x512.png"
|
||||
sips -z 16 16 "$SRC" --out "$ICONSET/icon_16x16.png"
|
||||
sips -z 32 32 "$SRC" --out "$ICONSET/icon_16x16@2x.png"
|
||||
sips -z 32 32 "$SRC" --out "$ICONSET/icon_32x32.png"
|
||||
sips -z 64 64 "$SRC" --out "$ICONSET/icon_32x32@2x.png"
|
||||
sips -z 128 128 "$SRC" --out "$ICONSET/icon_128x128.png"
|
||||
sips -z 256 256 "$SRC" --out "$ICONSET/icon_128x128@2x.png"
|
||||
sips -z 256 256 "$SRC" --out "$ICONSET/icon_256x256.png"
|
||||
sips -z 512 512 "$SRC" --out "$ICONSET/icon_256x256@2x.png"
|
||||
sips -z 512 512 "$SRC" --out "$ICONSET/icon_512x512.png"
|
||||
cp "$SRC" "$ICONSET/icon_512x512@2x.png"
|
||||
iconutil -c icns "$ICONSET" -o "~/Applications/Brave Browser Apps.localized/AIUI.app/Contents/Resources/app.icns"
|
||||
```
|
||||
|
||||
### 5. Flush macOS icon cache
|
||||
|
||||
```bash
|
||||
touch "~/Applications/Brave Browser Apps.localized/AIUI.app"
|
||||
killall Finder
|
||||
killall Dock
|
||||
```
|
||||
|
||||
### 6. Bump PWA_CACHE_VERSION in main.ts
|
||||
|
||||
Increment the `PWA_CACHE_VERSION` constant — this nukes all SW caches on next page load for web-layer caching.
|
||||
|
||||
### 7. Delete stale build artifacts
|
||||
|
||||
Remove old `dist/` and `dev-dist/` SW/manifest files.
|
||||
|
||||
## Browser-Specific Paths
|
||||
|
||||
- **Brave**: `~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/`
|
||||
- **Chrome**: `~/Library/Application Support/Google/Chrome/Default/Web Applications/`
|
||||
- **PWA apps (Brave)**: `~/Applications/Brave Browser Apps.localized/`
|
||||
- **PWA apps (Chrome)**: `~/Applications/Chrome Apps.localized/`
|
||||
|
||||
## Key Insight
|
||||
|
||||
Chromium assigns a permanent extension ID per origin (e.g., `localhost:5173`). This ID persists across uninstall/reinstall. The icon cache in `Manifest Resources/{ID}/Icons/` is populated ONCE and never refreshed from the manifest. The only fix is to overwrite the files directly on disk.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: test-prompts
|
||||
description: Test AI prompt quality by simulating queries and checking extraction results
|
||||
allowed-tools: Bash(*), Read, Edit, Glob, Grep, Agent
|
||||
---
|
||||
|
||||
Test the AIUI AI prompt and content extraction pipeline end-to-end. This skill does NOT call the actual AI — it uses the seed prompts and extraction functions directly.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Read seed prompts**: Read `packages/app/src/__tests__/fixtures/seedPrompts.ts` to get all test cases.
|
||||
|
||||
2. **Run extraction tests**: For each seed prompt, run the test via `pnpm --filter @aiui/app test -- --run -t "seed"` and report results.
|
||||
|
||||
3. **Test edge cases**: Create and test these additional scenarios by calling extraction functions in a test:
|
||||
- Mixed content response (films + songs + books in one response)
|
||||
- App recommendation response (should trigger app tab)
|
||||
- News query with web search results
|
||||
- Place/restaurant recommendations
|
||||
- Code response with 3+ code blocks
|
||||
- Nostr-related query
|
||||
- Empty/minimal response
|
||||
|
||||
4. **Verify tab routing**: For each scenario, check that `filterTabsByContext()` returns the expected tabs in the expected order.
|
||||
|
||||
5. **Report**: Summary of what works, what's broken, and what's missing. Include specific test cases that fail.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: trace
|
||||
description: End-to-end trace of a query through prompt, extraction, tabs, and rendering
|
||||
allowed-tools: Bash(*), Read, Glob, Grep, Agent
|
||||
---
|
||||
|
||||
Trace how a specific user query flows through the entire AIUI pipeline. The user will provide a sample query (e.g., "best nostr apps", "recommend some films", "bitcoin news").
|
||||
|
||||
## Trace each stage:
|
||||
|
||||
1. **Query classifiers**: Run the query through each classifier in `contentFiltering.ts`:
|
||||
- `isNewsQuery()`, `isMusicQuery()`, `isBookQuery()`, `isTVQuery()`, `isImageQuery()`, `isPlaceQuery()`, `isRecipeQuery()`, `isCodeQuery()`, `isNostrQuery()`, `isAppQuery()`, `isWebsitesQuery()`
|
||||
- Report which ones return true
|
||||
|
||||
2. **Preferred tab**: What does `preferredFirstTab()` return for this query?
|
||||
|
||||
3. **System prompt**: What would `buildSystemPrompt()` include? Read `useAI.ts` and trace all dynamic sections.
|
||||
|
||||
4. **Expected AI response**: Based on the system prompt instructions, what tags would the AI likely use? Construct a realistic sample response.
|
||||
|
||||
5. **Extraction**: Run the sample response through each extraction function and report what gets found:
|
||||
- `extractAllFilms()`, `extractAllSongs()`, `extractAllPodcasts()`, `extractAllBooks()`, `extractAllTVSeries()`, `extractAllImages()`, `extractAllPlaces()`, `extractApps()`, `extractCodeBlocks()`, `extractRecipes()`
|
||||
|
||||
6. **Tab filtering**: What tabs would `filterTabsByContext()` return? In what order?
|
||||
|
||||
7. **Rendering**: Which grid component would render? Trace through ContentPanel.vue and/or ContentGridView.vue.
|
||||
|
||||
8. **Report**: Complete flow diagram showing: Query -> Classifiers -> Prompt -> Expected Response -> Extraction -> Tabs -> Grid
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
description: Core development philosophy for AIUI - the foundational rules that govern all code and design decisions
|
||||
globs: "**/*"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Master Philosophy
|
||||
|
||||
## Mission
|
||||
Build the next-generation AI content surface UI — a paradigm where AI responses are rendered as rich, interactive content, not plain text. Delivered as a reusable component library (@aiui/core) and a reference application (AIUI App).
|
||||
|
||||
## Philosophical Pillars
|
||||
|
||||
### 1. Open Source Only
|
||||
Every dependency must be OSS (MIT, Apache-2.0, GPL-compatible). No proprietary SDKs, no vendor-locked services. Before adding any dependency, verify its license.
|
||||
|
||||
### 2. Decentralized-First
|
||||
No hard dependency on any centralized service. AI backends, messaging protocols, storage, search — all connect through pluggable adapter interfaces. Users choose their own providers.
|
||||
|
||||
### 3. Bitcoin Only
|
||||
Bitcoin is the only monetary unit. On-chain, Lightning, ecash (Cashu, Fedimint/Fedi). No fiat payment rails, no altcoins, no stablecoins — anywhere in the UI or codebase. AIUI is never a wallet and never handles funds directly. See `10-bitcoin-only.mdc` for full rules.
|
||||
|
||||
### 4. Cryptography for Everything Sensitive
|
||||
E2E encryption for messages, encrypted local storage, proper key management. Privacy is not a feature — it is a requirement.
|
||||
|
||||
### 5. Mobile-First, Everywhere-Perfect
|
||||
Every component works flawlessly on mobile, tablet, and desktop. Mobile is the foundation, not an afterthought. Touch targets, viewport management, and safe areas are first-class citizens.
|
||||
|
||||
### 6. Consistency is Sacred
|
||||
Mobile and desktop versions show identical content and functionality unless explicitly designed otherwise. Design tokens ensure visual consistency across all breakpoints.
|
||||
|
||||
### 7. Theme-First Architecture
|
||||
Theming is a core architectural decision from day one. Themes are CSS-based with reactive state management. Dark mode and light mode are equals.
|
||||
|
||||
### 8. Utility-First, Component-Second
|
||||
Tailwind CSS utilities in templates for maximum flexibility. Component classes only for truly reusable patterns. Extract components when you repeat, not before.
|
||||
|
||||
### 9. Performance as a Feature
|
||||
Initial load < 250KB gzipped. Lazy load everything that isn't immediately visible. CSS transforms for GPU acceleration. SVG over raster images. Code splitting by default.
|
||||
|
||||
### 10. Plugin-Everything
|
||||
Every external integration connects through a typed plugin interface. AI providers, media sources, messaging protocols, wallets, social embeds — all pluggable.
|
||||
|
||||
### 11. Accessibility is Not Optional
|
||||
WCAG AA compliance minimum. Keyboard navigation everywhere. Screen reader friendly. Color contrast tested and validated.
|
||||
|
||||
### 12. MCP-Native
|
||||
First-class Model Context Protocol support for AI tool interoperability.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Desktop-first thinking
|
||||
- Hardcoded values (use design tokens)
|
||||
- Premature abstraction (build three times before abstracting)
|
||||
- Magic numbers without comments
|
||||
- Invisible state (user should always know what's happening)
|
||||
- Handling funds or private keys
|
||||
- Loading third-party tracking scripts
|
||||
- Proprietary dependencies
|
||||
|
||||
## The Ultimate Goal
|
||||
|
||||
When someone uses AIUI, they should think: "This feels incredibly polished", "Everything just works", "My data is safe", "I control my own setup."
|
||||
|
||||
When a developer reads the code: "This is well organized", "I understand exactly what's happening", "Adding a new renderer is straightforward."
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
description: Vue 3 Composition API conventions and best practices for AIUI
|
||||
globs: "**/*.vue,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Vue 3 Conventions
|
||||
|
||||
## Composition API with `<script setup>`
|
||||
Always use `<script setup lang="ts">`. Never use Options API.
|
||||
|
||||
## Component Organization Order
|
||||
1. Imports — external, then internal
|
||||
2. Props — with TypeScript-style validation
|
||||
3. Emits — explicitly defined
|
||||
4. State (refs and reactive)
|
||||
5. Computed — derived values, always pure
|
||||
6. Watchers — side effects only
|
||||
7. Methods — business logic
|
||||
8. Lifecycle hooks — ordered by execution
|
||||
9. Expose — public API (if needed)
|
||||
|
||||
## File Organization
|
||||
```
|
||||
src/
|
||||
components/
|
||||
ui/ # Primitives (Button, Card, Badge, Input)
|
||||
chat/ # Chat window, message list, input
|
||||
content-panel/ # Side panel for surfaced content
|
||||
renderers/ # Content type renderers
|
||||
layout/ # Shell, split-pane, responsive containers
|
||||
composables/ # Shared composition functions (useTheme, useMedia, useCrypto)
|
||||
stores/ # Pinia stores
|
||||
plugins/ # Plugin system
|
||||
types/ # Shared TypeScript types
|
||||
styles/ # Global CSS, themes, design tokens
|
||||
utils/ # Pure utility functions
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
- Components: PascalCase (`ProjectCard.vue`)
|
||||
- Composables: camelCase, prefixed with "use" (`useTheme.ts`)
|
||||
- Props: camelCase in JS, kebab-case in templates
|
||||
- Boolean props: prefix with `is`, `has`, `can`, `should`
|
||||
- Handler props: prefix with `on` (`onClick`, `onClose`)
|
||||
- Emits: explicit, kebab-case in templates (`project:updated`)
|
||||
|
||||
## Props — Always Validate
|
||||
```typescript
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
count: { type: Number, default: 0 },
|
||||
status: {
|
||||
type: String as PropType<'pending' | 'active' | 'complete'>,
|
||||
default: 'pending'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Never use array-style props: `defineProps(['title', 'count'])`
|
||||
|
||||
## Reactive State
|
||||
- `ref` for primitives and single values
|
||||
- `reactive` for objects with multiple properties
|
||||
- `computed` for derived state (never side effects in computed)
|
||||
- `shallowRef` for large objects that change at top level only
|
||||
|
||||
## Templates — Keep Clean
|
||||
Move complex logic to computed properties or methods. No inline logic in templates. Use `v-if` for infrequent toggles, `v-show` for frequent ones.
|
||||
|
||||
## Composables
|
||||
- One responsibility per composable
|
||||
- Return only what's needed
|
||||
- Handle cleanup in `onUnmounted`
|
||||
- Make composables testable
|
||||
|
||||
## Performance
|
||||
- Lazy load heavy components: `defineAsyncComponent(() => import(...))`
|
||||
- Use `shallowRef` for large lists
|
||||
- Use `:key` with unique identifiers, never index
|
||||
- Avoid reactive objects in templates (create in script)
|
||||
|
||||
## Error Handling
|
||||
Use `onErrorCaptured` for component-level error boundaries. Always handle async errors with try/catch/finally pattern (loading, error, data states).
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
description: Tailwind CSS utility-first styling conventions for AIUI, ported from Archy
|
||||
globs: "**/*.vue,**/*.css,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Tailwind CSS Styling
|
||||
|
||||
## Source of Truth
|
||||
All glass morphism, container, and button patterns originate from the Archy project (`/Projects/Archy/neode-ui/src/style.css`). When in doubt, match Archy exactly.
|
||||
|
||||
## Utility-First
|
||||
Use Tailwind utilities directly in templates. Extract to component classes only when a pattern repeats 3+ times.
|
||||
|
||||
## 4px Spacing Grid
|
||||
```
|
||||
1 = 4px, 2 = 8px, 3 = 12px, 4 = 16px, 5 = 20px, 6 = 24px, 7 = 28px, 8 = 32px
|
||||
```
|
||||
|
||||
## Typography Scale
|
||||
```
|
||||
text-xs = 12px (metadata, timestamps)
|
||||
text-sm = 14px (body text, buttons)
|
||||
text-base = 16px (default body, inputs)
|
||||
text-lg = 18px (subtitles)
|
||||
text-xl = 20px (card titles)
|
||||
text-2xl = 24px (section headings)
|
||||
text-3xl = 30px (page headings)
|
||||
text-4xl = 36px (hero headings)
|
||||
```
|
||||
|
||||
Font weights: `font-normal` (body), `font-medium` (emphasis), `font-semibold` (headings/buttons), `font-bold` (strong emphasis).
|
||||
|
||||
## Glass Morphism (from Archy)
|
||||
|
||||
### Containers (exact Archy values)
|
||||
- `.glass` — base: `bg: rgba(0,0,0,0.35)`, `blur(18px)`, `border: 1px solid rgba(255,255,255,0.18)`, `shadow: 0 8px 24px rgba(0,0,0,0.45)`
|
||||
- `.glass-strong` — stronger blur: same bg but `blur(24px)`
|
||||
- `.glass-card` — primary card: `bg: rgba(0,0,0,0.65)`, `blur(18px)`, `border-radius: 1rem`, same border/shadow
|
||||
- `.gradient-card` — gradient: `linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(0,0,0,0.8) 100%)`
|
||||
- `.gradient-card-dark` — dark gradient: `linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.9) 100%)`
|
||||
- `.gradient-border-container` — gradient border with inner glass, `border-radius: 1.5rem`
|
||||
- `.toast-glass` — `border-radius: 0.75rem`, same glass as `.glass-card`
|
||||
|
||||
### Buttons (exact Archy values)
|
||||
- `.glass-button` — 48px height, `bg: rgba(0,0,0,0.6)`, `blur(18px)`, border `rgba(255,255,255,0.18)`, `color: rgba(255,255,255,0.9)`
|
||||
- `.glass-button-sm` — compact variant (auto height, `py-1.5 px-3`)
|
||||
|
||||
### Icon / Ghost buttons (Archy pattern)
|
||||
```html
|
||||
<button class="p-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-colors">
|
||||
```
|
||||
Touch target: minimum 44x44px via padding.
|
||||
|
||||
### Active Navigation
|
||||
`.nav-tab-active` — `bg: rgba(0,0,0,0.35)`, inset highlight, gradient border via CSS mask `::before`
|
||||
|
||||
### Usage Rules
|
||||
- ✅ Cards, panels, modals, sidebars
|
||||
- ✅ Navigation bars, headers (fixed positioning)
|
||||
- ✅ Hover states, buttons
|
||||
- ❌ Body text containers (readability)
|
||||
- ❌ Form input fields (confusing UX)
|
||||
|
||||
## Inset Highlight
|
||||
The signature Archy inset glow:
|
||||
```css
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
```
|
||||
Apply to headers, selected cards, active nav items.
|
||||
|
||||
## Border — No Separators Between Sections
|
||||
Per Archy theme rules: no borders between sidebar and content, or header and content. Only subtle `rgba(255,255,255,0.06-0.08)` borders for internal dividers.
|
||||
|
||||
## Gradient Text
|
||||
```html
|
||||
<h1 class="gradient-text">Title</h1>
|
||||
```
|
||||
`linear-gradient(to right, #ffffff, #9ca3af)` with `background-clip: text`.
|
||||
|
||||
## Focus States — Gamepad/Keyboard Glow
|
||||
All focusable elements get a blue glow (no outline):
|
||||
```css
|
||||
*:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 16px rgba(120, 180, 255, 0.2), 0 0 32px rgba(100, 160, 255, 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
## Scrollbar
|
||||
- `.custom-scrollbar` — gradient thumb (`rgba(255,255,255,0.3)` to `0.1`), dark track
|
||||
- `.scrollbar-hide` — hidden scrollbar, keeps scroll functionality
|
||||
|
||||
## Responsive — Mobile First
|
||||
Base styles for mobile, enhance with breakpoints:
|
||||
```html
|
||||
<div class="text-base md:text-lg lg:text-xl p-4 md:p-6 lg:p-8">
|
||||
```
|
||||
Breakpoints: `sm` (640px), `md` (768px), `lg` (1024px), `xl` (1280px), `2xl` (1536px).
|
||||
|
||||
## Hover States (from Archy)
|
||||
```html
|
||||
<div class="transition-all duration-300 hover:bg-white/10 hover:text-white">
|
||||
```
|
||||
Interactive card lift: `hover:translateY(-2px)` with intensified shadow.
|
||||
|
||||
## Animations (Archy timings)
|
||||
- `animate-fade-up` — 900ms `cubic-bezier(0.22, 1, 0.36, 1)` with 120ms delay
|
||||
- `animate-fade-up-fast` — 400ms, no delay (for chat messages)
|
||||
- `animate-fade-in` — 500ms ease
|
||||
- `animate-scale-in` — 250ms for modals/popups
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
description: Design system foundations - glassmorphism from Archy, colors, typography, spacing
|
||||
globs: "**/*.vue,**/*.css,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Design System
|
||||
|
||||
All glass morphism, container, and button patterns are ported from the Archy project and must match exactly.
|
||||
|
||||
## Glass Morphism Hierarchy (from Archy)
|
||||
|
||||
### Glass Intensity Levels
|
||||
| Class | Background | Blur | Use Case |
|
||||
|-------|-----------|------|----------|
|
||||
| `.glass` | `rgba(0,0,0,0.35)` | 18px | Sidebar, panels, inputs |
|
||||
| `.glass-strong` | `rgba(0,0,0,0.35)` | 24px | Headers, message bubbles (user) |
|
||||
| `.glass-card` | `rgba(0,0,0,0.65)` | 18px | Primary cards, modals, main containers |
|
||||
| `.gradient-card` | gradient white→black | 18px | Feature cards |
|
||||
| `.gradient-card-dark` | gradient black→black | 18px | Dark feature cards |
|
||||
|
||||
All share: `border: 1px solid rgba(255,255,255,0.18)`, `box-shadow: 0 8px 24px rgba(0,0,0,0.45)`.
|
||||
|
||||
### Button Hierarchy (from Archy)
|
||||
| Class | Purpose | Details |
|
||||
|-------|---------|---------|
|
||||
| `.glass-button` | Default | 48px height, `rgba(0,0,0,0.6)`, blur 18px |
|
||||
| `.glass-button-sm` | Compact | Auto height, smaller padding |
|
||||
| Ghost | Icon/text actions | `p-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10` |
|
||||
|
||||
### Inset Highlight
|
||||
Signature Archy top-edge glow on focused/active elements:
|
||||
```css
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
```
|
||||
|
||||
### Gradient Border (CSS mask technique)
|
||||
For premium-feel borders on selected cards and active nav:
|
||||
```css
|
||||
::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 2px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), transparent);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
```
|
||||
|
||||
## Design Tokens
|
||||
|
||||
### Color Palette
|
||||
Semantic color tokens defined by purpose:
|
||||
- `primary` — main brand actions (#606060)
|
||||
- `accent` — highlight, Bitcoin orange (#F7931A)
|
||||
- `success` — positive states (#10B981)
|
||||
- `error` — negative states (#EF4444)
|
||||
- `warning` — caution states (#F59E0B)
|
||||
- `info` — informational (#3B82F6)
|
||||
|
||||
### Glass Tokens (from Archy Tailwind config)
|
||||
- `glass-dark`: `rgba(0, 0, 0, 0.35)`
|
||||
- `glass-darker`: `rgba(0, 0, 0, 0.6)`
|
||||
- `glass-border`: `rgba(255, 255, 255, 0.18)`
|
||||
- `glass-highlight`: `rgba(255, 255, 255, 0.22)`
|
||||
|
||||
### Shadows
|
||||
- `shadow-glass`: `0 8px 24px rgba(0, 0, 0, 0.45)`
|
||||
- `shadow-glass-sm`: `0 6px 18px rgba(0, 0, 0, 0.35)`
|
||||
- `shadow-glass-inset`: `inset 0 1px 0 rgba(255, 255, 255, 0.22)`
|
||||
|
||||
### Typography
|
||||
- Body font: Inter, system-ui (AIUI default)
|
||||
- Mono font: Menlo, Monaco, Courier New
|
||||
- Text opacity scale: `text-white/25` (placeholders), `text-white/40` (muted), `text-white/60` (secondary), `text-white/70` (interactive default), `text-white/80` (body), `text-white/90` (emphasis), `text-white/96` (headings), `text-white` (active/selected)
|
||||
|
||||
### Spacing
|
||||
4px grid: `4, 8, 12, 16, 20, 24, 28, 32` px.
|
||||
|
||||
### Border Radius
|
||||
- `rounded-lg` (8px) — buttons, nav items, inputs
|
||||
- `rounded-xl` (12px) — toasts, small cards
|
||||
- `rounded-2xl` (16px) — main cards, modals
|
||||
- `rounded-3xl` (24px) — bottom sheets
|
||||
- `rounded-full` — pills, avatars, FABs
|
||||
- `1rem` (16px) — `.glass-card` default
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Cards
|
||||
Use `.glass-card` with additional padding:
|
||||
```html
|
||||
<div class="glass-card p-6">Content</div>
|
||||
```
|
||||
|
||||
### Modals
|
||||
```html
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div class="glass-card p-6 max-w-md w-full">...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Icons
|
||||
- SVG, using `currentColor`
|
||||
- Sizes: 16px, 20px, 24px, 32px
|
||||
- Icon-only buttons: `p-2 rounded-lg` (reaches 44px touch target with 24px icon)
|
||||
- Must have `aria-label`
|
||||
|
||||
## Theme Architecture
|
||||
- Base background: `#0a0a0a` (near-black)
|
||||
- No separator borders between sidebar/header/content
|
||||
- Header, sidebar, root share same visual weight
|
||||
- CSS-based themes with reactive Vue state
|
||||
- `localStorage` persistence
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
description: Component architecture principles - composition, patterns, and structure
|
||||
globs: "**/*.vue,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Component Architecture
|
||||
|
||||
## Core Philosophy: Composition Over Configuration
|
||||
Build complex UIs from simple, focused components that compose well together.
|
||||
|
||||
- Single Responsibility: each component does one thing well
|
||||
- Use slots instead of complex prop APIs
|
||||
- Provide sensible defaults
|
||||
- Clear TypeScript interfaces for props
|
||||
- Keep component state local and minimal
|
||||
|
||||
## Anti-Patterns
|
||||
- God components that do everything
|
||||
- Prop drilling through many layers (use provide/inject or Pinia)
|
||||
- Hard-coded values instead of props
|
||||
- Component logic mixed with layout
|
||||
- Tight coupling between components
|
||||
|
||||
## Compound Component Pattern
|
||||
Components that work together as a cohesive unit:
|
||||
```vue
|
||||
<Card>
|
||||
<Card.Header>Title</Card.Header>
|
||||
<Card.Body>Content</Card.Body>
|
||||
<Card.Footer>Actions</Card.Footer>
|
||||
</Card>
|
||||
```
|
||||
|
||||
## Container/Presenter Pattern
|
||||
Separate logic from presentation:
|
||||
- Container: handles data fetching, state, side effects
|
||||
- Presenter: pure rendering, receives data via props, emits events
|
||||
|
||||
## Slot Pattern (Vue)
|
||||
Use named slots for flexible content injection:
|
||||
```vue
|
||||
<template>
|
||||
<div class="section">
|
||||
<slot name="title" />
|
||||
<slot name="content" />
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Prop Interface Design
|
||||
```typescript
|
||||
interface BaseComponentProps {
|
||||
class?: string
|
||||
testId?: string
|
||||
}
|
||||
|
||||
interface ButtonProps extends BaseComponentProps {
|
||||
variant?: 'primary' | 'secondary' | 'ghost'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Component File Template
|
||||
```
|
||||
1. Imports (external, then internal)
|
||||
2. Types/Interfaces
|
||||
3. Constants
|
||||
4. Main component (props, emits, state, computed, methods, lifecycle)
|
||||
5. Sub-components (if any)
|
||||
```
|
||||
|
||||
## Error Boundaries
|
||||
Every major section should have error boundary handling via `onErrorCaptured`. Show fallback UI, never a blank screen.
|
||||
|
||||
## Responsive Components
|
||||
Use CSS-based responsive (`hidden md:block`) over JS-based (`useMediaQuery`) when possible. JS-based only when behavior changes (not just visibility).
|
||||
|
||||
## Component Checklist
|
||||
Before shipping any component:
|
||||
- [ ] TypeScript interface defined
|
||||
- [ ] Sensible default props
|
||||
- [ ] Loading and error states handled
|
||||
- [ ] ARIA attributes added
|
||||
- [ ] Keyboard navigation works
|
||||
- [ ] Responsive behavior tested
|
||||
- [ ] Dark mode styling works
|
||||
- [ ] Touch interactions verified on mobile
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
description: The five content surfaces that define how content is rendered in AIUI
|
||||
globs: "**/renderers/**,**/chat/**,**/content-panel/**"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Content Surfaces
|
||||
|
||||
AIUI has five distinct surfaces where content can appear. Every renderer must define how it behaves in each applicable surface.
|
||||
|
||||
## Surface 1: Chat Preview
|
||||
- Location: inline in chat message bubble
|
||||
- Max height: ~120px
|
||||
- Purpose: identify content at a glance (thumbnail, title, brief metadata)
|
||||
- Always tappable/clickable to expand to Panel Preview or Panel Play
|
||||
- Lightweight rendering only — no heavy libraries loaded
|
||||
- Examples: film poster thumbnail strip, file icon with name, code snippet (first 5 lines), image thumbnail
|
||||
|
||||
## Surface 2: Chat Play
|
||||
- Location: inline in chat message bubble
|
||||
- Max height: ~200px
|
||||
- Purpose: inline playback without leaving the chat
|
||||
- Must not disrupt chat scrolling
|
||||
- Has an "expand" button to open in Panel Play
|
||||
- Examples: voice note waveform with play button, short video player, audio player, small interactive widget
|
||||
|
||||
## Surface 3: Panel Preview
|
||||
- Location: content panel (beside chat on desktop, overlay on mobile)
|
||||
- No height limit (scrollable within panel)
|
||||
- Purpose: full browsing/exploration experience
|
||||
- Supports: filtering, sorting, searching, pagination
|
||||
- Click items to go to Panel Play or Panel Edit
|
||||
- Examples: film grid (tiled, filterable), image gallery, search results list, document preview, file tree
|
||||
|
||||
## Surface 4: Panel Play
|
||||
- Location: content panel
|
||||
- Purpose: full immersive media playback
|
||||
- Examples: full video player with controls, audio with spectrum visualization, slideshow, trailer playback
|
||||
|
||||
## Surface 5: Panel Edit/Interactive
|
||||
- Location: content panel
|
||||
- Purpose: full interaction and editing
|
||||
- Changes can be sent back to chat as new messages
|
||||
- Examples: code editor (CodeMirror), form filling, approval workflow, spreadsheet editing, diagram creation
|
||||
|
||||
## Surface Transitions
|
||||
```
|
||||
Chat Preview --tap--> Panel Preview --tap item--> Panel Play
|
||||
--tap item--> Panel Edit
|
||||
Chat Play --expand--> Panel Play
|
||||
Panel Edit --submit--> Chat (new message with result)
|
||||
```
|
||||
|
||||
## Renderer Interface
|
||||
Every renderer must export:
|
||||
```typescript
|
||||
interface RendererDefinition {
|
||||
id: string
|
||||
name: string
|
||||
contentType: string // MIME-like type identifier
|
||||
surfaces: SurfaceType[] // which surfaces this renderer supports
|
||||
chatPreview?: Component // Surface 1
|
||||
chatPlay?: Component // Surface 2
|
||||
panelPreview?: Component // Surface 3
|
||||
panelPlay?: Component // Surface 4
|
||||
panelEdit?: Component // Surface 5
|
||||
lazyDependencies?: () => Promise<any> // heavy libs loaded on demand
|
||||
}
|
||||
```
|
||||
|
||||
## Mobile Behavior
|
||||
- On mobile, there is no side-by-side layout
|
||||
- Panel surfaces open as a full-screen overlay or bottom sheet
|
||||
- Chat Preview and Chat Play remain inline
|
||||
- Transition: tap Chat Preview → full-screen Panel Preview (slide up)
|
||||
- Back gesture or button returns to chat
|
||||
|
||||
## Performance Rules
|
||||
- Chat Preview and Chat Play must render with zero lazy-loaded dependencies
|
||||
- Panel surfaces may lazy-load heavy libraries (CodeMirror, pdf.js, etc.)
|
||||
- Never block the chat scroll with renderer loading
|
||||
- Use skeleton/placeholder while panel content loads
|
||||
|
||||
## Content Type Expert Rules
|
||||
For extraction, parsing, and surfacing logic, see:
|
||||
- `20-content-films.mdc` — Films
|
||||
- `21-content-songs.mdc` — Songs (includes looksLikeSong blocklist)
|
||||
- `22-content-podcasts.mdc` — Podcasts (includes looksLikePodcast)
|
||||
- `23-content-news.mdc` — News + RSS, ArticleDetail security
|
||||
- `24-content-websites.mdc` — Websites vs News, overlay
|
||||
- `25-content-magazine.mdc` — Magazine/Brief parsing, hero, meme
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
description: Plugin architecture rules - interfaces, registration, lifecycle, sandboxing
|
||||
globs: "**/plugins/**,**/*.plugin.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Plugin System
|
||||
|
||||
## Philosophy
|
||||
Every external integration connects through a typed plugin interface. No direct coupling to any service, provider, or protocol.
|
||||
|
||||
## Plugin Types
|
||||
```typescript
|
||||
type PluginType =
|
||||
| 'ai-provider' // LLM backends (OpenRouter, Ollama, Claude, etc.)
|
||||
| 'media-source' // Content sources (Plex, YouTube, Nextcloud, Archive.org)
|
||||
| 'messaging' // Chat protocols (Nostr, Matrix, local)
|
||||
| 'storage' // File storage (local FS, IPFS, Nextcloud)
|
||||
| 'renderer' // Custom content renderers
|
||||
| 'file-handler' // File open/preview handlers
|
||||
| 'crypto' // Encryption providers
|
||||
| 'search' // Search backends (SearXNG, local)
|
||||
| 'auth' // Authentication (Nostr keys, DID, passkeys)
|
||||
| 'wallet' // Bitcoin wallet deep-linking (Phoenix, Zeus, Alby, etc.)
|
||||
| 'social-embed' // Social post fetching (X, Nostr, Mastodon)
|
||||
| 'mcp' // Model Context Protocol servers
|
||||
| 'media' // Media processing (ffmpeg.wasm, whisper, TTS)
|
||||
```
|
||||
|
||||
## Base Plugin Interface
|
||||
```typescript
|
||||
interface AIUIPlugin {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
type: PluginType
|
||||
description?: string
|
||||
icon?: string
|
||||
init(context: PluginContext): Promise<void>
|
||||
destroy(): Promise<void>
|
||||
isAvailable(): Promise<boolean>
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Context
|
||||
Plugins receive a context object with access to:
|
||||
- Settings store (read/write plugin-specific settings)
|
||||
- Event bus (emit/listen for app events)
|
||||
- Logger (structured logging)
|
||||
- Crypto utilities (for encrypting plugin data at rest)
|
||||
|
||||
Plugins do NOT receive:
|
||||
- Direct DOM access (community plugins)
|
||||
- File system access (without explicit capability grant)
|
||||
- Network access to arbitrary hosts (without declaration)
|
||||
|
||||
## Sandboxing Tiers
|
||||
|
||||
### Tier 1: Trusted (built-in, official)
|
||||
Run in main thread with full API access. AI adapters, core renderers, crypto providers.
|
||||
|
||||
### Tier 2: Community
|
||||
Run in sandboxed iframes with `postMessage` API. Custom renderers, themes, visual extensions. Cannot access host DOM, file system, or network directly.
|
||||
|
||||
### Tier 3: External Processes
|
||||
MCP servers, local AI runners. Run as separate processes (Tauri IPC) or connect via HTTP. Isolated by OS process boundary.
|
||||
|
||||
## Plugin Lifecycle
|
||||
1. `register()` — declare plugin to registry
|
||||
2. `init()` — plugin sets up, connects to services
|
||||
3. Active — plugin responds to requests
|
||||
4. `destroy()` — cleanup on disable/uninstall
|
||||
|
||||
## Registration
|
||||
```typescript
|
||||
import { registerPlugin } from '@aiui/core'
|
||||
|
||||
registerPlugin({
|
||||
id: 'ai-openrouter',
|
||||
name: 'OpenRouter',
|
||||
type: 'ai-provider',
|
||||
version: '1.0.0',
|
||||
async init(ctx) { /* setup */ },
|
||||
async destroy() { /* cleanup */ },
|
||||
// ... adapter methods
|
||||
})
|
||||
```
|
||||
|
||||
## Plugin Settings
|
||||
Each plugin can declare settings schema. Settings are stored encrypted and exposed through a standard settings UI.
|
||||
|
||||
## Rules
|
||||
- Every plugin must declare its type
|
||||
- Every plugin must implement `init()` and `destroy()`
|
||||
- Every plugin must implement `isAvailable()` to report its status
|
||||
- Plugins must handle errors gracefully — never crash the host
|
||||
- Community plugins must not load external scripts
|
||||
- All network requests must go through the plugin context (for privacy/proxy control)
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
description: AI adapter patterns, streaming, tool calling, context injection
|
||||
globs: "**/ai/**,**/plugins/ai-*/**"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# AI Integration
|
||||
|
||||
## Universal AI Adapter
|
||||
All AI providers connect through the `AIProviderAdapter` interface:
|
||||
|
||||
```typescript
|
||||
interface AIProviderAdapter extends AIUIPlugin {
|
||||
type: 'ai-provider'
|
||||
chat(messages: Message[], options: ChatOptions): AsyncIterable<ChatChunk>
|
||||
models(): Promise<Model[]>
|
||||
supportsStreaming: boolean
|
||||
supportsVision: boolean
|
||||
supportsTools: boolean
|
||||
supportsMultimodal: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Provider Hierarchy
|
||||
1. **OpenAI-Compatible Adapter** — covers OpenRouter, Ollama, vLLM, llama.cpp, LocalAI, Mistral, DeepSeek, xAI, Qwen. Just change `baseURL` + API key.
|
||||
2. **Anthropic Adapter** — Claude. Different tool_use format (content blocks vs tool_calls).
|
||||
3. **Gemini Adapter** — Google. Different multimodal format.
|
||||
4. **MCP Client** — connects to any MCP server for tools, resources, prompts.
|
||||
|
||||
## Streaming
|
||||
- All AI responses use Server-Sent Events (SSE) over HTTP
|
||||
- Pattern: `data: {"token": "Hello"}\n\n` with `data: [DONE]\n\n` termination
|
||||
- Client: parse SSE stream, feed tokens to `StreamingTextRenderer`
|
||||
- Always show a typing indicator while waiting for first token
|
||||
- Handle connection drops gracefully (show error, offer retry)
|
||||
|
||||
## Tool Calling
|
||||
AI can invoke tools. The adapter normalizes tool call formats:
|
||||
```typescript
|
||||
interface ToolCall {
|
||||
id: string
|
||||
name: string
|
||||
arguments: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface ToolResult {
|
||||
toolCallId: string
|
||||
content: string | StructuredContent
|
||||
isError: boolean
|
||||
}
|
||||
```
|
||||
|
||||
Normalize across providers:
|
||||
- OpenAI: `tool_calls` in assistant message → `role: "tool"` result
|
||||
- Claude: `type: "tool_use"` content block → `tool_result` in user message
|
||||
- Map both to AIUI's unified `ToolCall` / `ToolResult` types
|
||||
|
||||
## Context Injection
|
||||
The system prompt includes context about the user's environment:
|
||||
- Connected media sources and their capabilities
|
||||
- Available tools and plugins
|
||||
- User preferences (language, theme, preferred wallet)
|
||||
- In dev mode: mock data summaries
|
||||
|
||||
Never include sensitive data (API keys, passwords) in system prompts.
|
||||
|
||||
## Model Selection
|
||||
Users can switch models within a conversation. The UI shows:
|
||||
- Available models from all connected providers
|
||||
- Model capabilities (vision, tools, streaming)
|
||||
- Cost per token in sats (if applicable)
|
||||
|
||||
## Dev Mode
|
||||
- `VITE_OPENROUTER_API_KEY` in `.env.local`
|
||||
- Free models available (Llama, Mistral via OpenRouter)
|
||||
- Mock tool responses available via dev fixtures
|
||||
- Debug panel shows: raw messages, token count, latency
|
||||
|
||||
## Error Handling
|
||||
- Rate limits: show user-friendly message, auto-retry with backoff
|
||||
- Auth errors: prompt to check API key in settings
|
||||
- Network errors: show offline indicator, queue message for retry
|
||||
- Model errors: show error in chat, suggest alternative model
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
description: How to build content renderers - interfaces, lazy loading, accessibility
|
||||
globs: "**/renderers/**"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Renderer Development
|
||||
|
||||
## What is a Renderer?
|
||||
A renderer is a set of Vue components that know how to display a specific content type across the five content surfaces (chat-preview, chat-play, panel-preview, panel-play, panel-edit).
|
||||
|
||||
## Renderer Registration
|
||||
```typescript
|
||||
import { registerRenderer } from '@aiui/core'
|
||||
|
||||
registerRenderer({
|
||||
id: 'film',
|
||||
name: 'Film',
|
||||
contentType: 'application/x-aiui-film',
|
||||
surfaces: ['chat-preview', 'panel-preview', 'panel-play'],
|
||||
chatPreview: () => import('./FilmChatPreview.vue'),
|
||||
panelPreview: () => import('./FilmGrid.vue'),
|
||||
panelPlay: () => import('./FilmDetail.vue'),
|
||||
})
|
||||
```
|
||||
|
||||
## Content Type Detection
|
||||
Renderers are matched to content by `contentType` field in the message data:
|
||||
```typescript
|
||||
interface ContentBlock {
|
||||
contentType: string // e.g., 'application/x-aiui-film'
|
||||
data: Record<string, unknown> // renderer-specific data
|
||||
title?: string // human-readable title for panel tab
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Rules
|
||||
1. Chat surfaces (preview, play) must render with ZERO lazy-loaded heavy dependencies
|
||||
2. Panel surfaces may lazy-load libraries (CodeMirror, pdf.js, etc.)
|
||||
3. Use `defineAsyncComponent` for panel components
|
||||
4. Show skeleton/placeholder while loading
|
||||
5. Never block the main thread — use Web Workers for heavy parsing
|
||||
|
||||
## Data Contracts
|
||||
Each renderer defines its expected data shape as a TypeScript interface:
|
||||
```typescript
|
||||
interface FilmRendererData {
|
||||
films: Film[]
|
||||
query?: string
|
||||
filters?: FilmFilters
|
||||
}
|
||||
```
|
||||
Document the interface. Validate incoming data. Show graceful error if data is malformed.
|
||||
|
||||
## Accessibility Requirements
|
||||
- All renderers must be keyboard navigable
|
||||
- Images need alt text
|
||||
- Interactive elements need ARIA labels
|
||||
- Media players need captions/transcripts when available
|
||||
- Focus management when transitioning between surfaces
|
||||
|
||||
## Mobile Behavior
|
||||
- Chat Preview: constrained to message bubble width
|
||||
- Chat Play: full message width, max 200px height
|
||||
- Panel surfaces on mobile: full-screen overlay with back gesture
|
||||
- Touch targets: minimum 44x44px
|
||||
- Swipe gestures where appropriate (image gallery, film cards)
|
||||
|
||||
## Renderer Checklist
|
||||
- [ ] TypeScript data interface defined and exported
|
||||
- [ ] All applicable surfaces implemented
|
||||
- [ ] Lazy loading for heavy dependencies
|
||||
- [ ] Skeleton/placeholder states
|
||||
- [ ] Error state (malformed data)
|
||||
- [ ] Empty state (no data)
|
||||
- [ ] Keyboard navigation
|
||||
- [ ] ARIA labels on interactive elements
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Dark mode compatible
|
||||
- [ ] Transition animations (per motion design rules)
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
description: Cryptography and security rules - E2E encryption, key management, storage
|
||||
globs: "**/crypto/**,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Security & Cryptography
|
||||
|
||||
## Principles
|
||||
- Privacy is a requirement, not a feature
|
||||
- Zero telemetry, zero analytics unless user explicitly opts in
|
||||
- Never transmit unencrypted sensitive data
|
||||
- Never store plaintext credentials
|
||||
- Minimal data collection — store only what's needed
|
||||
|
||||
## Encryption Stack
|
||||
|
||||
### E2E Message Encryption
|
||||
- Library: **tweetnacl.js** (6KB, audited by Cure53)
|
||||
- Algorithm: XSalsa20-Poly1305 via NaCl `box` (public-key authenticated encryption)
|
||||
- Each conversation has a shared secret derived from key exchange
|
||||
|
||||
### Local Storage Encryption
|
||||
- Library: **Web Crypto API** (native, zero bundle cost)
|
||||
- Algorithm: AES-256-GCM for encrypting IndexedDB values
|
||||
- Key derived from user's master password via PBKDF2 (100K+ iterations)
|
||||
|
||||
### Key Management
|
||||
- **Desktop (Tauri)**: OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service)
|
||||
- **Web**: Encrypted IndexedDB with user-derived key
|
||||
- **Nostr compatibility**: secp256k1 keys via @noble/curves, NIP-07 browser extension support
|
||||
- **Passkeys/WebAuthn**: For passwordless authentication
|
||||
|
||||
### Credential Storage
|
||||
- API keys encrypted at rest using AES-256-GCM
|
||||
- Never stored in localStorage (use encrypted IndexedDB or OS keychain)
|
||||
- Never included in logs, error reports, or system prompts
|
||||
- Display as masked values in settings UI (show last 4 chars only)
|
||||
|
||||
## Dev Mode Bypass
|
||||
When `VITE_DISABLE_CRYPTO=true` (dev only):
|
||||
- Skip E2E encryption (messages stored in plain text)
|
||||
- Skip storage encryption (IndexedDB unencrypted)
|
||||
- API keys stored in `.env.local` (gitignored)
|
||||
- This flag must NEVER exist in production builds
|
||||
|
||||
## Security Rules for Code
|
||||
- Never log sensitive data (keys, tokens, passwords, message content)
|
||||
- Never include secrets in error messages
|
||||
- Sanitize all user input before rendering (XSS prevention)
|
||||
- Use Content Security Policy headers
|
||||
- Validate all data from plugins before rendering
|
||||
- Community plugins run in sandboxed iframes (no direct DOM access)
|
||||
- Never eval() or innerHTML with untrusted content
|
||||
|
||||
## Network Security
|
||||
- All external requests over HTTPS only
|
||||
- Certificate pinning for known services (Tauri)
|
||||
- Proxy social media fetches to avoid leaking user IP
|
||||
- No third-party tracking scripts, analytics, or telemetry SDKs
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
description: Bitcoin-only payment and monetary policy - on-chain, Lightning, ecash
|
||||
globs: "**/*"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Bitcoin Only
|
||||
|
||||
## Core Rule
|
||||
Bitcoin is the only monetary unit in AIUI. This applies everywhere — UI labels, data models, API responses, documentation, and conversation context.
|
||||
|
||||
## Supported Payment Protocols
|
||||
- **On-chain Bitcoin**: BIP21 URI scheme (`bitcoin:bc1q...?amount=0.001`)
|
||||
- **Lightning Network**: BOLT11 invoices, LNURL-pay, LNURL-withdraw, keysend
|
||||
- **Cashu ecash**: Cashu tokens, mint interactions (`cashu:` URI, `web+cashu:`)
|
||||
- **Fedimint/Fedi**: Federation ecash (`fedi:` URI)
|
||||
- **Nostr Zaps**: NIP-57 Lightning zaps (social tipping)
|
||||
|
||||
## AIUI is NEVER a Wallet
|
||||
|
||||
### Never Do
|
||||
- Store private keys or seed phrases
|
||||
- Sign Bitcoin transactions
|
||||
- Build or broadcast transactions
|
||||
- Track wallet balances
|
||||
- Display transaction history
|
||||
- Create send/receive screens
|
||||
- Implement payment processing logic
|
||||
- Hold funds in custody
|
||||
|
||||
### Always Do
|
||||
- Construct deep-link URIs and hand off to external wallet apps
|
||||
- Detect installed wallet apps (via URI scheme probing or Tauri app detection)
|
||||
- Let users configure preferred wallets in settings
|
||||
- Display payment requests as QR codes with "Open in Wallet" buttons
|
||||
- Show invoice/address details (amount, memo, expiry) as read-only information
|
||||
|
||||
## Wallet Deep-Linking
|
||||
```typescript
|
||||
// Construct URI, open external wallet — that's it
|
||||
const uri = `lightning:${bolt11Invoice}`
|
||||
window.open(uri) // or Tauri shell.open(uri)
|
||||
```
|
||||
|
||||
Supported wallet URI schemes:
|
||||
- `bitcoin:` — BIP21 (any on-chain wallet)
|
||||
- `lightning:` — BOLT11 (any Lightning wallet)
|
||||
- `cashu:` — Cashu tokens
|
||||
- `fedi:` — Fedimint
|
||||
- Wallet-specific: `phoenix://`, `zeus://`, `mutiny://`, `alby://`
|
||||
|
||||
## Denomination
|
||||
- Primary unit: **sats** (1 BTC = 100,000,000 sats)
|
||||
- Display: `1,234 sats` or `₿0.00001234`
|
||||
- User preference: sats or BTC (configurable in settings)
|
||||
- AI cost tracking: show token costs in sats
|
||||
|
||||
## Prohibited
|
||||
- No fiat currencies (USD, EUR, etc.) — not in UI, not in code, not in variable names
|
||||
- No altcoins or tokens
|
||||
- No stablecoins (USDT, USDC, etc.)
|
||||
- No fiat-denominated pricing
|
||||
- No payment processor integrations (Stripe, PayPal, etc.)
|
||||
- No KYC/AML flows
|
||||
|
||||
## Renderer Components
|
||||
- `LightningInvoiceRenderer` — BOLT11 QR + amount + memo + "Open in Wallet"
|
||||
- `BitcoinAddressRenderer` — BIP21 QR + "Open in Wallet"
|
||||
- `CashuTokenRenderer` — ecash token + mint info + "Redeem in Wallet"
|
||||
- `FedimintRenderer` — federation ecash + "Open in Fedi"
|
||||
- `PaymentRequestRenderer` — unified card with payment method options
|
||||
- `ZapRenderer` — Nostr zap display (NIP-57)
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
description: Development vs production configuration, feature flags, mock data patterns
|
||||
globs: "**/*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Dev & Prod Modes
|
||||
|
||||
## Development Mode
|
||||
|
||||
### Environment
|
||||
```env
|
||||
# .env.local (gitignored)
|
||||
VITE_OPENROUTER_API_KEY=sk-or-...
|
||||
VITE_TMDB_API_KEY=...
|
||||
VITE_DEV_MODE=true
|
||||
VITE_MOCK_MEDIA_SOURCES=true
|
||||
VITE_DISABLE_CRYPTO=true
|
||||
```
|
||||
|
||||
### What's Enabled
|
||||
- Hot reload via Vite HMR
|
||||
- Debug panel overlay (AI context, plugin status, renderer registry, message data)
|
||||
- Mock media source plugins (Plex, YouTube, Nextcloud from JSON fixtures)
|
||||
- OpenRouter AI connection (real API, free models available)
|
||||
- Component playground (Storybook/Histoire)
|
||||
- Verbose logging
|
||||
- TypeScript strict mode
|
||||
- All renderers available without lazy loading (for dev speed)
|
||||
|
||||
### What's Disabled
|
||||
- E2E encryption (plain text messages for debugging)
|
||||
- Storage encryption (plain IndexedDB)
|
||||
- Tauri features (dev runs as pure web app)
|
||||
- Production optimizations (tree-shaking, minification)
|
||||
- Service worker / offline mode
|
||||
|
||||
### Mock Data
|
||||
- Film fixtures: 50-100 films with real TMDB poster URLs
|
||||
- Media source mocks: JSON files returning fake Plex/YouTube/Nextcloud responses
|
||||
- Located in: `packages/app/src/mocks/`
|
||||
- Auto-loaded when `VITE_MOCK_MEDIA_SOURCES=true`
|
||||
- Mock data must match production data interfaces exactly
|
||||
|
||||
### Dev Scripts
|
||||
```
|
||||
pnpm dev # Web dev server
|
||||
pnpm dev:desktop # Tauri dev (when needed)
|
||||
pnpm storybook # Component playground
|
||||
pnpm test # Vitest
|
||||
pnpm lint # ESLint + Prettier
|
||||
pnpm typecheck # TypeScript
|
||||
pnpm build # Production build
|
||||
pnpm turbo build # Turborepo cached build
|
||||
```
|
||||
|
||||
## Production Mode
|
||||
|
||||
### What's Enabled
|
||||
- E2E encryption for all messages
|
||||
- Encrypted local storage
|
||||
- Key management via OS keychain (Tauri) or encrypted IndexedDB (web)
|
||||
- User-configured AI providers (settings page)
|
||||
- Real media source connections (Plex API, YouTube, etc.)
|
||||
- Optimized builds (tree-shaken, code-split, minified)
|
||||
- Lazy loading for all heavy renderers
|
||||
- Service worker for offline support
|
||||
- Auto-update (Tauri)
|
||||
|
||||
### What's Disabled
|
||||
- Debug panels
|
||||
- Mock data
|
||||
- Dev logging
|
||||
- Source maps (in distributed builds)
|
||||
- `VITE_DISABLE_CRYPTO` flag (must not exist)
|
||||
|
||||
### Build Targets
|
||||
- Web: Static SPA bundle (< 250KB initial gzipped)
|
||||
- Desktop: Tauri app (macOS .dmg, Windows .msi, Linux .AppImage)
|
||||
- Mobile: Tauri mobile (iOS .ipa, Android .apk)
|
||||
|
||||
## Feature Flags
|
||||
Use composable `useFeatureFlags()`:
|
||||
```typescript
|
||||
const { isDev, isTauri, isMobile, isCryptoEnabled, isMockData } = useFeatureFlags()
|
||||
```
|
||||
|
||||
Gate platform-specific features:
|
||||
```typescript
|
||||
if (isTauri()) {
|
||||
// Native file system access
|
||||
} else {
|
||||
// File System Access API or file picker
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variable Rules
|
||||
- All env vars prefixed with `VITE_` (Vite requirement)
|
||||
- Secrets only in `.env.local` (gitignored)
|
||||
- `.env.example` committed with placeholder values
|
||||
- Never read `process.env` directly — use typed config module
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
description: Accessibility standards - WCAG AA, keyboard navigation, screen readers
|
||||
globs: "**/*.vue"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Accessibility
|
||||
|
||||
## Standard
|
||||
WCAG AA compliance minimum. Target AAA where feasible.
|
||||
|
||||
## Color Contrast
|
||||
- Normal text: 4.5:1 minimum ratio
|
||||
- Large text (18px+ or 14px+ bold): 3:1 minimum
|
||||
- Interactive elements: 3:1 against adjacent colors
|
||||
- Test with browser DevTools accessibility panel
|
||||
|
||||
## Keyboard Navigation
|
||||
- All interactive elements focusable via Tab
|
||||
- Visible focus indicators on every focusable element (`focus:ring-2`)
|
||||
- Escape closes modals, drawers, dropdowns
|
||||
- Arrow keys navigate within lists, grids, tabs
|
||||
- Enter/Space activates buttons and controls
|
||||
- Focus trap inside modals (Tab cycles within modal)
|
||||
|
||||
## Semantic HTML
|
||||
```html
|
||||
<header>, <nav>, <main>, <article>, <aside>, <footer>
|
||||
```
|
||||
Never `<div class="header">`. Use semantic elements.
|
||||
|
||||
## ARIA
|
||||
- Icon-only buttons: `aria-label="Close modal"`
|
||||
- Dynamic content: `aria-live="polite"` for updates
|
||||
- Screen reader only text: `class="sr-only"`
|
||||
- Expandable sections: `aria-expanded="true/false"`
|
||||
- Form fields: `aria-describedby` for help text, `aria-invalid` for errors
|
||||
|
||||
## Images
|
||||
- All `<img>` tags need `alt` text
|
||||
- Decorative images: `alt=""`
|
||||
- Complex images: `aria-describedby` pointing to description
|
||||
|
||||
## Media
|
||||
- Audio/video players: keyboard-accessible controls
|
||||
- Provide transcripts/captions when available
|
||||
- Respect `prefers-reduced-motion` for animations
|
||||
|
||||
## Touch Targets
|
||||
- Minimum: 44x44px (Apple HIG)
|
||||
- Recommended: 48x48px (Material Design)
|
||||
- Minimum 8px gap between adjacent targets
|
||||
|
||||
## Reduced Motion
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
Check in JS: `window.matchMedia('(prefers-reduced-motion: reduce)').matches`
|
||||
|
||||
## Testing
|
||||
- VoiceOver (macOS), TalkBack (Android), NVDA (Windows)
|
||||
- Keyboard-only navigation test
|
||||
- axe DevTools or Lighthouse accessibility audit
|
||||
- High contrast mode test
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
description: Performance optimization - bundle budget, lazy loading, virtual scrolling
|
||||
globs: "**/*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Performance
|
||||
|
||||
## Bundle Budget
|
||||
- Initial load: **< 250KB gzipped**
|
||||
- Core (Vue + Tailwind + Pinia + Router + chat UI): ~150KB
|
||||
- First renderer batch (markdown, streaming text): ~50KB
|
||||
- Everything else: lazy-loaded on demand
|
||||
|
||||
## Lazy Loading Strategy
|
||||
- Route-based code splitting via Vue Router `() => import(...)`
|
||||
- Renderer components via `defineAsyncComponent`
|
||||
- Heavy libraries loaded only when their renderer is activated:
|
||||
- CodeMirror 6: ~300KB (on code edit)
|
||||
- Monaco: ~5MB (on IDE panel open)
|
||||
- pdf.js: ~400KB (on PDF view)
|
||||
- KaTeX: ~300KB (on math render)
|
||||
- Mermaid: ~200KB (on diagram render)
|
||||
- Leaflet: ~40KB (on map render)
|
||||
- Whisper WASM: ~50MB (on STT activation, cached)
|
||||
- Piper TTS: ~100MB (on TTS activation, cached)
|
||||
|
||||
## Virtual Scrolling
|
||||
- Chat message list uses TanStack Virtual
|
||||
- Dynamic row heights (messages vary in size)
|
||||
- Inverted scroll (newest at bottom, load older on scroll up)
|
||||
- Buffer: render 5 items above and below viewport
|
||||
- Recycle DOM nodes for off-screen messages
|
||||
|
||||
## GPU Acceleration
|
||||
Only animate `transform` and `opacity` — never `width`, `height`, `top`, `left`.
|
||||
Use `will-change` sparingly and remove after animation.
|
||||
|
||||
## Image Optimization
|
||||
- Use `loading="lazy"` on all non-critical images
|
||||
- Provide `srcset` with multiple sizes
|
||||
- Use WebP/AVIF where supported
|
||||
- Skeleton placeholders while loading
|
||||
|
||||
## Network
|
||||
- Preconnect to known API hosts
|
||||
- Preload critical resources
|
||||
- Debounce scroll and resize handlers (100ms)
|
||||
- Batch API requests where possible
|
||||
|
||||
## Memory
|
||||
- Clean up event listeners in `onUnmounted`
|
||||
- Use `shallowRef` for large data sets
|
||||
- Dispose heavy library instances when panel closes
|
||||
- Monitor memory with browser DevTools
|
||||
|
||||
## Core Web Vitals Targets
|
||||
- LCP (Largest Contentful Paint): < 2.5s
|
||||
- FID (First Input Delay): < 100ms
|
||||
- CLS (Cumulative Layout Shift): < 0.1
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
description: Animation principles - timing, easing, stagger, reduced motion
|
||||
globs: "**/*.vue,**/*.css"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Animation & Motion Design
|
||||
|
||||
## Philosophy
|
||||
Every animation serves a purpose: guide attention, provide feedback, show relationships, enhance perceived performance, or add delight. Never animate for decoration alone.
|
||||
|
||||
## Duration Scale
|
||||
```
|
||||
100ms - Instant: micro-feedback (hover states, button press)
|
||||
200ms - Fast: small elements (tooltips, dropdowns)
|
||||
300ms - Moderate: standard UI transitions (modals, cards)
|
||||
500ms - Normal: page sections, complex components
|
||||
600ms - Slow: hero animations, page transitions (max for UI)
|
||||
```
|
||||
Never exceed 600ms for UI element animations.
|
||||
|
||||
## Easing Functions
|
||||
- **ease-out** (90% of animations): elements entering viewport
|
||||
- **ease-in**: elements exiting viewport
|
||||
- **ease-in-out**: elements moving within viewport
|
||||
- **spring**: playful interactions (button press, drag-and-drop)
|
||||
- **linear**: progress bars, loading spinners only
|
||||
|
||||
Custom smooth deceleration: `cubic-bezier(0.16, 1, 0.3, 1)`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Fade & Slide Up (entrance)
|
||||
```css
|
||||
@keyframes fadeSlideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
```
|
||||
|
||||
### Scale & Fade (emphasis)
|
||||
```css
|
||||
@keyframes scaleIn {
|
||||
from { opacity: 0; transform: scale(0.8); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
```
|
||||
|
||||
### Hover feedback
|
||||
```css
|
||||
.interactive {
|
||||
transition: transform 0.1s ease, opacity 0.1s ease;
|
||||
}
|
||||
.interactive:active {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
}
|
||||
```
|
||||
|
||||
## Staggered Animations
|
||||
When animating multiple elements, stagger by 50-150ms per item:
|
||||
```css
|
||||
.card { animation-delay: calc(var(--index) * 0.1s); }
|
||||
```
|
||||
Max items in a stagger cascade: 6-8. Total cascade: under 1 second.
|
||||
|
||||
## Reduced Motion
|
||||
Always respect `prefers-reduced-motion`. Provide instant transitions as fallback.
|
||||
|
||||
## Performance
|
||||
- Only animate `transform` and `opacity` (GPU-composited)
|
||||
- Use `will-change` sparingly, remove after animation
|
||||
- Limit simultaneous animations
|
||||
- Use `requestAnimationFrame` for JS animations
|
||||
|
||||
## Loading States
|
||||
- Skeleton shimmer: 2s infinite, `linear-gradient` sweep
|
||||
- Pulse: 2s infinite, opacity 1 → 0.5 → 1
|
||||
- Spinner: 1s infinite linear rotation
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
description: Mobile UX patterns informed by Apple iOS HIG — touch targets, typography, spacing, navigation, animations
|
||||
globs: "**/*.vue,**/*.css"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Mobile UX (iOS HIG-Informed)
|
||||
|
||||
## Philosophy
|
||||
Design for mobile first, enhance for desktop. Follow Apple iOS Human Interface Guidelines for sizing, spacing, and interaction patterns. Adapt native iOS conventions to our glass morphism dark theme.
|
||||
|
||||
## Typography (iOS Dynamic Type Mapped to CSS)
|
||||
|
||||
| iOS Text Style | Default Size | CSS Equivalent | AIUI Usage |
|
||||
|---|---|---|---|
|
||||
| Large Title | 34pt | `text-[34px]` / `text-3xl` | Page titles (rare) |
|
||||
| Title 1 | 28pt | `text-[28px]` / `text-2xl` | Section headers |
|
||||
| Title 2 | 22pt | `text-[22px]` / `text-xl` | Sub-section headers |
|
||||
| Title 3 | 20pt | `text-[20px]` / `text-lg` | Card titles |
|
||||
| Headline | 17pt semibold | `text-[17px] font-semibold` | Emphasis labels |
|
||||
| Body | 17pt | `text-[17px]` / `text-base` | Primary content |
|
||||
| Callout | 16pt | `text-[16px]` | Secondary content |
|
||||
| Subheadline | 15pt | `text-[15px]` | Metadata |
|
||||
| Footnote | 13pt | `text-[13px]` / `text-xs` | Timestamps, captions |
|
||||
| Caption 1 | 12pt | `text-[12px]` | Badges, small labels |
|
||||
| Caption 2 | 11pt | `text-[11px]` | Smallest text (tab labels) |
|
||||
|
||||
### Key rules
|
||||
- **Minimum text size**: 11px (Caption 2) — never go smaller
|
||||
- **Body text on mobile**: 17px (not 14px/16px) for comfortable reading
|
||||
- Use `text-sm` (14px) sparingly — only for dense UI, not primary reading content
|
||||
- Chat messages should use at least 15-16px on mobile
|
||||
- Metadata/timestamps: 11-13px is acceptable
|
||||
|
||||
## Touch Targets
|
||||
|
||||
| Rule | Value | Tailwind |
|
||||
|---|---|---|
|
||||
| Minimum tap target | **44 × 44px** | `min-w-[44px] min-h-[44px]` |
|
||||
| Minimum gap between targets | **8px** | `gap-2` |
|
||||
| Comfortable button height | 44-50px | `h-11` to `h-[50px]` |
|
||||
| iOS nav bar button | 44px | `h-11` |
|
||||
|
||||
### Key rules
|
||||
- The 44px minimum applies to the **tappable area**, not the visual size
|
||||
- A 24px icon can have a 44px tap target via padding: `p-2.5` on a 24px icon
|
||||
- Our `w-9 h-9` (36px) header buttons are below 44px — compensate with generous spacing or padding hit areas
|
||||
- Text buttons must extend touch target beyond text bounds
|
||||
|
||||
## Spacing & Layout
|
||||
|
||||
| Element | iOS Value | CSS |
|
||||
|---|---|---|
|
||||
| Side margins (iPhone) | 16px | `px-4` |
|
||||
| Nav bar height | 44px | `h-11` |
|
||||
| Tab bar height | 49px (+34px safe area) | `h-[49px]` + `pb-[env(safe-area-inset-bottom)]` |
|
||||
| Bottom safe area (notch) | 34px | `env(safe-area-inset-bottom)` |
|
||||
| Search bar | 36px field + 8px padding | `h-9` + `py-1` |
|
||||
| Standard content inset | 16px horizontal | `px-4` |
|
||||
|
||||
### Safe area insets
|
||||
```css
|
||||
/* Always use for full-screen layouts */
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
height: 100dvh; /* Dynamic viewport height — avoids iOS Safari toolbar */
|
||||
```
|
||||
|
||||
## Navigation Patterns
|
||||
|
||||
### iOS-native patterns to follow
|
||||
- **Primary navigation**: Bottom tab bar (persists across screens)
|
||||
- **Secondary navigation**: Top nav bar with back button (left) and actions (right)
|
||||
- **Modals**: Sheet sliding up from bottom (half-screen or full)
|
||||
- **Context menus**: Long-press or action sheets from bottom
|
||||
|
||||
### Primary action placement
|
||||
```
|
||||
Top 20%: Navigation, info, secondary actions
|
||||
Middle 60%: Main content (scrollable)
|
||||
Bottom 20%: Primary actions (thumb zone) — send, approve, play
|
||||
```
|
||||
|
||||
### Sheets & modals on mobile
|
||||
- Use bottom sheets with three detents: small (~25%), medium (~50%), large (full)
|
||||
- Always provide a close button — don't rely solely on swipe-to-dismiss
|
||||
- Content panels: full-screen overlay or bottom sheet, never side-by-side
|
||||
|
||||
## Form Inputs
|
||||
|
||||
| Rule | Value | Why |
|
||||
|---|---|---|
|
||||
| **Minimum input font** | **16px** | Prevents iOS Safari auto-zoom on focus |
|
||||
| Minimum field height | 44px | Matches tap target |
|
||||
| Use `inputmode` | `numeric`, `email`, `tel`, `url`, `search` | Shows appropriate keyboard |
|
||||
| Use `autocomplete` | Standard attributes | Enables autofill |
|
||||
| Submit button placement | Bottom of form, thumb zone | Easy to reach |
|
||||
|
||||
## Animations & Motion (iOS Spring Model)
|
||||
|
||||
### Duration guidelines
|
||||
| Type | Duration | Tailwind |
|
||||
|---|---|---|
|
||||
| Micro-interaction (tap, toggle) | 100-200ms | `duration-150` |
|
||||
| Standard transition (push/pop) | 250-350ms | `duration-300` |
|
||||
| Modal presentation (sheet) | 300-400ms | `duration-300` |
|
||||
| Complex transitions | 400-500ms | `duration-500` |
|
||||
|
||||
### iOS-style easing
|
||||
```css
|
||||
/* Standard iOS-like transition (ease out / decelerate) */
|
||||
transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.0);
|
||||
|
||||
/* Bouncy spring-like (for playful entrances) */
|
||||
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
|
||||
/* Quick snap (micro-interactions) */
|
||||
transition: transform 0.25s cubic-bezier(0.0, 0.0, 0.2, 1.0);
|
||||
```
|
||||
|
||||
### Motion rules
|
||||
- Entrances: ease-out (decelerate)
|
||||
- Exits: ease-in (accelerate)
|
||||
- Only animate `transform` and `opacity`
|
||||
- **Always** respect `prefers-reduced-motion`:
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gestures
|
||||
- Swipe left/right: gallery nav, dismiss
|
||||
- Swipe down: close overlay/bottom sheet, pull-to-refresh
|
||||
- Long press: context menu, selection
|
||||
- Pinch: zoom on images
|
||||
- Minimum swipe distance: 50px before triggering
|
||||
|
||||
## Scroll Behavior
|
||||
- Lock body scroll when modal/drawer is open
|
||||
- `overscroll-behavior: contain` on modal content
|
||||
- `touch-action: manipulation` to prevent zoom on double-tap
|
||||
- `-webkit-overflow-scrolling: touch` for smooth iOS scroll
|
||||
|
||||
## Iconography
|
||||
| Context | Size | Style |
|
||||
|---|---|---|
|
||||
| Tab bar | 25px | Filled/solid |
|
||||
| Nav bar / toolbar | 22px | Outlined, 1.5px stroke |
|
||||
| Inline with text | Match font size | Outlined |
|
||||
| Standalone | 28-33px | Filled or outlined |
|
||||
|
||||
## AIUI Custom Overrides (Keep These)
|
||||
These deviate from stock iOS but are intentional for our design language:
|
||||
- **Dark-only theme**: No light mode. Background `#0a0a0a`, not iOS system colors
|
||||
- **Glass morphism**: Translucent surfaces with backdrop-blur instead of iOS solid materials
|
||||
- **Accent color**: Bitcoin orange `#F7931A` instead of iOS systemBlue
|
||||
- **Text opacity scale**: Our `/25` → `/96` scale instead of iOS label hierarchy
|
||||
- **No separator borders**: We use spacing and glass layering instead
|
||||
- **Custom animations**: `animate-fade-up`, `animate-scale-in` per our design system
|
||||
- **Header buttons**: Currently 36px (`w-9 h-9`), acceptable with generous spacing
|
||||
|
||||
## Performance on Mobile
|
||||
- Test on real devices, not just emulators
|
||||
- Test on 3G/4G connections
|
||||
- Debounce scroll handlers
|
||||
- Lazy load images with `loading="lazy"`
|
||||
- Critical CSS inlined, rest loaded async
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
description: Git workflow - commit conventions, branching, PR process
|
||||
globs: "**/*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Git Workflow
|
||||
|
||||
## Commit Messages
|
||||
Format: `type(scope): description`
|
||||
|
||||
Types:
|
||||
- `feat`: new feature
|
||||
- `fix`: bug fix
|
||||
- `refactor`: code restructuring (no behavior change)
|
||||
- `style`: formatting, whitespace (no code change)
|
||||
- `docs`: documentation
|
||||
- `test`: adding/updating tests
|
||||
- `chore`: build, dependencies, tooling
|
||||
- `perf`: performance improvement
|
||||
|
||||
Scope: the package or area (`core`, `app`, `plugin-x`, `renderer-film`, etc.)
|
||||
|
||||
Examples:
|
||||
```
|
||||
feat(core): add renderer registry with lazy loading
|
||||
fix(chat): prevent scroll jump on new message
|
||||
refactor(plugin-system): simplify adapter interface
|
||||
chore(deps): update Vue to 3.6
|
||||
```
|
||||
|
||||
## Branching
|
||||
- `main`: production-ready, always deployable
|
||||
- `dev`: integration branch for features
|
||||
- `feat/description`: feature branches (from dev)
|
||||
- `fix/description`: bug fix branches
|
||||
- `release/x.y.z`: release preparation
|
||||
|
||||
## Pull Requests
|
||||
- One feature per PR
|
||||
- Description: what changed, why, how to test
|
||||
- All tests pass
|
||||
- TypeScript strict mode passes
|
||||
- No linter errors
|
||||
- Reviewed before merge
|
||||
|
||||
## Rules
|
||||
- Never force push to `main` or `dev`
|
||||
- Never commit `.env.local` or any secrets
|
||||
- Never commit `node_modules`
|
||||
- Squash merge feature branches to keep history clean
|
||||
- Tag releases with semver: `v1.0.0`
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
description: Expert rules for Film content extraction, display, and surfacing
|
||||
globs: "**/useContentPanel.ts,**/FilmCard.vue,**/FilmGrid.vue,**/FilmDetail.vue,**/mocks/films*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Films Content Surface
|
||||
|
||||
## Extraction Patterns
|
||||
|
||||
- **Tagged**: `[[film:f123]]` or `[[film:123]]` → resolved from mock library
|
||||
- **External**: `[[film_ext:Title|YYYY|Director]]` → create external film with fallback poster
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- `normalizeFilmId`: `f123` and `123` both become `f123`
|
||||
- Duplicate prevention: key by `title|year` for externals
|
||||
- Empty/malformed: skip if title < 2 chars, year invalid
|
||||
- Poster: use `generatePosterFallback(title, year)` for externals
|
||||
|
||||
## Strip Rules
|
||||
|
||||
- `stripFilmTags` removes `[[film:...]]` and `[[film_ext:...]]` before displaying text
|
||||
- Preserve `\n{3,}` → `\n\n` to avoid excessive whitespace
|
||||
|
||||
## Display
|
||||
|
||||
- FilmCard: poster, title, year, director
|
||||
- FilmDetail: full metadata, sources, cast
|
||||
- Panel: grid of FilmCards, click opens FilmDetail in panel
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
description: Expert rules for Song content extraction, display, and surfacing
|
||||
globs: "**/useContentPanel.ts,**/SongCard.vue,**/SongGrid.vue,**/SongDetail.vue,**/mocks/songs*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Songs Content Surface
|
||||
|
||||
## Extraction Priority
|
||||
|
||||
1. Tagged: `[[song:s123]]` or `[[song_ext:Title|Artist|YYYY]]`
|
||||
2. Library match: title + artist within 120 chars
|
||||
3. Patterns: `"Title" by Artist`, `Title – Artist`, `**Title** by Artist`
|
||||
|
||||
## looksLikeSong Rejection
|
||||
|
||||
Reject when title/artist contains: news phrases, "BIP", "protocol", "web search", "mailing list", "training cutoff", etc. See `looksLikeSong()` blocklist.
|
||||
|
||||
- Max length: title 55 chars, artist 40 chars
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- If `extractFilmIds` or `extractPodcastIds` found → return [] (don't mix film/podcast with song patterns)
|
||||
- If `isNewsLikeResponse` → return [] (news bullets often look like "X – Y")
|
||||
- Skip if title/artist is 4-digit year
|
||||
- Skip if contains `[[film` or `[[song` tags
|
||||
- Dedupe by `title|artist` lowercase
|
||||
|
||||
## Strip Rules
|
||||
|
||||
- `stripSongTags` removes song tags before displaying text
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: Expert rules for Podcast content extraction, display, and surfacing
|
||||
globs: "**/useContentPanel.ts,**/PodcastCard.vue,**/PodcastGrid.vue,**/PodcastDetail.vue,**/mocks/podcasts*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Podcasts Content Surface
|
||||
|
||||
## Extraction Patterns
|
||||
|
||||
- **Tagged**: `[[podcast:p123]]` or `[[podcast_ext:Title|Host|YYYY]]`
|
||||
- No pattern fallback (unlike songs) — only tags
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- Duplicate prevention: key by `title|host` lowercase
|
||||
- Empty: skip if title or host < 2 chars
|
||||
- Year optional in external format
|
||||
|
||||
## looksLikePodcast (when added)
|
||||
|
||||
Reject when title/host looks like: news source names, documentation sites, "Bitcoin Mailing List", etc. — same philosophy as `looksLikeSong`.
|
||||
|
||||
## Strip Rules
|
||||
|
||||
- `stripPodcastTags` removes podcast tags before displaying text
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
description: Expert rules for News content extraction, merge, and surfacing
|
||||
globs: "**/useContentPanel.ts,**/useRssFetch.ts,**/NewsGrid.vue,**/ArticleDetail.vue,**/vite-rss*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# News Content Surface
|
||||
|
||||
## Sources
|
||||
|
||||
1. **Web search**: `message.webResults` from AI (with imgSrc, content)
|
||||
2. **RSS**: Fetched from website URLs only when `newsContext` is true
|
||||
|
||||
## newsContext
|
||||
|
||||
- `isNewsQuery(userQuery)` — "news", "latest", "what's happening", "what are people saying", etc.
|
||||
- `isNewsLikeResponse(text)` — "for instant news", "check these sources", "access to web search", etc.
|
||||
|
||||
## Merge Rules
|
||||
|
||||
- `mergeNewsResults(web, rss)` — dedupe by URL (normalized: lowercase, no trailing slash)
|
||||
- Web results take precedence when URL collision
|
||||
|
||||
## RSS Fetch Guard
|
||||
|
||||
- **Only fetch RSS when `newsContext` is true and `mergedWebsites.length > 0`** — avoid surfacing irrelevant RSS from docs/resource links when user asked "websites"
|
||||
- Max 8 URLs, 15 articles total, 5 sites tried
|
||||
- Timeout: 15s client, 5s per feed server-side
|
||||
|
||||
## Display
|
||||
|
||||
- NewsGrid (variant=news): articles open in **ArticleDetail** (in-panel)
|
||||
- Relevance sort when `query` provided
|
||||
- Search filter by title, content, url
|
||||
- imgSrc: validate with `isSafeImgUrl` (https only)
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **RSS language**: Feeds return whatever the site publishes; no query/language filtering — may surface non-English articles
|
||||
- **RSS relevance**: No semantic filtering; articles are shown as published
|
||||
|
||||
## ArticleDetail Security
|
||||
|
||||
- `sanitizeHtml`: allow only safe tags (p, br, a, strong, em, ul, ol, li, blockquote, h1-h4)
|
||||
- Strip script, style, iframe, object, embed
|
||||
- Links: `href` must be `https?://`, reject `javascript:`
|
||||
- Images: `src` must be `https?://`
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
description: Expert rules for Websites content extraction and surfacing
|
||||
globs: "**/useContentPanel.ts,**/NewsGrid.vue,**/articleOverlay*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Websites Content Surface
|
||||
|
||||
## Extraction
|
||||
|
||||
1. **Markdown links**: `[Title](https://...)` — extract all with `extractMarkdownLinks`
|
||||
2. **Bold domains**: `**Name** (domain.tld)` — extract with `extractBoldDomainLinks`
|
||||
3. Merge with `mergeNewsResults` (dedupe by URL)
|
||||
|
||||
## URLs Validation
|
||||
|
||||
- Scheme: `https?://` only
|
||||
- `new URL(raw)` must not throw
|
||||
- Min length: title 2, url 10 chars
|
||||
- Normalize for dedupe: lowercase, no trailing slash
|
||||
|
||||
## Display
|
||||
|
||||
- NewsGrid (variant=websites): card with favicon/globe icon
|
||||
- Click → **overlay iframe** (not ArticleDetail)
|
||||
- Use `articleOverlayStore.open(url, title, undefined, imgSrc)`
|
||||
|
||||
## Distinction from News
|
||||
|
||||
- News = articles (web search + RSS) → ArticleDetail in panel
|
||||
- Websites = plain links from response → overlay iframe
|
||||
- Same NewsGrid component, different `variant` and click handler
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
description: Expert rules for Magazine/Brief content extraction and surfacing
|
||||
globs: "**/useContentPanel.ts,**/MagazineGrid.vue"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Magazine Content Surface
|
||||
|
||||
## Detection
|
||||
|
||||
- `hasMagazine` = sections ≥ 1 AND (newsQuery OR newsLikeResponse OR context keywords)
|
||||
- Context keywords: sentiment, bearish, bull case, macro, %, BTC, bitcoin, BIP, protocol, debate, what's happening
|
||||
|
||||
## Section Extraction Order
|
||||
|
||||
1. `## Heading` blocks — content until next ## or **Section**
|
||||
2. `**Pro/Anti camp**` blocks with emoji
|
||||
3. Bullets: `- **Title**: Content` or `- **Title** — Content` (em/en dash)
|
||||
4. Attributed: `- **Name** (Role) description`
|
||||
5. Intro paragraph (before first ##)
|
||||
6. "Key takeaway" / "This is being called..."
|
||||
7. "For deeper analysis" / further reading
|
||||
|
||||
## Section Rules
|
||||
|
||||
- Min: title 2 chars, content 15 chars
|
||||
- Max content: 2000 chars per section
|
||||
- Dedupe by title prefix (first 50 chars)
|
||||
- Skip bullets already inside ## blocks (`blockContents`)
|
||||
- `addSection` extracts: url, author, imageUrl from content
|
||||
|
||||
## Hero Image
|
||||
|
||||
1. First markdown image in text
|
||||
2. First `.jpg|.png|.gif|.webp` URL
|
||||
3. `webResults[0]?.imgSrc`
|
||||
4. Picsum fallback seeded by query
|
||||
|
||||
## Format & Security
|
||||
|
||||
- `formatContent`: escape `&<>`, preserve `**bold**` as `<strong>`, `\n\n` → `</p><p>`
|
||||
- Meme: imgflip URLs, contextual by topic (bearish, bull, Bitcoin, macro)
|
||||
@@ -0,0 +1,33 @@
|
||||
# AIUI Development Environment
|
||||
# Copy this file to .env.local and fill in your values
|
||||
|
||||
# AI Provider (OpenRouter - gives access to many models including free ones)
|
||||
# Get your key at: https://openrouter.ai/keys
|
||||
VITE_OPENROUTER_API_KEY=sk-or-your-key-here
|
||||
|
||||
# Anthropic Claude — for live web search (Claude invokes search mid-response):
|
||||
# Option 1: OAuth token from Max subscription (run: claude setup-token, save output)
|
||||
# → No extra cost; uses your existing Max subscription.
|
||||
ANTHROPIC_TOKEN=sk-ant-oat01-your-token-here
|
||||
# Option 2: API key from https://console.anthropic.com/settings/keys
|
||||
ANTHROPIC_API_KEY=sk-ant-your-key-here
|
||||
#
|
||||
# Without either: proxy uses CLI with built-in WebSearch + pre-fetched context.
|
||||
|
||||
# TMDB API (free, fetches posters on-demand when images fail)
|
||||
# Get your key at: https://www.themoviedb.org/settings/api
|
||||
TMDB_API_KEY=your-tmdb-key-here
|
||||
|
||||
# Jamendo API (optional, extends music search - free 35k req/mo)
|
||||
# Get your client_id at: https://devportal.jamendo.com/
|
||||
JAMENDO_CLIENT_ID=your-jamendo-client-id
|
||||
|
||||
# SearXNG instance for web search (optional)
|
||||
# Uses public instances by default; falls back to DuckDuckGo when they fail.
|
||||
# For reliable dev: host your own (https://docs.searxng.org/) or rely on DDG fallback.
|
||||
# SEARXNG_URL=https://your-searxng.instance
|
||||
|
||||
# Development flags
|
||||
VITE_DEV_MODE=true
|
||||
VITE_MOCK_MEDIA_SOURCES=true
|
||||
VITE_DISABLE_CRYPTO=true
|
||||
@@ -0,0 +1,131 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, development]
|
||||
pull_request:
|
||||
branches: [main, development]
|
||||
|
||||
jobs:
|
||||
lint-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm typecheck
|
||||
- run: pnpm lint
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm test
|
||||
|
||||
bundle-size:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- name: Check bundle size
|
||||
run: |
|
||||
BUNDLE_SIZE=$(find packages/app/dist/assets -name '*.js' -o -name '*.css' | xargs gzip -c | wc -c)
|
||||
BUNDLE_KB=$((BUNDLE_SIZE / 1024))
|
||||
echo "Bundle size: ${BUNDLE_KB}KB gzipped"
|
||||
if [ "$BUNDLE_KB" -gt 250 ]; then
|
||||
echo "::error::Bundle size ${BUNDLE_KB}KB exceeds 250KB budget"
|
||||
exit 1
|
||||
fi
|
||||
echo "Bundle size ${BUNDLE_KB}KB is within 250KB budget"
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
browser: [chromium, firefox, webkit]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: cd packages/app && npx playwright install --with-deps ${{ matrix.browser }}
|
||||
- run: cd packages/app && pnpm test:e2e --project=${{ matrix.browser }}
|
||||
|
||||
e2e-mobile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: cd packages/app && npx playwright install --with-deps chromium webkit
|
||||
- run: cd packages/app && pnpm test:e2e --project=iphone14 --project=galaxy-s21
|
||||
|
||||
lighthouse:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- name: Run Lighthouse
|
||||
uses: treosh/lighthouse-ci-action@v12
|
||||
with:
|
||||
configPath: packages/app/lighthouserc.json
|
||||
uploadArtifacts: true
|
||||
|
||||
dependency-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Audit dependencies
|
||||
run: pnpm audit --audit-level=critical || true
|
||||
- name: Check licenses
|
||||
run: |
|
||||
npx license-checker --production --onlyAllow 'MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD;CC0-1.0;Unlicense;CC-BY-4.0;Python-2.0;BlueOak-1.0.0' --excludePrivatePackages || echo "::warning::Non-approved licenses found"
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Weekly Dependency Audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * 1' # Every Monday at 9am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Audit for vulnerabilities
|
||||
id: audit
|
||||
run: |
|
||||
AUDIT_RESULT=$(pnpm audit --audit-level=moderate 2>&1) || true
|
||||
echo "$AUDIT_RESULT"
|
||||
if echo "$AUDIT_RESULT" | grep -q "critical"; then
|
||||
echo "has_critical=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_critical=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check licenses
|
||||
id: licenses
|
||||
run: |
|
||||
LICENSE_RESULT=$(npx license-checker --production --onlyAllow 'MIT;Apache-2.0;ISC;BSD-2-Clause;BSD-3-Clause;0BSD;CC0-1.0;Unlicense;CC-BY-4.0;Python-2.0;BlueOak-1.0.0' --excludePrivatePackages 2>&1) || true
|
||||
echo "$LICENSE_RESULT"
|
||||
if echo "$LICENSE_RESULT" | grep -q "FAIL"; then
|
||||
echo "has_violations=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_violations=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create issue if violations found
|
||||
if: steps.audit.outputs.has_critical == 'true' || steps.licenses.outputs.has_violations == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: '⚠️ Dependency audit: violations found',
|
||||
body: `The weekly dependency audit found issues:\n\n- Critical vulnerabilities: ${{ steps.audit.outputs.has_critical }}\n- License violations: ${{ steps.licenses.outputs.has_violations }}\n\nRun \`pnpm audit\` and \`npx license-checker\` locally for details.`,
|
||||
labels: ['security', 'dependencies'],
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Turborepo
|
||||
.turbo/
|
||||
|
||||
# Environment (secrets)
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Tauri
|
||||
packages/app/src-tauri/target/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Dev chat history
|
||||
.dev/
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# Overnight loop logs
|
||||
loop/loop.log
|
||||
|
||||
# Playwright
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
|
||||
# Storybook
|
||||
storybook-static/
|
||||
@@ -0,0 +1,345 @@
|
||||
# CLAUDE.md — AIUI Project Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
AIUI is a next-generation AI content surface UI. It's a **pnpm monorepo** with two packages:
|
||||
|
||||
- `@aiui/app` — Reference application (Vite + Vue 3 + Tailwind CSS)
|
||||
- `@aiui/core` — Reusable component library
|
||||
|
||||
**Stack**: Vue 3 (Composition API), TypeScript ~5.8 (strict), Vite, Tailwind CSS, Pinia, Vue Router, Turborepo
|
||||
**Node**: >=20.0.0 | **pnpm**: >=10.0.0
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
pnpm dev # Run app dev server + Claude proxy
|
||||
pnpm dev:core # Watch-build core library
|
||||
pnpm build # Build all packages (turbo)
|
||||
pnpm test # Run tests (vitest)
|
||||
pnpm lint # Lint all packages (eslint)
|
||||
pnpm typecheck # Type-check all packages (vue-tsc)
|
||||
pnpm clean # Remove dist/ directories
|
||||
```
|
||||
|
||||
Dev server: `http://localhost:5173` | Claude proxy: `http://localhost:3141`
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
- **Open source only** — MIT/Apache-2.0 licensed dependencies only
|
||||
- **Decentralized-first** — Pluggable adapters, no vendor lock-in
|
||||
- **Bitcoin only** — sats/Lightning/Cashu/Fedimint. Never fiat, never altcoins. AIUI is never a wallet — always deep-link to external wallets
|
||||
- **Privacy-first** — E2E encryption (tweetnacl.js), encrypted local storage (AES-256-GCM), no tracking/telemetry
|
||||
- **Mobile-first, everywhere-perfect** — Desktop is an enhancement of the mobile experience
|
||||
- **Plugin-everything** — All integrations go through typed plugin interfaces
|
||||
|
||||
## Vue 3 Conventions
|
||||
|
||||
**Always use `<script setup lang="ts">`** — never Options API.
|
||||
|
||||
### Script section ordering
|
||||
|
||||
Imports → Props (`defineProps`) → Emits (`defineEmits`) → Reactive state → Computed → Watchers → Methods → Lifecycle hooks → `defineExpose`
|
||||
|
||||
### Naming
|
||||
|
||||
| Thing | Convention | Example |
|
||||
|-------|-----------|---------|
|
||||
| Components | PascalCase | `ProjectCard.vue` |
|
||||
| Composables | camelCase, `use` prefix | `useTheme.ts` |
|
||||
| Props (JS) | camelCase | `projectName` |
|
||||
| Props (template) | kebab-case | `project-name` |
|
||||
| Boolean props | `is`/`has`/`can`/`should` prefix | `isVisible`, `canEdit` |
|
||||
| Emits (template) | kebab-case with colon namespacing | `project:updated` |
|
||||
| Stores | camelCase, `use` prefix, `Store` suffix | `useSettingsStore` |
|
||||
|
||||
### Reactive state rules
|
||||
|
||||
- `ref()` for primitives, `reactive()` for objects
|
||||
- `computed()` for derived values — no side effects in computed
|
||||
- `shallowRef()` for large collections/objects not requiring deep reactivity
|
||||
- Always use unique IDs for `:key` — never array index
|
||||
|
||||
### Props
|
||||
|
||||
Always use object-style with type annotations, never array-style:
|
||||
|
||||
```ts
|
||||
// Correct
|
||||
defineProps<{ title: string; count?: number }>()
|
||||
|
||||
// Wrong
|
||||
defineProps(['title', 'count'])
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
- Lazy load with `defineAsyncComponent` for non-critical components
|
||||
- Use `onErrorCaptured` for error boundaries
|
||||
- Always handle loading/error/data states in async operations
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
packages/app/src/
|
||||
├── components/
|
||||
│ ├── ui/ # Generic UI components
|
||||
│ ├── chat/ # Chat interface components
|
||||
│ ├── content-panel/ # Content panel components
|
||||
│ ├── renderers/ # Content type renderers
|
||||
│ └── layout/ # Layout components
|
||||
├── composables/ # Shared composition functions
|
||||
├── stores/ # Pinia stores
|
||||
├── pages/ # Route-level components
|
||||
├── styles/ # Global CSS, themes, tokens
|
||||
├── utils/ # Pure utility functions
|
||||
├── types/ # TypeScript type definitions
|
||||
├── plugins/ # Plugin system
|
||||
└── mocks/ # Dev fixtures & mock data
|
||||
|
||||
packages/core/src/
|
||||
├── plugins/ # Plugin system interfaces
|
||||
└── types/ # Shared TypeScript types
|
||||
```
|
||||
|
||||
## Tailwind & Design System
|
||||
|
||||
### Glass Morphism (Archy-derived)
|
||||
|
||||
This project uses a glass morphism design language. Key utility classes:
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `.glass` | Standard glass: `rgba(0,0,0,0.35)`, `blur(18px)`, white border 0.18 opacity |
|
||||
| `.glass-strong` | Stronger blur: `blur(24px)` |
|
||||
| `.glass-card` | Card variant: `rgba(0,0,0,0.65)`, `border-radius: 1rem` |
|
||||
| `.glass-button` | Button: 48px height, `rgba(0,0,0,0.6)`, `blur(18px)` |
|
||||
| `.glass-button-sm` | Compact button variant |
|
||||
| `.gradient-card` | Gradient background card |
|
||||
|
||||
### Spacing
|
||||
|
||||
4px grid system: `1`=4px, `2`=8px, `3`=12px, `4`=16px, etc.
|
||||
|
||||
### Colors
|
||||
|
||||
- Background: `#0a0a0a` (near-black)
|
||||
- Accent / Bitcoin orange: `#F7931A`
|
||||
- Primary: `#606060`
|
||||
- Text opacity scale: `/25` (placeholder) → `/40` (muted) → `/60` (secondary) → `/70` (interactive) → `/80` (body) → `/90` (emphasis) → `/96` (headings) → `text-white` (active)
|
||||
- No separator borders between major sections
|
||||
|
||||
### Typography
|
||||
|
||||
`Inter`/`system-ui` for body, `Menlo`/`Monaco` for monospace.
|
||||
|
||||
### Responsive breakpoints (mobile-first)
|
||||
|
||||
`sm` 640px → `md` 768px → `lg` 1024px → `xl` 1280px → `2xl` 1536px
|
||||
|
||||
### Animations
|
||||
|
||||
- `animate-fade-up` (900ms), `animate-fade-up-fast` (400ms), `animate-fade-in` (500ms), `animate-scale-in` (250ms)
|
||||
- Duration: 100ms micro, 200ms fast, 300ms moderate, 500ms normal, 600ms max
|
||||
- Easing: `ease-out` for entrances (90% of animations), `ease-in` for exits
|
||||
- Only animate `transform` and `opacity` — avoid animating layout properties
|
||||
- Always respect `prefers-reduced-motion`
|
||||
|
||||
## Content Surfaces Architecture
|
||||
|
||||
Every content renderer supports up to five surfaces:
|
||||
|
||||
1. **Chat Preview** (~120px max) — inline bubble, identify content at a glance
|
||||
2. **Chat Play** (~200px max) — inline playback with expand button
|
||||
3. **Panel Preview** (unlimited) — full browsing, filtering, sorting
|
||||
4. **Panel Play** — full immersive playback
|
||||
5. **Panel Edit** — full interaction, sends changes back to chat
|
||||
|
||||
On mobile, Panel surfaces open as full-screen overlays, not side-by-side.
|
||||
|
||||
```ts
|
||||
interface RendererDefinition {
|
||||
id: string
|
||||
name: string
|
||||
contentType: string
|
||||
surfaces: SurfaceType[]
|
||||
chatPreview?: Component
|
||||
chatPlay?: Component
|
||||
panelPreview?: Component
|
||||
panelPlay?: Component
|
||||
panelEdit?: Component
|
||||
lazyDependencies?: () => Promise<any>
|
||||
}
|
||||
```
|
||||
|
||||
Chat surfaces must have zero lazy dependencies. Panel surfaces may lazy-load heavy libraries.
|
||||
|
||||
## Plugin System
|
||||
|
||||
All integrations are plugins. Plugin types: `ai-provider`, `media-source`, `messaging`, `storage`, `renderer`, `file-handler`, `crypto`, `search`, `auth`, `wallet`, `social-embed`, `mcp`, `media`.
|
||||
|
||||
```ts
|
||||
interface AIUIPlugin {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
type: PluginType
|
||||
description?: string
|
||||
init(context: PluginContext): Promise<void>
|
||||
destroy(): Promise<void>
|
||||
isAvailable(): Promise<boolean>
|
||||
}
|
||||
```
|
||||
|
||||
Sandboxing: Tier 1 (trusted built-in), Tier 2 (community — sandboxed iframes), Tier 3 (external processes). Community plugins get no direct DOM access.
|
||||
|
||||
## AI Provider Integration
|
||||
|
||||
```ts
|
||||
interface AIProviderAdapter extends AIUIPlugin {
|
||||
type: 'ai-provider'
|
||||
chat(messages: Message[], options: ChatOptions): AsyncIterable<ChatChunk>
|
||||
models(): Promise<Model[]>
|
||||
supportsStreaming: boolean
|
||||
supportsVision: boolean
|
||||
supportsTools: boolean
|
||||
}
|
||||
```
|
||||
|
||||
Normalize tool calling across providers (OpenAI `tool_calls` vs Claude `tool_use`). Never include API keys in context injection.
|
||||
|
||||
## Security & Crypto
|
||||
|
||||
- E2E encryption: tweetnacl.js XSalsa20-Poly1305
|
||||
- Local storage: Web Crypto API AES-256-GCM + PBKDF2 (100K+ iterations)
|
||||
- API keys: encrypted at rest, never in localStorage, never logged, masked in UI (last 4 chars)
|
||||
- No `eval()` or `innerHTML` with untrusted content
|
||||
- Sanitize all user input against XSS
|
||||
- HTTPS only, CSP headers in production
|
||||
- Dev bypass: `VITE_DISABLE_CRYPTO=true` (never in production)
|
||||
|
||||
## Accessibility
|
||||
|
||||
WCAG AA minimum compliance:
|
||||
|
||||
- Color contrast: 4.5:1 normal text, 3:1 large/interactive
|
||||
- Keyboard: all elements focusable via Tab, visible focus indicators, Escape closes modals
|
||||
- Semantic HTML: use `<header>`, `<nav>`, `<main>`, `<article>`, `<aside>`, `<footer>` — not div soup
|
||||
- ARIA: `aria-label` for icon buttons, `aria-live="polite"` for dynamic updates, `sr-only` for screen reader text
|
||||
- Touch targets: min 44x44px with 8px gaps
|
||||
- All images need `alt` attributes (decorative: `alt=""`)
|
||||
- Respect `prefers-reduced-motion`
|
||||
|
||||
## Performance Budget
|
||||
|
||||
- **Initial load**: < 250KB gzipped
|
||||
- Core bundle: Vue + Tailwind + Pinia + Router + chat UI (~150KB) + markdown + streaming (~50KB)
|
||||
- Everything else: lazy-loaded on demand
|
||||
- Virtual scrolling (TanStack Virtual) for chat lists
|
||||
- Clean up listeners in `onUnmounted`, use `shallowRef` for large data
|
||||
- Core Web Vitals: LCP < 2.5s, FID < 100ms, CLS < 0.1
|
||||
- Preconnect to API hosts, debounce inputs (100ms)
|
||||
|
||||
## Mobile UX (iOS HIG-Informed)
|
||||
|
||||
Follows Apple iOS Human Interface Guidelines. See `.cursor/rules/15-mobile-ux.mdc` for full reference.
|
||||
|
||||
- **Typography**: Body 17px, Footnote 13px, Caption 11px minimum — never smaller than 11px
|
||||
- **Touch targets**: min 44×44px tappable area, 8px gap between targets
|
||||
- **Side margins**: 16px (`px-4`)
|
||||
- **Primary actions**: Bottom thumb zone
|
||||
- **Viewport**: `height: 100dvh` with `env(safe-area-inset-*)` for notched devices
|
||||
- **Form inputs**: min 16px font (prevents iOS zoom), appropriate `inputmode`
|
||||
- **Content panels**: Full-screen overlay or bottom sheet on mobile, never side-by-side
|
||||
- **Transitions**: 150ms micro, 300ms standard, 400ms modal — ease-out entrances, ease-in exits
|
||||
- **Sheets**: Bottom sheets with close button, don't rely solely on swipe-to-dismiss
|
||||
- Support both portrait and landscape
|
||||
|
||||
## Environment & Dev Mode
|
||||
|
||||
Env vars must be prefixed `VITE_`. Secrets go in `.env.local` (gitignored). See `.env.example` for template.
|
||||
|
||||
Feature flags via `useFeatureFlags()`: `isDev`, `isTauri`, `isMobile`, `isCryptoEnabled`, `isMockData`
|
||||
|
||||
Dev mode enables: mock data, debug panel, verbose logging, disabled encryption, all renderers without lazy loading.
|
||||
|
||||
## Git Conventions
|
||||
|
||||
### Commit format
|
||||
|
||||
```
|
||||
type(scope): description
|
||||
```
|
||||
|
||||
**Types**: `feat`, `fix`, `refactor`, `style`, `docs`, `test`, `chore`, `perf`
|
||||
**Scope**: package or area — `core`, `app`, `chat`, `renderer-film`, `plugin-x`
|
||||
|
||||
### Branches
|
||||
|
||||
`main` (production), `dev` (integration), `feat/description`, `fix/description`
|
||||
|
||||
### Rules
|
||||
|
||||
- One feature per PR
|
||||
- All tests pass, TypeScript strict passes, no lint errors
|
||||
- No force push to main/dev
|
||||
- Never commit `.env.local`, secrets, or `node_modules`
|
||||
- Squash merge features, tag releases `v1.0.0`
|
||||
|
||||
## Archipelago (Archy) Integration
|
||||
|
||||
AIUI runs inside an iframe in Archipelago's Chat mode. All communication with the host happens via `window.postMessage()` through a strict protocol.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
AIUI (iframe) ←→ postMessage ←→ Archy ContextBroker ←→ Node data
|
||||
```
|
||||
|
||||
AIUI is **quarantined** — it never directly accesses Archy's APIs, stores, or node data. The Archy ContextBroker fetches and sanitizes data before passing it to AIUI.
|
||||
|
||||
### Protocol
|
||||
|
||||
Use `archyBridge.ts` (`src/services/archyBridge.ts`) for all Archy communication:
|
||||
|
||||
```ts
|
||||
import { archyBridge } from '@/services/archyBridge'
|
||||
|
||||
// Request context (respects user permissions)
|
||||
const apps = await archyBridge.requestContext('apps')
|
||||
if (!apps.permitted) {
|
||||
// Show: "Enable 'Installed Apps' access in Archy Settings"
|
||||
}
|
||||
|
||||
// Request an action
|
||||
await archyBridge.requestAction('open-app', { appId: 'btcpay-server' })
|
||||
|
||||
// Listen for theme/permission updates
|
||||
archyBridge.onPermissionsUpdate((categories) => { ... })
|
||||
archyBridge.onThemeUpdate((theme) => { ... })
|
||||
```
|
||||
|
||||
**Context categories** (user toggles each on/off in Archy Settings):
|
||||
- `apps` — App names, status, health (no credentials)
|
||||
- `system` — CPU, RAM, disk (no paths or IPs)
|
||||
- `network` — Connection status, peer count (no IPs)
|
||||
- `wallet` — Balance, channel count (no keys or seeds)
|
||||
- `files` — File/folder names (no contents)
|
||||
|
||||
### Critical Rules
|
||||
|
||||
1. **NEVER** fetch Archy APIs directly — always use `archyBridge`
|
||||
2. **NEVER** store or log raw user data from context responses
|
||||
3. **NEVER** make HTTP requests to the host machine
|
||||
4. Handle `permitted: false` gracefully — tell users what to enable
|
||||
5. Send `ready` message on mount so Archy knows the iframe loaded
|
||||
6. Build must output a static SPA servable from any base path
|
||||
7. All AI provider keys are user-provided and stored locally in AIUI only
|
||||
|
||||
### Build & Deploy
|
||||
|
||||
AIUI deploys as a Podman container on the Archy node:
|
||||
- Build: `pnpm build` → `packages/app/dist/`
|
||||
- Container: nginx:alpine serving the dist
|
||||
- Proxied at `/aiui/` via Archy's nginx
|
||||
- Updates independently of Archy — new container image = new version
|
||||
@@ -0,0 +1,267 @@
|
||||
# Claude Code Overnight Automation
|
||||
|
||||
Run Claude Code headlessly overnight to execute a full task checklist — with rate-limit resilience, macOS sleep prevention, and a stop hook that prevents Claude from quitting until every task is done.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
loop.sh (orchestrator)
|
||||
|
|
||||
+--> Reads plan.md for unchecked [ ] tasks
|
||||
+--> Pipes prompt.md into `claude -p` (headless mode)
|
||||
| |
|
||||
| +--> Claude reads your plan, specs, and project rules
|
||||
| +--> Implements tasks one by one
|
||||
| +--> Runs typecheck/lint/test after each
|
||||
| +--> Commits, marks [x], moves to next
|
||||
| |
|
||||
| +--> Claude tries to stop
|
||||
| |
|
||||
| +--> Stop Hook intercepts
|
||||
| +--> Checks plan.md for remaining [ ] tasks
|
||||
| +--> If incomplete: BLOCKS the stop (Claude continues)
|
||||
| +--> If all done: allows stop
|
||||
|
|
||||
+--> Detects rate limits in output
|
||||
| +--> Sleeps 1 hour, retries (up to 5x)
|
||||
| +--> After 5 retries: schedules macOS launchd job to resume later
|
||||
|
|
||||
+--> Loops N iterations (default 10)
|
||||
+--> Exits when all tasks checked or iterations exhausted
|
||||
```
|
||||
|
||||
### The "Ralph Wiggum" Stop Hook
|
||||
|
||||
The secret sauce. Claude Code supports a `Stop` hook — a shell script that runs every time Claude tries to end its session. By returning `{"decision":"block"}`, the hook **prevents Claude from stopping**. Combined with `--dangerously-skip-permissions`, Claude becomes a fully autonomous task executor that won't quit until the job is done.
|
||||
|
||||
### Sleep Prevention
|
||||
|
||||
On macOS, `caffeinate -i` prevents idle sleep during long runs. A hook starts it when Claude begins and kills it when Claude finishes.
|
||||
|
||||
### Rate Limit Resilience
|
||||
|
||||
If Claude hits API rate limits:
|
||||
1. **Inline retry**: Sleep 1 hour, then retry the same iteration
|
||||
2. **Scheduled retry**: After 5 failed retries, create a macOS `launchd` plist that auto-runs the loop later
|
||||
3. The plist self-destructs after executing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Claude Code CLI** (`claude` command available in PATH)
|
||||
- Install: https://docs.anthropic.com/en/docs/claude-code
|
||||
- Must be logged in: run `claude login` first
|
||||
- **macOS** (for `caffeinate` and `launchd` — see Linux notes below)
|
||||
- **Git** (the script commits after each task)
|
||||
- A project with `package.json` or similar build tooling
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Clone or copy this folder into your project
|
||||
cp -r "For Others/templates" ~/my-project/loop
|
||||
|
||||
# 2. Run the setup script (creates hooks, updates settings)
|
||||
cd ~/my-project
|
||||
bash "path/to/For Others/setup.sh"
|
||||
|
||||
# 3. Edit your task list
|
||||
vim loop/plan.md
|
||||
|
||||
# 4. Edit your prompt (project-specific rules)
|
||||
vim loop/prompt.md
|
||||
|
||||
# 5. Start the overnight run
|
||||
./loop/loop.sh
|
||||
```
|
||||
|
||||
Or just run the setup script — it walks you through everything:
|
||||
|
||||
```bash
|
||||
bash "For Others/setup.sh"
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
After setup, your project will have:
|
||||
|
||||
```
|
||||
your-project/
|
||||
loop/
|
||||
loop.sh # Main orchestrator (run this)
|
||||
prompt.md # Instructions piped to Claude each iteration
|
||||
plan.md # Task checklist ([ ] = todo, [x] = done)
|
||||
loop.log # Full output log (auto-created)
|
||||
|
||||
~/.claude/
|
||||
hooks/
|
||||
prevent-sleep.sh # Starts caffeinate on session start
|
||||
stop-hook-autonomous.sh # Blocks stop until tasks complete
|
||||
allow-sleep.sh # Kills caffeinate on session end
|
||||
settings.json # Hook registrations (auto-updated by setup)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All config is via environment variables (set before running `loop.sh` or export in your shell):
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CLAUDE_AUTONOMOUS` | `1` | Set to `0` to disable the stop hook (Claude can quit freely) |
|
||||
| `ITERATION_COUNT` | `10` | Max loop iterations |
|
||||
| `ITERATION_DELAY` | `30` | Seconds to pause between iterations |
|
||||
| `RATE_LIMIT_WAIT` | `3600` | Seconds to sleep when rate limited (1 hour) |
|
||||
| `MAX_RATE_LIMIT_RETRIES` | `5` | Retries before scheduling launchd |
|
||||
| `CLAUDE_BIN` | `claude` | Path to Claude CLI binary |
|
||||
| `PROMPT_FILE` | `loop/prompt.md` | Path to prompt file |
|
||||
| `LOG_FILE` | `loop/loop.log` | Path to log file |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Quick test run (2 iterations, 10s delay, no stop hook)
|
||||
CLAUDE_AUTONOMOUS=0 ITERATION_COUNT=2 ITERATION_DELAY=10 ./loop/loop.sh
|
||||
|
||||
# Full overnight run (20 iterations, 1 min between)
|
||||
ITERATION_COUNT=20 ITERATION_DELAY=60 ./loop/loop.sh
|
||||
|
||||
# Use a custom prompt
|
||||
PROMPT_FILE=my-prompt.md ./loop/loop.sh
|
||||
```
|
||||
|
||||
## Writing Your Plan
|
||||
|
||||
`loop/plan.md` is a markdown checklist. Each line starting with `- [ ]` is a pending task:
|
||||
|
||||
```markdown
|
||||
## Phase 1: Core Features
|
||||
- [ ] **1.1** — Add user authentication (JWT + refresh tokens)
|
||||
- [ ] **1.2** — Create user profile page with avatar upload
|
||||
- [ ] **1.3** — Add settings page with theme toggle
|
||||
|
||||
## Phase 2: API
|
||||
- [ ] **2.1** — REST endpoints for CRUD operations
|
||||
- [ ] **2.2** — WebSocket support for real-time updates
|
||||
|
||||
## Final
|
||||
- [ ] **FINAL** — Run full test suite, fix any failures, tag release
|
||||
```
|
||||
|
||||
Claude will:
|
||||
1. Find the first `- [ ]` line
|
||||
2. Read the spec from your prompt or a separate spec file
|
||||
3. Implement it
|
||||
4. Mark it `- [x]`
|
||||
5. Move to the next
|
||||
|
||||
### Tips for good plans
|
||||
|
||||
- **Be specific**: "Add JWT auth with refresh tokens, store in httpOnly cookies" > "Add auth"
|
||||
- **Order matters**: Put foundational tasks first (types, utils, config) before features that depend on them
|
||||
- **Include testing gates**: "Run `pnpm test` and fix failures" as part of each task
|
||||
- **Keep tasks small**: 30-60 minutes of work each. Large tasks lead to context window exhaustion
|
||||
- **Add a FINAL task**: A catchall that runs the full test suite
|
||||
|
||||
## Writing Your Prompt
|
||||
|
||||
`loop/prompt.md` is what Claude reads at the start of every iteration. Include:
|
||||
|
||||
1. **What files to read** (your plan, specs, project conventions)
|
||||
2. **Project-specific rules** (coding style, frameworks, constraints)
|
||||
3. **Per-task workflow** (implement → test → commit → mark done)
|
||||
4. **Hard rules** (what to never do, minimum effort before skipping)
|
||||
|
||||
See `templates/prompt.md` for a starting template.
|
||||
|
||||
## Operating the Loop
|
||||
|
||||
### Starting
|
||||
|
||||
```bash
|
||||
# Foreground (see output live)
|
||||
./loop/loop.sh
|
||||
|
||||
# Background with logging
|
||||
nohup ./loop/loop.sh > /dev/null 2>&1 &
|
||||
|
||||
# With caffeinate (prevents sleep even if hooks fail)
|
||||
caffeinate -i ./loop/loop.sh
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
```bash
|
||||
# Watch the log live
|
||||
tail -f loop/loop.log
|
||||
|
||||
# Check progress
|
||||
grep -c '\- \[x\]' loop/plan.md # completed
|
||||
grep -c '\- \[ \]' loop/plan.md # remaining
|
||||
|
||||
# Check git commits
|
||||
git log --oneline -20
|
||||
```
|
||||
|
||||
### Stopping
|
||||
|
||||
- **Let it finish**: The loop stops automatically when all tasks are checked
|
||||
- **Kill it**: `Ctrl+C` or `kill %1` — Claude's current task will be interrupted but committed work is preserved
|
||||
- **Disable stop hook**: Set `CLAUDE_AUTONOMOUS=0` in the environment before the next iteration
|
||||
|
||||
### Resuming
|
||||
|
||||
Just run `./loop/loop.sh` again. It reads `plan.md` fresh each iteration, so it picks up where it left off (skipping `[x]` tasks).
|
||||
|
||||
## Customizing the Prompt
|
||||
|
||||
The prompt template has `{{PLACEHOLDER}}` markers. Replace them with your project's specifics:
|
||||
|
||||
| Placeholder | What to put |
|
||||
|-------------|-------------|
|
||||
| `{{SPEC_FILE}}` | Path to your detailed spec (e.g., `SPEC.md`, `docs/plan.md`) |
|
||||
| `{{PROJECT_RULES_FILE}}` | Path to your coding conventions file |
|
||||
| `{{PROJECT_RULES}}` | Inline coding rules (style, frameworks, constraints) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Claude exits immediately
|
||||
- Make sure `claude login` has been run
|
||||
- Check that `claude -p "hello"` works in your terminal
|
||||
- Verify `~/.claude/hooks/stop-hook-autonomous.sh` exists and is executable
|
||||
|
||||
### Rate limit loop
|
||||
- Default wait is 1 hour. Increase `RATE_LIMIT_WAIT` if your limits are longer
|
||||
- Check `loop.log` for the specific rate limit message
|
||||
- Claude Max subscriptions have higher limits than API keys
|
||||
|
||||
### Mac goes to sleep
|
||||
- Run `caffeinate -i ./loop/loop.sh` as a belt-and-suspenders approach
|
||||
- Check that `~/.claude/hooks/prevent-sleep.sh` is executable: `chmod +x ~/.claude/hooks/prevent-sleep.sh`
|
||||
|
||||
### Tasks not getting marked complete
|
||||
- Ensure your plan uses exact format: `- [ ]` (dash, space, brackets, space)
|
||||
- The stop hook matches `^\s*[-*]?\s*\[\s*\]` — standard markdown checkboxes
|
||||
|
||||
### Stop hook not working
|
||||
- Verify `CLAUDE_AUTONOMOUS=1` is set: `echo $CLAUDE_AUTONOMOUS`
|
||||
- Check hook is registered in `~/.claude/settings.json`
|
||||
- Test the hook manually: `echo '{}' | bash ~/.claude/hooks/stop-hook-autonomous.sh`
|
||||
|
||||
## Linux Notes
|
||||
|
||||
The system is macOS-focused but works on Linux with minor changes:
|
||||
|
||||
- **Sleep prevention**: Replace `caffeinate` with `systemd-inhibit --what=idle --who=claude-loop --why="Overnight automation" sleep infinity &` or simply disable sleep via `systemctl mask sleep.target`
|
||||
- **Scheduled retry**: Replace the launchd plist section in `loop.sh` with a `systemd-run --on-calendar` or `at` command
|
||||
- **Hooks work identically** — they're plain bash scripts
|
||||
|
||||
## Security Notes
|
||||
|
||||
- `--dangerously-skip-permissions` gives Claude **full system access** within the project. Only run on trusted codebases.
|
||||
- The loop runs as your user — Claude can read/write anything you can
|
||||
- API keys in `.env.local` are accessible to Claude during the session
|
||||
- Review commits after an overnight run before pushing to production
|
||||
- Consider running in a VM or container for additional isolation
|
||||
|
||||
## License
|
||||
|
||||
MIT. Use it however you want.
|
||||
@@ -0,0 +1,496 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Claude Code Overnight Automation — One-File Setup
|
||||
# ============================================================================
|
||||
# Run from your project root:
|
||||
# bash setup.sh
|
||||
#
|
||||
# This single script creates everything:
|
||||
# loop/loop.sh — main orchestrator
|
||||
# loop/prompt.md — template prompt for Claude
|
||||
# loop/plan.md — your task checklist
|
||||
# ~/.claude/hooks/ — sleep prevention + autonomous stop hook
|
||||
# ~/.claude/settings.json — hook registrations
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
ok() { echo -e " ${GREEN}+${NC} $1"; }
|
||||
warn() { echo -e " ${YELLOW}!${NC} $1"; }
|
||||
err() { echo -e " ${RED}x${NC} $1"; }
|
||||
info() { echo -e " ${DIM}$1${NC}"; }
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD} Claude Code Overnight Automation${NC}"
|
||||
echo -e " ${DIM}────────────────────────────────────${NC}"
|
||||
echo ""
|
||||
|
||||
# ── Prerequisites ────────────────────────────────────────────────────────────
|
||||
echo -e " ${BOLD}Checking prerequisites...${NC}"
|
||||
echo ""
|
||||
|
||||
MISSING=0
|
||||
|
||||
if command -v claude &>/dev/null; then
|
||||
ok "Claude CLI: $(which claude)"
|
||||
else
|
||||
err "Claude CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
|
||||
MISSING=1
|
||||
fi
|
||||
|
||||
if command -v git &>/dev/null; then
|
||||
ok "Git: $(which git)"
|
||||
else
|
||||
err "Git not found."
|
||||
MISSING=1
|
||||
fi
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
ok "macOS (caffeinate + launchd available)"
|
||||
else
|
||||
warn "Not macOS — sleep hooks need Linux equivalents (see README)"
|
||||
fi
|
||||
|
||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
PROJECT_DIR="$(git rev-parse --show-toplevel)"
|
||||
ok "Project: $PROJECT_DIR"
|
||||
else
|
||||
PROJECT_DIR="$(pwd)"
|
||||
warn "Not a git repo — using: $PROJECT_DIR"
|
||||
fi
|
||||
|
||||
[[ "$MISSING" -eq 1 ]] && { echo ""; err "Fix the above and re-run."; exit 1; }
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Create loop/ directory ───────────────────────────────────────────────────
|
||||
echo -e " ${BOLD}Creating loop files...${NC}"
|
||||
echo ""
|
||||
|
||||
LOOP_DIR="$PROJECT_DIR/loop"
|
||||
mkdir -p "$LOOP_DIR"
|
||||
|
||||
# ── loop.sh (embedded) ──────────────────────────────────────────────────────
|
||||
if [[ -f "$LOOP_DIR/loop.sh" ]]; then
|
||||
warn "loop/loop.sh exists — skipping"
|
||||
else
|
||||
cat > "$LOOP_DIR/loop.sh" << 'LOOPEOF'
|
||||
#!/usr/bin/env sh
|
||||
# Claude Code Overnight Automation — Loop Script
|
||||
# Usage: ./loop/loop.sh
|
||||
# Config via env vars: ITERATION_COUNT, ITERATION_DELAY, CLAUDE_AUTONOMOUS, etc.
|
||||
set -u
|
||||
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}"
|
||||
LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}"
|
||||
ITERATION_COUNT="${ITERATION_COUNT:-10}"
|
||||
ITERATION_DELAY="${ITERATION_DELAY:-30}"
|
||||
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
|
||||
RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}"
|
||||
MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}"
|
||||
CLAUDE_EXIT=0
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
log() { echo "$1" | tee -a "$LOG_FILE"; }
|
||||
banner() {
|
||||
log ""; log "════════════════════════════════════════════════════════════════"
|
||||
log " $1"; log " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log "════════════════════════════════════════════════════════════════"; log ""
|
||||
}
|
||||
section() { log ""; log "────────────────────────────────────────"; log " $1"; log "────────────────────────────────────────"; log ""; }
|
||||
|
||||
plan_has_tasks() { grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null; }
|
||||
remaining_tasks() { grep -c '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null || echo "0"; }
|
||||
next_task() { grep -m1 '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null | sed 's/^- \[ \] //' || echo "(none)"; }
|
||||
|
||||
check_rate_limit() {
|
||||
[ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1
|
||||
tail -50 "$LOG_FILE" 2>/dev/null | grep -v "^Rate limit" | grep -v "^Sleeping" | grep -v "^═" | grep -v "^─" \
|
||||
| grep -qi -e "rate.limit" -e "too.many.requests" -e "429" -e "quota.exceeded" -e "usage.limit" -e "limit.reached" 2>/dev/null
|
||||
}
|
||||
|
||||
banner "OVERNIGHT AUTOMATION STARTED"
|
||||
log " Project: $PROJECT_DIR"
|
||||
log " Prompt: $PROMPT_FILE"
|
||||
log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}"
|
||||
log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s delay)"
|
||||
log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry ${MAX_RATE_LIMIT_RETRIES}x"
|
||||
log " Tasks left: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
log ""
|
||||
|
||||
i=1; rate_limit_retries=0
|
||||
while [ "$i" -le "$ITERATION_COUNT" ]; do
|
||||
if ! plan_has_tasks; then
|
||||
banner "ALL TASKS COMPLETE"; log " No remaining [ ] tasks. Stopping."; break
|
||||
fi
|
||||
|
||||
section "ITERATION $i/$ITERATION_COUNT"
|
||||
log " Remaining: $(remaining_tasks)"; log " Next: $(next_task)"; log ""
|
||||
|
||||
export CLAUDE_PROJECT_DIR="$PROJECT_DIR"
|
||||
export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}"
|
||||
|
||||
if [ -f "$PROMPT_FILE" ]; then
|
||||
log " Starting Claude..."; log ""
|
||||
"$CLAUDE_BIN" -p --dangerously-skip-permissions < "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE"
|
||||
CLAUDE_EXIT=$?; log ""; log " Exit code: $CLAUDE_EXIT"
|
||||
else
|
||||
log " ERROR: $PROMPT_FILE not found"; exit 1
|
||||
fi
|
||||
|
||||
if check_rate_limit; then
|
||||
rate_limit_retries=$((rate_limit_retries + 1))
|
||||
if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then
|
||||
section "RATE LIMITED — SCHEDULING RETRY"
|
||||
PLIST_LABEL="com.claude-loop.overnight-retry"
|
||||
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
|
||||
RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M')
|
||||
RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1); RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2)
|
||||
cat > "$PLIST_PATH" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>Label</key><string>${PLIST_LABEL}</string>
|
||||
<key>ProgramArguments</key><array>
|
||||
<string>/bin/sh</string><string>-c</string>
|
||||
<string>cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH}</string>
|
||||
</array>
|
||||
<key>StartCalendarInterval</key><dict><key>Hour</key><integer>${RETRY_HOUR}</integer><key>Minute</key><integer>${RETRY_MIN}</integer></dict>
|
||||
<key>EnvironmentVariables</key><dict>
|
||||
<key>CLAUDE_AUTONOMOUS</key><string>1</string>
|
||||
<key>CLAUDE_PROJECT_DIR</key><string>${PROJECT_DIR}</string>
|
||||
<key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key><string>${LOG_FILE}</string>
|
||||
<key>StandardErrorPath</key><string>${LOG_FILE}</string>
|
||||
</dict></plist>
|
||||
PLIST
|
||||
launchctl load "$PLIST_PATH" 2>/dev/null || true
|
||||
log " Scheduled retry at ~${RETRY_TIME}"; exit 0
|
||||
fi
|
||||
section "RATE LIMITED — WAITING"
|
||||
log " Attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES"; log " Sleeping ${RATE_LIMIT_WAIT}s..."
|
||||
sleep "$RATE_LIMIT_WAIT"
|
||||
if ! plan_has_tasks; then banner "ALL TASKS COMPLETE"; break; fi
|
||||
log " Retrying..."; continue
|
||||
fi
|
||||
|
||||
rate_limit_retries=0
|
||||
section "ITERATION $i COMPLETE"
|
||||
log " Remaining: $(remaining_tasks)"; log " Next: $(next_task)"
|
||||
i=$((i + 1))
|
||||
if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then
|
||||
log " Pausing ${ITERATION_DELAY}s..."; sleep "$ITERATION_DELAY"
|
||||
fi
|
||||
done
|
||||
|
||||
banner "LOOP FINISHED"
|
||||
log " Completed $((i - 1)) iterations"; log " Remaining: $(remaining_tasks)"; log ""
|
||||
LOOPEOF
|
||||
chmod +x "$LOOP_DIR/loop.sh"
|
||||
ok "Created loop/loop.sh"
|
||||
fi
|
||||
|
||||
# ── prompt.md and plan.md are created later after interactive input ────────
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Install hooks ────────────────────────────────────────────────────────────
|
||||
echo -e " ${BOLD}Installing hooks...${NC}"
|
||||
echo ""
|
||||
|
||||
HOOKS_DIR="$HOME/.claude/hooks"
|
||||
mkdir -p "$HOOKS_DIR"
|
||||
|
||||
# prevent-sleep.sh
|
||||
if [[ -f "$HOOKS_DIR/prevent-sleep.sh" ]]; then
|
||||
warn "prevent-sleep.sh exists — skipping"
|
||||
else
|
||||
cat > "$HOOKS_DIR/prevent-sleep.sh" << 'HOOKEOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
old_pid=$(cat "$PID_FILE")
|
||||
kill -0 "$old_pid" 2>/dev/null && kill "$old_pid" 2>/dev/null || true
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
caffeinate -i &
|
||||
echo $! > "$PID_FILE"
|
||||
exit 0
|
||||
HOOKEOF
|
||||
chmod +x "$HOOKS_DIR/prevent-sleep.sh"
|
||||
ok "Installed ~/.claude/hooks/prevent-sleep.sh"
|
||||
fi
|
||||
|
||||
# stop-hook-autonomous.sh
|
||||
if [[ -f "$HOOKS_DIR/stop-hook-autonomous.sh" ]]; then
|
||||
warn "stop-hook-autonomous.sh exists — skipping"
|
||||
else
|
||||
cat > "$HOOKS_DIR/stop-hook-autonomous.sh" << 'HOOKEOF'
|
||||
#!/usr/bin/env bash
|
||||
# "Ralph Wiggum" — blocks Claude from stopping until all plan tasks are done.
|
||||
# Requires CLAUDE_AUTONOMOUS=1 to activate.
|
||||
set -euo pipefail
|
||||
BASE="${CLAUDE_PROJECT_DIR:-}"
|
||||
if [[ -z "$BASE" ]] && command -v jq &>/dev/null; then
|
||||
BASE=$(jq -r '.cwd // empty' 2>/dev/null || true)
|
||||
fi
|
||||
[[ -z "$BASE" ]] && BASE="$(pwd)"
|
||||
PLAN_FILE="${CLAUDE_PLAN_FILE:-plan.md}"
|
||||
ALT_FILES="loop/plan.md todo.md loop/todo.md"
|
||||
AUTO_SLEEP_HOOK="$HOME/.claude/hooks/allow-sleep.sh"
|
||||
plan=""
|
||||
for f in "$PLAN_FILE" $ALT_FILES; do
|
||||
[[ -z "$f" ]] && continue
|
||||
if [[ "$f" == /* ]]; then path="$f"; else path="$BASE/$f"; fi
|
||||
if [[ -f "$path" ]]; then plan="$path"; break; fi
|
||||
done
|
||||
if [[ -z "${CLAUDE_AUTONOMOUS:-}" ]] || [[ "$CLAUDE_AUTONOMOUS" == "0" ]]; then
|
||||
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true; exit 0
|
||||
fi
|
||||
if [[ -z "$plan" ]] || [[ ! -f "$plan" ]]; then
|
||||
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true; exit 0
|
||||
fi
|
||||
incomplete=$(grep -c -E '^\s*[-*]?\s*\[\s*\]' "$plan" 2>/dev/null || echo 0)
|
||||
if [[ "${incomplete:-0}" -gt 0 ]]; then
|
||||
echo '{"decision":"block","reason":"Plan has '"$incomplete"' incomplete task(s). Continue with the next item."}'
|
||||
exit 0
|
||||
fi
|
||||
[[ -x "$AUTO_SLEEP_HOOK" ]] && "$AUTO_SLEEP_HOOK" || true
|
||||
exit 0
|
||||
HOOKEOF
|
||||
chmod +x "$HOOKS_DIR/stop-hook-autonomous.sh"
|
||||
ok "Installed ~/.claude/hooks/stop-hook-autonomous.sh"
|
||||
fi
|
||||
|
||||
# allow-sleep.sh
|
||||
if [[ -f "$HOOKS_DIR/allow-sleep.sh" ]]; then
|
||||
warn "allow-sleep.sh exists — skipping"
|
||||
else
|
||||
cat > "$HOOKS_DIR/allow-sleep.sh" << 'HOOKEOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
PID_FILE="${CLAUDE_CAFFEINATE_PID:-$HOME/.claude/caffeinate.pid}"
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
pid=$(cat "$PID_FILE")
|
||||
kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null || true
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
exit 0
|
||||
HOOKEOF
|
||||
chmod +x "$HOOKS_DIR/allow-sleep.sh"
|
||||
ok "Installed ~/.claude/hooks/allow-sleep.sh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Register hooks in settings.json ─────────────────────────────────────────
|
||||
echo -e " ${BOLD}Configuring Claude settings...${NC}"
|
||||
echo ""
|
||||
|
||||
SETTINGS_FILE="$HOME/.claude/settings.json"
|
||||
|
||||
if [[ -f "$SETTINGS_FILE" ]]; then
|
||||
cp "$SETTINGS_FILE" "${SETTINGS_FILE}.backup.$(date +%s)"
|
||||
info "Backed up settings.json"
|
||||
fi
|
||||
|
||||
if [[ -f "$SETTINGS_FILE" ]] && grep -q "stop-hook-autonomous" "$SETTINGS_FILE" 2>/dev/null; then
|
||||
ok "Hooks already registered"
|
||||
else
|
||||
if command -v python3 &>/dev/null; then
|
||||
python3 << 'PYEOF'
|
||||
import json, os
|
||||
p = os.path.expanduser("~/.claude/settings.json")
|
||||
h = os.path.expanduser("~/.claude/hooks")
|
||||
s = json.load(open(p)) if os.path.exists(p) else {}
|
||||
if "hooks" not in s: s["hooks"] = {}
|
||||
for event, script in [("UserPromptSubmit","prevent-sleep.sh"),("Stop","stop-hook-autonomous.sh"),("SessionEnd","allow-sleep.sh")]:
|
||||
if event not in s["hooks"]: s["hooks"][event] = []
|
||||
if not any(script in json.dumps(x) for x in s["hooks"][event]):
|
||||
s["hooks"][event].append({"matcher":"","hooks":[{"type":"command","command":f"{h}/{script}"}]})
|
||||
with open(p,"w") as f: json.dump(s,f,indent=2); f.write("\n")
|
||||
PYEOF
|
||||
ok "Registered hooks in settings.json"
|
||||
else
|
||||
warn "python3 not found — add hooks to ~/.claude/settings.json manually"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# INTERACTIVE SETUP — collect tasks and project context
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
echo -e " ${DIM}════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Now let's set up your tasks and project context.${NC}"
|
||||
echo ""
|
||||
|
||||
# ── Collect tasks ────────────────────────────────────────────────────────────
|
||||
if [[ -f "$LOOP_DIR/plan.md" ]]; then
|
||||
echo -e " ${YELLOW}loop/plan.md already exists.${NC}"
|
||||
echo -ne " Overwrite with new tasks? [y/N] "
|
||||
read -r OVERWRITE_PLAN
|
||||
[[ "$OVERWRITE_PLAN" =~ ^[Yy] ]] || SKIP_PLAN=1
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_PLAN:-0}" != "1" ]]; then
|
||||
echo -e " ${BOLD}Enter your tasks${NC} — one per line."
|
||||
echo -e " ${DIM}Be specific. Claude will execute these literally.${NC}"
|
||||
echo -e " ${DIM}Example: \"Add JWT authentication with refresh tokens\"${NC}"
|
||||
echo -e " ${DIM}Press Enter on an empty line when done.${NC}"
|
||||
echo ""
|
||||
|
||||
TASKS=()
|
||||
TASK_NUM=1
|
||||
while true; do
|
||||
echo -ne " ${CYAN}Task $TASK_NUM:${NC} "
|
||||
read -r TASK_LINE
|
||||
[[ -z "$TASK_LINE" ]] && break
|
||||
TASKS+=("$TASK_LINE")
|
||||
TASK_NUM=$((TASK_NUM + 1))
|
||||
done
|
||||
|
||||
if [[ ${#TASKS[@]} -eq 0 ]]; then
|
||||
warn "No tasks entered — writing example plan"
|
||||
cat > "$LOOP_DIR/plan.md" << 'PLANEOF'
|
||||
# Task Plan
|
||||
|
||||
## Phase 1
|
||||
- [ ] **1.1** — First task description
|
||||
- [ ] **1.2** — Second task description
|
||||
|
||||
## Final
|
||||
- [ ] **FINAL** — Run full test suite, fix failures, tag release
|
||||
PLANEOF
|
||||
else
|
||||
echo "# Task Plan" > "$LOOP_DIR/plan.md"
|
||||
echo "" >> "$LOOP_DIR/plan.md"
|
||||
i=1
|
||||
for task in "${TASKS[@]}"; do
|
||||
echo "- [ ] **$i** — $task" >> "$LOOP_DIR/plan.md"
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo "" >> "$LOOP_DIR/plan.md"
|
||||
echo "- [ ] **FINAL** — Run full test suite, fix any failures" >> "$LOOP_DIR/plan.md"
|
||||
ok "Wrote ${#TASKS[@]} tasks to loop/plan.md"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Collect project context ──────────────────────────────────────────────────
|
||||
if [[ -f "$LOOP_DIR/prompt.md" ]] && [[ "${SKIP_PLAN:-0}" == "1" ]]; then
|
||||
SKIP_PROMPT=1
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_PROMPT:-0}" != "1" ]]; then
|
||||
echo -e " ${BOLD}Project context${NC} — tell Claude about your project."
|
||||
echo -e " ${DIM}Stack, test commands, coding style, anything important.${NC}"
|
||||
echo -e " ${DIM}Example: \"TypeScript + React, run 'npm test', use Prettier formatting\"${NC}"
|
||||
echo -e " ${DIM}Press Enter on an empty line when done (or just Enter to skip).${NC}"
|
||||
echo ""
|
||||
|
||||
RULES=()
|
||||
while true; do
|
||||
echo -ne " ${CYAN}>${NC} "
|
||||
read -r RULE_LINE
|
||||
[[ -z "$RULE_LINE" ]] && break
|
||||
RULES+=("$RULE_LINE")
|
||||
done
|
||||
|
||||
# Build prompt.md
|
||||
cat > "$LOOP_DIR/prompt.md" << 'PROMPTEOF'
|
||||
You are executing a project roadmap autonomously. Read these files first:
|
||||
|
||||
1. `loop/plan.md` — Your task checklist (mark items `- [x]` as you complete them)
|
||||
2. Read any project documentation (README, CLAUDE.md, etc.) for conventions
|
||||
|
||||
PROMPTEOF
|
||||
|
||||
if [[ ${#RULES[@]} -gt 0 ]]; then
|
||||
echo "## Project Rules" >> "$LOOP_DIR/prompt.md"
|
||||
echo "" >> "$LOOP_DIR/prompt.md"
|
||||
for rule in "${RULES[@]}"; do
|
||||
echo "- $rule" >> "$LOOP_DIR/prompt.md"
|
||||
done
|
||||
echo "" >> "$LOOP_DIR/prompt.md"
|
||||
ok "Added ${#RULES[@]} project rules to prompt"
|
||||
fi
|
||||
|
||||
cat >> "$LOOP_DIR/prompt.md" << 'PROMPTEOF'
|
||||
## For each task in loop/plan.md:
|
||||
|
||||
1. Find the first unchecked `- [ ]` item
|
||||
2. Understand what needs to be done
|
||||
3. Implement it following the project's existing patterns and conventions
|
||||
4. Run the project's type checker / linter / tests — fix all errors
|
||||
5. Commit with a conventional message: `type(scope): description`
|
||||
6. Mark the task `- [x]` in `loop/plan.md`
|
||||
7. Move to the next unchecked task immediately
|
||||
|
||||
## Rules
|
||||
|
||||
- If tests fail, fix them before moving on
|
||||
- If a task is difficult, make at least 30 genuine attempts before skipping
|
||||
- Always run linter + type checker after code changes
|
||||
- Do not stop until all tasks are checked or you are rate limited
|
||||
PROMPTEOF
|
||||
ok "Created loop/prompt.md"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Summary & launch ─────────────────────────────────────────────────────────
|
||||
TASK_COUNT=$(grep -c '^\- \[ \]' "$LOOP_DIR/plan.md" 2>/dev/null || echo "0")
|
||||
|
||||
echo -e " ${DIM}════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}${GREEN}Ready to go!${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Tasks:${NC} $TASK_COUNT in loop/plan.md"
|
||||
echo -e " ${BOLD}Prompt:${NC} loop/prompt.md"
|
||||
echo -e " ${BOLD}Log:${NC} loop/loop.log (created on first run)"
|
||||
echo ""
|
||||
echo -e " ${DIM}────────────────────────────────────────────────────────────${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}To start:${NC}"
|
||||
echo -e " ${GREEN}./loop/loop.sh${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}To start with sleep prevention (macOS):${NC}"
|
||||
echo -e " ${GREEN}caffeinate -i ./loop/loop.sh${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Monitor:${NC}"
|
||||
echo -e " ${DIM}tail -f loop/loop.log${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Config:${NC}"
|
||||
echo -e " ${DIM}CLAUDE_AUTONOMOUS=0${NC} — let Claude stop freely (testing)"
|
||||
echo -e " ${DIM}ITERATION_COUNT=20${NC} — more iterations"
|
||||
echo -e " ${DIM}ITERATION_DELAY=60${NC} — longer pause between rounds"
|
||||
echo ""
|
||||
echo -e " ${DIM}────────────────────────────────────────────────────────────${NC}"
|
||||
echo ""
|
||||
echo -ne " ${BOLD}Start the loop now?${NC} [y/N] "
|
||||
read -r START_NOW
|
||||
|
||||
if [[ "$START_NOW" =~ ^[Yy] ]]; then
|
||||
echo ""
|
||||
echo -e " ${GREEN}Launching...${NC}"
|
||||
echo ""
|
||||
exec ./loop/loop.sh
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${DIM}Run ./loop/loop.sh whenever you're ready.${NC}"
|
||||
echo ""
|
||||
@@ -0,0 +1,443 @@
|
||||
# AIUI Plan 2 — Extended Roadmap
|
||||
|
||||
## Context & Philosophy
|
||||
|
||||
This plan continues from M0–M7 (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 (0–1), Max tokens (256–8192), 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 2–3 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: 50ms–50ms–50ms), 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**
|
||||
@@ -0,0 +1,157 @@
|
||||
# AIUI Progress
|
||||
|
||||
## Current Status
|
||||
**Active Milestone**: COMPLETE
|
||||
**Overall**: M0–M20 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 M3–M7 (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 M9–M20 (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
|
||||
@@ -0,0 +1,113 @@
|
||||
# iOS App Research — AIUI
|
||||
|
||||
## Overview
|
||||
|
||||
Three approaches for shipping AIUI (Vue 3 + Vite SPA) as an iOS app.
|
||||
|
||||
## Approach 1: Capacitor (Recommended)
|
||||
|
||||
Capacitor wraps the Vite build output (`dist/`) in a native iOS Xcode project. The web app runs inside WKWebView with a JavaScript bridge to native device APIs.
|
||||
|
||||
```bash
|
||||
pnpm add @capacitor/core @capacitor/cli @capacitor/ios
|
||||
npx cap init && npx cap add ios
|
||||
pnpm build && npx cap sync
|
||||
npx cap open ios # opens Xcode
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Near-zero code changes to existing Vue 3 app — one codebase for web + iOS + Android
|
||||
- Large, mature plugin ecosystem (camera, biometrics, push, geolocation, haptics)
|
||||
- Hot reload during dev via `npx cap run ios --livereload`
|
||||
- OTA live updates possible via Capgo, bypassing App Store review for JS changes
|
||||
- `@capacitor/push-notifications` wraps APNs natively
|
||||
|
||||
**Cons:**
|
||||
- Service workers do NOT work in WKWebView on iOS (capacitor:// protocol breaks SW registration)
|
||||
- Performance ceiling is WebKit JS engine (not V8)
|
||||
- Each iOS SDK bump requires Capacitor + plugin updates
|
||||
|
||||
**Push Notifications:** Full support via `@capacitor/push-notifications` (APNs). Production-grade.
|
||||
|
||||
**Offline:** Entire app bundle ships inside .ipa — available offline. Dynamic data must use `@capacitor/preferences` or local SQLite. Workbox/SW caching does not work.
|
||||
|
||||
**Performance:** Modern WKWebView uses Nitro JS engine (same as Safari). For a chat UI like AIUI, indistinguishable from Safari. GPU-accelerated CSS transforms work well.
|
||||
|
||||
## Approach 2: Custom WKWebView Swift Wrapper
|
||||
|
||||
Write a native Swift/SwiftUI app embedding WKWebView. Use `WKScriptMessageHandler` for JS↔Swift communication.
|
||||
|
||||
**Pros:**
|
||||
- Maximum native control — own the shell, native navigation, gestures
|
||||
- Can implement App Clips, Share Extensions, Widgets alongside web content
|
||||
- Full access to all iOS APIs at the native layer
|
||||
|
||||
**Cons:**
|
||||
- Requires Swift knowledge — adds second language + build system
|
||||
- JS↔Swift bridge must be hand-written for every integration
|
||||
- No structured plugin community; each integration is bespoke
|
||||
- More setup friction vs Capacitor
|
||||
|
||||
**Push/Offline/Performance:** Same as Capacitor (all use WKWebView). More manual setup.
|
||||
|
||||
## Approach 3: React Native WebView
|
||||
|
||||
Create a React Native app with `react-native-webview` rendering the Vite build output.
|
||||
|
||||
**Pros:**
|
||||
- RN has deep native API access and large ecosystem
|
||||
- Surrounding shell can be fully native
|
||||
|
||||
**Cons:**
|
||||
- Two separate tech stacks (Vue + RN) — highest maintenance burden
|
||||
- No code sharing between Vue app and RN shell
|
||||
- Performance often worse (full RN runtime + WebView engine)
|
||||
- RN's own breaking changes cadence adds risk
|
||||
|
||||
**Verdict:** Only justified if an existing RN app is already in production.
|
||||
|
||||
## App Store Risk: Guideline 4.2
|
||||
|
||||
Apple's Guideline 4.2 (Minimum Functionality) is the primary risk for all webview-based apps. Apps that pass share these traits:
|
||||
- Native tab bar or navigation (not web-based menus)
|
||||
- At least one native API integration (push, biometrics, camera, Apple Pay)
|
||||
- Offline functionality beyond what a browser bookmark offers
|
||||
- UI formatted for iOS, not a desktop website in a phone frame
|
||||
|
||||
For AIUI: the chat interface, push notifications, and offline message history constitute sufficient native functionality.
|
||||
|
||||
## Service Workers in WKWebView
|
||||
|
||||
**SWs do not run inside WKWebView** — this is a fundamental WebKit limitation, not framework-specific. The correct offline strategy for all three approaches: ship assets in app bundle + implement dynamic caching via native storage APIs.
|
||||
|
||||
## Deep Linking
|
||||
|
||||
All three support iOS Universal Links via AASA file + Associated Domains capability:
|
||||
- **Capacitor:** `@capacitor/app` `appUrlOpen` event → Vue Router
|
||||
- **Custom WKWebView:** `AppDelegate.application(_:continue:...)` → JS evaluation
|
||||
- **RN:** React Navigation linking config → WebView `postMessage`
|
||||
|
||||
## Comparison
|
||||
|
||||
| Dimension | Capacitor | Custom WKWebView | RN WebView |
|
||||
|---|---|---|---|
|
||||
| Vue code reuse | 100% | 100% | 100% |
|
||||
| Native shell effort | Low | High | Very high |
|
||||
| Push notifications | First-class | Manual APNs | Via RN layer |
|
||||
| App Store risk | Moderate* | Moderate* | Moderate* |
|
||||
| Performance | Good | Good | Adequate |
|
||||
| Maintenance burden | Low-moderate | High | Very high |
|
||||
| Team fit (web-first) | Best | Poor | Poor |
|
||||
|
||||
*All face identical Guideline 4.2 scrutiny — framework choice is irrelevant to reviewers.
|
||||
|
||||
## Concrete Next Steps
|
||||
|
||||
1. Add `@capacitor/core`, `@capacitor/cli`, `@capacitor/ios` to `packages/app`
|
||||
2. Set Vite `base: './'` for the Capacitor build config
|
||||
3. Disable PWA service worker for native builds (partially done already)
|
||||
4. Add `@capacitor/push-notifications` for APNs
|
||||
5. Implement native splash screen and app icon
|
||||
6. Test on iOS Simulator via `npx cap run ios`
|
||||
7. Set up Apple Developer account + code signing
|
||||
8. Submit TestFlight build for internal testing
|
||||
@@ -0,0 +1,119 @@
|
||||
# Mac Desktop App Research — AIUI
|
||||
|
||||
## Overview
|
||||
|
||||
Two approaches for shipping AIUI as a Mac desktop app: Tauri v2 (Rust-based, system WebView) vs Electron (Chromium-based).
|
||||
|
||||
## Tauri v2 (Recommended)
|
||||
|
||||
Released stable October 2024. Uses OS-native WebView (WKWebView on macOS). The Vue 3 + Vite frontend runs inside the WebView unchanged. JS calls into Rust via typed IPC bridge.
|
||||
|
||||
**Binary Size:** 2–8 MB installer (no bundled runtime)
|
||||
**Memory Usage:** ~30–40 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 ~2–5 min per build.
|
||||
|
||||
### Build Pipeline
|
||||
|
||||
- Prerequisites: Rust toolchain + Xcode CLI tools
|
||||
- First build: 5–15 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:** 80–150 MB installer
|
||||
**Memory Usage:** 200–350 MB idle
|
||||
**Startup Time:** 1–2s
|
||||
|
||||
### 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: 2–5 min (no Rust compilation) + 2–5 min notarization
|
||||
- Complexity: Medium (main/renderer process split requires understanding)
|
||||
|
||||
## Comparison
|
||||
|
||||
| Dimension | Tauri v2 | Electron |
|
||||
|---|---|---|
|
||||
| Installer size | 2–8 MB | 80–150 MB |
|
||||
| Idle RAM | 30–40 MB | 200–350 MB |
|
||||
| Startup time | < 500ms | 1–2s |
|
||||
| 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
|
||||
@@ -0,0 +1,209 @@
|
||||
# Plugin System Hardening Research — AIUI
|
||||
|
||||
## Current State
|
||||
|
||||
The plugin system has these existing components:
|
||||
- `packages/core/src/types/plugin.ts` — `AIUIPlugin` interface, `PluginContext`, `PluginType`
|
||||
- `packages/core/src/plugins/registry.ts` — in-memory Vue ref-based registry
|
||||
- `packages/app/src/stores/pluginMarketplace.ts` — `InstalledPlugin`, `PluginPermission`, `installPlugin`, `hasPermission`
|
||||
- `packages/app/src/components/settings/PluginMarketplace.vue` — permissions dialog
|
||||
- `packages/app/src/components/renderers/CodeRunner.vue` — existing `<iframe sandbox="allow-scripts">` + postMessage pattern
|
||||
|
||||
**Gaps:** No cryptographic signature verification, no runtime permission enforcement in sandbox, no CSP on plugin iframes.
|
||||
|
||||
## 1. Signature Validation for Community Plugins
|
||||
|
||||
### Problem
|
||||
|
||||
`importFromUrl` fetches arbitrary `aiui-plugin.json` from any URL with no integrity check. A compromised URL grants arbitrary code execution.
|
||||
|
||||
### Recommended: Ed25519 Detached Signatures
|
||||
|
||||
Aligns with existing crypto posture (tweetnacl.js for E2E, Web Crypto for storage).
|
||||
|
||||
**Manifest format:**
|
||||
```json
|
||||
{
|
||||
"id": "my-plugin",
|
||||
"name": "My Plugin",
|
||||
"version": "1.2.0",
|
||||
"signature": {
|
||||
"algorithm": "ed25519",
|
||||
"publicKey": "base64-public-key",
|
||||
"value": "base64-signature-over-canonical-manifest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verification:** Canonical payload = manifest JSON minus `signature` field, sorted keys. Verify via `crypto.subtle.verify('Ed25519', ...)` (Chrome 113+, Firefox 130+, Safari 17+) with `tweetnacl.js` fallback.
|
||||
|
||||
**Integration point:** Gate `installPlugin()` on signature verification for Tier 2+ plugins.
|
||||
|
||||
### Secondary: SRI Hash
|
||||
|
||||
`bundleUrl` + `integrity` field enables browser-native subresource integrity enforcement at load time.
|
||||
|
||||
### Trust Model
|
||||
|
||||
| Tier | Who | Signing | Sandbox | Verification |
|
||||
|---|---|---|---|---|
|
||||
| 1 — Built-in | AIUI maintainers | Bundled in app | None (trusted) | None needed |
|
||||
| 2 — Verified | Reviewed by AIUI team | Ed25519 by author | Full iframe sandbox | Signature + hash |
|
||||
| 3 — Sideloaded | User-imported URL | Optional | Full iframe sandbox, stricter CSP | Warn prominently |
|
||||
|
||||
## 2. Sandboxed iframe Execution
|
||||
|
||||
### Architecture
|
||||
|
||||
Each community plugin runs in an isolated iframe. AIUI manages a `PluginBridge` service:
|
||||
|
||||
```
|
||||
AIUI App (parent)
|
||||
│
|
||||
│ postMessage (structured protocol)
|
||||
│
|
||||
└── Plugin Sandbox iframe (origin: null, sandbox="allow-scripts")
|
||||
└── Plugin code (no DOM, no storage, no cross-origin network)
|
||||
```
|
||||
|
||||
### Sandbox Configuration
|
||||
|
||||
```html
|
||||
<!-- Tier 2 plugin (headless, no UI) -->
|
||||
<iframe sandbox="allow-scripts" srcdoc="..." style="display: none" />
|
||||
|
||||
<!-- Tier 2 plugin with UI panel -->
|
||||
<iframe sandbox="allow-scripts allow-popups-to-escape-sandbox" srcdoc="..." />
|
||||
```
|
||||
|
||||
**Never grant:** `allow-same-origin` (breaks isolation), `allow-forms`, `allow-top-navigation`, `allow-modals` unless explicitly user-approved.
|
||||
|
||||
### postMessage Protocol
|
||||
|
||||
Typed message envelopes following the archyBridge pattern:
|
||||
|
||||
```typescript
|
||||
// Plugin → Host
|
||||
interface PluginRequest {
|
||||
type: 'plugin:request'
|
||||
id: string // correlation ID
|
||||
pluginId: string
|
||||
capability: string // e.g. 'storage:get', 'network:fetch'
|
||||
payload: unknown
|
||||
}
|
||||
|
||||
// Host → Plugin
|
||||
interface PluginResponse {
|
||||
type: 'plugin:response'
|
||||
id: string
|
||||
success: boolean
|
||||
data?: unknown
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
### Host-side Validation
|
||||
|
||||
1. `event.origin` must be `null` (sandboxed srcdoc iframes)
|
||||
2. `pluginId` must match the iframe→plugin mapping
|
||||
3. Requested capability must be in `grantedPermissions`
|
||||
4. Rate-limit: reject if > N requests/second (DoS prevention)
|
||||
|
||||
### Network Restriction
|
||||
|
||||
With `sandbox="allow-scripts"` alone, iframes can still `fetch()`. Block direct network via CSP in srcdoc:
|
||||
|
||||
```html
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'unsafe-inline'; connect-src 'none'">
|
||||
```
|
||||
|
||||
Plugins requiring network use the `network:fetch` capability — host proxies the request after validating the URL.
|
||||
|
||||
### Storage Isolation
|
||||
|
||||
Sandboxed iframes without `allow-same-origin` cannot access parent's localStorage/IndexedDB. Plugin storage goes through the `storage` capability, namespaced under `plugin::{id}::`.
|
||||
|
||||
## 3. Permission System Per Plugin
|
||||
|
||||
### Expanded Permissions
|
||||
|
||||
```typescript
|
||||
export type PluginPermission =
|
||||
| 'chat-read' // read chat history
|
||||
| 'chat-inject' // inject messages (high risk)
|
||||
| 'chat-messages' // read + inject (legacy, maps to both)
|
||||
| 'network' // outbound HTTPS via host proxy
|
||||
| 'favorites' // read/write favorites
|
||||
| 'storage' // namespaced plugin storage
|
||||
| 'nostr' // access Nostr identity (high risk)
|
||||
| 'wallet' // deep-link to wallet (high risk)
|
||||
| 'clipboard' // read/write clipboard
|
||||
| 'notifications' // send notifications
|
||||
| 'media-playback' // control media player
|
||||
| 'renderer' // register content renderer
|
||||
| 'settings-read' // read AIUI settings
|
||||
```
|
||||
|
||||
### Risk Classification
|
||||
|
||||
```typescript
|
||||
const permissionRisk: Record<PluginPermission, 'low' | 'medium' | 'high'> = {
|
||||
'network': 'low',
|
||||
'storage': 'low',
|
||||
'chat-read': 'low',
|
||||
'favorites': 'low',
|
||||
'notifications': 'medium',
|
||||
'clipboard': 'medium',
|
||||
'media-playback': 'medium',
|
||||
'renderer': 'medium',
|
||||
'settings-read': 'medium',
|
||||
'chat-messages': 'high',
|
||||
'chat-inject': 'high',
|
||||
'nostr': 'high',
|
||||
'wallet': 'high',
|
||||
}
|
||||
```
|
||||
|
||||
### User Consent Flow
|
||||
|
||||
1. High-risk permissions default to unchecked in consent dialog
|
||||
2. Show risk badges (low/medium/high) next to each permission
|
||||
3. Unverified plugins (Tier 3) show prominent warning before permissions dialog
|
||||
4. Progressive disclosure: summary before detailed checkboxes
|
||||
|
||||
### Permission Revocation
|
||||
|
||||
1. Terminate plugin iframe immediately (`iframe.remove()`)
|
||||
2. Host handler rechecks `hasPermission()` on every capability call
|
||||
3. Emit `plugin:permission-revoked` event so plugin can react gracefully
|
||||
|
||||
### Least Privilege
|
||||
|
||||
- Plugins declare minimum permissions in manifest
|
||||
- All capabilities gated on `hasPermission()` — no admin override
|
||||
- Storage namespaced under `plugin::{id}::`
|
||||
- Optional `allowedOrigins[]` in manifest restricts network targets
|
||||
- Audit log: capability invocations logged with plugin ID
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Files to modify:
|
||||
|
||||
**`packages/core/src/types/plugin.ts`** — Add `tier`, `signature`, `sandbox` fields to manifest type.
|
||||
|
||||
**`packages/app/src/stores/pluginMarketplace.ts`** — Add `verifyManifestSignature()` in `installPlugin()`. Add `revokePermission()`. Move `grantedPermissions` to encrypted storage.
|
||||
|
||||
**`packages/core/src/plugins/registry.ts`** — Evolve into `PluginHost` service that manages sandbox iframe lifecycle and routes postMessage capability requests.
|
||||
|
||||
**`packages/app/src/plugins/index.ts`** — Tier 1 plugins use `registerPlugin()` directly. Tier 2+ load through `PluginHost.loadSandboxed(manifest)`.
|
||||
|
||||
## Concrete Next Steps
|
||||
|
||||
1. Define `PluginSignature` type and `verifyManifestSignature()` using tweetnacl.js
|
||||
2. Create `PluginSandbox` service to manage iframe lifecycle + postMessage routing
|
||||
3. Add CSP meta tag to plugin srcdoc template
|
||||
4. Implement capability handlers (storage, network, chat) with permission checks
|
||||
5. Update consent dialog with risk classification badges
|
||||
6. Add `revokePermission()` with live iframe termination
|
||||
7. Create CLI tool for plugin authors to sign manifests
|
||||
@@ -0,0 +1,150 @@
|
||||
# Overnight Claude Automation
|
||||
|
||||
Run Claude Code autonomously while you're away. Combines sleep prevention, task-based execution, the Ralph Wiggum Technique (Stop hook blocks until plan is complete), and security hooks that restrict AI to project files and block destructive commands.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Claude Code CLI** ([claude.ai/code](https://claude.ai/code)) — installed at `~/.local/bin/claude` or in PATH
|
||||
- **Hooks** — user-level hooks in `~/.claude/` (sleep, Ralph Wiggum)
|
||||
- **jq** — for security hook scripts (`brew install jq`)
|
||||
|
||||
## Flow
|
||||
|
||||
### Pre-run (before 5–6pm)
|
||||
|
||||
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 1–2 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
|
||||
@@ -0,0 +1,286 @@
|
||||
# AIUI Debug, Fix & Hardening Plan
|
||||
|
||||
## Execution Rules
|
||||
|
||||
- Run on `development` branch
|
||||
- Each `- [ ]` task is one agent iteration — complete fully before moving on
|
||||
- After each phase: `pnpm typecheck && pnpm lint && pnpm test -- --run`
|
||||
- If checks fail, fix before proceeding
|
||||
- Commit at end of each phase with `type(scope): description` format
|
||||
- Do NOT push — human will review and push
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0: Baseline Verification
|
||||
|
||||
- [ ] **P0.1 — Baseline check**
|
||||
- Run: `pnpm install && pnpm typecheck && pnpm lint && pnpm test -- --run`
|
||||
- Record output. Note existing failures. Fix any blockers before proceeding.
|
||||
- Commit: `chore(app): verify baseline before hardening`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: Critical Security Fixes
|
||||
|
||||
- [ ] **P1.1 — Fix CORS wildcard in claude-proxy.ts**
|
||||
- File: `packages/app/server/claude-proxy.ts`
|
||||
- Replace all `'Access-Control-Allow-Origin': '*'` (lines ~415, 473, 531) with the correct localhost origin or import `ALLOWED_ORIGIN` from `dev-auth.ts`. Match the pattern already used on lines 294/303.
|
||||
- Verify: `grep -n "Allow-Origin.*\*" packages/app/server/claude-proxy.ts` returns zero matches.
|
||||
|
||||
- [ ] **P1.2 — Fix dev auth token bypass**
|
||||
- File: `packages/app/server/dev-auth.ts`
|
||||
- Line 11: `if (!token) return true` skips auth entirely when token empty.
|
||||
- Fix: Only skip in non-production. `if (!token) { if (process.env.NODE_ENV === 'production') { res.writeHead(401); res.end('Unauthorized'); return false; } return true; }`
|
||||
- Add console.warn when auth disabled.
|
||||
|
||||
- [ ] **P1.3 — Symlink traversal protection in vite-fs.ts**
|
||||
- File: `packages/app/vite-fs.ts`
|
||||
- In `walk` function: after constructing `fullPath`, add `if (lstatSync(fullPath).isSymbolicLink()) continue;`
|
||||
- In `handleRead`: before `statSync`, check `lstatSync(filePath).isSymbolicLink()` → return 403.
|
||||
- Import `lstatSync` from `fs`.
|
||||
|
||||
- [ ] **P1.4 — Body size limit on vite-fs.ts mkdir**
|
||||
- File: `packages/app/vite-fs.ts`
|
||||
- `handleMkdir` reads body with no size cap (lines 185-209).
|
||||
- Add `const MAX_BODY_SIZE = 1024`. Track `let size = 0` on `data` events. Return 413 if exceeded.
|
||||
|
||||
- [ ] **P1.5 — JSON schema validation in vite-dev-chats.ts**
|
||||
- File: `packages/app/vite-dev-chats.ts`
|
||||
- After `JSON.parse(body)` on line 66, validate shape: must be object with optional `conversations` (object) and `activeConversationId` (string|null). Reject with 400 if invalid.
|
||||
|
||||
- [ ] **P1.6 — CSP headers in nginx-archy.conf**
|
||||
- File: `packages/app/server/nginx-archy.conf`
|
||||
- Add inside `/aiui/` location block:
|
||||
```
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https://api.anthropic.com https://openrouter.ai https://wavlake.com https://itunes.apple.com https://openlibrary.org https://covers.openlibrary.org https://en.wikipedia.org https://www.googleapis.com https://image.tmdb.org; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
```
|
||||
|
||||
- [ ] **P1.7 — OpenRouter validation + streaming timeout**
|
||||
- File: `packages/app/server/claude-proxy.ts`
|
||||
- OpenRouter handler: parse `reqBody`, validate has `model` (string), `messages` (array), `stream` (boolean). Return 400 if invalid.
|
||||
- Both streaming reader loops: add idle timeout (120s). `let idleTimer = setTimeout(() => reader.cancel(), 120000)`. Reset on each chunk. Clear on completion.
|
||||
- Verify: `pnpm typecheck`
|
||||
- **Commit**: `fix(app): critical security — CORS, auth, symlink, CSP, body validation`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: Proxy & Server Hardening
|
||||
|
||||
- [ ] **P2.1 — Fix tool use loop in claude-proxy.ts**
|
||||
- File: `packages/app/server/claude-proxy.ts` (lines 164-222)
|
||||
- Unknown tool names: push error tool_result `{ type: 'tool_result', tool_use_id: tu.id, content: 'Error: unknown tool', is_error: true }`.
|
||||
- Add turnMessages size guard: `if (JSON.stringify(turnMessages).length > 500_000)` break loop.
|
||||
- Add `AbortSignal.timeout(30000)` on each API call within loop.
|
||||
|
||||
- [ ] **P2.2 — SSRF mitigation in vite-rss.ts**
|
||||
- File: `packages/app/vite-rss.ts`
|
||||
- After `tryParseFeed` returns, validate returned article URLs with `isPrivateUrl`. Filter out any private URLs.
|
||||
- Add comment documenting residual TOCTOU risk (dev-only middleware).
|
||||
|
||||
- [ ] **P2.3 — Sanitize web search context in useAI.ts**
|
||||
- File: `packages/app/src/composables/useAI.ts` (lines 399-408)
|
||||
- Create helper: `function sanitizeSearchText(s: string): string { return s.replace(/[[\]()#*_~` >]/g, '\\$&').replace(/\n/g, ' ').slice(0, 500) }`
|
||||
- Apply to `r.title` and `r.content` in `formatWebSearchContext`.
|
||||
- **Commit**: `fix(app): proxy hardening — tool loop, SSRF, prompt injection`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: State & Logic Bugs
|
||||
|
||||
- [ ] **P3.1 — Fix contentType assignment in useContentPanel.ts**
|
||||
- File: `packages/app/src/composables/useContentPanel.ts` (lines 219-225)
|
||||
- The `contentType` ref type is `'film' | 'song' | 'podcast'`. Expand to include all content types or use `ContentTab`. Fix assignments: books→'film' is acceptable if type can't expand, but news/TV should map correctly. If the type is only used for player logic, keep narrow type but fix assignments to be semantically correct.
|
||||
|
||||
- [ ] **P3.2 — Fix apiFetching race in useBannerFallback.ts**
|
||||
- File: `packages/app/src/composables/useBannerFallback.ts`
|
||||
- Change `let apiFetching = false` to `const apiFetching = ref(false)`. Update all references to `.value`.
|
||||
|
||||
- [ ] **P3.3 — Fix event listener cleanup in chat.ts**
|
||||
- File: `packages/app/src/stores/chat.ts` (lines 111-118)
|
||||
- Extract handlers to named functions. Add `import.meta.hot?.dispose()` cleanup for HMR.
|
||||
|
||||
- [ ] **P3.4 — Fix news RSS race condition in useContentPanel.ts**
|
||||
- File: `packages/app/src/composables/useContentPanel.ts` (lines 134-154)
|
||||
- Capture `const tabAtStart = activeTab.value` before RSS fetch. In `.then()`, only set `activeTab.value = 'news'` if `activeTab.value === tabAtStart` (user hasn't manually switched).
|
||||
|
||||
- [ ] **P3.5 — Add error logging to silent catch blocks**
|
||||
- Files: `useImageFallback.ts`, `useAI.ts`, `stores/chat.ts`
|
||||
- Replace all `catch { }` and `catch { /* ignore */ }` with `catch(e) { console.debug('[module] op failed:', e) }`.
|
||||
- Keep `console.debug` (not `error`) for expected failures.
|
||||
- **Commit**: `fix(app): state bugs — contentType, race conditions, error logging`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4: Content Extraction & Filtering
|
||||
|
||||
- [ ] **P4.1 — Add recipe instructions to system prompt**
|
||||
- File: `packages/app/src/composables/useAI.ts` (SYSTEM_PROMPT)
|
||||
- Add after Apps section:
|
||||
```
|
||||
**Recipes:** When sharing recipes, use the <recipe_ext> XML tag:
|
||||
<recipe_ext title="Name" servings="4" time="30 min" calories="450">
|
||||
- ingredient 1
|
||||
1. Step one
|
||||
</recipe_ext>
|
||||
```
|
||||
|
||||
- [ ] **P4.2 — Fix app category validation**
|
||||
- File: `packages/app/src/composables/contentExtraction.ts` (line ~1330)
|
||||
- Add `const VALID_CATEGORIES = new Set(['nostr-client','lightning-wallet','bitcoin-wallet','privacy','node','dev-tool','relay'])`. Validate before cast, fallback to `'dev-tool'`.
|
||||
|
||||
- [ ] **P4.3 — Fix song deduplication**
|
||||
- File: `packages/app/src/composables/contentExtraction.ts` (lines 606-638)
|
||||
- After combining library + external songs, deduplicate by normalized `title|artist` key. Library songs (with real IDs) take priority.
|
||||
|
||||
- [ ] **P4.4 — Fix film/TV tag ambiguity**
|
||||
- File: `packages/app/src/composables/contentExtraction.ts` (lines 914-916)
|
||||
- Only convert `film_ext` to TV when: query is TV-like AND no explicit `tv_ext` tags present AND response reads TV-like. If AI used both `film_ext` and `tv_ext`, keep both as-is.
|
||||
|
||||
- [ ] **P4.5 — Fix isBookLikeResponse false positives**
|
||||
- File: `packages/app/src/composables/contentFiltering.ts` (lines 60-62)
|
||||
- Increase `by [Author]` threshold from `>= 1` to `>= 2`.
|
||||
|
||||
- [ ] **P4.6 — Fix isRecipeLikeResponse for prose recipes**
|
||||
- File: `packages/app/src/composables/contentFiltering.ts` (lines 92-94)
|
||||
- Add prose detection: recipe keywords + list structure pattern.
|
||||
|
||||
- [ ] **P4.7 — Fix looksLikeSong false positives**
|
||||
- File: `packages/app/src/composables/contentExtraction.ts` (lines 488-506)
|
||||
- Add financial terms to blocklist: `'market cap','etf','price','trading','volume','earnings','valuation','stock','portfolio','investment','yield','inflation','interest rate'`.
|
||||
- **Commit**: `fix(app): extraction — dedup, tag ambiguity, false positives, recipes`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5: Memory & Cache Hardening
|
||||
|
||||
- [ ] **P5.1 — Bound caches in useImageFallback.ts**
|
||||
- File: `packages/app/src/composables/useImageFallback.ts`
|
||||
- Create helper: `function boundedSet<K,V>(map: Map<K,V>, key: K, val: V, max=500) { if (map.size >= max) { const first = map.keys().next().value; if (first !== undefined) map.delete(first); } map.set(key, val); }`
|
||||
- Replace all `.set()` calls on memory caches with `boundedSet()`.
|
||||
- Also bound `failedUrls` Set to 1000 entries.
|
||||
|
||||
- [ ] **P5.2 — Bound caches in usePlayer.ts + cleanup**
|
||||
- File: `packages/app/src/composables/usePlayer.ts`
|
||||
- Bound `resultCache` and `nullCacheTimestamps` to 200 entries.
|
||||
- Add `destroy()` method that calls `destroyPlayer()`, resets DOM refs, cancels active search controller.
|
||||
- **Commit**: `fix(app): bound caches, player cleanup`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 6: Accessibility
|
||||
|
||||
- [ ] **P6.1 — Fix touch targets and aria attributes**
|
||||
- Audit all `*Grid.vue` and `*Detail.vue` for: interactive elements < 44px, images without alt, SVG fallbacks without aria-label.
|
||||
- ImageGrid.vue: add `role="img"` `aria-label="Image unavailable"` to fallback SVG wrapper.
|
||||
- Ensure all interactive genre/tag pills in grids have adequate touch targets (min 44x44 including padding).
|
||||
- Non-interactive info pills are exempt.
|
||||
- **Commit**: `fix(app): accessibility — touch targets, alt text, aria`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 7: Security Tests
|
||||
|
||||
- [ ] **P7.1 — Write dev-auth tests**
|
||||
- New file: `packages/app/src/__tests__/dev-auth.test.ts`
|
||||
- Tests: auth bypass when no token (dev), auth required when token set, rate limiting 429, CORS headers correct.
|
||||
|
||||
- [ ] **P7.2 — Extend proxy tests**
|
||||
- File: `packages/app/src/__tests__/proxy.test.ts`
|
||||
- Tests: unknown tool error result, max rounds termination, turnMessages size guard, streaming timeout.
|
||||
|
||||
- [ ] **P7.3 — Write vite-fs security tests**
|
||||
- New file: `packages/app/src/__tests__/vite-fs.test.ts`
|
||||
- Tests: path traversal rejected, symlink blocked, sensitive files blocked, body size limit enforced.
|
||||
|
||||
- [ ] **P7.4 — Write SSRF tests for vite-rss**
|
||||
- New file: `packages/app/src/__tests__/vite-rss.test.ts`
|
||||
- Tests: private IP detection, localhost blocked, non-http blocked, private URLs filtered from results.
|
||||
- **Commit**: `test(app): security tests — auth, proxy, fs, SSRF`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 8: Content Extraction Tests
|
||||
|
||||
- [ ] **P8.1 — Write content filtering tests**
|
||||
- New file: `packages/app/src/composables/__tests__/contentFiltering.test.ts`
|
||||
- Tests: isBookLikeResponse false positive fix, isRecipeLikeResponse prose detection, isMusicQuery rejects financial terms, filterTabsByContext ordering.
|
||||
|
||||
- [ ] **P8.2 — Extend content extraction tests**
|
||||
- File: `packages/app/src/__tests__/contentExtraction.test.ts`
|
||||
- Tests: song dedup, looksLikeSong rejects financial terms, app category fallback, recipe parsing.
|
||||
|
||||
- [ ] **P8.3 — Write TV/film resolution tests**
|
||||
- Same file as P8.2.
|
||||
- Tests: film_ext→TV conversion rules, book_ext in news context, explicit tags respected.
|
||||
- **Commit**: `test(app): content extraction — filtering, dedup, tag resolution`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 9: State & Composable Tests
|
||||
|
||||
- [ ] **P9.1 — Write useContentPanel tests**
|
||||
- File: `packages/app/src/composables/__tests__/useContentPanel.test.ts`
|
||||
- Tests: contentType assignment per content type, RSS tab override prevention, closePanel resets, empty text → no tabs.
|
||||
|
||||
- [ ] **P9.2 — Write useBannerFallback tests**
|
||||
- New file: `packages/app/src/composables/__tests__/useBannerFallback.test.ts`
|
||||
- Tests: primary URL fallthrough, API fetch on exhaustion, apiFetching guard, gradient fallback.
|
||||
|
||||
- [ ] **P9.3 — Write chat store tests**
|
||||
- New file: `packages/app/src/__tests__/chat-store.test.ts`
|
||||
- Tests: createConversation, addMessage, deleteConversation, branchFromMessage, debouncedIDBSave.
|
||||
- **Commit**: `test(app): state tests — contentPanel, bannerFallback, chat store`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 10: AI Integration Tests
|
||||
|
||||
- [ ] **P10.1 — Extend useAI tests**
|
||||
- File: `packages/app/src/__tests__/useAI.test.ts`
|
||||
- Tests: system prompt includes recipe instructions, web search results sanitized, editAndResend truncates, sanitizeHistory merges consecutive roles.
|
||||
|
||||
- [ ] **P10.2 — Write useImageFallback cache tests**
|
||||
- New file: `packages/app/src/composables/__tests__/useImageFallback.test.ts`
|
||||
- Tests: boundedSet evicts at max, generatePosterFallback valid SVG, escapeXml handles specials.
|
||||
- **Commit**: `test(app): AI integration, image fallback cache tests`
|
||||
|
||||
---
|
||||
|
||||
## PHASE 11: Final Verification
|
||||
|
||||
- [ ] **P11.1 — Full integration check**
|
||||
- Run: `pnpm typecheck && pnpm lint && pnpm test -- --run`
|
||||
- Run: `pnpm build` — verify production build succeeds
|
||||
- Fix any regressions.
|
||||
|
||||
- [ ] **P11.2 — Final commit**
|
||||
- Run coverage report if configured: `pnpm test -- --run --coverage`
|
||||
- Ensure all changes committed on `development`.
|
||||
- **Commit**: `chore(app): hardening complete — all checks pass`
|
||||
|
||||
---
|
||||
|
||||
## Phase Dependencies
|
||||
|
||||
```
|
||||
P0 → P1 → P2 → P3 → P4 → P5 ─┐
|
||||
├→ P7 (tests P1,P2)
|
||||
P6 ──┤
|
||||
├→ P8 (tests P4)
|
||||
├→ P9 (tests P3,P5)
|
||||
├→ P10 (tests P2.3,P4.1)
|
||||
└→ P11 (final)
|
||||
```
|
||||
|
||||
P5 and P6 can run in parallel. P7-P10 can run in any order after their fix phases.
|
||||
|
||||
## Summary
|
||||
|
||||
- **12 phases**, **42 tasks**
|
||||
- Phases 0-6: fixes (security → proxy → state → extraction → caches → a11y)
|
||||
- Phases 7-10: tests (security → extraction → state → AI)
|
||||
- Phase 11: final verification
|
||||
- Target: all checks pass, 40%+ test coverage on critical paths, secure for Archy deployment
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env sh
|
||||
# Headless loop script for overnight Claude Code automation.
|
||||
# Set CLAUDE_AUTONOMOUS=1 for Ralph Wiggum (Stop hook blocks until plan is complete).
|
||||
# Rate-limit aware: detects limits, sleeps until reset, and retries automatically.
|
||||
set -u
|
||||
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
PROMPT_FILE="${PROMPT_FILE:-$PROJECT_DIR/loop/prompt.md}"
|
||||
LOG_FILE="${LOG_FILE:-$PROJECT_DIR/loop/loop.log}"
|
||||
ITERATION_COUNT="${ITERATION_COUNT:-10}"
|
||||
ITERATION_DELAY="${ITERATION_DELAY:-30}"
|
||||
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
|
||||
RATE_LIMIT_WAIT="${RATE_LIMIT_WAIT:-3600}" # Default: wait 1 hour on rate limit
|
||||
MAX_RATE_LIMIT_RETRIES="${MAX_RATE_LIMIT_RETRIES:-5}" # Max retries before giving up
|
||||
CLAUDE_EXIT=0
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Human-readable log with visual separators
|
||||
log() {
|
||||
echo "$1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
banner() {
|
||||
log ""
|
||||
log "════════════════════════════════════════════════════════════════"
|
||||
log " $1"
|
||||
log " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
log "════════════════════════════════════════════════════════════════"
|
||||
log ""
|
||||
}
|
||||
|
||||
section() {
|
||||
log ""
|
||||
log "────────────────────────────────────────"
|
||||
log " $1"
|
||||
log "────────────────────────────────────────"
|
||||
log ""
|
||||
}
|
||||
|
||||
# Check if plan has remaining tasks
|
||||
plan_has_tasks() {
|
||||
grep -q '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null
|
||||
}
|
||||
|
||||
# Show remaining task count
|
||||
remaining_tasks() {
|
||||
grep -c '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
# Show next task
|
||||
next_task() {
|
||||
grep -m1 '^\- \[ \]' "$PROJECT_DIR/loop/plan.md" 2>/dev/null | sed 's/^- \[ \] //' || echo "(none)"
|
||||
}
|
||||
|
||||
# Detect rate limit from Claude output (only when Claude exited non-zero)
|
||||
check_rate_limit() {
|
||||
[ "${CLAUDE_EXIT:-0}" -eq 0 ] && return 1
|
||||
# Check last 50 lines for rate limit indicators, excluding our own log lines
|
||||
tail -50 "$LOG_FILE" 2>/dev/null | grep -v "^Rate limit detected" | grep -v "^Sleeping" | grep -v "^═" | grep -v "^─" | grep -qi \
|
||||
-e "rate.limit" \
|
||||
-e "too.many.requests" \
|
||||
-e "429" \
|
||||
-e "quota.exceeded" \
|
||||
-e "usage.limit" \
|
||||
-e "limit.reached" 2>/dev/null
|
||||
}
|
||||
|
||||
banner "OVERNIGHT AUTOMATION STARTED"
|
||||
log " Project: $PROJECT_DIR"
|
||||
log " Prompt: $PROMPT_FILE"
|
||||
log " Autonomous: ${CLAUDE_AUTONOMOUS:-0}"
|
||||
log " Iterations: $ITERATION_COUNT (${ITERATION_DELAY}s between each)"
|
||||
log " Rate limit: wait ${RATE_LIMIT_WAIT}s, retry up to ${MAX_RATE_LIMIT_RETRIES}x"
|
||||
log " Tasks left: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
log ""
|
||||
|
||||
i=1
|
||||
rate_limit_retries=0
|
||||
while [ "$i" -le "$ITERATION_COUNT" ]; do
|
||||
|
||||
# Check if there are tasks remaining before starting
|
||||
if ! plan_has_tasks; then
|
||||
banner "ALL TASKS COMPLETE"
|
||||
log " No remaining tasks in plan.md. Stopping."
|
||||
break
|
||||
fi
|
||||
|
||||
section "ITERATION $i/$ITERATION_COUNT"
|
||||
log " Tasks remaining: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
log ""
|
||||
|
||||
export CLAUDE_PROJECT_DIR="$PROJECT_DIR"
|
||||
export CLAUDE_AUTONOMOUS="${CLAUDE_AUTONOMOUS:-1}"
|
||||
|
||||
# Run Claude with autonomous permissions; prompt from file
|
||||
if [ -f "$PROMPT_FILE" ]; then
|
||||
log " Starting Claude session..."
|
||||
log ""
|
||||
"$CLAUDE_BIN" -p --dangerously-skip-permissions \
|
||||
< "$PROMPT_FILE" 2>&1 | tee -a "$LOG_FILE"
|
||||
CLAUDE_EXIT=$?
|
||||
log ""
|
||||
log " Claude exited with code: $CLAUDE_EXIT"
|
||||
else
|
||||
log " ERROR: $PROMPT_FILE not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for rate limit after Claude exits
|
||||
if check_rate_limit; then
|
||||
rate_limit_retries=$((rate_limit_retries + 1))
|
||||
if [ "$rate_limit_retries" -ge "$MAX_RATE_LIMIT_RETRIES" ]; then
|
||||
section "RATE LIMITED — SCHEDULING LAUNCHD RETRY"
|
||||
log " Hit rate limit $rate_limit_retries times. Creating launchd job to retry later."
|
||||
|
||||
# Schedule a retry using launchd for after rate limit resets
|
||||
PLIST_LABEL="com.aiui.overnight-retry"
|
||||
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_LABEL}.plist"
|
||||
RETRY_TIME=$(date -v+${RATE_LIMIT_WAIT}S '+%H:%M' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M')
|
||||
RETRY_HOUR=$(echo "$RETRY_TIME" | cut -d: -f1)
|
||||
RETRY_MIN=$(echo "$RETRY_TIME" | cut -d: -f2)
|
||||
|
||||
cat > "$PLIST_PATH" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>${PLIST_LABEL}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/sh</string>
|
||||
<string>-c</string>
|
||||
<string>cd ${PROJECT_DIR} && caffeinate -i ./loop/loop.sh >> ${LOG_FILE} 2>&1; launchctl unload ${PLIST_PATH}; rm -f ${PLIST_PATH}</string>
|
||||
</array>
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>${RETRY_HOUR}</integer>
|
||||
<key>Minute</key>
|
||||
<integer>${RETRY_MIN}</integer>
|
||||
</dict>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>CLAUDE_AUTONOMOUS</key>
|
||||
<string>1</string>
|
||||
<key>CLAUDE_PROJECT_DIR</key>
|
||||
<string>${PROJECT_DIR}</string>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>${LOG_FILE}</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>${LOG_FILE}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
launchctl load "$PLIST_PATH" 2>/dev/null || true
|
||||
log " Scheduled retry at ~${RETRY_TIME}"
|
||||
log " Plist: $PLIST_PATH (auto-removes after running)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
section "RATE LIMITED — WAITING"
|
||||
log " Attempt $rate_limit_retries/$MAX_RATE_LIMIT_RETRIES"
|
||||
log " Sleeping ${RATE_LIMIT_WAIT}s until $(date -v+${RATE_LIMIT_WAIT}S '+%H:%M:%S' 2>/dev/null || date -d "+${RATE_LIMIT_WAIT} seconds" '+%H:%M:%S')..."
|
||||
sleep "$RATE_LIMIT_WAIT"
|
||||
|
||||
# Check if plan still has tasks before retrying
|
||||
if ! plan_has_tasks; then
|
||||
banner "ALL TASKS COMPLETE (during rate limit wait)"
|
||||
break
|
||||
fi
|
||||
log " Retrying..."
|
||||
continue # Retry same iteration
|
||||
fi
|
||||
|
||||
# Reset rate limit counter on successful run
|
||||
rate_limit_retries=0
|
||||
|
||||
section "ITERATION $i COMPLETE"
|
||||
log " Tasks remaining: $(remaining_tasks)"
|
||||
log " Next task: $(next_task)"
|
||||
|
||||
i=$((i + 1))
|
||||
if [ "$i" -le "$ITERATION_COUNT" ] && [ "$ITERATION_DELAY" -gt 0 ]; then
|
||||
log " Pausing ${ITERATION_DELAY}s before next iteration..."
|
||||
sleep "$ITERATION_DELAY"
|
||||
fi
|
||||
done
|
||||
|
||||
banner "LOOP FINISHED"
|
||||
log " Completed $((i - 1)) iterations"
|
||||
log " Tasks remaining: $(remaining_tasks)"
|
||||
log ""
|
||||
@@ -0,0 +1,59 @@
|
||||
# Overnight Plan — 2026-03-04
|
||||
|
||||
## Phase 1: Critical Fixes
|
||||
|
||||
- [x] P1-1: Brighten SVG fallbacks — increase background lightness from 18% to 28% across all 8 generators in `useImageFallback.ts` (generateSongCoverFallback, generatePodcastCoverFallback, generateNewsFallback, generateImageFallback, generatePosterFallback, generateTVSeriesFallback, generateBookCoverFallback, generatePlaceFallback). Proportionally increase all inner element lightness by +10%. TEST: run `pnpm typecheck` and visually confirm SVGs generate valid data URIs.
|
||||
- [x] P1-2: Add `.catch(() => {})` to all cover fetch promise chains in grid components — SongGrid.vue (line 157), FilmGrid.vue (line 143), TVSeriesGrid.vue (line 160), BookGrid.vue (line 141), PodcastGrid.vue (line 130). Prevents unhandled rejection if fetch throws unexpectedly. TEST: `pnpm typecheck && pnpm lint`.
|
||||
- [x] P1-3: Refine mobile keyboard handling — In `useVisualViewport.ts`, add debounce to viewport change handler (50ms) to prevent jittery resizing. In `App.vue`, ensure the `rootStyle` computed applies `overflow: hidden` when keyboard is open. TEST: `pnpm typecheck`.
|
||||
- [x] P1-4: Verify service worker cleanup — Confirm `dev-dist/sw.js` contains the self-destructing SW and `vite.config.ts` has `devOptions.enabled: false`. If not, fix. TEST: read both files and verify.
|
||||
|
||||
## Phase 2: Error Handling Hardening
|
||||
|
||||
- [x] P2-1: Wrap JSON.parse calls in try/catch — `useContentDiscovery.ts` sessionStorage parse, all sessionStorage/localStorage reads in composables. Search for `JSON.parse` across all `.ts` and `.vue` files, wrap any unprotected calls. TEST: `pnpm typecheck && pnpm lint`.
|
||||
- [x] P2-2: Add `.ok` checks before `.json()` on fetch calls — `useBitcoinPrice.ts` (Mempool API), `MempoolTxCard.vue` (tip height), `useNip05Verification.ts` (NIP-05 lookup), `ZapDialog.vue` (Lightning address). Search for `fetch(` → `.json()` patterns without `.ok` check. TEST: `pnpm typecheck && pnpm lint`.
|
||||
- [x] P2-3: Harden SSE streaming — In `useAI.ts` `readSSE()`, wrap `reader.read()` in try/catch, close reader on error. In `openrouter-adapter.ts`, add same pattern. TEST: `pnpm typecheck`.
|
||||
- [x] P2-4: Add error handling to async watchers — `PdfViewer.vue` watch calling `renderPage()`, `VideoPlayer.vue` `initHls()` in onMounted. Wrap in try/catch with user-friendly error state. TEST: `pnpm typecheck`.
|
||||
|
||||
## Phase 3: Security Hardening
|
||||
|
||||
- [x] P3-1: postMessage origin validation — In `archyBridge.ts`, replace `'*'` targetOrigin with configurable origin. Add origin check on incoming message handler. TEST: `pnpm typecheck`.
|
||||
- [x] P3-2: URL validation — In `contentExtraction.ts`, add URL length limit (2048 chars) to `extractUrlFromText()`. Validate URLs before fetch. TEST: `pnpm typecheck && pnpm lint`.
|
||||
- [x] P3-3: Content sanitization — Review `html.ts` for innerHTML usage, ensure SVG injection is covered. Replace `innerHTML = ''` with `textContent = ''` in `usePlayer.ts`. TEST: `pnpm typecheck`.
|
||||
- [x] P3-4: Add CSP meta tag — Add `<meta http-equiv="Content-Security-Policy" ...>` to `index.html` with appropriate directives for the app (allow self, API hosts, image CDNs). TEST: `pnpm typecheck`.
|
||||
|
||||
## Phase 4: Test Suite
|
||||
|
||||
- [x] P4-1: Unit tests for usePlayer — Create `packages/app/src/composables/__tests__/usePlayer.test.ts`. Test playback state, queue management, play/pause/next/prev. Minimum 8 test cases. TEST: `pnpm test`.
|
||||
- [x] P4-2: Unit tests for useContentPanel — Create `packages/app/src/composables/__tests__/useContentPanel.test.ts`. Test tab switching, detail opening, panel state management. Minimum 6 test cases. TEST: `pnpm test`.
|
||||
- [x] P4-3: Unit tests for useVisualViewport — Create `packages/app/src/composables/__tests__/useVisualViewport.test.ts`. Mock visualViewport API, test keyboard detection, viewport height calculation. Minimum 5 test cases. TEST: `pnpm test`.
|
||||
- [x] P4-4: Content extraction edge case tests — Create `packages/app/src/composables/__tests__/contentExtraction.test.ts`. Test interleaved tags, malformed tags, unicode content, missing fields. Minimum 10 test cases. TEST: `pnpm test`.
|
||||
- [x] P4-5: Seeded prompt regression tests — Create `packages/app/src/__tests__/seed-conversations.test.ts`. Import all seed conversations from mocks, run content extraction on each, verify expected content types are produced. Minimum 1 test per seed. TEST: `pnpm test`.
|
||||
|
||||
## Phase 5: Feature Work
|
||||
|
||||
- [x] P5-1: File browser page — Create `packages/app/src/pages/BrowsePage.vue` with file tree navigation. Add route `/browse` to router. Use the existing `vite-fs.ts` plugin for file reading. Show files/folders with icons, breadcrumb nav. TEST: `pnpm typecheck && pnpm lint`.
|
||||
- [x] P5-2: File tree component — Create `packages/app/src/components/browse/FileTree.vue`. Recursive tree with expand/collapse, file type icons (folder, code, image, document). Use glass morphism styling. TEST: `pnpm typecheck`.
|
||||
- [x] P5-3: File preview component — Create `packages/app/src/components/browse/FilePreview.vue`. Preview text files with syntax highlighting (reuse code viewer), images inline, show file metadata. TEST: `pnpm typecheck`.
|
||||
- [x] P5-4: Allow .claude folder in code viewer — Update `vite-fs.ts` to allow `.claude/` path. Update any path validation that blocks dotfiles. Show CLAUDE.md, settings, hooks, memory files. TEST: `pnpm typecheck`.
|
||||
- [x] P5-5: Archy local search guide — Create `packages/app/src/docs/archy-local-search.md` documenting how file types map to content surfaces (images→ImageGrid, music→SongGrid, etc.), how ContextBroker filtering works. Also add a help section component that can display this in-app. TEST: file exists and is valid markdown.
|
||||
|
||||
## Phase 6: Accessibility
|
||||
|
||||
- [x] P6-1: Add aria-labels to icon buttons — Audit all icon-only buttons across chat components (ChatHeader.vue, ChatMessage.vue, ChatInput.vue, ChatSearch.vue). Add descriptive `aria-label` to each. TEST: `pnpm lint`.
|
||||
- [x] P6-2: Add aria-labels to content grids — All Grid components (SongGrid, FilmGrid, TVSeriesGrid, PlaceGrid, BookGrid, PodcastGrid, NewsGrid, ImageGrid). Each card button needs `aria-label` with content title. TEST: `pnpm lint`.
|
||||
- [x] P6-3: Focus management for dialogs — `ZapDialog.vue`: add focus trap, auto-focus close button, `aria-modal="true"`, `role="dialog"`. Same for `SettingsModal.vue`. Ensure Escape key closes. TEST: `pnpm typecheck`.
|
||||
- [x] P6-4: Color contrast audit — Check `text-white/40` against dark backgrounds for WCAG AA (4.5:1). Verify `#F7931A` accent contrast. Fix any failing ratios by increasing opacity. Document findings in comments. TEST: `pnpm lint`.
|
||||
- [x] P6-5: Alt text improvements — `ImageGrid.vue`: use `img.title || img.alt` instead of generic. All content grids: ensure img alt includes meaningful content (title + artist/director/author). TEST: `pnpm lint`.
|
||||
|
||||
## Phase 7: Performance & Compatibility
|
||||
|
||||
- [x] P7-1: Lazy load heavy renderers — Use `defineAsyncComponent` for PdfViewer, VideoPlayer, MapView. Add loading skeleton components for each. TEST: `pnpm typecheck`.
|
||||
- [x] P7-2: Add in-memory caching — `useNip05Verification.ts`: cache results with 5-min TTL. `useBitcoinPrice.ts`: cache price with 30s TTL. TEST: `pnpm typecheck`.
|
||||
- [x] P7-3: Error boundaries for grid items — Create `packages/app/src/components/ui/ErrorBoundary.vue` using `onErrorCaptured`. Wrap each grid item renderer to prevent cascade failures. Show fallback UI on component crash. TEST: `pnpm typecheck`.
|
||||
- [x] P7-4: Code file size limits — In `useCodeContext.ts` `openFile()`, add file size check before reading (reject > 1MB). Add loading indicator for large files. TEST: `pnpm typecheck`.
|
||||
|
||||
## Phase 8: Research & Documentation
|
||||
|
||||
- [x] P8-1: iOS app research — Research Capacitor vs WKWebView wrapper vs React Native WebView for shipping AIUI as iOS app. Document in `docs/research/ios-app.md`: pros/cons, App Store requirements, push notification integration, offline capability. Include concrete next steps.
|
||||
- [x] P8-2: Mac desktop app research — Research Tauri v2 vs Electron for Mac desktop app. Document in `docs/research/mac-desktop.md`: binary size, memory usage, menu bar app pattern (like Raycast), global hotkey/command invocation, tray API. Include concrete next steps.
|
||||
- [x] P8-3: Plugin system hardening research — Document in `docs/research/plugin-security.md`: signature validation for community plugins, sandboxed iframe execution, permission system per plugin. Reference existing plugin interfaces in `packages/core/src/plugins/`.
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env sh
|
||||
# Pre-run script: verify repo state and create overnight branch.
|
||||
# Run before 5-6pm to ensure you can safely start the overnight loop.
|
||||
set -eu
|
||||
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
DATE=$(date '+%Y-%m-%d')
|
||||
BRANCH="overnight/${DATE}"
|
||||
|
||||
echo "=== Overnight pre-run check @ $(date '+%Y-%m-%dT%H:%M:%S') ==="
|
||||
|
||||
# 1. Check git status is clean
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
echo "Error: Working tree not clean. Commit or stash changes first." >&2
|
||||
echo " git status" >&2
|
||||
git status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Check we're not already on an overnight branch
|
||||
current=$(git branch --show-current 2>/dev/null || true)
|
||||
if [ -n "$current" ] && [ "$current" = "$BRANCH" ]; then
|
||||
echo "Already on $BRANCH. Ready to run." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 3. Create date-stamped branch
|
||||
if git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then
|
||||
echo "Branch $BRANCH already exists. Checkout or use a different date." >&2
|
||||
exit 1
|
||||
fi
|
||||
git checkout -b "$BRANCH"
|
||||
echo "Created branch $BRANCH"
|
||||
|
||||
# 4. Remind to push
|
||||
echo ""
|
||||
echo "Reminder: Push before starting overnight run: git push -u origin $BRANCH"
|
||||
echo "Then run: caffeinate -i ./loop/loop.sh"
|
||||
echo "=== Ready ==="
|
||||
@@ -0,0 +1,73 @@
|
||||
You are working through an overnight automation plan for the AIUI app. Read these files first:
|
||||
|
||||
1. `loop/plan.md` — Your task checklist (mark items `- [x]` as you complete them)
|
||||
2. `CLAUDE.md` — Project conventions, design system rules, and coding standards
|
||||
|
||||
## Project Context
|
||||
|
||||
AIUI is an AI content surface UI — a Vue 3 + TypeScript + Tailwind CSS app with chat, content panels (films, music, books, TV, places, news, images, podcasts), and a plugin system. It runs as a PWA and inside Archy (an iframe host).
|
||||
|
||||
Key directories:
|
||||
- `packages/app/src/` — Main application source
|
||||
- `packages/app/src/composables/` — Shared composition functions
|
||||
- `packages/app/src/components/content/` — Content grid and detail components
|
||||
- `packages/app/src/components/chat/` — Chat interface components
|
||||
- `packages/app/src/styles/main.css` — Glass morphism design system
|
||||
- `packages/core/src/` — Core library and types
|
||||
|
||||
## Working Process
|
||||
|
||||
For each task in `loop/plan.md`:
|
||||
|
||||
1. Find the first unchecked `- [ ]` item
|
||||
2. Read the task description carefully — it tells you what to change and where
|
||||
3. Read the relevant source files before making changes
|
||||
4. Make the change following CLAUDE.md conventions
|
||||
5. Run the TEST command specified in the task
|
||||
6. Fix any errors from the test command before proceeding
|
||||
7. Commit with conventional commit format: `type(scope): description`
|
||||
8. Mark the task done: change `- [ ]` to `- [x]` in `loop/plan.md`
|
||||
9. Move to the next unchecked task immediately
|
||||
|
||||
## Testing Gates
|
||||
|
||||
Every task specifies a TEST command. You MUST run it and pass before committing:
|
||||
- `pnpm typecheck` — TypeScript strict mode compilation
|
||||
- `pnpm lint` — ESLint checks
|
||||
- `pnpm test` — Vitest unit tests
|
||||
- Multiple commands joined with `&&` must ALL pass
|
||||
|
||||
If a test fails, fix the issue and re-run. Do not skip tests. Do not mark a task as done if tests fail.
|
||||
|
||||
## Coding Rules
|
||||
|
||||
- **Vue 3 Composition API only** — `<script setup lang="ts">`, never Options API
|
||||
- **Glass morphism design** — use `.glass`, `.glass-card`, `.glass-button` from `main.css`
|
||||
- **Dark theme** — `bg-white/5`, `text-white/80`, never `bg-gray-*` or plain `bg-white`
|
||||
- **Text opacity scale** — `text-white/25` → `/40` → `/60` → `/70` → `/80` → `/90` → `/96`
|
||||
- **Accent** — `text-accent` (`#F7931A`)
|
||||
- **Touch targets** — minimum 44x44px for all interactive elements
|
||||
- **Font minimums** — never smaller than 11px
|
||||
- **No over-engineering** — only change what the task asks for
|
||||
- **Keep existing patterns** — match the style of surrounding code
|
||||
|
||||
## Commit Format
|
||||
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `perf`
|
||||
Scope: `app`, `core`, or specific area like `chat`, `content`, `player`
|
||||
|
||||
## Rules
|
||||
|
||||
- Never skip a testing gate — if tests fail, fix them before moving on
|
||||
- If a task is proving difficult, make at least 10 genuine attempts before moving on
|
||||
- Always read source files before editing them
|
||||
- Do not stop until all tasks are checked or you are rate limited
|
||||
- Commit after each completed task
|
||||
- For research tasks (Phase 8), create the docs directory if needed: `mkdir -p docs/research`
|
||||
- For test tasks (Phase 4), create the test directory if needed: `mkdir -p packages/app/src/composables/__tests__`
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "aiui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "The next-generation AI content surface UI",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "pnpm --filter @aiui/app dev",
|
||||
"dev:core": "pnpm --filter @aiui/core dev",
|
||||
"build": "turbo build",
|
||||
"test": "turbo test",
|
||||
"lint": "turbo lint",
|
||||
"typecheck": "turbo typecheck",
|
||||
"clean": "turbo clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "^2.8.12",
|
||||
"typescript": "~5.8.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.30.3",
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"pdfjs-dist": "^5.5.207"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { StorybookConfig } from '@storybook/vue3-vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/__stories__/*.stories.ts'],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {},
|
||||
},
|
||||
addons: ['@storybook/addon-essentials'],
|
||||
viteFinal(config) {
|
||||
config.resolve ??= {}
|
||||
config.resolve.alias ??= {}
|
||||
// Match app aliases
|
||||
const alias = config.resolve.alias as Record<string, string>
|
||||
alias['@'] = new URL('../src', import.meta.url).pathname
|
||||
alias['@aiui/core'] = new URL('../../core/src', import.meta.url).pathname
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Preview } from '@storybook/vue3'
|
||||
import '../src/styles/main.css'
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
],
|
||||
},
|
||||
layout: 'centered',
|
||||
},
|
||||
}
|
||||
|
||||
export default preview
|
||||
@@ -0,0 +1 @@
|
||||
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// If the loader is already loaded, just stop.
|
||||
if (!self.define) {
|
||||
let registry = {};
|
||||
|
||||
// Used for `eval` and `importScripts` where we can't get script URL by other means.
|
||||
// In both cases, it's safe to use a global var because those functions are synchronous.
|
||||
let nextDefineUri;
|
||||
|
||||
const singleRequire = (uri, parentUri) => {
|
||||
uri = new URL(uri + ".js", parentUri).href;
|
||||
return registry[uri] || (
|
||||
|
||||
new Promise(resolve => {
|
||||
if ("document" in self) {
|
||||
const script = document.createElement("script");
|
||||
script.src = uri;
|
||||
script.onload = resolve;
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
nextDefineUri = uri;
|
||||
importScripts(uri);
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
let promise = registry[uri];
|
||||
if (!promise) {
|
||||
throw new Error(`Module ${uri} didn’t register its module`);
|
||||
}
|
||||
return promise;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
self.define = (depsNames, factory) => {
|
||||
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
|
||||
if (registry[uri]) {
|
||||
// Module is already loading or loaded.
|
||||
return;
|
||||
}
|
||||
let exports = {};
|
||||
const require = depUri => singleRequire(depUri, uri);
|
||||
const specialDeps = {
|
||||
module: { uri },
|
||||
exports,
|
||||
require
|
||||
};
|
||||
registry[uri] = Promise.all(depsNames.map(
|
||||
depName => specialDeps[depName] || require(depName)
|
||||
)).then(deps => {
|
||||
factory(...deps);
|
||||
return exports;
|
||||
});
|
||||
};
|
||||
}
|
||||
define(['./workbox-f97094b3'], (function (workbox) { 'use strict';
|
||||
|
||||
self.skipWaiting();
|
||||
workbox.clientsClaim();
|
||||
|
||||
/**
|
||||
* The precacheAndRoute() method efficiently caches and responds to
|
||||
* requests for URLs in the manifest.
|
||||
* See https://goo.gl/S9QRab
|
||||
*/
|
||||
workbox.precacheAndRoute([{
|
||||
"url": "registerSW.js",
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.rat89nkoims"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
allowlist: [/^\/$/]
|
||||
}));
|
||||
workbox.registerRoute(/^https:\/\/api\.anthropic\.com\/.*/i, new workbox.NetworkOnly(), 'GET');
|
||||
workbox.registerRoute(/^https:\/\/openrouter\.ai\/.*/i, new workbox.NetworkOnly(), 'GET');
|
||||
workbox.registerRoute(/\/api\/web-search\?.*/i, new workbox.NetworkOnly(), 'GET');
|
||||
workbox.registerRoute(/\/api\/rss-articles\?.*/i, new workbox.NetworkOnly(), 'GET');
|
||||
workbox.registerRoute(/\/api\/tmdb\/.*/i, new workbox.StaleWhileRevalidate({
|
||||
"cacheName": "tmdb-cache",
|
||||
plugins: [new workbox.ExpirationPlugin({
|
||||
maxEntries: 200,
|
||||
maxAgeSeconds: 86400
|
||||
})]
|
||||
}), 'GET');
|
||||
workbox.registerRoute(/^https:\/\/image\.tmdb\.org\/.*/i, new workbox.CacheFirst({
|
||||
"cacheName": "tmdb-images",
|
||||
plugins: [new workbox.ExpirationPlugin({
|
||||
maxEntries: 500,
|
||||
maxAgeSeconds: 604800
|
||||
})]
|
||||
}), 'GET');
|
||||
workbox.registerRoute(/^https:\/\/upload\.wikimedia\.org\/.*/i, new workbox.CacheFirst({
|
||||
"cacheName": "wiki-images",
|
||||
plugins: [new workbox.ExpirationPlugin({
|
||||
maxEntries: 200,
|
||||
maxAgeSeconds: 604800
|
||||
})]
|
||||
}), 'GET');
|
||||
workbox.registerRoute(/^https:\/\/d12wklypp119aj\.cloudfront\.net\/image\/.*/i, new workbox.CacheFirst({
|
||||
"cacheName": "wavlake-images",
|
||||
plugins: [new workbox.ExpirationPlugin({
|
||||
maxEntries: 300,
|
||||
maxAgeSeconds: 604800
|
||||
})]
|
||||
}), 'GET');
|
||||
|
||||
}));
|
||||
@@ -0,0 +1,208 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Content surfaces', () => {
|
||||
test('empty state shows when no conversation selected', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const main = page.locator('main.path-glass-card')
|
||||
await expect(main).toBeVisible()
|
||||
})
|
||||
|
||||
test('chat can receive input', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
const input = page.getByPlaceholder(/Message AIUI/)
|
||||
await input.click()
|
||||
await input.pressSequentially('Recommend some films')
|
||||
await expect(input).toHaveValue('Recommend some films', { timeout: 3000 })
|
||||
})
|
||||
|
||||
test('films surface: films conversation loads and shows film cards', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
await page.getByRole('button', { name: /View all \d+ films/i }).click()
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('films surface: clicking assistant bubble opens panel', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('magazine surface: BIP brief shows sections', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'BIP 110 brief' }).click()
|
||||
await expect(page.getByText(/BIP 110|Pro camp|Summary/i).first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: 'View brief' }).click()
|
||||
await expect(page.getByText(/AI Brief|Summary|Pro camp/i).first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('songs surface: songs conversation shows song cards', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'Music recommendations' }).click()
|
||||
await expect(page.getByText('Never Meant').first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: /View all \d+ songs/i }).click()
|
||||
await expect(page.locator('main').getByText('Never Meant').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('podcasts surface: podcasts conversation shows podcast cards', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'Bitcoin podcasts' }).click()
|
||||
await expect(page.getByText('What Bitcoin Did').first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: /View all \d+ podcasts/i }).click()
|
||||
await expect(page.locator('main').getByText('What Bitcoin Did').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('websites surface: websites tab shows link cards', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'Bitcoin resources' }).click()
|
||||
await expect(page.getByText('Bitcoin Magazine').first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: /View all \d+ websites/i }).click()
|
||||
await expect(page.locator('main').getByText('Bitcoin Magazine').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('news surface: news conversation shows articles', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.locator('aside button').filter({ has: page.locator('h2') }).first().click()
|
||||
await page.getByRole('button', { name: 'Latest Bitcoin news' }).click()
|
||||
await expect(page.getByText(/Bitcoin hits|ETF inflows/i).first()).toBeVisible({ timeout: 8000 })
|
||||
await page.getByRole('button', { name: /View all \d+ articles/i }).click()
|
||||
await expect(page.locator('main').getByText(/Bitcoin hits|ETF inflows/i).first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Chat interactions', () => {
|
||||
test('sends a message and receives streaming response', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await page.waitForLoadState('networkidle')
|
||||
const input = page.getByPlaceholder(/Message AIUI/)
|
||||
await input.click()
|
||||
await input.fill('Hello')
|
||||
await input.press('Enter')
|
||||
// User message should appear in chat
|
||||
await expect(page.getByText('Hello').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('content panel shows film cards when AI mentions films', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Film cards should be visible inline in assistant message
|
||||
await expect(page.getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
// Open panel via "View all" button
|
||||
await page.getByRole('button', { name: /View all \d+ films/i }).click()
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('clicking a film card opens detail view', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Open panel
|
||||
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
|
||||
const filmCard = page.locator('main').getByText('Blade Runner 2049').first()
|
||||
await expect(filmCard).toBeVisible({ timeout: 5000 })
|
||||
// Click film card to open detail
|
||||
await filmCard.click()
|
||||
// Detail view should show film metadata
|
||||
await expect(page.getByText(/Denis Villeneuve|2017|Sci-Fi/i).first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('mobile viewport shows full-screen overlay for content', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 812 })
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Open panel on mobile
|
||||
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
|
||||
// Content should be visible as overlay on mobile
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('stop button halts generation', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
// When not streaming, stop button should not be visible
|
||||
const stopButton = page.getByRole('button', { name: 'Stop generation' })
|
||||
await expect(stopButton).toBeHidden({ timeout: 3000 })
|
||||
// Chat input should be available instead
|
||||
const input = page.getByPlaceholder(/Message AIUI/)
|
||||
await expect(input).toBeVisible({ timeout: 3000 })
|
||||
})
|
||||
|
||||
test('web search toggle works', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
const toggle = page.getByRole('button', { name: 'Toggle web search' })
|
||||
await expect(toggle).toBeVisible({ timeout: 5000 })
|
||||
// Click to toggle web search on
|
||||
await toggle.click()
|
||||
// The button styling should change (it gains accent color when active)
|
||||
await expect(toggle).toBeVisible()
|
||||
// Click again to toggle off
|
||||
await toggle.click()
|
||||
await expect(toggle).toBeVisible()
|
||||
})
|
||||
|
||||
test('new conversation clears messages', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Click "New conversation" button
|
||||
await page.getByRole('button', { name: 'New conversation' }).click()
|
||||
// Previous messages should be cleared
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeHidden({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('panel side toggle switches layout', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 800 })
|
||||
await Promise.all([
|
||||
page.waitForResponse((res) => res.url().includes('dev-chats') && res.status() === 200, { timeout: 15000 }),
|
||||
page.goto('/'),
|
||||
])
|
||||
await expect(page.getByText('Recommend some sci-fi films')).toBeVisible({ timeout: 10000 })
|
||||
// Open the panel
|
||||
await page.locator('.path-glass-bubble').filter({ hasText: /Blade Runner|Arrival|Dune/ }).first().click()
|
||||
await expect(page.locator('main').getByText('Blade Runner 2049').first()).toBeVisible({ timeout: 5000 })
|
||||
// Both chat and panel sections should be visible on desktop
|
||||
const chatSection = page.locator('section').first()
|
||||
await expect(chatSection).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { Conversation } from '@aiui/core/types/message'
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
/** Films: user asks for films, assistant responds with [[film:f1]] etc */
|
||||
export const filmsConversation: Conversation = {
|
||||
id: 'e2e-films',
|
||||
title: 'Film recommendations',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: 'Recommend some sci-fi films',
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Here are some great sci-fi films:\n\n- [[film:f1]] - Blade Runner 2049\n- [[film:f2]] - Arrival\n- [[film:f3]] - Dune\n\nAll from Denis Villeneuve.`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** Magazine: news-like query + bullet sections (BIP/debate context) */
|
||||
export const magazineConversation: Conversation = {
|
||||
id: 'e2e-magazine',
|
||||
title: 'BIP 110 brief',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: "What's the latest on BIP 110? What are people saying?",
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `## Summary\n\nBIP 110 is being debated. Macro sentiment is bearish. BTC holding.\n\n- **Pro camp** — Technical improvement, faster.\n- **Anti camp** — Too risky, prefer status quo.\n\n**Henrik Zeberg** (analyst) says this could be bullish long-term.\n\nFor deeper analysis: check **Bitcoin Mailing List** (gnusha.org).`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** Websites: user asks for resources, assistant gives markdown links */
|
||||
export const websitesConversation: Conversation = {
|
||||
id: 'e2e-websites',
|
||||
title: 'Bitcoin resources',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: 'Best websites to check for Bitcoin news?',
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Here are the best places to check:\n\n- [Bitcoin Magazine](https://bitcoinmagazine.com)\n- [Bitcoin.org](https://bitcoin.org)\n- [Mempool.space](https://mempool.space)`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** News: web search results + news-like response */
|
||||
export const newsConversation: Conversation = {
|
||||
id: 'e2e-news',
|
||||
title: 'Latest Bitcoin news',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: "What's the latest Bitcoin news?",
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Here's what's happening. For the latest news check these sources:\n\n- [Bitcoin hits new high](https://example.com/btc-high)\n- [ETF inflows surge](https://example.com/etf-inflows)`,
|
||||
timestamp: now - 30000,
|
||||
webResults: [
|
||||
{ title: 'Bitcoin hits new high', url: 'https://example.com/btc-high', content: 'BTC reached...' },
|
||||
{ title: 'ETF inflows surge', url: 'https://example.com/etf-inflows', content: 'Spot ETF...' },
|
||||
],
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** Songs: user asks for music, assistant responds with [[song:s1]] */
|
||||
export const songsConversation: Conversation = {
|
||||
id: 'e2e-songs',
|
||||
title: 'Music recommendations',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: 'Recommend some math rock',
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Here are great math rock tracks:\n\n- [[song:s1]] Never Meant by American Football\n- [[song:s2]] The Kill by Toe`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
/** Podcasts */
|
||||
export const podcastsConversation: Conversation = {
|
||||
id: 'e2e-podcasts',
|
||||
title: 'Bitcoin podcasts',
|
||||
messages: [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'user',
|
||||
content: 'Best Bitcoin podcasts?',
|
||||
timestamp: now - 60000,
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'assistant',
|
||||
content: `Check these:\n\n- [[podcast:p1]] What Bitcoin Did\n- [[podcast:p2]] The Audacity to Podcast`,
|
||||
timestamp: now - 30000,
|
||||
},
|
||||
],
|
||||
createdAt: now - 120000,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
export const allTestConversations = {
|
||||
[filmsConversation.id]: filmsConversation,
|
||||
[magazineConversation.id]: magazineConversation,
|
||||
[websitesConversation.id]: websitesConversation,
|
||||
[newsConversation.id]: newsConversation,
|
||||
[songsConversation.id]: songsConversation,
|
||||
[podcastsConversation.id]: podcastsConversation,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { allTestConversations } from './fixtures/test-chats'
|
||||
|
||||
const CHATS_PATH = resolve(process.cwd(), '.dev', 'chats.json')
|
||||
|
||||
export default async function globalSetup() {
|
||||
const dir = resolve(process.cwd(), '.dev')
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||
|
||||
// Backup existing chats if present (for local dev)
|
||||
let backup: string | null = null
|
||||
if (existsSync(CHATS_PATH)) {
|
||||
backup = readFileSync(CHATS_PATH, 'utf-8')
|
||||
}
|
||||
|
||||
const payload = {
|
||||
conversations: allTestConversations,
|
||||
activeConversationId: 'e2e-films',
|
||||
}
|
||||
writeFileSync(CHATS_PATH, JSON.stringify(payload, null, 2), 'utf-8')
|
||||
|
||||
// Store backup path for teardown (we pass via env since globalSetup/Teardown don't share scope easily)
|
||||
if (backup) {
|
||||
process.env.AIUI_E2E_CHATS_BACKUP = backup
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { writeFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const CHATS_PATH = resolve(process.cwd(), '.dev', 'chats.json')
|
||||
|
||||
export default async function globalTeardown() {
|
||||
const backup = process.env.AIUI_E2E_CHATS_BACKUP
|
||||
if (backup) {
|
||||
writeFileSync(CHATS_PATH, backup, 'utf-8')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('AIUI smoke tests', () => {
|
||||
test('app loads and shows chat interface', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page).toHaveTitle(/AIUI/)
|
||||
})
|
||||
|
||||
test('chat input is visible and focusable', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const input = page.getByPlaceholder(/Message AIUI|Waiting for/)
|
||||
await expect(input).toBeVisible()
|
||||
})
|
||||
|
||||
test('content panel area exists', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const main = page.locator('main.path-glass-card')
|
||||
await expect(main).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Visual Regression', () => {
|
||||
test('ChatPage renders correctly', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await expect(page).toHaveScreenshot('chat-page.png', {
|
||||
maxDiffPixelRatio: 0.005,
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('ContentPanel renders correctly', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
// Open content panel by clicking a content tab
|
||||
const filmTab = page.getByRole('button', { name: /film/i }).first()
|
||||
if (await filmTab.isVisible()) {
|
||||
await filmTab.click()
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page).toHaveScreenshot('content-panel-films.png', {
|
||||
maxDiffPixelRatio: 0.005,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('PassphraseDialog renders correctly', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
// PassphraseDialog shows on first load if crypto is enabled
|
||||
const dialog = page.locator('.glass-card').filter({ hasText: 'Unlock AIUI' })
|
||||
if (await dialog.isVisible()) {
|
||||
await expect(dialog).toHaveScreenshot('passphrase-dialog.png', {
|
||||
maxDiffPixelRatio: 0.005,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('BottomSheet renders correctly on mobile', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await expect(page).toHaveScreenshot('mobile-view.png', {
|
||||
maxDiffPixelRatio: 0.005,
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import js from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import vueParser from 'vue-eslint-parser'
|
||||
import globals from 'globals'
|
||||
|
||||
export default tseslint.config(
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
{
|
||||
files: ['src/**/*.vue'],
|
||||
languageOptions: {
|
||||
parser: vueParser,
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
extraFileExtensions: ['.vue'],
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// TypeScript
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
|
||||
// Vue
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/no-v-html': 'warn',
|
||||
|
||||
// General
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['dist/', 'node_modules/', 'e2e/', 'server/'],
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full overflow-hidden">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#faf9f6" media="(prefers-color-scheme: light)" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="AIUI" />
|
||||
<meta name="description" content="AI chat interface with rich content surfaces" />
|
||||
<!-- CSP set via HTTP headers in production nginx — not in HTML meta to avoid breaking Vite HMR -->
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
|
||||
<title>AIUI</title>
|
||||
</head>
|
||||
<body class="antialiased h-full overflow-hidden fixed w-full">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"ci": {
|
||||
"collect": {
|
||||
"staticDistDir": "./dist",
|
||||
"numberOfRuns": 3,
|
||||
"settings": {
|
||||
"preset": "desktop"
|
||||
}
|
||||
},
|
||||
"assert": {
|
||||
"assertions": {
|
||||
"categories:performance": ["error", { "minScore": 0.7 }],
|
||||
"largest-contentful-paint": ["error", { "maxNumericValue": 3000 }],
|
||||
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.15 }],
|
||||
"categories:accessibility": ["warn", { "minScore": 0.8 }],
|
||||
"categories:best-practices": ["warn", { "minScore": 0.8 }]
|
||||
}
|
||||
},
|
||||
"upload": {
|
||||
"target": "temporary-public-storage"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "@aiui/app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "AIUI reference application",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bash scripts/dev.sh",
|
||||
"dev:vite": "vite",
|
||||
"dev:proxy": "tsx server/claude-proxy.ts",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"lint": "eslint src/",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"clean": "rm -rf dist",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aiui/core": "workspace:*",
|
||||
"@tanstack/vue-virtual": "^3.13.19",
|
||||
"hls.js": "^1.6.15",
|
||||
"katex": "^0.16.33",
|
||||
"leaflet": "^1.9.4",
|
||||
"markdown-it": "^14.1.1",
|
||||
"mermaid": "^11.12.3",
|
||||
"pdfjs-dist": "^5.5.207",
|
||||
"pinia": "^3.0.4",
|
||||
"plyr": "^3.8.4",
|
||||
"vue": "^3.5.29",
|
||||
"vue-router": "^5.0.3",
|
||||
"wavesurfer.js": "^7.12.1",
|
||||
"dompurify": "^3.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@storybook/addon-essentials": "^8.6.0",
|
||||
"@storybook/vue3": "^8.6.0",
|
||||
"@storybook/vue3-vite": "^8.6.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@vitejs/plugin-basic-ssl": "^2.1.4",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"duck-duck-scrape": "^2.2.7",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"globals": "^17.4.0",
|
||||
"happy-dom": "^20.8.3",
|
||||
"rss-parser": "^3.13.0",
|
||||
"storybook": "^8.6.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.0",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vitest": "^4.0.18",
|
||||
"vue-eslint-parser": "^10.4.0",
|
||||
"vue-tsc": "^3.2.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
globalSetup: './e2e/global-setup.ts',
|
||||
globalTeardown: './e2e/global-teardown.ts',
|
||||
reporter: 'html',
|
||||
snapshotPathTemplate: '{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}',
|
||||
expect: {
|
||||
toHaveScreenshot: { maxDiffPixelRatio: 0.005 },
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [
|
||||
// Desktop browsers
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
||||
// Mobile viewports
|
||||
{
|
||||
name: 'iphone14',
|
||||
use: {
|
||||
...devices['iPhone 14'],
|
||||
viewport: { width: 390, height: 844 },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'galaxy-s21',
|
||||
use: {
|
||||
userAgent: 'Mozilla/5.0 (Linux; Android 12; SM-G991B) AppleWebKit/537.36',
|
||||
viewport: { width: 360, height: 800 },
|
||||
deviceScaleFactor: 3,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'pnpm run dev:vite',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
})
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 83.44 122.88">
|
||||
<path fill="currentColor" d="M45.04,95.45v24.11c0,1.83-1.49,3.32-3.32,3.32c-1.83,0-3.32-1.49-3.32-3.32V95.45c-10.16-0.81-19.32-5.3-26.14-12.12C4.69,75.77,0,65.34,0,53.87c0-1.83,1.49-3.32,3.32-3.32s3.32,1.49,3.32,3.32c0,9.64,3.95,18.41,10.31,24.77c6.36,6.36,15.13,10.31,24.77,10.31h0c9.64,0,18.41-3.95,24.77-10.31c6.36-6.36,10.31-15.13,10.31-24.77c0-1.83,1.49-3.32,3.32-3.32s3.32,1.49,3.32,3.32c0,11.48-4.69,21.91-12.25,29.47C64.36,90.16,55.2,94.64,45.04,95.45z M41.94,0c6.38,0,12.18,2.61,16.38,6.81c4.2,4.2,6.81,10,6.81,16.38v30c0,6.38-2.61,12.18-6.81,16.38c-4.2,4.2-10,6.81-16.38,6.81s-12.18-2.61-16.38-6.81c-4.2-4.2-6.81-10-6.81-16.38v-30c0-6.38,2.61-12.18,6.81-16.38C29.76,2.61,35.56,0,41.94,0z M53.62,11.51c-3-3-7.14-4.86-11.68-4.86c-4.55,0-8.68,1.86-11.68,4.86c-3,3-4.86,7.14-4.86,11.68v30c0,4.55,1.86,8.68,4.86,11.68c3,3,7.14,4.86,11.68,4.86c4.55,0,8.68-1.86,11.68-4.86c3-3,4.86-7.14,4.86-11.68v-30C58.49,18.64,56.62,14.51,53.62,11.51z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1022 B |
|
After Width: | Height: | Size: 1.0 MiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="6" fill="#111111"/>
|
||||
<path d="M 16,5.5 Q 16,16 26.5,16 Q 16,16 16,26.5 Q 16,16 5.5,16 Q 16,16 16,5.5 Z" fill="#fafafa"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#1a1a1a"/>
|
||||
<stop offset="100%" stop-color="#0a0a0a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="border" x1="0" y1="0" x2="0" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="rgba(255,255,255,0.18)"/>
|
||||
<stop offset="100%" stop-color="rgba(255,255,255,0.04)"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="108" fill="url(#bg)"/>
|
||||
<rect x="2" y="2" width="508" height="508" rx="106" fill="none" stroke="url(#border)" stroke-width="2"/>
|
||||
<circle cx="256" cy="256" r="120" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="2.5"/>
|
||||
<path d="M 256,106 Q 256,256 406,256 Q 256,256 256,406 Q 256,256 106,256 Q 256,256 256,106 Z" fill="#fafafa"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 922 B |
@@ -0,0 +1,24 @@
|
||||
<svg width="200" height="300" viewBox="0 0 200 300" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="clip-poster">
|
||||
<rect width="200" height="300" rx="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#clip-poster)">
|
||||
<rect width="200" height="300" fill="currentColor" fill-opacity="0.15"/>
|
||||
<g fill="currentColor" fill-opacity="0.2">
|
||||
<rect x="8" y="12" width="16" height="24" rx="2"/>
|
||||
<rect x="40" y="12" width="16" height="24" rx="2"/>
|
||||
<rect x="72" y="12" width="16" height="24" rx="2"/>
|
||||
<rect x="104" y="12" width="16" height="24" rx="2"/>
|
||||
<rect x="136" y="12" width="16" height="24" rx="2"/>
|
||||
<rect x="168" y="12" width="16" height="24" rx="2"/>
|
||||
<rect x="8" y="48" width="16" height="24" rx="2"/>
|
||||
<rect x="40" y="48" width="16" height="24" rx="2"/>
|
||||
<rect x="72" y="48" width="16" height="24" rx="2"/>
|
||||
<rect x="104" y="48" width="16" height="24" rx="2"/>
|
||||
<rect x="136" y="48" width="16" height="24" rx="2"/>
|
||||
<rect x="168" y="48" width="16" height="24" rx="2"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 26 KiB |