# AIUI D-14 Embed Contract **Written:** 2026-07-30 (Phase 02 Plan 07, Task 1) **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 now located and was cloned for this plan:** - **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. ### Original Task 1 search (superseded, kept for history) 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) 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). Read from AIUI source (`development` branch, commit `9176324` before this plan's changes): - **`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. - **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. - **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` 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: 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. 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.