diff --git a/docs/DOM-EDGE-STATES.md b/docs/DOM-EDGE-STATES.md new file mode 100644 index 0000000..14049d3 --- /dev/null +++ b/docs/DOM-EDGE-STATES.md @@ -0,0 +1,311 @@ +# muse.ai Edge States DOM Reference + +> Captured live via CDP on `opm` browser (warp-opm netns, CDP 9440) on 2026-10-04. +> Method: settled-state probe + `Page.reload` with timed probes at t=0.5/1.5/3/6/10s +> to catch transient load states. +> Baseline page: `https://muse.ai/thread/2e2c90a1-4b17-4590-b8ba-fb17d8018d36` +> (a real sidechat thread), title `Chat — operator-main`. +> Companion docs: `DOM-CHAT-PANEL.md`, `DOM-MESSAGES.md`, `DOM-PAGE-STRUCTURE.md`. +> This is a development reference for reliable automation. Prefer these selectors +> over text matching. + +--- + +## 1. Loading states + +### 1.1 Skeleton shimmer (the actual "spinner") + +There is **no** `animate-spin` spinner, no `[role="progressbar"]`, and no +`aria-busy="true"` anywhere in the app DOM. Loading is shown with shimmer +skeleton spans: + +```html + +``` + +**Key facts:** + +| Property | Value | +|---|---| +| Selector | `span.animate-pulse-light` (class match: `/animate-pulse-light/`) | +| `data-testid` | None — class is the only hook | +| Present in settled state | **Yes** (2 hits even when fully loaded) | +| Parent chain | `SPAN > DIV > DIV > DIV > DIV` (no testids in chain — not localizable by testid) | +| `role="status"` count | 4–7 (see 1.3) | + +Because shimmer spans exist in the settled state too, **do not use them as a +"still loading" signal**. They mark skeleton slots (e.g. avatar/media slots), +not page load progress. + +### 1.2 Avatar/media loading slot + +```html +
+``` + +A container that hides the avatar until `data-hatch-avatar-media-loading` clears. +Class-match: `/data-hatch-avatar-media-loading/`. Present in settled state; +same caveat as 1.1 — not a page-load signal. + +### 1.3 Screen-reader live regions (always present, usually empty) + +| Element | `role` | `aria-live` | `data-testid` | Content | +|---|---|---|---|---| +| `SPAN` ×2 | `status` | `polite` | — | empty | +| `DIV` | `log` | `polite` | — | **the message list** (see 1.4) | +| `DIV` | `status` | — | `hatch-chat-typing-indicator` | empty when idle | +| `DIV` | `status` | `polite` | — | empty | +| `SECTION` | — | `polite` | — | empty | + +Count varies 4–7 across probes. They are persistent accessibility plumbing, not +transient loading UI. The `role="log"` div **is** the message list container. + +### 1.4 Ready-state transition (reload sequence) + +What the DOM looks like as the page settles after `Page.reload` on a thread URL: + +| t (s) | `document.readyState` | Title | Buttons | Markers | Notes | +|---|---|---|---|---|---| +| 0.5 | `complete` | `Muse` | 10 | none | **S3 partial** — nav rail only | +| 1.5 | `complete` | `Muse` | 15 | none | **S3 partial** — switcher appears | +| 3.0 | `complete` | `Chat — operator-main` | 49 | `retry`* | settled | +| 6.0 | `complete` | `Chat — operator-main` | 49 | `retry`* | settled | +| 10.0 | `complete` | `Chat — operator-main` | 51 | `retry`* | settled | + +\* `retry` is a **false positive** (see §5.1). Note `readyState` is already +`complete` at t=0.5 — **do not use `readyState` as a readiness signal**; the +React app hydrates after it. + +**Settled-state signature** (what "ready" looks like for automation): + +```javascript +document.title.startsWith('Chat —') && + !!document.querySelector('[data-testid="hatch-chat-switcher-trigger"]') && + document.querySelectorAll('button').length > 30 +``` + +--- + +## 2. S3 PARTIAL_LOAD dissected + +The transient state the reliability investigation called S3 (title `Muse`, +~10–21 buttons, no switcher or a just-appeared switcher). Captured at t=0.5 +and t=1.5 after reload. + +### 2.1 t=0.5 — nav rail only (10 buttons) + +```text +button text='' aria='Search' testid='hatch-nav-search' +button text='' aria='Settings' testid='hatch-dock-more' +button text='Show in library' aria=None testid=None +button text='' aria='Close panel' testid='hatch-status-panel-close' +button text='' aria='Edit avatar and name' testid=None +button text='' aria='Activity' testid=None +button text='' aria='Approvals' testid=None +button text='' aria='Upcoming' testid=None +button text='' aria='Identity' testid=None +button text='' aria='Attach file' testid=None +``` + +No chat switcher. No compose `+`. No `hatch-nav-chat`. The left dock rail +renders before the chat app. + +### 2.2 t=1.5 — switcher appears (15 buttons) + +Same as above, plus: + +```text +button text='' aria='Get the Muse app' testid=None +button text='' aria='Back to main chat' testid=None +button text='[JOB heartbeat-20261004-034747-6b3d4128]' + aria=None testid='hatch-chat-switcher-trigger' +button text='' aria='More options' testid=None +button text='' aria='Dictate a message' testid=None +``` + +Notable: the switcher's visible text is the **current thread title** (here the +auto-generated heartbeat thread title), not the literal `Chats`. The +`Back to main chat` button exists this early — it is the S8 escape hatch even +during partial load. + +### 2.3 Detection and recovery + +```javascript +// S3 detection +document.title === 'Muse' && + !document.querySelector('[data-testid="hatch-chat-switcher-trigger"]') && + document.querySelectorAll('button').length < 30 && + !window.location.href.includes('/thread/new') +``` + +Recovery (from the state-machine design): sleep 5s, re-probe; if still S3, +`Page.reload` via CDP, re-probe; cap at 2 reloads, then abort with a state +dump. Never treat S3 as a failure of the *action* — it is a transient render +phase. + +--- + +## 3. Empty states + +### 3.1 Sidechat panel: closed vs open-but-empty + +```javascript +const shell = document.querySelector('[data-testid="hatch-side-chats-panel-shell"]'); +const isOpen = shell && shell.innerHTML !== ''; +const rows = document.querySelectorAll('[data-testid="hatch-thread-row"]').length; +``` + +| Condition | Meaning | +|---|---| +| `shell.innerHTML === ''` | Panel closed (shell is always in DOM, emptied when closed) | +| `shell.innerHTML !== '' && rows === 0` | Panel open, no chats listed | +| `rows > 0` | Panel open with chats | + +Caveat: open-but-empty was **not directly observed** (opm has sidechats); the +`rows === 0` check is the detection to use, but the visual empty-state copy is +unrecorded. + +### 3.2 Message list + +The message list is the `DIV[role="log"][aria-live="polite"]` (plain DIVs, no +testids up the chain — 5 levels of bare `DIV`). Observed with 2 children in a +populated thread. No `no messages` / `start a conversation` / `nothing here +yet` text markers exist anywhere in `document.body.innerText` in the observed +states. An empty thread's exact DOM is unrecorded — detect via +`log.children.length === 0`. + +### 3.3 Composer placeholder + +`[data-testid="hatch-composer-placeholder-overlay"]` exists in the testid +inventory (observed in settled state). It overlays the composer when empty. + +--- + +## 4. Typing indicator + +```html +
+``` + +| Property | Value | +|---|---| +| Selector | `[data-testid="hatch-chat-typing-indicator"]` | +| Tag / role | `DIV` / `status` | +| Present when idle | **No** — transient; absent from DOM in one probe, present-but-empty in another | +| Inner HTML when idle | `""` (empty) | + +Useful as a "someone is typing / response streaming" signal: present with +content means activity. Do not assume it is always in the DOM. + +--- + +## 5. Error banners and toasts + +### 5.1 Nothing observed — and two documented traps + +Across all probes: **zero** toasts (`[data-testid*="toast"]`, +`[data-sonner-toast]`, `*toast*` classes), **zero** `[role="alert"]` elements +with content, and none of these body-text markers: +`something went wrong`, `try again`, `failed to load`, `went wrong`, `oops`, +`error`, `blocked`, `suspended`. + +Two false positives to not trip on: + +| Marker | Actual source | Why it matches | +|---|---|---| +| `retry` | `DIV.text-subheadline-medium`: **"Refine JOB/DM system and retry"** — an auto-generated sidechat title from earlier automation | substring match on chat content | +| `retry` | a `