Files
box/docs/DOM-MESSAGES.md
T

12 KiB
Raw Blame History

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

document.querySelector('textarea[aria-label="Message"]')

Fallback chain used by muse-chat-api.py (any one may match depending on render):

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

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:

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:

const input = document.querySelector('textarea[aria-label="Message"]');
input.focus();
document.execCommand('insertText', false, message);

Alternative when React ignores execCommand — native setter + input event:

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

[...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):
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:

document.querySelector('[data-testid="hatch-composer-stop-button"]')
// present only while a response is streaming; absent when idle

3. Message list

Scroll container

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

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:

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

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)

[...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.

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

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

// 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

// 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.