docs/DOM-CHAT-PANEL.md — re-verified 2026-10-04 on opm/muse/pip; unread indicator, stripped state, row differences

Session: sidechat/dom-panel
This commit is contained in:
dom-panel-inspector
2026-10-04 04:29:16 +00:00
parent e28b10edc8
commit 7d0ccba64c
+240 -145
View File
@@ -1,18 +1,20 @@
# muse.ai Chat Panel & Sidechat DOM Reference # muse.ai Chat Panel & Sidechat DOM Reference
> Captured live via CDP on `opm` browser (warp-opm netns, CDP 9440) on 2026-10-04. > Originally captured live via CDP on `opm` browser (warp-opm netns, CDP 9440).
> Page: `https://muse.ai/`, title "Chat — operator-main". > **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 > This is a development reference for reliable automation of the chat navigation panel
> and sidechat creation. Prefer these selectors over text matching. > and sidechat creation. Prefer these selectors over text matching.
--- ---
## 1. Chat Panel Switcher ## 1. Chat Panel Switcher ✅ (with corrections)
**Selector:** `[data-testid="hatch-chat-switcher-trigger"]` **Selector:** `[data-testid="hatch-chat-switcher-trigger"]`
The button that toggles the chat navigation panel open/closed. Always present in the DOM, The button that toggles the chat navigation panel open/closed.
top-left of the viewport.
```html ```html
<button type="button" <button type="button"
@@ -23,31 +25,35 @@ top-left of the viewport.
<span class="relative flex shrink-0 items-center"> <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-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;"> <span data-testid="hatch-chat-switcher-navigation-icon" aria-hidden="true" style="width: 20px; height: 20px;">
<!-- hamburger SVG: three horizontal lines --> <!-- hamburger SVG: three horizontal <path> elements -->
</span> </span>
</span> </span>
<!-- 🆕 unread dot appears here when unread chats exist (see §1b) -->
</span> </span>
<span class="flex min-w-0 flex-col" title="Chats"> <span class="flex min-w-0 flex-col" title="Chats">
<span class="block min-w-0 truncate">Chats</span> <span class="block min-w-0 truncate">Chats</span> <!-- 🔧 actually the ACTIVE CHAT TITLE, see below -->
</span> </span>
<span class="sr-only">Open chat and side chats</span> <span class="sr-only">Open chat and side chats</span>
</button> </button>
``` ```
**Key facts:** **Key facts (✅ verified 2026-10-04 on opm/muse/pip):**
| Property | Value | | Property | Value |
|---|---| |---|---|
| Tag | `BUTTON` | | Tag | `BUTTON` |
| Always in DOM | Yes (both panel states) | | 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`) | | Visible | Yes (`offsetParent !== null`) |
| Position | Top-left: `x:84, y:12, w:82, h:36` | | 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 | | `aria-expanded` | **Not set** (`null`) — do NOT use for state detection |
| Visible text | `Chats` | | `data-pel-click` | `chat_threads_open_click` |
| Screen-reader text | `Open chat and side chats` | | `data-slot` | `popover-anchor` |
| Icon | Hamburger menu (3 horizontal lines), not a chat bubble | | 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):** **Parent chain (bottom-up, 4 levels):** ✅ unchanged
``` ```
1. DIV [data-testid="hatch-chat-leading-controls"] 1. DIV [data-testid="hatch-chat-leading-controls"]
@@ -57,17 +63,42 @@ top-left of the viewport.
4. DIV (no testid) class="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden" 4. DIV (no testid) class="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
``` ```
**Behavior:** The switcher **toggles**. Clicking when the panel is closed opens it; **Behavior (✅ verified atomically 2026-10-04):** The switcher **toggles in place —
clicking when open closes it. Always check panel state before clicking — never no navigation**. Click when closed → panel opens (shell `innerHTML` 0 → 60667,
blind-click. 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): **Child testids** (for reference):
- `[data-testid="hatch-chat-switcher-icon-knockout"]` — icon wrapper span - `[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-navigation-icon"]` — the hamburger SVG span
- 🆕 `[data-testid="hatch-chat-switcher-unread-indicator"]` — unread blue dot (see §1b)
--- ---
## 2. Sidechat Create Button ("New side chat") ## 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"]` **Selector:** `[data-testid="hatch-chat-compose"]`
@@ -88,48 +119,31 @@ is OPEN.** This makes it the most reliable "panel is open and ready" signal.
</button> </button>
``` ```
**Key facts:** **Key facts (✅ verified):**
| Property | Value | | Property | Value |
|---|---| |---|---|
| Tag | `BUTTON` | | Tag | `BUTTON` |
| In DOM when panel closed | **No** — completely absent | | In DOM when panel closed | **No** — completely absent |
| In DOM when panel open | Yes | | In DOM when panel open | ✅ Yes (`visible: offsetParent !== null`) |
| Text content | None (icon-only) | | Text content | None (icon-only) |
| `aria-label` | `New side chat` | | `aria-label` / `title` | `New side chat` |
| `title` | `New side chat` |
| Icon | Plus sign (`+`), single SVG path | | Icon | Plus sign (`+`), single SVG path |
**Parent chain (bottom-up, 8 levels):** **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.
1. SPAN (no testid) class="contents" ← direct wrapper Browser navigates `https://muse.ai/` → `https://muse.ai/thread/new`
2. DIV (no testid) class="group/nav-section flex w-full items-center" (placeholder URL; the real thread UUID appears after the first message is sent
children: [H3 "Side chats", SPAN(contents) > BUTTON#compose] or navigation settles). **No sidechat exists server-side until a message is sent**
3. SECTION (no testid) class="flex flex-col" ← "Side chats" section — navigating to `/thread/new` and back is harmless.
children: [DIV(header: H3 + compose), DIV(chat list)]
4. DIV (no testid) class="flex flex-col relative grow-1"
children: [DIV[data-testid="hatch-thread-row"] (Main chat),
SECTION (Side chats),
DIV[data-testid="active-side-chat-load-more-sentinel"]]
5. DIV (no testid) class="flex flex-col overflow-hidden relative z-0 overflow-y-auto overscroll-y-contain min-h-0 min-w-0 flex-1 px-3 pb-3"
6. DIV (no testid) class="flex min-h-0 flex-1 flex-col"
7. DIV (no testid) class="text-text-primary flex h-full min-h-0 w-full flex-col overflow-hidden"
8. DIV (no testid) class="min-h-0 min-w-0 flex-1"
```
**Location in UI:** Inside the "Side chats" section header row, to the right of the
`H3` "Side chats" collapsible heading.
**What clicking does:** Creates a new sidechat. Browser navigates to
`https://muse.ai/thread/new` (placeholder URL; the real thread UUID appears after
the first message is sent or navigation settles).
--- ---
## 3. Chat Navigation Panel Structure ## 3. Chat Navigation Panel Structure ✅
### 3a. Panel shell (always in DOM) ### 3a. Panel shell (always in DOM — except §8)
**Selector:** `[data-testid="hatch-side-chats-panel-shell"]` **Selector:** `[data-testid="hatch-side-chats-panel-shell"]`
@@ -141,68 +155,48 @@ the first message is sent or navigation settles).
</div> </div>
``` ```
| State | `innerHTML` | `innerText` | | State | `innerHTML` | `style.width` / computed |
|---|---|---| |---|---|---|
| Closed | `""` (empty string) | `""` | | Closed | ✅ `""` (empty string) | ✅ `0px` |
| Open | Full `<nav>` tree (see below) | `Main chat\njust now\nSide chats\n<chat titles>...` | | Open | ✅ Full `<nav>` tree (60KB+ on opm, 26 rows) | ✅ `240px` |
**Panel width:** Fixed `240px`. ### 3b. Open panel tree ✅ (verified — 26 rows on opm)
### 3b. Open panel tree 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"]`.
When open, the shell contains: ### 3c. Related panel testids ✅ (extended)
``` | Testid | Element | Purpose | Status |
DIV[data-testid="hatch-side-chats-panel-shell"] (width: 240px) |---|---|---|---|
└── DIV[data-hatch-toast-pane="side-chats"] | `hatch-side-chats-panel-shell` | `DIV` | Outer shell; always in DOM (except §8); empty when closed | ✅ |
└── DIV[data-testid="hatch-side-chats-panel-sliding-surface"] (absolute inset-y-0 start-0, width: 240px) | `hatch-side-chats-panel-sliding-surface` | `DIV` | Sliding animation surface (open state only) | ✅ |
└── NAV[aria-label="Side chats"] (data-pel-impression="chat_threads_overflow_impression", width: 240px) | `hatch-side-chats-panel-content` | `DIV` | Panel content wrapper | ✅ |
└── DIV[data-testid="hatch-side-chats-panel-content"] (flex h-full w-full flex-col) | `hatch-chat-search-toolbar` | `DIV` | Top toolbar (search + options) | ✅ |
├── DIV[data-testid="hatch-chat-search-toolbar"] (height: 60px) | `hatch-chat-search-field` | `DIV` | Search input container (`input[aria-label="Search"][placeholder="Search"]`) | ✅ |
│ ├── DIV[data-testid="hatch-chat-search-field"] | `hatch-side-chats-options` | `BUTTON` | `aria-label="Side chat options"` three-dots dropdown | ✅ |
│ │ └── INPUT[aria-label="Search"][placeholder="Search"] | `hatch-thread-row` | `DIV` | **Each** chat row (Main chat + every sidechat) | ✅ |
│ └── BUTTON[data-testid="hatch-side-chats-options"] | `hatch-chat-compose` | `BUTTON` | "New side chat" `+` button | ✅ |
│ [aria-label="Side chat options"] (three-dots menu, dropdown trigger) | `active-side-chat-load-more-sentinel` | `DIV` | Infinite-scroll sentinel at list end | ✅ |
└── DIV (scrollable: min-h-0 min-w-0 flex-1) | `hatch-side-chats-open-drag-handle` | `DIV` | Resize drag handle (separate from shell) | ✅ |
└── DIV (thread list container) | `hatch-side-chats-open-drag-handle-divider-line` | `DIV` | Drag handle divider | 🆕 |
├── DIV[data-testid="hatch-thread-row"][role="button"] ← Main chat row | `hatch-chat-nav-fade` | `DIV` | Fade effect element | ✅ |
├── SECTION (flex flex-col) ← Side chats section | `hatch-chat-switcher-unread-indicator` | `SPAN` | Unread blue dot on switcher | 🆕 (see §1b) |
│ ├── DIV.group/nav-section (header)
│ │ ├── H3 > BUTTON[aria-expanded="true"] "Side chats" (collapsible)
│ │ └── SPAN.contents > BUTTON[data-testid="hatch-chat-compose"]
│ └── DIV (chat list)
│ └── DIV.overflow-hidden > DIV.flex.flex-col.gap-px > DIV.flex.flex-col.gap-px
│ └── DIV[data-testid="hatch-thread-row"][role="button"] ← per sidechat
└── DIV[data-testid="active-side-chat-load-more-sentinel"] ← infinite scroll
```
### 3c. Related panel testids ### 3d. Thread row structure (each chat / sidechat) ✅ (with corrections)
| Testid | Element | Purpose | Every row — Main chat and each sidechat — uses the same base structure:
|---|---|---|
| `hatch-side-chats-panel-shell` | `DIV` | Outer shell; always in DOM; 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 |
| `hatch-side-chats-options` | `BUTTON` | "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-chat-nav-fade` | `DIV` | Fade effect element |
### 3d. Thread row structure (each chat / sidechat)
Every row — Main chat and each sidechat — uses the same structure:
```html ```html
<div data-testid="hatch-thread-row" <div data-testid="hatch-thread-row"
role="button" role="button"
tabindex="0" tabindex="0"
draggable="false" draggable="false"
data-state="closed" data-state="closed" <!-- 🔧 sidechat rows ONLY; absent on Main chat row -->
data-slot="context-menu-trigger" data-slot="context-menu-trigger" <!-- 🔧 sidechat rows ONLY; absent on Main chat row -->
data-pel-click="chat_thread_options_open" data-pel-click="chat_thread_options_open"
aria-current="page" <!-- ONLY on the currently active chat --> 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 ..."> 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 ...">
@@ -212,60 +206,61 @@ Every row — Main chat and each sidechat — uses the same structure:
</p> </p>
</div> </div>
<span class="text-caption-1 text-text-secondary shrink-0 whitespace-nowrap ...">just now</span> <span class="text-caption-1 text-text-secondary shrink-0 whitespace-nowrap ...">just now</span>
<!-- hover-only action buttons --> <div class="ms-[3px] shrink-0 items-center"> <!-- 🔧 sidechat rows ONLY -->
<button aria-label="More thread actions">…</button> <!-- 🆕 hover-only -->
</div>
</div> </div>
``` ```
**Key facts:** **Key facts (✅ verified 2026-10-04):**
| Property | Value | | Property | Value |
|---|---| |---|---|
| Selector (all rows) | `[data-testid="hatch-thread-row"]` | | Selector (all rows) | `[data-testid="hatch-thread-row"]` |
| Tag | `DIV` (not an anchor) | | Tag | `DIV` (not an anchor) |
| `role` | `button` | | `role` | `button` |
| Navigation | React `onClick` — **no `<a href="/thread/...">` links exist** in the panel | | 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 | | Active chat marker | `aria-current="page"` on the active row only |
| Title | Inner `span[title]` — the chat's display name | | Title | Inner `span[title]` — the chat's display name |
| Timestamp | Trailing span: `just now`, `1m`, `4m`, etc. | | Timestamp | Trailing span: `just now`, `1m`, `4m`, `11m`, etc. |
| Main chat row | First `hatch-thread-row`, text `Main chat` | | 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 **Distinguishing Main chat from sidechats:** Same `data-testid`. Match on text
content (`Main chat` vs sidechat title) or check `aria-current="page"`. 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 ### 3e. "Side chats" section header ✅
```html Unchanged: `H3 > BUTTON` toggles section collapse (`aria-expanded="true"/"false"`;
<div class="group/nav-section flex w-full items-center"> verified `true` on opm). The compose `+` button sits beside it.
<h3 class="flex min-w-0 flex-1">
<button type="button" aria-expanded="true" aria-controls="_r_19e_"
class="flex h-9 min-w-0 flex-1 cursor-pointer items-center gap-1 rounded-full px-2.5 text-start ...">
<span class="text-subheadline-medium text-text-secondary truncate">Side chats</span>
<!-- chevron collapse icon -->
</button>
</h3>
<span class="contents">
<button data-testid="hatch-chat-compose" ...>...</button>
</span>
</div>
```
The `H3 > BUTTON` toggles section collapse (`aria-expanded="true"/"false"`).
The compose `+` button sits beside it.
--- ---
## 4. Panel Open vs Closed — Detection Guide ## 4. Panel Open vs Closed — Detection Guide ✅
### Reliable signals ### Reliable signals (✅ all re-verified)
| Signal | Panel OPEN | Panel CLOSED | | Signal | Panel OPEN | Panel CLOSED |
|---|---|---| |---|---|---|
| `[data-testid="hatch-chat-compose"]` exists | ✅ Yes | ❌ No | | `[data-testid="hatch-chat-compose"]` exists | ✅ Yes | ❌ No |
| `[data-testid="hatch-thread-row"]` count | ≥ 1 | 0 | | `[data-testid="hatch-thread-row"]` count | ≥ 1 | 0 |
| Panel shell `innerHTML` | Full nav tree | `""` (empty) | | Panel shell `innerHTML` | Full nav tree | `""` (empty) |
| Panel shell `innerText` | `Main chat\njust now\nSide chats\n...` | `""` | | Panel shell computed width | `240px` | `0px` |
### Recommended checks (in order of reliability) ### Recommended checks (in order of reliability) ✅ unchanged
```javascript ```javascript
// BEST: panel is open and ready for sidechat creation // BEST: panel is open and ready for sidechat creation
@@ -279,16 +274,17 @@ const panelOpenAlt = shell && shell.innerHTML !== "";
const rowCount = document.querySelectorAll('[data-testid="hatch-thread-row"]').length; const rowCount = document.querySelectorAll('[data-testid="hatch-thread-row"]').length;
``` ```
### Signals that DO NOT work ### Signals that DO NOT work ✅ unchanged
| Signal | Why it fails | | Signal | Why it fails |
|---|---| |---|---|
| `document.body.innerText.includes('Side chats')` | **False positive.** Body text contains "Side chats" in both states — it matches the switcher's sr-only text `"Open chat and side chats"` and chat message content. | | `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. | | `document.body.innerText.includes('Main chat')` | Same problem — matches message content |
| Switcher `aria-expanded` | Never set (`null` in both states). | | Switcher `aria-expanded` | Never set (`null` in both states) |
| `a[href*="/thread/"]` links | **Zero exist.** Panel rows are `DIV[role=button]`, not anchors. | | `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 ### Safe toggle sequence ✅ (behavior verified both directions 2026-10-04)
```javascript ```javascript
// 1. Detect actual panel state (not body text) // 1. Detect actual panel state (not body text)
@@ -305,25 +301,37 @@ const nowOpen = !!document.querySelector('[data-testid="hatch-chat-compose"]');
if (!nowOpen) throw new Error("Panel did not open"); 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) ## 5. Full data-testid Inventory (chat-related) ✅ (extended 2026-10-04)
Captured on `https://muse.ai/` main chat view. `visible` = computed style check. 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 | | testid | Tag | aria-label | Notes |
|---|---|---|---| |---|---|---|---|
| `hatch-dock-rail` | `DIV` | — | Left dock rail | | `hatch-dock-rail` | `DIV` | — | Left dock rail |
| `hatch-nav-chat` | `A` | `Chat` | Dock nav: Chat | | `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-search` | `BUTTON` | `Search` | Dock nav: Search |
| `hatch-nav-system-item-*` | `DIV` | `Feed`/`Ideas`/`Goals`/`Library` | Dock system items | | `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 | | `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-leading-controls` | `DIV` | — | Top-left container for switcher |
| `hatch-chat-switcher-trigger` | `BUTTON` | — | **Panel toggle** (text: "Chats") | | `hatch-chat-switcher-trigger` | `BUTTON` | — | **Panel toggle** (label = active chat title) |
| `hatch-chat-switcher-icon-knockout` | `SPAN` | — | Icon wrapper | | `hatch-chat-switcher-icon-knockout` | `SPAN` | — | Icon wrapper |
| `hatch-chat-switcher-navigation-icon` | `SPAN` | — | Hamburger SVG | | `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` | `DIV` | — | Panel resize handle |
| `hatch-side-chats-panel-shell` | `DIV` | — | **Panel shell** (always in DOM) | | `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-sliding-surface` | `DIV` | — | Open-state sliding surface |
| `hatch-side-chats-panel-content` | `DIV` | — | Panel content wrapper | | `hatch-side-chats-panel-content` | `DIV` | — | Panel content wrapper |
| `hatch-chat-search-toolbar` | `DIV` | — | Search toolbar (60px) | | `hatch-chat-search-toolbar` | `DIV` | — | Search toolbar (60px) |
@@ -335,27 +343,40 @@ Captured on `https://muse.ai/` main chat view. `visible` = computed style check.
| `active-side-chat-load-more-sentinel` | `DIV` | — | Infinite scroll sentinel | | `active-side-chat-load-more-sentinel` | `DIV` | — | Infinite scroll sentinel |
| `hatch-invite-friends-button` | `BUTTON` | `Invite` | Invite button | | `hatch-invite-friends-button` | `BUTTON` | `Invite` | Invite button |
| `hatch-composer-placeholder-overlay` | `SPAN` | — | Message input placeholder | | `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-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-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 ## 6. Automation Notes ✅ (extended)
1. **Ctrl+J is unreliable** as a generic panel toggle. It navigates toward main chat 1. **Ctrl+J is unreliable** as a generic panel toggle. Prefer the detect →
but does not guarantee the panel ends up open. Prefer the detect → click-switcher click-switcher → verify sequence in §4.
→ verify sequence in §4.
2. **The `+` button has no text.** Never search for `"+"` text — the button contains 2. **The `+` button has no text.** Never search for `"+"` text — use
only an SVG. Use `[data-testid="hatch-chat-compose"]` or `aria-label="New side chat"`. `[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`. 3. **After creating a sidechat**, the browser navigates to
This stripped state has no switcher/panel — navigate back to `https://muse.ai/` `https://muse.ai/thread/new`. This **stripped state** has no switcher at
(or click Main chat) before the next panel operation. 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.** To open a specific sidechat, click its 4. **Sidechat rows are not links and carry no identity.** To open a specific
`DIV[data-testid="hatch-thread-row"]` (match by inner `span[title]` text). sidechat, click its `DIV[data-testid="hatch-thread-row"]` (match by inner
There is no URL to navigate to directly until the thread UUID is known. `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, 5. **Thread UUID capture:** After sending the first message in a new sidechat,
`window.location.href` transitions from `/thread/new` to `/thread/<uuid>`. `window.location.href` transitions from `/thread/new` to `/thread/<uuid>`.
@@ -365,3 +386,77 @@ Captured on `https://muse.ai/` main chat view. `visible` = computed style check.
7. **The "Side chats" H3 is collapsible** (`aria-expanded`). If rows are missing 7. **The "Side chats" H3 is collapsible** (`aria-expanded`). If rows are missing
while the panel is open, check whether the section is collapsed. 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.