21 KiB
muse.ai DOM Page Structure Reference
Box is the main surface. All operator work goes through Box (box.muse-dev.online). The web UI,
boxCLI, and agents share the same API endpoints. No UI-only powers.
Generated: 2026-10-04 via live CDP inspection (opm browser, warp-opm netns, CDP 9440) Purpose: Reference for future automation development. All selectors verified against live DOM.
1. Page States / Modes
The muse.ai SPA has distinct DOM states. Detect state before acting — selectors valid in one state are absent in another.
1.1 Main Chat
| Property | Value |
|---|---|
| URL | https://muse.ai/ |
| Title | Chat — <agent-name> (e.g., Chat — operator-main) |
Switcher [data-testid="hatch-chat-switcher-trigger"] |
✅ Present |
Compose [data-testid="hatch-chat-compose"] |
❌ Absent (panel closed) |
Panel content [data-testid="hatch-side-chats-panel-content"] |
❌ Absent (panel closed) |
Panel shell [data-testid="hatch-side-chats-panel-shell"] |
✅ Present (empty/collapsed) |
Message input [aria-label="Message"] |
✅ Present, visible |
Detection:
// Reliable main-chat detection
const isMainChat =
window.location.href === 'https://muse.ai/' &&
document.title.startsWith('Chat —') &&
!!document.querySelector('[data-testid="hatch-chat-switcher-trigger"]');
1.2 Panel Open (Chat Navigation Panel Visible)
Reached by clicking the switcher on main chat. This is the required precondition for sidechat creation.
| Property | Value |
|---|---|
| URL | https://muse.ai/ (unchanged) |
| Title | Chat — <agent-name> (unchanged) |
| Switcher | ✅ Present |
Compose [data-testid="hatch-chat-compose"] |
✅ Present and visible |
| Panel content | ✅ Present |
Thread rows [data-testid="hatch-thread-row"] |
✅ Present (26 rows observed) |
Detection:
// Panel is open and ready for sidechat creation
const isPanelOpen =
!!document.querySelector('[data-testid="hatch-side-chats-panel-content"]') &&
!!document.querySelector('[data-testid="hatch-chat-compose"]');
// Verify compose button is actually clickable
const composeBtn = document.querySelector('[data-testid="hatch-chat-compose"]');
const isClickable = composeBtn && composeBtn.getBoundingClientRect().width > 0;
Reliable open sequence:
// 1. Ensure on main chat (Ctrl+J via CDP Input.dispatchKeyEvent)
// 2. Check if panel already open
if (!document.querySelector('[data-testid="hatch-chat-compose"]')) {
// 3. Click switcher to open panel
document.querySelector('[data-testid="hatch-chat-switcher-trigger"]').click();
// 4. Wait 2s, re-check for compose button
}
1.3 /thread/new (Fresh Sidechat / Poisoned State)
This is the "poisoned" state. After creating a sidechat, the browser lands here with a stripped DOM that lacks the switcher and compose button. The next sidechat create will fail unless you navigate back to main first.
| Property | Value |
|---|---|
| URL | https://muse.ai/thread/new |
| Title | Muse (just "Muse", not "Chat — ...") |
| Switcher | ❌ Absent |
| Compose | ❌ Absent |
| Panel shell | ✅ Present but stripped (no panel-content) |
| Message input | ✅ Present |
| Button count | ~45 (vs ~21 on partially-loaded states) |
Detection:
const isThreadNew =
window.location.href.includes('/thread/new') ||
(document.title === 'Muse' &&
!document.querySelector('[data-testid="hatch-chat-switcher-trigger"]'));
Recovery: Click button[aria-label="Back to main chat"] (present on /thread/new
and on regular /thread/<uuid> pages — verified on all fleet nodes 2026-10-04),
or click [data-testid="hatch-nav-chat"], then open the panel via the switcher.
Do NOT use Ctrl+J here: stripped states lack keyboard focus, so the keystroke
goes nowhere (supersedes earlier Ctrl+J guidance; see DOM-INDEX §5).
Distinct testid set (no chat-switcher testids, dock-rail based):
hatch-dock-rail, hatch-dock-rail-divider, hatch-nav-chat,
hatch-dock-chat-notification-badge, hatch-nav-search,
hatch-nav-system-item-*, app-download-dock-mark, hatch-dock-more,
hatch-side-chats-open-drag-handle, hatch-side-chats-panel-shell,
hatch-chat-nav-fade, hatch-chat-leading-controls,
hatch-status-panel-sliding-surface, hatch-composer-placeholder-overlay,
hatch-browser-task-banner-host
1.4 Sidechat Thread (/thread/<uuid>)
A real sidechat with a UUID. DOM structure is similar to main chat (switcher present, panel available). Thread rows in the panel show the sidechat title.
Note: After sending the first message in a /thread/new chat, the URL transitions to /thread/<uuid>. Poll window.location.href after send to capture the real thread ID.
1.5 Landing Page (Not Logged In)
| Property | Value |
|---|---|
| Title | muse.ai (just the domain) |
| Indicators | Body contains "Log in" button, no chat UI |
Detection:
const isLanding = document.title === 'muse.ai';
// or
const isLoggedOut = document.body.innerText.includes('Log in') &&
!document.querySelector('[data-testid="hatch-chat-switcher-trigger"]');
1.6 State Summary Table
| State | URL | Title | Switcher | Compose | Panel Content |
|---|---|---|---|---|---|
| Main chat | muse.ai/ |
Chat — ... |
✅ | ❌ | ❌ |
| Panel open | muse.ai/ |
Chat — ... |
✅ | ✅ | ✅ |
/thread/new |
muse.ai/thread/new |
Muse |
❌ | ❌ | ❌ |
/thread/<uuid> |
muse.ai/thread/<uuid> |
Chat — ... |
✅ | ❌ | ❌ |
| Landing | muse.ai/ |
muse.ai |
❌ | ❌ | ❌ |
2. Key Selectors
2.1 Chat Switcher (Panel Toggle)
document.querySelector('[data-testid="hatch-chat-switcher-trigger"]')
| Attribute | Value |
|---|---|
| Tag | BUTTON |
data-testid |
hatch-chat-switcher-trigger |
| Inner text | The current thread's title (e.g. 646 pip cross-operator coordination chat\nOpen chat and side chats) — NOT the literal Chats. Never match the switcher by text. |
aria-expanded |
Not used (always null) |
| Class (partial) | bg-fill-blur-thick shadow-blur-elevation-01 text-body-medium |
Parent chain (3-4 levels):
BUTTON[data-testid="hatch-chat-switcher-trigger"]
└── DIV[data-testid="hatch-chat-leading-controls"] (cls: pointer-events-auto absolute start-3)
└── DIV (cls: pointer-events-none absolute inset-x-0)
└── DIV (cls: absolute inset-0 overflow-hidden)
└── DIV (cls: relative flex min-h-0)
Visibility: Present on main chat and /thread/<uuid>. Absent on /thread/new and landing page.
Note: The old detection document.body.innerText.includes('Side chats') is invalid — it matches the switcher's "Open chat and side chats" text and DM content mentioning "Side chats". Use the exact header check or hatch-side-chats-panel-content presence instead.
2.2 Compose Button (New Side Chat +)
document.querySelector('[data-testid="hatch-chat-compose"]')
| Attribute | Value |
|---|---|
| Tag | BUTTON |
data-testid |
hatch-chat-compose |
aria-label |
New side chat |
title |
New side chat |
| Inner text | (empty — SVG icon only) |
| Class (partial) | inline-flex shrink-0 cursor-pointer |
Parent chain:
BUTTON[data-testid="hatch-chat-compose"]
└── SPAN (cls: contents)
└── DIV (cls: group/nav-section flex w-full)
└── SECTION (cls: flex flex-col)
└── DIV (cls: flex flex-col relative)
Visibility conditions:
- ✅ Present and visible only when panel is open
- ❌ Absent when panel is closed (even on main chat)
- ❌ Absent on
/thread/new(stripped DOM) - ❌ Absent on landing page
Critical: This button contains only an SVG icon. Do NOT search by text "+" — it will never match.
2.3 Sidechat Panel Shell
document.querySelector('[data-testid="hatch-side-chats-panel-shell"]')
Container for the entire chat navigation panel. Present in most states but may be empty/collapsed.
Children when panel is open:
hatch-side-chats-panel-sliding-surface
hatch-side-chats-panel-content
hatch-chat-search-toolbar
hatch-chat-search-field
hatch-side-chats-options
hatch-thread-row (× N)
hatch-chat-compose
hatch-side-chat-unread-indicator
2.4 Sidechat Panel Content
document.querySelector('[data-testid="hatch-side-chats-panel-content"]')
This is the reliable "panel is open" indicator. When present, the compose button should also be present.
2.5 Thread Rows (Chat List Items)
document.querySelectorAll('[data-testid="hatch-thread-row"]')
| Attribute | Value |
|---|---|
| Tag | DIV |
data-testid |
hatch-thread-row |
role |
button |
tabindex |
0 |
| Active row | aria-current="page" |
| Class (partial) | group/nav-row relative flex h-9 w-full shrink-0 items-center |
Not links — they are div[role="button"] with click handlers. No href. To navigate, click the row.
Text format: <title>\n<unread-indicator>\n<timestamp> (e.g., "Main chat\n\njust now")
Exact header check (reliable "Side chats" panel header detection):
// Find leaf element with exact text "Side chats"
const header = [...document.querySelectorAll('*')].find(e =>
e.children.length === 0 && (e.innerText || '').trim() === 'Side chats'
);
// Returns SPAN element when panel is open, null otherwise
2.6 Message Input (Composer)
document.querySelector('[aria-label="Message"]')
| Attribute | Value |
|---|---|
| Tag | TEXTAREA |
aria-label |
Message |
| Class (partial) | text-text-primary block max-h-48 |
Parent chain:
TEXTAREA[aria-label="Message"]
└── DIV (cls: relative flex min-h-8)
└── DIV (cls: flex shrink-0 flex-col)
└── DIV (cls: backdrop-blur-elevation-01 bg-fill-blur-thick shadow-blur-elevation-01)
2.7 "Main Chat" Navigation
Primary (verified 2026-10-04 on muse/pip/opm): the button with
aria-label="Back to main chat" — present on /thread/<uuid> pages and on
/thread/new, even with the panel closed:
const backBtn = [...document.querySelectorAll('button')]
.find(b => b.getAttribute('aria-label') === 'Back to main chat');
backBtn?.click();
Panel-open alternative: when the sidechat panel is open, find the element with
exact text "Main chat":
const mainBtn = [...document.querySelectorAll('button, [role="button"]')]
.find(e => (e.innerText || '').trim() === 'Main chat');
Must use exact match (=== 'Main chat'), not substring — thread rows may contain
"Main chat" as part of longer text. Note: with the panel closed on a thread
page there is no exact-text "Main chat" element at all — use the aria-label
button above.
Do not use Ctrl+J for this (superseded 2026-10-04): it needs keyboard focus,
which stripped/partial states lack. Prefer DOM clicks (commit 72574ba).
3. Approval / Permission Dialogs
3.1 How check_approvals Works
Location: /home/super/Projects/NetVM/bin/muse-chat-api.py, check_approvals(ws)
Detection logic:
- Scans
document.body.innerTextfor"Allow"+"to share"pattern - Finds elements containing both strings with text length < 500 chars
- Fallback: looks for buttons with text containing
allow/deny/block(if ≥2 found and no dialogs yet)
Critical filter (added 2026-10-04 to fix false positives):
# Only block for IP-based permission dialogs.
# Dialogs without IPs are likely false positives (chat content, UI text).
ips = re.findall(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', dialog_text)
if not ips:
continue # Skip — don't block
Trusted IPs (auto-approve "Allow"):
TRUSTED_IPS = {
"34.139.37.135", # VM (gateway)
"100.123.153.75", # bl (main compute)
"100.81.31.9", # VM tailnet
}
Auto-approve action: Clicks button with text "allow once" or exact "allow".
Exit code 2 with APPROVAL_NEEDED if untrusted IP dialog is found.
3.2 Dialog Selectors
No [role="dialog"] or [role="alertdialog"] elements were observed in the current DOM state. Permission dialogs appear as overlay elements when triggered (e.g., by download attempts, screen sharing requests).
Patterns to watch for:
// Dialog containers (when present)
'[role="dialog"]'
'[role="alertdialog"]'
'[data-testid*="dialog"]'
'[data-testid*="approval"]'
'[data-testid*="permission"]'
// Permission buttons (when dialog is active)
'button' // with text: /allow|deny|block/i
Known false positive: Task/checklist buttons in chat content may contain "Confirm" in their text (e.g., "Confirm 03:45 heartbeat in main thread"). The IP filter prevents these from blocking automation.
3.3 React DOM Selectors
Per AGENTS.md (2026-10-03): "The Muse platform is a React app. DOM selectors for dialogs exist identically in headful and headless environments." The check_approvals selectors work in both.
4. Overlays and Interaction Blockers
4.1 Observed Overlays
One fixed-position overlay with z-index: 26 was observed:
{
tag: "DIV",
id: null,
testid: null,
role: null,
z: "26",
cls: "absolute start-0 top-0"
}
4.2 Potential Blockers
| Element | Impact |
|---|---|
| Permission dialogs | Block sends until approved/dismissed |
hatch-browser-task-banner-host |
Browser task notification banner |
hatch-composer-placeholder-overlay |
Composer placeholder (not a blocker) |
hatch-status-panel-sliding-surface |
Status panel (may overlay content) |
Detection:
const blockers = [...document.querySelectorAll('*')].filter(e => {
const cs = getComputedStyle(e);
const r = e.getBoundingClientRect();
return (cs.position === 'fixed' || cs.position === 'absolute') &&
r.width > 100 && r.height > 100 &&
parseInt(cs.zIndex || '0') > 10;
});
5. URL Patterns
| URL Pattern | Meaning | DOM State |
|---|---|---|
https://muse.ai/ |
Main chat | Full DOM, switcher present |
https://muse.ai/thread/new |
New/unsent sidechat | Stripped DOM, no switcher/compose |
https://muse.ai/thread/<uuid> |
Existing sidechat | Full DOM, switcher present |
UUID format: Standard UUID v4 (e.g., 550e8400-e29b-41d4-a716-446655440000).
Navigation notes:
- After
sidechat create, URL goes to/thread/new(placeholder) - After first message send, URL transitions to
/thread/<uuid>(real ID) - Poll
window.location.hrefafter send to capture the real thread ID /thread/newpoisons subsequent creates — always navigate to main first
6. Full data-testid Inventory
57 unique data-testid values observed on main chat with panel open:
Chat Core
hatch-chat-switcher-trigger # Panel toggle button
hatch-chat-switcher-icon-knockout
hatch-chat-switcher-navigation-icon
hatch-chat-compose # New side chat (+) button
hatch-chat-leading-controls # Switcher container
hatch-chat-nav-fade
hatch-chat-search-field # Panel search input
hatch-chat-search-toolbar
Sidechat Panel
hatch-side-chats-panel-shell
hatch-side-chats-panel-sliding-surface
hatch-side-chats-panel-content
hatch-side-chats-options
hatch-side-chat-unread-indicator
hatch-side-chats-open-drag-handle # present when panel CLOSED (verified 2026-10-04)
hatch-side-chats-open-drag-handle-divider-line # NEW 2026-10-04: divider for the above; absent when panel open
hatch-thread-row # Individual chat rows (× N)
active-side-chat-load-more-sentinel
Navigation Dock
hatch-dock-rail # DIV; children: dock-rail-divider (SPAN),
# nav-chat, dock-chat-notification-badge,
# nav-search, 4× nav-system-item-<hex>,
# app-download-dock-mark, dock-more
hatch-dock-rail-divider # SPAN (not DIV)
hatch-dock-more
hatch-nav-chat # A, href="/", aria-label dynamic:
# "Chat" | "Chat, N notification(s)" —
# match testid, NEVER exact aria-label.
# aria-current="page" when Chat section active.
# Verified 2026-10-04 on muse/pip/opm.
hatch-nav-search
hatch-nav-system-item-66656564 # (hex-encoded IDs)
hatch-nav-system-item-676f616c73
hatch-nav-system-item-6964656173
hatch-nav-system-item-6c696272617279
hatch-panel-resize-divider-line
Status/Other
hatch-status-panel-close
hatch-status-panel-close-drag-handle
hatch-status-panel-close-drag-handle-divider-line
hatch-status-panel-close-toolbar
hatch-status-panel-sliding-surface
hatch-composer-placeholder-overlay
hatch-browser-task-banner-host
hatch-inline-approval-card # NEW 2026-10-04 (pip): DIV, no role —
# browser-task approval card ("A task needs review" + Review button),
# no IP in text. Structure TBD — see DOM-APPROVALS-SPEC.md (inspector 3/5).
hatch-invite-friends-button
app-download-dock-mark
7. Reliable Automation Sequences
7.1 Sidechat Creation (Robust)
# 1. Recover to a chat-active state (click-based; never Ctrl+J — see §2.7):
# priority: compose (already ready) -> switcher -> hatch-nav-chat
# (muse-chat-api.py cmd_sidechat_main clicks the exact-text "Main chat"
# element; the aria-label "Back to main chat" button also works)
# Poll up to ~20s for the compose button.
# NOTE: plain JS .click() on the switcher does NOT reliably toggle the panel
# (Radix control — trusted CDP Input.dispatchMouseEvent needed, verified
# 2026-10-04). Prefer the compose-button presence check over blind toggling.
# 2. Verify we're on main chat
assert window.location.href == 'https://muse.ai/'
assert document.querySelector('[data-testid="hatch-chat-switcher-trigger"]')
# 3. Open panel if compose button not present
if not document.querySelector('[data-testid="hatch-chat-compose"]'):
document.querySelector('[data-testid="hatch-chat-switcher-trigger"]').click()
time.sleep(2)
# 4. Verify compose button is present AND visible
btn = document.querySelector('[data-testid="hatch-chat-compose"]')
assert btn and btn.getBoundingClientRect().width > 0
# 5. Click to create
btn.click()
# 6. Poll for /thread/ URL (up to 15s)
# 7. Send message directly (browser is on the new sidechat)
# 8. Poll URL for /thread/<uuid> to capture real thread ID
7.2 State Detection (One-Liner)
(() => {
const url = window.location.href;
const title = document.title;
const hasSwitcher = !!document.querySelector('[data-testid="hatch-chat-switcher-trigger"]');
const hasCompose = !!document.querySelector('[data-testid="hatch-chat-compose"]');
const hasPanelContent = !!document.querySelector('[data-testid="hatch-side-chats-panel-content"]');
if (title === 'muse.ai') return 'LANDING';
if (url.includes('/thread/new')) return 'THREAD_NEW';
if (url.includes('/thread/')) return hasSwitcher ? 'SIDECHAT' : 'THREAD_NEW_STRIPPED';
if (url === 'https://muse.ai/') {
if (!hasSwitcher) return 'UNKNOWN_NO_SWITCHER';
if (hasCompose && hasPanelContent) return 'MAIN_PANEL_OPEN';
return 'MAIN_PANEL_CLOSED';
}
return 'UNKNOWN';
})()
8. Body Landmarks (Top-Level Structure)
<body>
<div> <!-- React root (has_react: true) -->
<!-- App content -->
</div>
<iframe class="hidden"> × 4 <!-- Hidden utility iframes -->
<script id="hatch-early-gateway-bootstrap">
<div class="min-w-0 flex group/sidebar-wrapper min-h-svh"> <!-- Sidebar wrapper -->
<section aria-label="Notifications alt+T">
<script> × N <!-- App scripts -->
</body>
Appendix: Change Log
- 2026-10-04: Initial mapping via live CDP inspection. Documented 5 page states, 57 testids, approval dialog logic, and the
/thread/newpoisoning behavior. - 2026-10-04 (inspector 5/5 re-verification, muse/pip/opm via CDP):
hatch-nav-chatverified:A,href="/", dynamicaria-label(Chat/Chat, N notification(s)),aria-current="page".- Switcher inner text is the current thread title, not
Chats— corrected §2.1 (never match switcher by text). button[aria-label="Back to main chat"]is present on/thread/<uuid>pages too, not only/thread/new— §2.7 rewritten around it as the primary main-chat affordance; Ctrl+J guidance removed as superseded.- Plain JS
.click()on the switcher does not reliably toggle the panel (Radix control needs trusted CDP pointer events) — noted in §7.1. - New testids:
hatch-side-chats-open-drag-handle-divider-line(all nodes),hatch-inline-approval-card(pip; browser-task approval card, no IP — structure deferred to DOM-APPROVALS-SPEC.md). - Title gains an unread prefix:
(1) Chat — <agent>(observed on pip). - Settled-state variance: shimmer spans 1 (not 2),
role="status"3–4 (not 4–7), buttons 46–57 — ranges, not fixed counts. - Fleet browsers are shared: another agent drove pip's browser mid-probe (URL/panel state changed between probes). Never assume a probe snapshot is still current — re-probe before acting.