Files
box/docs/DOM-MESSAGES.md
T

333 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# muse.ai Message DOM Map
Reference for automation against the muse.ai chat UI. Captured live via CDP
on the `opm` headless browser (viewport 780×493), 2026-10-04 ~03:45 UTC.
Verified states: main chat (`https://muse.ai/`) and empty new thread
(`https://muse.ai/thread/new`).
Related maps: sidechat panel selectors live in the sidechat work
(`hatch-chat-switcher-trigger`, `hatch-chat-compose`); this doc covers the
message composer, send button, message list, and chat activation.
---
## 1. Message input area
### Primary selector
```js
document.querySelector('textarea[aria-label="Message"]')
```
Fallback chain used by `muse-chat-api.py` (any one may match depending on render):
```js
document.querySelector('[contenteditable="true"]')
|| document.querySelector('textarea[placeholder*="Message"]')
|| document.querySelector('div[role="textbox"]')
```
### Observed attributes (main chat)
| Attribute | Value |
|---------------|--------------------------------------------------------------|
| tag | `TEXTAREA` |
| `aria-label` | `Message` |
| `placeholder` | `Message` |
| `rows` | `1` |
| `disabled` | `false` |
| `readOnly` | `false` |
| class | `text-text-primary block max-h-48 w-full resize-none bg-transparent` |
| visible | `offsetParent !== null` → `true` |
### Parent chain (4 levels)
```
textarea[aria-label="Message"]
└── div.relative.flex.min-h-8.min-w-0.items-center.ps-10 (also contains an <input> and a <div> sibling)
└── div.flex.shrink-0.flex-col.px-3.py-3
└── div[role="button"].backdrop-blur-elevation-01.bg-fill-blur-thick.shadow-blur-elevation-01.rounded-32 ← composer "pill"
└── div.min-w-0.flex-1
```
### Ready-for-typing detection
```js
const ta = document.querySelector('textarea[aria-label="Message"]');
const ready = !!ta && ta.offsetParent !== null && !ta.disabled && !ta.readOnly;
```
Empty-state signal: the placeholder overlay is visible when nothing is typed:
```js
document.querySelector('[data-testid="hatch-composer-placeholder-overlay"]')
// → <span> innerText "Message", visible when input is empty
```
### Typing into it (React-safe)
`execCommand('insertText')` is what the current tooling uses:
```js
const input = document.querySelector('textarea[aria-label="Message"]');
input.focus();
document.execCommand('insertText', false, message);
```
Alternative when React ignores execCommand — native setter + input event:
```js
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(ta, text);
ta.dispatchEvent(new Event('input', { bubbles: true }));
```
Adjacent composer buttons (same pill, not the send button):
| `aria-label` | Purpose |
|---------------------|----------------|
| `Attach file` | file upload |
| `Dictate a message` | voice input |
---
## 2. Send button
### Selector
```js
[...document.querySelectorAll('button')]
.find(b => b.getAttribute('aria-label')?.toLowerCase().includes('send'))
// observed aria-label is exactly "Send"
```
- Icon: inline SVG, arrow-up glyph (path data starts `M11.45 2.32`).
- No `data-testid`, no `title`, no innerText — match on `aria-label` only.
### Enabled / disabled states
- Observed `disabled === false` in the DOM even with an empty composer; the
button is **rendered once text is typed** and may persist afterwards.
- Do not rely on `disabled` alone. Practical send flow (as implemented):
```js
const send = [...document.querySelectorAll('button')]
.find(b => b.getAttribute('aria-label')?.toLowerCase().includes('send'));
if (send) { send.click(); /* 'sent' */ }
else {
// fallback: Enter key on the input
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
}
```
### While the assistant is generating
The send button is replaced by the stop button:
```js
document.querySelector('[data-testid="hatch-composer-stop-button"]')
// present only while a response is streaming; absent when idle
```
---
## 3. Message list
### Scroll container
```js
document.getElementById('hatch-chat-scroll')
```
| Attribute | Value |
|-----------|--------------------------------------------------------------------|
| tag | `DIV` |
| id | `hatch-chat-scroll` |
| class | `flex flex-col overflow-hidden relative z-0 grow-1 shrink-1 min-h-0` |
### List element
```js
document.querySelector('div[role="log"]')
```
| Attribute | Value |
|--------------|----------------------------------------------------|
| tag | `DIV` |
| `role` | `log` |
| `aria-label` | `Chat messages` |
| class | `mx-auto w-full max-w-3xl min-w-0 @container flex` |
Parent chain of a message (bottom-up):
```
div[data-message-id] ← one per message, class "flex flex-col gap-2"
└── div[role="log"][aria-label="Chat messages"]
└── div
└── div
└── div
└── div#hatch-chat-scroll
```
### Individual message
Selector for all messages:
```js
document.querySelectorAll('[data-message-id]')
```
Author is encoded in the id prefix — **there are no author/time sub-elements**
(no `<article>`, no `<time>`, no `data-testid` containing "message"):
| Author | `data-message-id` format | `innerText` prefix |
|-----------|--------------------------------|--------------------------|
| assistant | `assistant-msg-<uuid>` | `Assistant message: ...` |
| user | `<uuid>` (bare, no prefix) | `User message: ...` * |
\* User-message text prefix not directly observed (no user messages were
rendered at capture time); author detection via the id prefix is the
reliable signal.
### Extracting text / author
```js
const msgs = [...document.querySelectorAll('[data-message-id]')].map(m => {
const id = m.getAttribute('data-message-id');
return {
id,
author: id.startsWith('assistant-msg') ? 'assistant' : 'user',
text: m.innerText
};
});
```
### Simple read-back (what `muse-chat-api.py messages` does)
```js
[...document.querySelectorAll('p')].slice(-n * 2).map(p => p.innerText.slice(0, width))
```
Works because message bodies render as `<p>` elements; the last `2n`
paragraphs cover roughly the last `n` messages. Crude but effective for
"did my text land?" verification.
### Message-related testids seen in the wild
| testid | Meaning | Present when |
|---------------------------------------|--------------------------------------|--------------|
| `hatch-chat-typing-indicator` | assistant is typing/generating | generating only |
| `hatch-composer-stop-button` | stop-generation button | generating only |
| `hatch-composer-placeholder-overlay` | "Message" ghost text | composer empty |
| `hatch-chat-switcher-unread-indicator`| unread badge on the chat switcher | unread chats exist |
---
## 4. Chat activation button (speech-bubble / nav)
This is the dock-rail button that activates the Chat section. The user's
finding: sidechat create only works when the chat function is active, and
this button (not Ctrl+J) is the reliable activator.
### Selector
```js
document.querySelector('[data-testid="hatch-nav-chat"]')
```
### Observed attributes
| Attribute | Value |
|----------------|------------------------------------------------------------|
| tag | `A` (anchor, not button) |
| `data-testid` | `hatch-nav-chat` |
| `href` | `/` |
| `aria-label` | dynamic: `Chat` or `Chat, N notifications` (e.g. `Chat, 2 notifications`) |
| `aria-current` | `page` when Chat is the active section (absent otherwise) |
| visible | `true` (dock rail is always rendered) |
### Parent chain (4 levels)
```
a[data-testid="hatch-nav-chat"][aria-label="Chat, 2 notifications"][aria-current="page"]
└── div
└── div
└── div
└── div[data-testid="hatch-dock-rail"]
```
### States
| State | Signal |
|-------|--------|
| Chat section active | `aria-current="page"` present |
| Chat section inactive | `aria-current` absent (button still visible/clickable) |
| Unread messages | `aria-label` contains `N notifications` |
### Usage
```js
// Activate chat reliably (replaces Ctrl+J):
document.querySelector('[data-testid="hatch-nav-chat"]').click();
```
`cmd_sidechat_main` already falls back to this click when the "Main chat"
element lookup fails. Sibling nav buttons in the same rail:
`hatch-nav-search` (Search), plus `div[role="button"]` items for Feed, Ideas,
Goals, Library, and `hatch-dock-more` (Settings).
---
## 5. Loading / empty states
| State | How to detect |
|-------|---------------|
| New empty thread (`/thread/new`) | URL is `/thread/new`, `document.title === "Muse"`, zero `[data-message-id]` elements, composer present |
| Main chat loaded | URL is `/`, title is `Chat — <agent>`, `[data-message-id]` elements present |
| Assistant generating | `[data-testid="hatch-chat-typing-indicator"]` exists; send button replaced by `[data-testid="hatch-composer-stop-button"]` |
| Idle, ready to send | typing indicator absent, stop button absent, `textarea[aria-label="Message"]` visible and enabled |
| Composer empty | `[data-testid="hatch-composer-placeholder-overlay"]` visible with text `Message` |
| Side panel open (threads visible) | `[data-testid="hatch-chat-compose"]` ("New side chat") present |
### Page-title signals
| Title | Meaning |
|-------|---------|
| `Chat — <agent>` | logged in, chat UI loaded |
| `Muse` | partial load or `/thread/new` stripped state — treat as not-ready, navigate to `/` first |
---
## Quick-reference selector list
```js
// Composer
'textarea[aria-label="Message"]'
'[data-testid="hatch-composer-placeholder-overlay"]' // empty-state ghost text
// Send / stop
'button[aria-label="Send"]' // via aria-label includes 'send'
'[data-testid="hatch-composer-stop-button"]' // while generating
// Messages
'#hatch-chat-scroll' // scroll container
'div[role="log"][aria-label="Chat messages"]' // list
'[data-message-id]' // each message
'[data-message-id^="assistant-msg"]' // assistant messages only
// Chat activation
'[data-testid="hatch-nav-chat"]' // dock-rail Chat link
'[data-testid="hatch-chat-typing-indicator"]' // generating indicator
```
## Caveats
- Selectors observed on the `opm` account; class names are Tailwind-ish and
may shift with deploys — prefer `data-testid`, `aria-label`, `role`, and
`id` selectors over classes.
- `aria-label` on `hatch-nav-chat` embeds a live notification count; match
with `startsWith('Chat')` or the testid, never exact equality.
- During this capture another agent was driving the same browser; page URL
flipped between `/` and `/thread/new` between probes. Element identities
above were each verified at least once against the live DOM.