# muse.ai Edge States DOM Reference > **Box is the main surface.** All operator work goes through Box (box.muse-dev.online). The web UI, `box` CLI, and agents share the same API endpoints. No UI-only powers. > Captured live via CDP on `opm` browser (warp-opm netns, CDP 9440) on 2026-10-04. > Re-verified 2026-10-04 across `muse` (9410), `pip` (9420), `opm` (9440) — > see §10. No alerts, toasts, approval dialogs, or offline markers on any node. > 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. **Update 2026-10-04:** the thread-title switcher text is also the *settled*-state behavior (verified on muse/pip/opm thread pages, e.g. `646 pip cross-operator coordination chat | Open chat and side chats`). Never match the switcher by text — use the testid. Likewise, the `Back to main chat` aria-label button is present on **regular `/thread/` pages too**, not only during partial load — S8 recovery works from any thread state, panel open or closed. ### 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 `