2026-10-04 03:51:05 +00:00
# muse.ai Edge States DOM Reference
2026-10-05 15:58:37 +00:00
> **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.
2026-10-04 03:51:05 +00:00
> Captured live via CDP on `opm` browser (warp-opm netns, CDP 9440) on 2026-10-04.
2026-10-04 04:29:10 +00:00
> 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.
2026-10-04 03:51:05 +00:00
> 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
< span class = "rounded-6 motion-reduce:animate-none animate-pulse-light bg-fill-primary text-te ..." > < / span >
```
**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
< div class = "group-has-[[data-hatch-avatar-media-loading]]/status-avatar:hidden ..." > < / div >
```
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.
2026-10-04 04:29:10 +00:00
**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/<uuid>`
pages too**, not only during partial load — S8 recovery works from any thread
state, panel open or closed.
2026-10-04 03:51:05 +00:00
### 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
< div role = "status" data-testid = "hatch-chat-typing-indicator" > < / div >
```
| 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 `<script>` tag (React bundle code contains "retry") | `body.innerText` includes script text in some probes |
| `loading` | `<script>` bundle code | same as above |
**Rule: never trust bare `innerText.includes()` for error detection. ** Scope
error searches to rendered, non-script elements, or better, to `[role]`
regions:
``` javascript
[ ... document . querySelectorAll ( '[role="alert"], [role="status"]' ) ]
. filter ( e => e . tagName !== 'SCRIPT' && ( e . innerText || '' ) . trim ( ) . length > 0 )
```
### 5.2 Approval dialogs (the one real "banner" class)
Permission dialogs (`Allow … to share` ) are detected by `check_approvals` in
`muse-chat-api.py` : it only treats dialogs containing an **IP address ** as
real, and auto-approves IPs in `TRUSTED_IPS` (VM/bl). Dialogs without IPs are
ignored as false positives (chat content mentioning "allow"). Full structure
is in `DOM-PAGE-STRUCTURE.md` .
---
## 6. Offline / disconnected
- `navigator.onLine` → `true` (via CDP eval).
- No `offline` , `reconnecting` , `disconnected` , `connection lost` markers in
body text in any observed state.
- No service-worker or connectivity UI observed.
Detection guidance for automation:
``` javascript
// poll alongside state probes
( { onLine : navigator . onLine ,
markers : /offline|reconnecting|disconnected|connection lost/i . test (
document . body ? document . body . innerText : '' ) } )
```
If `onLine === false` or a marker appears: stop acting, back off, re-probe;
do not click through — a disconnected React app can swallow clicks silently.
---
## 7. Rate limit / blocked states
No `rate limit` , `too many requests` , `slow down` markers observed in any
state. The app's own rate limiting was not triggerable from these probes.
What we know from automation history (AGENTS.md): the constraint that matters
is **our own send rate ** — use the shared `bin/rate_limiter.py` token bucket
(1 op / 3s, burst 5, max 20/min per agent). There is no observed DOM signal
for server-side throttling; if sends start failing opaquely, back off and
check `dm-log.jsonl` / `job-log.jsonl` rather than the DOM.
---
## 8. Iframes and shadow DOM (no automation barrier)
### 8.1 Iframes — all marketing pixels
| `src` | `title` |
|---|---|
| `https://muse.ai/api/marketing/campaign-manager-pixel-frame` | Campaign Manager measurement |
| `https://muse.ai/api/marketing/dv360-pixel-frame` | DV360 measurement |
| `https://muse.ai/api/marketing/snapchat-pixel-frame` | Snapchat measurement |
| `https://muse.ai/api/marketing/tiktok-pixel-frame` | TikTok measurement |
4 iframes, all 1× 1-style measurement pixels. None contain app UI.
### 8.2 Shadow DOM
One shadow host: `<next-route-announcer>` (Next.js route announcer, empty).
Not app UI. **No shadow-DOM barrier ** for automation selectors, confirming
the earlier DOM investigation.
---
## 9. Automation cheat-sheet
| Signal | Selector / check | Meaning |
|---|---|---|
| Ready | `title.startsWith('Chat —') && has('[data-testid="hatch-chat-switcher-trigger"]') && buttons > 30` | settled, act |
| S3 partial | `title === 'Muse' && !has(switcher) && buttons < 30` | wait / reload (cap 2) |
| S8 stripped | `url.includes('/thread/new') && !has(switcher)` | click `Back to main chat` or `hatch-nav-chat` |
| Chat active | `has('[data-testid="hatch-chat-compose"]')` | panel open, ready to create |
| Panel open | `shell.innerHTML !== ''` | shell = `[data-testid="hatch-side-chats-panel-shell"]` |
| Empty chat list | `rows === 0` | rows = `[data-testid="hatch-thread-row"]` |
| Typing/streaming | `has('[data-testid="hatch-chat-typing-indicator"]')` with content | response in flight |
| Real error | non-empty `[role="alert"]` / IP-bearing approval dialog | stop, escalate |
| Offline | `!navigator.onLine` or offline markers | back off, re-probe |
**Never use as signals: ** `document.readyState` (already `complete` during S3),
`body.innerText.includes('retry'/'loading'/'Side chats')` (false positives
2026-10-04 04:29:10 +00:00
documented above), switcher `aria-expanded` (never set), switcher **inner text **
(it's the thread title, not `Chats` ), shimmer
2026-10-04 03:51:05 +00:00
`animate-pulse-light` spans (present when settled).
2026-10-04 04:29:10 +00:00
## 10. Re-verification log (2026-10-04, inspector 5/5)
Probed `muse` , `pip` , `opm` via CDP `Runtime.evaluate` in each netns:
| Check | muse | pip | opm |
|---|---|---|---|
| `navigator.onLine` | true | true | true |
| `[role="alert"]` with content | 0 | 0 | 0 |
| Toasts | 0 | 0 | 0 |
| `Allow … to share` in body text | no | no | no |
| Offline/rate-limit markers | none | none | none |
| Typing indicator | absent | absent | absent |
| Composer present | yes | yes | yes |
Additional observations:
- **Title unread prefix:** pip's title was `(1) Chat — operator-pip` — the title
gains an `(N)` prefix when unreads exist. State detectors should use
`startsWith('Chat —')` on the * end * of the title or strip a leading
`\(\d+\)\s*` before comparing.
- **Shared browsers:** another agent drove pip's browser mid-probe (URL changed
from `/thread/<uuid>` to `/` and back, panel toggled between probes).
Fleet browsers are a shared resource — re-probe immediately before acting,
and never treat a minutes-old snapshot as current state.
- **Settled-state counts are ranges:** shimmer spans 1 (doc said 2),
`role="status"` 3– 4 (doc said 4– 7), buttons 46– 57 on settled thread pages.
The cheat-sheet's `buttons > 30` threshold still holds; don't hard-code
exact counts.