22 KiB
muse.ai Chat Panel & Sidechat DOM 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.
Originally captured live via CDP on
opmbrowser (warp-opm netns, CDP 9440). Re-verified 2026-10-04 via CDPRuntime.evaluateon three nodes:opm(warp-opm, CDP 9440),muse(warp-muse, CDP 9410),pip(warp-pip, CDP 9420). Page:https://muse.ai/. Sections marked ✅ verified 2026-10-04, 🔧 corrected, 🆕 new since original capture. This is a development reference for reliable automation of the chat navigation panel and sidechat creation. Prefer these selectors over text matching.
1. Chat Panel Switcher ✅ (with corrections)
Selector: [data-testid="hatch-chat-switcher-trigger"]
The button that toggles the chat navigation panel open/closed.
<button type="button"
data-testid="hatch-chat-switcher-trigger"
data-slot="popover-anchor"
data-pel-click="chat_threads_open_click"
class="bg-fill-blur-thick shadow-blur-elevation-01 ... rounded-full ...">
<span class="relative flex shrink-0 items-center">
<span data-testid="hatch-chat-switcher-icon-knockout" class="flex size-5 shrink-0 items-center justify-center">
<span data-testid="hatch-chat-switcher-navigation-icon" aria-hidden="true" style="width: 20px; height: 20px;">
<!-- hamburger SVG: three horizontal <path> elements -->
</span>
</span>
<!-- 🆕 unread dot appears here when unread chats exist (see §1b) -->
</span>
<span class="flex min-w-0 flex-col" title="Chats">
<span class="block min-w-0 truncate">Chats</span> <!-- 🔧 actually the ACTIVE CHAT TITLE, see below -->
</span>
<span class="sr-only">Open chat and side chats</span>
</button>
Key facts (✅ verified 2026-10-04 on opm/muse/pip):
| Property | Value |
|---|---|
| Tag | BUTTON |
In DOM on https://muse.ai/ or /thread/<uuid> |
✅ Yes (both panel states) |
In DOM on /thread/new stripped state |
🔧 NO — completely absent (see §8) |
| Visible | Yes (offsetParent !== null) |
| Position | Top-left: x:84, y:12, w:82, h:36 (exact on opm) |
aria-expanded |
Not set (null) — do NOT use for state detection |
data-pel-click |
chat_threads_open_click |
data-slot |
popover-anchor |
| Visible text | 🔧 Active chat title, NOT static "Chats". Shows e.g. Heartbeat check for job scheduler, 646 pip cross-operator coordination chat; reads Chats only when main chat is active |
| Screen-reader text | Open chat and side chats (+ Unread chats when unread exist) |
| Icon | Hamburger menu (3 horizontal <path> lines), not a chat bubble |
Parent chain (bottom-up, 4 levels): ✅ unchanged
1. DIV [data-testid="hatch-chat-leading-controls"]
class="pointer-events-auto absolute start-3 top-0 flex h-[60px] items-center gap-2"
2. DIV (no testid) class="pointer-events-none absolute inset-x-0 top-0 z-10 h-44"
3. DIV (no testid) class="absolute inset-0 overflow-hidden"
4. DIV (no testid) class="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
Behavior (✅ verified atomically 2026-10-04): The switcher toggles in place —
no navigation. Click when closed → panel opens (shell innerHTML 0 → 60667,
rows 0 → 26 on opm). Click when open → panel closes. Always check panel state
before clicking — never blind-click. (An earlier non-atomic probe appeared to
navigate; that was a race with the live heartbeat job driving the same browser —
see §9.)
Child testids (for reference):
[data-testid="hatch-chat-switcher-icon-knockout"]— icon wrapper span[data-testid="hatch-chat-switcher-navigation-icon"]— the hamburger SVG span- 🆕
[data-testid="hatch-chat-switcher-unread-indicator"]— unread blue dot (see §1b)
1b. Unread Indicator 🆕 (verified 2026-10-04 on muse)
When unread chats exist, the switcher gains:
<span data-testid="hatch-chat-switcher-unread-indicator"
class="bg-fill-blue absolute -end-0.5 size-2 rounded-full"
aria-hidden="true"></span>
<span class="sr-only">Unread chats</span>
| Property | Value |
|---|---|
| Selector | [data-testid="hatch-chat-switcher-unread-indicator"] |
| Visual | Blue dot (bg-fill-blue, 8px circle) overlaid on the switcher icon |
aria-hidden |
true — screen readers get the sr-only Unread chats text instead |
| Programmatic signal | Presence = unread chats exist. Most reliable unread check |
| Companion signal | document.title gains an (N) prefix, e.g. (1) Chat — operator-pip |
2. Sidechat Create Button ("New side chat") ✅
Selector: [data-testid="hatch-chat-compose"]
The + button that creates a new sidechat. Only exists in the DOM when the panel
is OPEN. This makes it the most reliable "panel is open and ready" signal.
<button aria-label="New side chat"
title="New side chat"
data-slot="button"
data-testid="hatch-chat-compose"
class="inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full ... size-6 ... text-text-secondary hover:enabled:bg-fill-secondary me-1">
<span class="inline-flex shrink-0 items-center justify-center m-0.5" style="width: 20px; height: 20px;">
<svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15">
<path d="M12 3.13C12.48 3.13 12.88 3.52 12.88 4V11.12H20C20.48 11.12 20.87 11.51 20.88 12C20.88 12.48 20.48 12.87 20 12.87H12.88V20C12.88 20.48 12.48 20.88 12 20.88C11.52 20.87 11.13 20.48 11.13 20V12.87H4C3.52 12.87 3.13 12.48 3.13 12C3.13 11.51 3.52 11.12 4 11.12H11.13V4C11.13 3.52 11.52 3.13 12 3.13Z"></path>
</svg>
</span>
</button>
Key facts (✅ verified):
| Property | Value |
|---|---|
| Tag | BUTTON |
| In DOM when panel closed | No — completely absent |
| In DOM when panel open | ✅ Yes (visible: offsetParent !== null) |
| Text content | None (icon-only) |
aria-label / title |
New side chat |
| Icon | Plus sign (+), single SVG path |
Parent chain (bottom-up, 8 levels): ✅ unchanged — SPAN.contents > DIV.group/nav-section
(header row: H3 "Side chats" + compose) inside the SECTION "Side chats".
What clicking does (✅ verified 2026-10-04): Creates a new sidechat draft.
Browser navigates https://muse.ai/ → https://muse.ai/thread/new
(placeholder URL; the real thread UUID appears after the first message is sent
or navigation settles). No sidechat exists server-side until a message is sent
— navigating to /thread/new and back is harmless.
3. Chat Navigation Panel Structure ✅
3a. Panel shell (always in DOM — except §8)
Selector: [data-testid="hatch-side-chats-panel-shell"]
<div data-testid="hatch-side-chats-panel-shell"
class="relative h-full overflow-hidden"
style="width: 240px;">
<!-- EMPTY when panel closed; full nav tree when open -->
</div>
| State | innerHTML |
style.width / computed |
|---|---|---|
| Closed | ✅ "" (empty string) |
✅ 0px |
| Open | ✅ Full <nav> tree (60KB+ on opm, 26 rows) |
✅ 240px |
3b. Open panel tree ✅ (verified — 26 rows on opm)
Structure unchanged from original capture. NAV[aria-label="Side chats"] →
DIV[data-testid="hatch-side-chats-panel-content"] → search toolbar (60px) +
scrollable thread list → DIV[data-testid="hatch-thread-row"][role="button"]
(Main chat first) → SECTION (Side chats: collapsible H3 header + compose +
chat list) → DIV[data-testid="active-side-chat-load-more-sentinel"].
3c. Related panel testids ✅ (extended)
| Testid | Element | Purpose | Status |
|---|---|---|---|
hatch-side-chats-panel-shell |
DIV |
Outer shell; always in DOM (except §8); empty when closed | ✅ |
hatch-side-chats-panel-sliding-surface |
DIV |
Sliding animation surface (open state only) | ✅ |
hatch-side-chats-panel-content |
DIV |
Panel content wrapper | ✅ |
hatch-chat-search-toolbar |
DIV |
Top toolbar (search + options) | ✅ |
hatch-chat-search-field |
DIV |
Search input container (input[aria-label="Search"][placeholder="Search"]) |
✅ |
hatch-side-chats-options |
BUTTON |
aria-label="Side chat options" three-dots dropdown |
✅ |
hatch-thread-row |
DIV |
Each chat row (Main chat + every sidechat) | ✅ |
hatch-chat-compose |
BUTTON |
"New side chat" + button |
✅ |
active-side-chat-load-more-sentinel |
DIV |
Infinite-scroll sentinel at list end | ✅ |
hatch-side-chats-open-drag-handle |
DIV |
Resize drag handle (separate from shell) | ✅ |
hatch-side-chats-open-drag-handle-divider-line |
DIV |
Drag handle divider | 🆕 |
hatch-chat-nav-fade |
DIV |
Fade effect element | ✅ |
hatch-chat-switcher-unread-indicator |
SPAN |
Unread blue dot on switcher | 🆕 (see §1b) |
3d. Thread row structure (each chat / sidechat) ✅ (with corrections)
Every row — Main chat and each sidechat — uses the same base structure:
<div data-testid="hatch-thread-row"
role="button"
tabindex="0"
draggable="false"
data-state="closed" <!-- 🔧 sidechat rows ONLY; absent on Main chat row -->
data-slot="context-menu-trigger" <!-- 🔧 sidechat rows ONLY; absent on Main chat row -->
data-pel-click="chat_thread_options_open"
aria-current="page" <!-- ONLY on the currently active chat -->
class="group/nav-row relative flex h-9 w-full shrink-0 items-center gap-0.5 rounded-12 px-2 text-start cursor-pointer ...">
<div class="min-w-0 flex-1 px-1">
<p class="text-text-primary flex items-center gap-1 truncate text-subheadline">
<span class="min-w-0 flex-1 truncate" title="Check the board">Check the board</span>
</p>
</div>
<span class="text-caption-1 text-text-secondary shrink-0 whitespace-nowrap ...">just now</span>
<div class="ms-[3px] shrink-0 items-center"> <!-- 🔧 sidechat rows ONLY -->
<button aria-label="More thread actions">…</button> <!-- 🆕 hover-only -->
</div>
</div>
Key facts (✅ verified 2026-10-04):
| Property | Value |
|---|---|
| Selector (all rows) | [data-testid="hatch-thread-row"] |
| Tag | DIV (not an anchor) |
role |
button |
| Navigation | React onClick — zero <a href="/thread/..."> links exist in the panel (verified: a[href*="/thread/"] count = 0) |
| Thread UUIDs in panel DOM | ✅ None. Regex scan of panel innerHTML for UUIDs: 0 hits. Zero data-value attributes |
| Active chat marker | aria-current="page" on the active row only |
| Title | Inner span[title] — the chat's display name |
| Timestamp | Trailing span: just now, 1m, 4m, 11m, etc. |
| Main chat row | First hatch-thread-row, text Main chat |
🔧 Main chat row vs sidechat rows — they are NOT identical:
| Attribute | Main chat row | Sidechat rows |
|---|---|---|
data-state |
absent | closed |
data-slot |
absent | context-menu-trigger |
Hover action container (DIV.ms-[3px]) |
absent | present |
| Hover button | none | button[aria-label="More thread actions"] (🆕 named) |
aria-current="page" |
when main is active | when that sidechat is active |
Distinguishing Main chat from sidechats: Same data-testid. Match on text
content (Main chat vs sidechat title) or check aria-current="page".
There is no thread identity in the DOM — title text (or position +
aria-current) is the only handle. (Search results are the exception:
data-value="chat:thread:<uuid>" — see DOM-SEARCH.md.)
3e. "Side chats" section header ✅
Unchanged: H3 > BUTTON toggles section collapse (aria-expanded="true"/"false";
verified true on opm). The compose + button sits beside it.
4. Panel Open vs Closed — Detection Guide ✅
Reliable signals (✅ all re-verified)
| Signal | Panel OPEN | Panel CLOSED |
|---|---|---|
[data-testid="hatch-chat-compose"] exists |
✅ Yes | ❌ No |
[data-testid="hatch-thread-row"] count |
≥ 1 | 0 |
Panel shell innerHTML |
Full nav tree | "" (empty) |
| Panel shell computed width | 240px |
0px |
Recommended checks (in order of reliability) ✅ unchanged
// BEST: panel is open and ready for sidechat creation
const panelOpen = !!document.querySelector('[data-testid="hatch-chat-compose"]');
// ALTERNATIVE: panel shell has content
const shell = document.querySelector('[data-testid="hatch-side-chats-panel-shell"]');
const panelOpenAlt = shell && shell.innerHTML !== "";
// Row count (useful for "how many chats")
const rowCount = document.querySelectorAll('[data-testid="hatch-thread-row"]').length;
Signals that DO NOT work ✅ unchanged
| Signal | Why it fails |
|---|---|
document.body.innerText.includes('Side chats') |
False positive. Matches the switcher's sr-only text and message content |
document.body.innerText.includes('Main chat') |
Same problem — matches message content |
Switcher aria-expanded |
Never set (null in both states) |
a[href*="/thread/"] links |
Zero exist. Panel rows are DIV[role=button], not anchors |
| Switcher visible text | 🔧 Dynamic — shows the active chat title, not panel state |
Safe toggle sequence ✅ (behavior verified both directions 2026-10-04)
// 1. Detect actual panel state (not body text)
const isOpen = !!document.querySelector('[data-testid="hatch-chat-compose"]');
// 2. Click switcher ONLY if closed
if (!isOpen) {
document.querySelector('[data-testid="hatch-chat-switcher-trigger"]').click();
}
// 3. Wait, then re-verify
await new Promise(r => setTimeout(r, 2000));
const nowOpen = !!document.querySelector('[data-testid="hatch-chat-compose"]');
if (!nowOpen) throw new Error("Panel did not open");
Do the detect → click → verify atomically (single evaluate + awaitPromise)
— the fleet browsers are live and timer jobs may navigate between separate
round-trips (see §9).
5. Full data-testid Inventory (chat-related) ✅ (extended 2026-10-04)
Captured on https://muse.ai/ main chat view. visible = computed style check.
🆕 = found 2026-10-04, not in original capture.
| testid | Tag | aria-label | Notes |
|---|---|---|---|
hatch-dock-rail |
DIV |
— | Left dock rail |
hatch-dock-rail-divider |
DIV |
— | Rail divider |
hatch-nav-chat |
A |
Chat |
Dock nav: Chat (href="/") |
hatch-nav-search |
BUTTON |
Search |
Dock nav: Search |
hatch-nav-system-item-66656564 |
DIV |
Feed |
🆕 suffix = hex("feed") |
hatch-nav-system-item-676f616c73 |
DIV |
Goals |
🆕 suffix = hex("goals") |
hatch-nav-system-item-6964656173 |
DIV |
Ideas |
🆕 suffix = hex("ideas") |
hatch-nav-system-item-6c696272617279 |
DIV |
Library |
🆕 suffix = hex("library") |
hatch-dock-more |
BUTTON |
Settings |
Dock settings |
app-download-dock-mark |
DIV |
— | 🆕 App download badge in dock |
hatch-chat-leading-controls |
DIV |
— | Top-left container for switcher |
hatch-chat-switcher-trigger |
BUTTON |
— | Panel toggle (label = active chat title) |
hatch-chat-switcher-icon-knockout |
SPAN |
— | Icon wrapper |
hatch-chat-switcher-navigation-icon |
SPAN |
— | Hamburger SVG |
hatch-chat-switcher-unread-indicator |
SPAN |
— | 🆕 Unread blue dot (see §1b) |
hatch-side-chats-open-drag-handle |
DIV |
— | Panel resize handle |
hatch-side-chats-open-drag-handle-divider-line |
DIV |
— | 🆕 Handle divider |
hatch-side-chats-panel-shell |
DIV |
— | Panel shell (always in DOM, §8 excepted) |
hatch-side-chats-panel-sliding-surface |
DIV |
— | Open-state sliding surface |
hatch-side-chats-panel-content |
DIV |
— | Panel content wrapper |
hatch-chat-search-toolbar |
DIV |
— | Search toolbar (60px) |
hatch-chat-search-field |
DIV |
— | Search input container |
hatch-side-chats-options |
BUTTON |
Side chat options |
Three-dots dropdown |
hatch-chat-nav-fade |
DIV |
— | Nav fade effect |
hatch-thread-row |
DIV |
— | Each chat row (Main + sidechats) |
hatch-chat-compose |
BUTTON |
New side chat |
Create sidechat + (open only) |
active-side-chat-load-more-sentinel |
DIV |
— | Infinite scroll sentinel |
hatch-invite-friends-button |
BUTTON |
Invite |
Invite button |
hatch-composer-placeholder-overlay |
SPAN |
— | Message input placeholder |
hatch-composer-stop-button |
BUTTON |
— | 🆕 Stop-generation button (in composer) |
hatch-status-panel-sliding-surface |
DIV |
— | Agent status panel |
hatch-status-panel-close |
DIV |
— | 🆕 Status panel (closed state) |
hatch-status-panel-close-toolbar |
DIV |
— | 🆕 |
hatch-status-panel-close-drag-handle |
DIV |
— | 🆕 |
hatch-status-panel-close-drag-handle-divider-line |
DIV |
— | 🆕 |
hatch-browser-task-banner-host |
DIV |
— | Task banner (hidden) |
🆕 hatch-nav-system-item-* suffixes are hex-encoded names (verified):
66656564→feed, 676f616c73→goals, 6964656173→ideas,
6c696272617279→library — each matches the element's aria-label.
Do not hardcode the full testid; match on the decoded suffix or aria-label.
6. Automation Notes ✅ (extended)
-
Ctrl+J is unreliable as a generic panel toggle. Prefer the detect → click-switcher → verify sequence in §4.
-
The
+button has no text. Never search for"+"text — use[data-testid="hatch-chat-compose"]oraria-label="New side chat". -
After creating a sidechat, the browser navigates to
https://muse.ai/thread/new. This stripped state has no switcher at all (§8) — click[data-testid="hatch-nav-chat"](or navigate tohttps://muse.ai/) before the next panel operation. Navigating to/thread/newand back creates nothing server-side until a message is sent. -
Sidechat rows are not links and carry no identity. To open a specific sidechat, click its
DIV[data-testid="hatch-thread-row"](match by innerspan[title]text). There is no URL to navigate to directly until the thread UUID is known — capture it fromlocation.hrefafter the first message (poll/thread/new→/thread/<uuid>). -
Thread UUID capture: After sending the first message in a new sidechat,
window.location.hreftransitions from/thread/newto/thread/<uuid>. Poll for this to capture the durable thread URL. -
Panel width is fixed at 240px — useful for visual grounding / screenshots.
-
The "Side chats" H3 is collapsible (
aria-expanded). If rows are missing while the panel is open, check whether the section is collapsed. -
🆕 Switcher label is the active chat title — never match on it for state. Use compose presence (§4).
-
🆕 Unread detection:
[data-testid="hatch-chat-switcher-unread-indicator"]presence, or/^\(\d+\)/prefix ondocument.title.
7. Panel innerText Format (reference) ✅
Open panel shell innerText (opm, 26 rows), for text-based fallbacks:
Main chat\n\n2m\nSide chats\n\nCheck the board\n\n1m\n\nAlign chat and board API\n\n4m\n\n…
Pattern: <title>\n\n<relative-ts>\n\n…. Timestamps are relative
(just now, 1m, 4m, 11m). The H3 contributes the literal Side chats
line — which is why body-text matching for panel state is unreliable (§4).
8. Stripped State: /thread/new 🆕 (verified 2026-10-04)
After clicking compose (before any message is sent), the browser is at
https://muse.ai/thread/new. The chat chrome is stripped:
| Element | State |
|---|---|
[data-testid="hatch-chat-switcher-trigger"] |
ABSENT — not just closed, gone from DOM |
[data-testid="hatch-side-chats-panel-shell"] |
Present, innerHTML empty, computed width 0px |
[data-testid="hatch-chat-compose"] |
Absent |
[data-testid="hatch-thread-row"] |
0 |
[data-testid="hatch-chat-leading-controls"] |
Present (empty container) |
[data-testid="hatch-nav-chat"] |
✅ Present — A, aria-label="Chat", href="/" — the recovery path |
Dock rail, hatch-chat-nav-fade, drag handle |
Present |
Recovery (verified): click [data-testid="hatch-nav-chat"] or set
location.href = 'https://muse.ai/'. This is the click-based activation from
the 72574ba fix — do NOT use Ctrl+J here (unreliable in stripped states).
Automation rule: any panel operation must first assert the switcher exists.
If !document.querySelector('[data-testid="hatch-chat-switcher-trigger"]'),
you are in the stripped state — recover via hatch-nav-chat before proceeding.
9. Per-Node Differences 🆕 (verified 2026-10-04)
| Node | CDP | Page title | Notes |
|---|---|---|---|
opm |
9440 | Chat — operator-main |
26 panel rows at capture; doc baseline |
muse |
9410 | Chat — muse |
Unread indicator present at capture; panel closed |
pip |
9420 | (1) Chat — operator-pip |
(1) = unread count in title; panel was OPEN while on a /thread/<uuid> page — panel open-state persists across thread navigation |
All panel selectors, toggle behavior, and detection signals are identical across nodes. Only titles, row contents, and unread state differ.
10. Probing Methodology Notes 🆕
- Drive the DOM via CDP
Runtime.evaluate(returnByValue: true). - The fleet browsers are live: timer jobs (e.g. heartbeat every 5 min)
navigate the same pages. Non-atomic probe sequences (click → separate
round-trip → read) can race with job navigation and produce misleading
results. Do toggle tests atomically: click + wait + snapshot in a single
evaluate with
awaitPromise: true. document.titleisMuseduring load and becomesChat — <account>(optionally(N)-prefixed) once settled — don't assert titles too early.- Sibling inspectors share
/tmpon bl: namespace probe files (e.g./tmp/dip1_*.js) — a generic/tmp/cdp_eval.pywas overwritten mid-task by another agent.