diff --git a/.planning/phases/02-ui-performance/02-AIUI-D14.md b/.planning/phases/02-ui-performance/02-AIUI-D14.md index cb724f93..aa4a36b0 100644 --- a/.planning/phases/02-ui-performance/02-AIUI-D14.md +++ b/.planning/phases/02-ui-performance/02-AIUI-D14.md @@ -1,140 +1,186 @@ # AIUI D-14 Embed Contract **Written:** 2026-07-30 (Phase 02 Plan 07, Task 1) -**Purpose:** Record where AIUI's source lives, what its embed parameter contract is today, -which side implements each of D-14's two UX defaults, and what deploying the AIUI half -would require — so Task 2 (and any future agent) does not re-run this search. +**Amended:** 2026-07-30 (Phase 02 Plan 07, Task 2 resume) — the orchestrator supplied the +missing fact this document's original Task 1 pass could not obtain from within this +environment: AIUI's source repository location. This amendment replaces the "search +exhausted" conclusion below with the real contract read directly from source, and records +the D-14a/D-14b implementation actually shipped. ## Source Location -AIUI's source is **not present on this machine and not reachable via the noted build -server at this time.** Search performed and exhausted: +**AIUI's source is now located and was cloned for this plan:** -1. **Sibling-repo checks** (the two places `neode-ui`'s own tooling expects it): - - `neode-ui/package.json`'s `dev:mock` script: `cd ../../AIUI && pnpm dev` (relative to - `neode-ui/`, i.e. `/AIUI`) — degrades to - `echo '[AIUI] Not found at ../../AIUI — chat will show placeholder'` when absent. - - `scripts/setup-aiui-server.sh`: `AIUI_DIST="$PROJECT_DIR/../AIUI/packages/app/dist"` - (relative to the `archy` repo root, i.e. the same sibling directory), with a - documented prerequisite: "AIUI must be built locally first: - `cd AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build`". - - Checked directly: `/home/archipelago/Projects/AIUI` does **not** exist. - `ls /home/archipelago/Projects/` shows only `archy` and various `archy-*` - worktree/backup checkouts, `botfights`, and two unrelated Indeehub projects — no - `AIUI` directory. +- **Repository:** `https://git.tx1138.com/lfg2025/AIUI` (anonymous `git ls-remote` succeeds; + clone succeeded read-only, push is `403 Forbidden` anonymously — see Deployment Impact). +- **Cloned to:** `/home/archipelago/Projects/AIUI` — the exact sibling path + `neode-ui/package.json`'s `dev:mock` script and `scripts/setup-aiui-server.sh` both already + expected (`/AIUI`), so no changes to either script were needed once the + clone existed. +- **Branch used: `development`**, not the repository's default `main`. Verified via + `git log main..origin/development` vs `git log origin/development..main`: `development` is + 17 commits ahead of `main` (including a run of `fix(app):` security-hardening commits — + origin validation, rate limiting, CSP, SSRF protection, dev-server auth — and the + feature/content work this session's grep relies on) with **zero** commits unique to `main`. + `main` is simply stale here; `development` is the actual working branch. (AIUI's own + `CLAUDE.md` documents the intended name as `dev`, but the real remote branch is named + `development` — a doc/reality mismatch noted for AIUI's own maintainers, out of scope to + fix from this repository.) +- **ThinkPad build server:** confirmed reachable this session at `100.69.68.39` (Tailscale), + per the orchestrator-supplied fact. Not needed once the git clone succeeded — recorded here + for completeness since Task 1's original search treated it as the fallback path. +- **Hosting note (context only, not acted on):** the user noted AIUI hosting may later be + ported to "our main vps2." No action taken on this in this plan; recorded for whoever picks + up AIUI's deployment next. -2. **Broader filesystem search** for any directory literally named `AIUI` (case-insensitive) - containing AIUI's own `package.json`: `find / -maxdepth 6 -iname "AIUI" -type d` returned - only deployed **build output** directories — `/opt/archipelago/web-ui*/aiui` (dozens of - timestamped backup copies), `/opt/archipelago/apps/aiui` (this repo's manifest dir, no - source), `archy`'s own `apps/aiui` and `demo/aiui` (built static assets, mirrors of the - deployed bundle), and installer-ISO copies under `image-recipe/`. None of these are - AIUI's source repository — they are prebuilt `dist/` output, deployed or bundled. +### Original Task 1 search (superseded, kept for history) -3. **ThinkPad build server reachability** (project memory notes `.116` as the primary build - server, where AIUI may live if not here): both ping and SSH to `192.168.1.116` failed — - `ping` returned "Destination Host Unreachable" from the local gateway (`192.168.1.66`), - and `ssh -o BatchMode=yes -o ConnectTimeout=3` timed out after 3s with "Connection timed - out" on port 22. The build server is not reachable from this execution environment right - now (machine likely off or off this network segment). - -**Conclusion:** The search is exhausted for this session. AIUI's source cannot be read from -`archy` alone, confirming RESEARCH.md's Open Question 1. Task 2's `` — which -requires this section to name a reachable checkout — is **not met**; this plan halts before -any code targeting the AIUI side is written, per the plan's own instruction not to invent a -parameter and ship it. +The initial pass searched the sibling path (`/AIUI` — not present at that +time), a broader filesystem sweep (found only prebuilt `dist/` output under +`/opt/archipelago/web-ui*/aiui`, this repo's `apps/aiui`/`demo/aiui`, and installer-ISO +copies — never source), and the ThinkPad at `192.168.1.116` (ping/ssh both failed at that +time — host unreachable from this environment on that address/path). That search was +genuinely exhausted with the facts available at the time; the blocker was resolved +externally (see Amended note above), not by anything different this pass did on the same +inputs. ## Embed Parameter Contract -`neode-ui/src/views/Chat.vue`'s `aiuiUrl` computed (lines ~74-82) constructs the iframe URL -today with: -- `embedded=true` — always present when a URL is built. -- `hideClose=true` — always present when a URL is built. -- `&mockArchy=1&seed=1` — only appended when `IS_DEMO` is true. +`neode-ui/src/views/Chat.vue`'s `aiuiUrl` computed (lines ~74-82) sends, unchanged by this +plan: +- `embedded=true` — always present. +- `hideClose=true` — always present. +- `&mockArchy=1&seed=1` — only when `IS_DEMO` (build-time env var, stable for the life of + the bundle). -Because AIUI's own source could not be inspected, the exact file/line where each parameter -is *parsed inside AIUI* cannot be recorded from source, per the instruction not to invent -what wasn't verified. As a diligence step (not a substitute for source access), the -currently-**deployed** AIUI build's static JS bundle was inspected instead — this is a -built artifact, not source, so variable/function names are minified and the full parsing -logic cannot be confirmed with certainty from it. Findings, treated as supporting evidence -only: +Read from AIUI source (`development` branch, commit `9176324` before this plan's changes): -- Bundle inspected: `/opt/archipelago/web-ui/aiui/assets/index-Lh5NfTCq.js` (byte-identical - copy also present in this repo at `demo/aiui/assets/index-Lh5NfTCq.js` and - `web/dist/neode-ui/../aiui`-style deploy mirrors — same build hash everywhere checked). -- `embedded` — read via `new URLSearchParams(window.location.search).has("embedded")`, - stored as `window.__AIUI_EMBEDDED__`. This is a **presence-only** check (any value, - including none, counts as embedded=true). Confirmed present in the bundle. -- `mockArchy` — read via `.has("mockArchy")`, but only takes effect when the embedded flag - is *false* (`&&!embedded` in the minified logic). This does not match how `Chat.vue`'s - demo mode currently sends both `embedded=true` and `mockArchy=1` together — flagging as - an existing inconsistency in the deployed build, unrelated to D-14 and out of this plan's - scope to investigate or fix. -- `hideClose` — **zero** occurrences of the literal string anywhere in this build's JS. No - evidence this deployed build reads or honors it at all. -- `seed` — not exhaustively searched this pass; irrelevant to D-14, not pursued further. - -No parameter resembling an expanded-state or mobile-initial-view control (see D-14a/D-14b -below) was found anywhere in this bundle. +- **`embedded`** — `packages/app/src/main.ts:34-35`. Read once, before router init, via + `new URLSearchParams(window.location.search).has('embedded')` (presence-only check) and + stored on `window.__AIUI_EMBEDDED__` so it survives SPA navigation. Matches the deployed + build's minified behavior exactly (independently confirmed by the diligence pass on the + built bundle before source access — see history above). +- **`mockArchy`** — `packages/app/src/composables/useArchy.ts:83-96`. Read via + `import.meta.env.VITE_MOCK_ARCHY === 'true' || new URLSearchParams(...).has('mockArchy')`, + but the mock-data branch is only entered `if (useMock && !embedded)` — i.e. **mutually + exclusive with `embedded`** in this codebase today. This confirms the inconsistency flagged + during the original diligence pass: `Chat.vue`'s demo mode sends `embedded=true` and + `mockArchy=1` together, but AIUI only honors `mockArchy` when NOT embedded. Pre-existing, + unrelated to D-14, **not fixed here** (out of this plan's file scope — flagged for whoever + owns the demo-mode AIUI experience next). +- **`hideClose`** — confirmed absent from AIUI's source entirely (`grep -rn "hideClose" + packages/` returns nothing). AIUI does not read or honor this parameter at all today. Not + part of D-14; not investigated further. +- **`seed`** — confirmed absent from AIUI's source entirely (no `URLSearchParams` read + anywhere references it). `Chat.vue`'s `&seed=1` in demo mode currently does nothing on the + AIUI side. Pre-existing, unrelated to D-14, not fixed here. ## D-14a **Default:** the AIUI chat should start EXPANDED rather than requiring the user to expand it. -- **Status: needs a new AIUI-side parameter** (or a postMessage control, per the plan's - fallback path). No string resembling `expandedState`, `chatExpanded`, `expandChat`, or - similar chat-UI-state control was found in the deployed bundle. The one `expanded` hit in - the bundle is inside a third-party media-player library's ARIA `aria-expanded` attribute - code — unrelated to chat panel state. -- **Which side implements it:** AIUI (the receiving half must read the flag and render - itself in the expanded state on load). neode-ui's half — appending the agreed flag name - to the iframe URL — is inert and forward-compatible even before AIUI reads it, but cannot - usefully ship without the AIUI-side change, and the AIUI-side change itself is blocked: - its source is unreachable this session (see Source Location above). +- **Root cause, found in source:** `packages/app/src/stores/chat.ts:132` (before this plan's + change): `const chatCollapsed = ref(localStorage.getItem('aiui-chat-collapsed') !== 'false')` + — defaults to **collapsed** (`true`) on any fresh/unset `localStorage`, i.e. exactly the + behavior CONTEXT.md describes ("requiring the user to expand it"). `chatCollapsed` gates + `ChatWindow.vue`'s `PromptIndex` (collapsed quick-pick view) vs. the full virtualized + message list (expanded view) — confirmed at `ChatWindow.vue:35` / + `ChatWindow.vue:249`. Once a user manually expands, the choice is written back to + `localStorage` (`chat.ts:196-198`) and persists — so this bug only bites on a genuinely + fresh `localStorage`, matching the reported "starts collapsed" default. +- **Status: implemented, on the AIUI side, gated by a new query parameter** — `?chatExpanded`. + Not something the deployed build already read (see original diligence pass); a source + change was required and made. +- **Which side implements it:** AIUI reads the flag; neode-ui sends it. Implementation: + `chatCollapsed`'s initial ref now checks + `new URLSearchParams(window.location.search).has('chatExpanded')` first, defaulting to + `false` (expanded) when present, before falling back to the existing `localStorage` check. + **Never written back to `localStorage`** — it is a one-time initial-state override for this + session only, so a later manual collapse/expand while embedded still persists exactly as + before, and the standalone (non-embedded) app's own default is completely untouched (it + never sends `?chatExpanded`, so its `localStorage`-based behavior is unchanged). +- **Why a new parameter rather than reusing the existing `embedded` flag:** `embedded` is + already overloaded (transparent background, mock-Archy gating, passphrase-prompt skip); + tying `chatCollapsed`'s default to it directly would mean *every* embedded session forces + expanded with no way to decouple the two concerns later. A dedicated flag keeps the + contract self-documenting and matches this plan's acceptance criteria (the flag name must + be discoverable in the built neode-ui bundle). ## D-14b -**Default:** on a mobile viewport, AIUI should open on its CHAT view rather than its -context view. +**Default:** on a mobile viewport, AIUI should open on its CHAT view rather than its context +view. -- **Status: needs a new AIUI-side parameter** (or a postMessage control). No string - resembling `mobileView`, `initialView`, `view=chat`, `view=context`, or similar was found - in the deployed bundle. -- **Which side implements it:** AIUI (it owns the chat-view/context-view routing and would - need to read an initial-view flag or a fixed mobile-view-preference flag on load). - Same blocked status as D-14a — the AIUI-side change cannot be made without source access. +- **Root cause, found in source:** `packages/app/src/pages/ChatPage.vue:469` already + initializes `mobileTab` to `'chat'` (`const mobileTab = ref<'chat' | 'content' | + 'context'>('chat')`) — so a truly fresh mount already defaults correctly. The reported bug + is a **carry-over**, not a wrong default: `panelOpen`, `selectedFilm`/`selectedBook`/etc., + and `hasDetailOpen` (computed from those) are **module-scope singleton refs** in + `useContentPanel.ts` (`ref()` calls at module top level, not inside the composable + function), shared across every mount of `ChatPage.vue` within the same AIUI SPA session. + If a user previously opened a detail item (making `hasDetailOpen` true, which + `ChatPage.vue`'s own `watch(hasDetailOpen, ...)` at line ~507 flips `mobileTab` to + `'context'` for), and `ChatPage.vue` later remounts within the same iframe session (e.g. an + internal AIUI navigation to `/guide` and back) without those refs resetting, + `hasDetailOpen` is still `true` on the new mount and the "opens on chat" default never gets + a chance to hold — it's immediately superseded by state carried over from before. Note this + is now less likely to surface at all going forward, since Task 2's URL-stability fix (see + D-14 URL stability, below) keeps the iframe itself from reloading on an Archipelago tab + switch — but it can still happen from AIUI's own internal navigation, independent of + Archipelago. +- **Status: implemented, on the AIUI side, gated by a new query parameter** — `?mobileChat`. +- **Which side implements it:** AIUI reads the flag; neode-ui sends it. Implementation: a new + `onMounted` hook in `ChatPage.vue` re-asserts `mobileTab.value = 'chat'` when `?mobileChat` + is present and `isMobile.value` is true, at mount time only. This does not touch the + `panelOpen`/`hasDetailOpen` watchers that drive normal tab-switching in response to actual + user taps after the initial paint — those are unchanged, so a user can still navigate to + the content/context tabs exactly as before; only the *initial* tab on a (re)mount is + affected. +- **Why AIUI decides from its own viewport rather than neode-ui passing a computed + boolean:** per Task 2's URL-stability constraint, `aiuiUrl` cannot embed a reactive + viewport read (see below). `?mobileChat` is sent unconditionally and is unconditionally + interpreted by AIUI against its **own** `isMobile` computed (`windowWidth.value < 1024`, + read from the iframe's own `window.innerWidth`) — this is the "let AIUI decide from its own + viewport" option named in the plan's Task 2 action, chosen over resolving neode-ui's outer + viewport once at setup time, since AIUI's iframe has its own layout breakpoint and is the + side that actually owns the mobile/desktop layout switch. + +## D-14 URL Stability (how neode-ui sends both flags) + +Both `?chatExpanded` and `?mobileChat` are appended unconditionally in `Chat.vue`'s `aiuiUrl` +computed — static strings, not derived from any runtime-varying input (no viewport read, no +connection state, no timestamp). This preserves the property Task 2 exists to protect: the +iframe `src` stays byte-identical across re-renders, viewport resizes, and +deactivate/reactivate cycles, so the AIUI panel never reloads on an Archipelago tab switch. ## Deployment Impact -`apps/aiui/manifest.yml` declares: -```yaml -container: - image: localhost/archipelago-aiui:latest - pull_policy: always -``` +`apps/aiui/manifest.yml` still declares a **prebuilt container image** +(`localhost/archipelago-aiui:latest`, `pull_policy: always`) — this plan's AIUI-side change +does not alter that; shipping it still requires: -AIUI ships as a **prebuilt container image**, not as neode-ui-bundled static assets served -alongside the rest of the dashboard. Shipping any AIUI-side source change requires, in -order: +1. **Merging the AIUI-side commit.** Changes are committed locally in the cloned checkout at + `/home/archipelago/Projects/AIUI` on branch **`feat/d14-embed-defaults`** (based on + `development`), commit `900c0b9`. **Anonymous push to `origin` failed with `403 + Forbidden`** (`git push origin feat/d14-embed-defaults`) — this account has read-only + access to `git.tx1138.com/lfg2025/AIUI`. The branch and commit exist only in this local + checkout until someone with write access pushes it (or opens a PR from a fork/mirror they + do have write access to) and merges it into `development`. +2. Rebuilding AIUI's production bundle from the merged source: + `cd AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build` (per + `scripts/setup-aiui-server.sh`'s documented prerequisite). +3. Rebuilding the `localhost/archipelago-aiui:latest` container image from that build output + (a local/on-node image tag, not a registry pull). +4. Redeploying/restarting the `aiui` app container on the target node(s) so `pull_policy: + always` picks up the new local image. -1. Access to the AIUI source repository (currently unreachable — see Source Location). -2. Rebuilding AIUI's production bundle, per the documented prerequisite in - `scripts/setup-aiui-server.sh`: `cd AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build`. -3. Rebuilding the `localhost/archipelago-aiui:latest` container image from that fresh build - output (the manifest names a `localhost`-scoped image tag, implying a local/on-node - build or push into each target node's local image store — not a registry pull from - outside the fleet). -4. Redeploying/restarting the `aiui` app container on the target node(s) so - `pull_policy: always` picks up the new local image tag. - -Per D-15, this phase restricts deployment to the **dev pair only, with no OTA** — so even -once source access becomes available, an AIUI-side D-14 change would be built and deployed -only to the designated dev nodes (per plan 02-08, which owns the deploy step), not pushed -through the normal fleet release train. - -**Bottom line:** until AIUI source access is available (e.g. the ThinkPad build server at -`.116` comes back online and is confirmed to hold the AIUI checkout, or the repository is -otherwise made reachable), the AIUI half of D-14 cannot be implemented, built, or deployed -at all. This is a genuine blocking external dependency, not a scope reduction — D-14 is not -dropped, it is blocked pending a fact only the developer can supply. +Per D-15, deployment is **dev pair only this phase, no OTA** — plan 02-08 owns the deploy +step under that constraint, once the AIUI-side commit is actually mergeable (i.e., once a +maintainer with push rights lands `feat/d14-embed-defaults` on `development` and it's +rebuilt/redeployed). **Until that push/merge happens, the deployed AIUI build on any node +(including archi-dev-box) will NOT carry these two parameters** — neode-ui's side is fully +forward-compatible with that (both flags are inert query-string additions the current +deployed AIUI build simply ignores, per the confirmed absence of `hideClose`-style unknown +params breaking anything), but the two visual defaults (expanded start, mobile-chat-first +start) will not be observable against any currently-deployed AIUI build until the AIUI +commit ships.