Files

465 lines
22 KiB
Markdown
Raw Permalink Normal View History

# 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.
```html
<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:
```html
<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.
```html
<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"]`
```html
<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:
```html
<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
```javascript
// 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)
```javascript
// 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)
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.