225 lines
12 KiB
Markdown
225 lines
12 KiB
Markdown
|
|
# muse.ai DOM Index — Master Reference
|
||
|
|
|
||
|
|
> **Entry point for all muse.ai DOM automation work.**
|
||
|
|
> Read this first. It indexes every `data-testid`, every key selector, every
|
||
|
|
> automation recipe, and flags known contradictions between the detailed docs.
|
||
|
|
>
|
||
|
|
> Rule of thumb from the field: prefer `data-testid` > `aria-label` > `role`/`id`
|
||
|
|
> > class names. Tailwind classes shift with deploys; testids don't.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. Document Map
|
||
|
|
|
||
|
|
| Doc | Covers | Last verified |
|
||
|
|
|---|---|---|
|
||
|
|
| [DOM-CHAT-PANEL.md](./DOM-CHAT-PANEL.md) | Chat panel switcher, compose `+` button, panel shell/tree, thread rows, open-vs-closed detection, 24-test id inventory | 2026-10-04 (CDP, opm) |
|
||
|
|
| [DOM-MESSAGES.md](./DOM-MESSAGES.md) | Message composer, send/stop buttons, message list (`data-message-id`), chat activation button (`hatch-nav-chat`), loading/empty states | 2026-10-04 ~03:45 UTC (CDP, opm) |
|
||
|
|
| [DOM-PAGE-STRUCTURE.md](./DOM-PAGE-STRUCTURE.md) | Page states/modes, key selectors, approval dialogs, overlays, URL patterns, 57-testid inventory, automation sequences | 2026-10-04 (CDP, opm) |
|
||
|
|
| [DOM-APPROVALS-SPEC.md](./DOM-APPROVALS-SPEC.md) | Approval/permission dialog detection, classification (trusted IPs), handling, exit-code contract (`2` = `APPROVAL_NEEDED`) | 2026-10-03 |
|
||
|
|
|
||
|
|
**Cross-reference guide:**
|
||
|
|
- Sidechat creation mechanics → CHAT-PANEL (§4 safe toggle, §6 automation notes)
|
||
|
|
- What to type into / how to send → MESSAGES (§1 composer, §2 send)
|
||
|
|
- "Which state am I in?" → PAGE-STRUCTURE (§1 states, §7.2 one-liner) or §3 below
|
||
|
|
- "Am I blocked by a dialog?" → APPROVALS-SPEC (detection + exit codes)
|
||
|
|
- "Chat function not active" → MESSAGES (§4 `hatch-nav-chat`) + §4 recipe below
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Master `data-testid` Table (deduplicated)
|
||
|
|
|
||
|
|
`Docs` column: **C** = CHAT-PANEL, **M** = MESSAGES, **P** = PAGE-STRUCTURE.
|
||
|
|
|
||
|
|
### Chat core (panel toggle + creation)
|
||
|
|
|
||
|
|
| testid | Tag | Purpose | Docs |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `hatch-chat-switcher-trigger` | `BUTTON` | Panel open/close **toggle**. Text: `Chats`. Never blind-click — check compose first | C, P |
|
||
|
|
| `hatch-chat-switcher-icon-knockout` | `SPAN` | Icon wrapper inside switcher | C, P |
|
||
|
|
| `hatch-chat-switcher-navigation-icon` | `SPAN` | Hamburger SVG inside switcher | C, P |
|
||
|
|
| `hatch-chat-leading-controls` | `DIV` | Top-left container holding the switcher | C, P |
|
||
|
|
| `hatch-chat-compose` | `BUTTON` | **New side chat `+`**. Icon-only (`aria-label="New side chat"`). Exists **only when panel is open** → the canonical "ready" signal | C, M, P |
|
||
|
|
| `hatch-chat-nav-fade` | `DIV` | Nav fade effect | C, P |
|
||
|
|
| `hatch-chat-search-toolbar` | `DIV` | Panel search toolbar (60px) | C, P |
|
||
|
|
| `hatch-chat-search-field` | `DIV` | Search input container | C, P |
|
||
|
|
| `hatch-side-chats-options` | `BUTTON` | "Side chat options" three-dots dropdown | C, P |
|
||
|
|
| `hatch-chat-switcher-unread-indicator` | — | Unread badge on switcher | M |
|
||
|
|
|
||
|
|
### Sidechat panel tree
|
||
|
|
|
||
|
|
| testid | Tag | Purpose | Docs |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `hatch-side-chats-panel-shell` | `DIV` | Outer shell, **always in DOM**, `innerHTML === ""` when closed, 240px fixed | C, P |
|
||
|
|
| `hatch-side-chats-panel-sliding-surface` | `DIV` | Sliding animation surface (open only) | C, P |
|
||
|
|
| `hatch-side-chats-panel-content` | `DIV` | Content wrapper — presence = panel open | C, P |
|
||
|
|
| `hatch-side-chats-open-drag-handle` | `DIV` | Panel resize handle | C, P |
|
||
|
|
| `hatch-thread-row` | `DIV[role=button]` | **Every** chat row (Main + sidechats). Not links — click to navigate. Active row has `aria-current="page"` | C, P |
|
||
|
|
| `hatch-side-chat-unread-indicator` | — | Per-chat unread indicator | P |
|
||
|
|
| `active-side-chat-load-more-sentinel` | `DIV` | Infinite-scroll sentinel at list end | C, P |
|
||
|
|
|
||
|
|
### Dock rail (left nav)
|
||
|
|
|
||
|
|
| testid | Tag | Purpose | Docs |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `hatch-dock-rail` | `DIV` | Left dock rail container | C, M, P |
|
||
|
|
| `hatch-dock-rail-divider` | `DIV` | Rail divider | P |
|
||
|
|
| `hatch-nav-chat` | `A` | **Chat section link** (`href="/"`). `aria-label` is dynamic (`Chat` / `Chat, N notifications`) — match testid, never exact aria-label. `aria-current="page"` when Chat active. **The reliable chat activator** | C, M, P |
|
||
|
|
| `hatch-dock-chat-notification-badge` | — | Notification badge on nav-chat | P |
|
||
|
|
| `hatch-nav-search` | `BUTTON` | Search nav | C, M, P |
|
||
|
|
| `hatch-nav-system-item-66656564` etc. | `DIV` | Feed / Ideas / Goals / Library (hex-encoded ids) | C, P |
|
||
|
|
| `hatch-dock-more` | `BUTTON` | Settings (`aria-label="Settings"`) | C, M, P |
|
||
|
|
| `app-download-dock-mark` | — | App download mark | P |
|
||
|
|
| `hatch-panel-resize-divider-line` | `DIV` | Panel resize divider | P |
|
||
|
|
|
||
|
|
### Composer & messages
|
||
|
|
|
||
|
|
| testid | Tag | Purpose | Docs |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `hatch-composer-placeholder-overlay` | `SPAN` | "Message" ghost text — visible when composer empty | C, M, P |
|
||
|
|
| `hatch-composer-stop-button` | `BUTTON` | Stop-generation — present **only while streaming** | M |
|
||
|
|
| `hatch-chat-typing-indicator` | — | Typing indicator — present **only while generating** | M |
|
||
|
|
|
||
|
|
### Status / misc
|
||
|
|
|
||
|
|
| testid | Tag | Purpose | Docs |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `hatch-status-panel-sliding-surface` | `DIV` | Agent status panel | C, P |
|
||
|
|
| `hatch-status-panel-close` | `BUTTON` | Status panel close | P |
|
||
|
|
| `hatch-status-panel-close-drag-handle` | `DIV` | Status panel drag handle | P |
|
||
|
|
| `hatch-status-panel-close-drag-handle-divider-line` | `DIV` | Divider | P |
|
||
|
|
| `hatch-status-panel-close-toolbar` | `DIV` | Toolbar | P |
|
||
|
|
| `hatch-invite-friends-button` | `BUTTON` | Invite (`aria-label="Invite"`) | C, P |
|
||
|
|
| `hatch-browser-task-banner-host` | `DIV` | Browser task banner (hidden) | C, P |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Non-`testid` Selectors (equally load-bearing)
|
||
|
|
|
||
|
|
| Selector | Purpose | Doc |
|
||
|
|
|---|---|---|
|
||
|
|
| `textarea[aria-label="Message"]` | Message composer input. Fallbacks: `[contenteditable="true"]`, `textarea[placeholder*="Message"]`, `div[role="textbox"]` | M |
|
||
|
|
| `button[aria-label*="send" i]` | Send button (no testid, SVG-only). Fallback: Enter keydown on input | M |
|
||
|
|
| `#hatch-chat-scroll` | Message scroll container | M |
|
||
|
|
| `div[role="log"][aria-label="Chat messages"]` | Message list | M |
|
||
|
|
| `[data-message-id]` | Each message. `assistant-msg-<uuid>` = assistant, bare `<uuid>` = user | M |
|
||
|
|
| `button[aria-label="Back to main chat"]` | **Escape hatch from `/thread/new`** stripped state (empty text, match aria-label) | *(subagent report 2026-10-04 — not yet in a detail doc)* |
|
||
|
|
| Exact-text `Main chat` element | Main-chat navigation target — match `(e.innerText).trim() === 'Main chat'` on `button, [role="button"]`, never substring | P (§2.7), AGENTS.md |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Quick-Reference Automation Recipes
|
||
|
|
|
||
|
|
### 4a. Activate chat (recover from ANY state) — replaces Ctrl+J
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// Priority: compose (fast path) → switcher → nav-chat (stripped-state recovery)
|
||
|
|
function ensureChatActive() {
|
||
|
|
if (document.querySelector('[data-testid="hatch-chat-compose"]')) return true; // already ready
|
||
|
|
const sw = document.querySelector('[data-testid="hatch-chat-switcher-trigger"]');
|
||
|
|
if (sw) { sw.click(); return "clicked-switcher"; } // closed panel
|
||
|
|
const nc = document.querySelector('[data-testid="hatch-nav-chat"]');
|
||
|
|
if (nc) { nc.click(); return "clicked-nav-chat"; } // /thread/new stripped
|
||
|
|
return false; // not on a chat page at all — abort
|
||
|
|
}
|
||
|
|
// Poll up to ~20s, then verify compose exists before proceeding.
|
||
|
|
```
|
||
|
|
|
||
|
|
**Why not Ctrl+J:** it needs keyboard focus, which stripped states (`/thread/new`)
|
||
|
|
lack — the keystroke goes nowhere. DOM clicks don't need focus. (Commit `72574ba`.)
|
||
|
|
|
||
|
|
### 4b. Detect current state (one-liner)
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
(() => {
|
||
|
|
const url = window.location.href, title = document.title;
|
||
|
|
const q = s => !!document.querySelector(s);
|
||
|
|
if (title === 'muse.ai') return 'LANDING';
|
||
|
|
if (url.includes('/thread/new')) return 'THREAD_NEW_STRIPPED';
|
||
|
|
if (url.includes('/thread/')) return q('[data-testid="hatch-chat-switcher-trigger"]') ? 'SIDECHAT' : 'THREAD_NEW_STRIPPED';
|
||
|
|
if (url === 'https://muse.ai/') {
|
||
|
|
if (!q('[data-testid="hatch-chat-switcher-trigger"]')) return 'UNKNOWN_NO_SWITCHER';
|
||
|
|
return q('[data-testid="hatch-chat-compose"]') ? 'MAIN_PANEL_OPEN' : 'MAIN_PANEL_CLOSED';
|
||
|
|
}
|
||
|
|
return 'UNKNOWN';
|
||
|
|
})()
|
||
|
|
```
|
||
|
|
|
||
|
|
State table: LANDING · MAIN_PANEL_CLOSED · MAIN_PANEL_OPEN · THREAD_NEW_STRIPPED
|
||
|
|
(`/thread/new`, no switcher) · SIDECHAT (`/thread/<uuid>`). Full matrix: PAGE-STRUCTURE §1.
|
||
|
|
|
||
|
|
### 4c. Create sidechat (robust)
|
||
|
|
|
||
|
|
```
|
||
|
|
1. ensureChatActive() (§4a) — poll ≤20s, abort if false
|
||
|
|
2. Verify: !!document.querySelector('[data-testid="hatch-chat-compose"]')
|
||
|
|
3. Click compose
|
||
|
|
4. Poll window.location.href ≤15s for "/thread/" → /thread/new (placeholder)
|
||
|
|
5. Send first message directly (browser is on the new sidechat)
|
||
|
|
6. Poll href for /thread/<uuid> to capture the durable thread URL
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4d. Send a message
|
||
|
|
|
||
|
|
```
|
||
|
|
1. ta = document.querySelector('textarea[aria-label="Message"]') (+ fallbacks)
|
||
|
|
2. Verify ready: ta.offsetParent !== null && !ta.disabled && !ta.readOnly
|
||
|
|
3. ta.focus(); document.execCommand('insertText', false, message)
|
||
|
|
(React fallback: native value setter + bubbling 'input' event)
|
||
|
|
4. send = [...document.querySelectorAll('button')].find(b => b.getAttribute('aria-label')?.toLowerCase().includes('send'))
|
||
|
|
→ send.click() else Enter-keydown on ta
|
||
|
|
5. If streaming: [data-testid="hatch-composer-stop-button"] appears; wait for it to vanish
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4e. Open a specific sidechat / go to main chat
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// By title (rows are DIV[role=button], NOT links — no hrefs exist):
|
||
|
|
const row = [...document.querySelectorAll('[data-testid="hatch-thread-row"]')]
|
||
|
|
.find(r => r.querySelector('span[title]')?.getAttribute('title') === '<title>');
|
||
|
|
row?.click();
|
||
|
|
// Main chat: exact text match, never substring:
|
||
|
|
const main = [...document.querySelectorAll('button, [role="button"]')]
|
||
|
|
.find(e => (e.innerText || '').trim() === 'Main chat');
|
||
|
|
main?.click();
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4f. Check for blocking approval dialogs
|
||
|
|
|
||
|
|
Text-based (no stable testids observed). See APPROVALS-SPEC for the full contract:
|
||
|
|
- Body text contains `Allow` + `to share` in an element < 500 chars, **and** an IPv4 is present
|
||
|
|
- Trusted IPs (`34.139.37.135`, `100.123.153.75`, `100.81.31.9`) → auto-click `allow once`/`allow`
|
||
|
|
- Anything else → print `APPROVAL_NEEDED`, exit **2**, do NOT click
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. Known Contradictions (resolved here — detail docs may lag)
|
||
|
|
|
||
|
|
| # | Conflict | Resolution (this index wins) |
|
||
|
|
|---|---|---|
|
||
|
|
| 1 | PAGE-STRUCTURE §7.1 and §2.7 recommend **Ctrl+J** as "proven reliable" for main-chat navigation / state recovery | **Superseded.** Ctrl+J needs keyboard focus; stripped states lack it. Use the §4a click sequence (`hatch-nav-chat` → switcher). CHAT-PANEL §6.1 already agrees Ctrl+J is unreliable |
|
||
|
|
| 2 | PAGE-STRUCTURE §1.3 "Recovery: Send Ctrl+J" from `/thread/new` | **Use `button[aria-label="Back to main chat"]` click** or `hatch-nav-chat` click instead |
|
||
|
|
| 3 | `/thread/new` title: MESSAGES §5 and PAGE-STRUCTURE §1.3 say `Muse`; a 2026-10-04 probe table said `Chat — …` | **Docs consensus: `Muse`.** Treat title as advisory only — detect via URL + switcher absence (§4b), never title alone |
|
||
|
|
| 4 | "Sidechat create navigates to main first" (older code comment) | **Unnecessary.** The ready state for *create* is any chat-active view; `hatch-nav-chat` recovery lands on main anyway |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. Gaps — Unmapped Territory
|
||
|
|
|
||
|
|
1. **`button[aria-label="Back to main chat"]`** — verified in live probes, referenced in §3, but has no dedicated section in any detail doc yet.
|
||
|
|
2. **Feed / Ideas / Goals / Library pages** — dock testids listed, page content unmapped.
|
||
|
|
3. **Search overlay** (`hatch-nav-search` target) — unmapped.
|
||
|
|
4. **OTP / account-selection states** — body-text markers known (`To log in, enter the code`, `Your email matches multiple accounts`), DOM unmapped. Automation should abort to human here.
|
||
|
|
5. **`cmd_sidechat_main`'s exact-text `Main chat` lookup** — documented in AGENTS.md and §3/§4e here, no DOM-doc section.
|
||
|
|
6. **Partial-load state** (title `Muse`, ~21 buttons, transient) — mentioned in MESSAGES §5, no recovery procedure beyond "navigate to `/` and re-probe".
|
||
|
|
7. **Message read-back verification selectors** — MESSAGES §3 covers extraction; the `dm.py` tail-match verification loop is code-only.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. Maintenance
|
||
|
|
|
||
|
|
- This index is the **entry point**; detail docs hold the deep dumps. When a selector changes, update the detail doc **and** the tables/recipes here.
|
||
|
|
- Contradiction rule: **newer verified probe beats older doc text**; record the resolution in §5 with the commit or date.
|
||
|
|
- "Last verified" in §1 is per-doc. If you re-probe a doc's selectors, bump its date.
|
||
|
|
- Prefer adding a dated note over rewriting history — these docs are also an audit trail.
|