187 lines
12 KiB
Markdown
187 lines
12 KiB
Markdown
# 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 (`<parent-of-archy>/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 (`<parent-of-archy>/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.
|