# muse.ai Message DOM Map > **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. 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`). **Re-verified 2026-10-04 ~04:30 UTC** on all three fleet nodes (muse/CDP 9410, pip/9420, opm/9440) against live main chats with real message history. No per-node structural differences found. Corrections and additions from that pass are inline below. 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('div[data-message-id]') ``` **Structural correction (verified 2026-10-04, pip/muse/opm):** the interactive `.group/msg` wrapper is *inside* `div[data-message-id]`, not a sibling rendering. True per-message structure: ``` div[data-message-id] ← class "flex flex-col gap-2", one per message ├── div.group/msg ← interactive bubble + hover action rail │ ├── div (action rail, absolute inset-y-0 end-full) │ │ └── button[aria-label="More options"] │ └── div.hatch-chat-groupable-bubble │ ├── span.sr-only "You:" (own messages only, bubble-level) │ └── p / div (visible body) └── span.sr-only (direct child of the wrapper) "User message: " / "Assistant message: " ``` The direct-child `span.sr-only` carries the **full accessible label** (author prefix + entire message text, observed up to 2000+ chars). Own messages therefore have *two* sr-only spans: `"User message: …"` on the wrapper and `"You:"` in the bubble. Agent messages have only the wrapper-level `"Assistant message: …"` span. There are still no `
` / `