diff --git a/docs/DOM-MESSAGES.md b/docs/DOM-MESSAGES.md new file mode 100644 index 0000000..259d946 --- /dev/null +++ b/docs/DOM-MESSAGES.md @@ -0,0 +1,332 @@ +# 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 and a
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"]') +// → 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 `
`, no `