Files
box/docs/DOM-CHAT-PANEL.md

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, box CLI, and agents share the same API endpoints. No UI-only powers.

Originally captured live via CDP on opm browser (warp-opm netns, CDP 9440). Re-verified 2026-10-04 via CDP Runtime.evaluate on 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"].

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
// 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).


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)

  1. Ctrl+J is unreliable as a generic panel toggle. Prefer the detect → click-switcher → verify sequence in §4.

  2. The + button has no text. Never search for "+" text — use [data-testid="hatch-chat-compose"] or aria-label="New side chat".

  3. 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 to https://muse.ai/) before the next panel operation. Navigating to /thread/new and back creates nothing server-side until a message is sent.

  4. Sidechat rows are not links and carry no identity. To open a specific sidechat, click its DIV[data-testid="hatch-thread-row"] (match by inner span[title] text). There is no URL to navigate to directly until the thread UUID is known — capture it from location.href after the first message (poll /thread/new → /thread/<uuid>).

  5. Thread UUID capture: After sending the first message in a new sidechat, window.location.href transitions from /thread/new to /thread/<uuid>. Poll for this to capture the durable thread URL.

  6. Panel width is fixed at 240px — useful for visual grounding / screenshots.

  7. The "Side chats" H3 is collapsible (aria-expanded). If rows are missing while the panel is open, check whether the section is collapsed.

  8. 🆕 Switcher label is the active chat title — never match on it for state. Use compose presence (§4).

  9. 🆕 Unread detection: [data-testid="hatch-chat-switcher-unread-indicator"] presence, or /^\(\d+\)/ prefix on document.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.title is Muse during load and becomes Chat — <account> (optionally (N)-prefixed) once settled — don't assert titles too early.
  • Sibling inspectors share /tmp on bl: namespace probe files (e.g. /tmp/dip1_*.js) — a generic /tmp/cdp_eval.py was overwritten mid-task by another agent.