2026-10-04 03:53:52 +00:00
# DOM Reference: Message Actions & Thread Interactions
2026-10-05 15:58:37 +00:00
> **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.
2026-10-04 03:53:52 +00:00
Inspected live via CDP on the opm browser (warp-opm netns, CDP 9440), 2026-10-04.
Covers main chat (`https://muse.ai/` ) and sidechat threads (`/thread/<uuid>` ).
Read-mostly probes; the only state-changing probes were opening/closing the
"More options" menu and one Reply-draft open/cancel cycle (composer verified clean after).
2026-10-04 04:28:21 +00:00
**Re-verified 2026-10-04 ~04:30 UTC ** on all three fleet nodes (muse/9410,
pip/9420, opm/9440) against live main chats. Menu contents re-probed on muse
(own + other's menus, "More reactions" dialog opened and Escape-closed, no
residue). Corrections inline; no per-node structural differences.
2026-10-04 03:53:52 +00:00
Related: `DOM-CHAT-PANEL.md` (panel/sidechat create), `DOM-MESSAGES.md` , `DOM-PAGE-STRUCTURE.md` .
---
## 1. Message wrapper
Messages carry **no `data-testid` ** . The structural hook is the Tailwind group class:
``` html
< div class = "group/msg ms-auto flex w-fit flex-col items-end
[--hatch-message-narrow-max-width:78%]
[--hatch-message-compact-action-rail-width:2.75rem]" >
```
Selector:
``` javascript
document . querySelectorAll ( '.group\\/msg' ) // note: escaped slash in class name
```
2026-10-04 04:28:21 +00:00
**Structural correction (verified 2026-10-04): ** `.group/msg` is * inside *
`div[data-message-id]` , not the top-level message element. Full chain:
```
div[data-message-id] ← class "flex flex-col gap-2", the message identity hook
└── div.group/msg ← interactive wrapper (this section's hook)
├── 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)
└── p / div (visible body)
```
`div[data-message-id]` additionally carries a direct-child
`span.sr-only` with the full accessible label (`"User message: <text>"` /
`"Assistant message: <text>"` ) — see `DOM-MESSAGES.md` §3.
Every rendered message has exactly one `.group/msg` , and every `.group/msg`
has exactly one `More options` rail button (verified rails == groups on
muse, pip, opm — including agent messages).
2026-10-04 03:53:52 +00:00
Parent chain (own message, bottom-up):
```
DIV.group/msg (ms-auto, items-end → right-aligned = own message)
└── DIV.touch:select-none.flex.w-full.flex-col.items-end
└── DIV.relative.flex.w-full.flex-col.items-end
├── DIV.<action rail> (absolute inset-y-0 end-full …)
└── DIV.relative.max-w-full
└── DIV.relative.max-w-full
└── DIV.hatch-chat-groupable-bubble
├── SPAN.sr-only "You:" (author label, a11y only)
└── P.text-[16px].leading-[22px].whitespace-pre-wrap (body)
```
Message list container:
``` javascript
document . querySelector ( 'div[role="log"][aria-label="Chat messages"]' )
```
### Bubble variants
| Variant | Bubble classes | Alignment |
|---|---|---|
| Own (user) message | `hatch-chat-groupable-bubble … bg-chat-user-bubble text-chat-user-text` | `group/msg` has `ms-auto items-end` (right) |
| Agent message | `hatch-chat-groupable-bubble … bg-[var(--hatch-agent-bubble-bg)]` | left-aligned |
| Agent body text | `DIV.!text-[15px].text-text-primary` (prose container) | — |
---
## 2. Action rail (hover-revealed)
Each `div.group/msg` contains exactly one action-rail div:
``` javascript
// selector: the rail inside a message
msg . querySelector ( ':scope > div > div > div[class*="end-full"]' )
```
Full classes (desktop):
```
touch:static touch:mb-0 touch:h-auto touch:scale-100 touch:opacity-100
absolute inset-y-0 end-full my-auto flex h-fit shrink-0 scale-95 opacity-0
transition-[opacity,scale]
group-hover/msg:scale-100 group-hover/msg:opacity-100
```
Key facts:
- Positioned `absolute inset-y-0 end-full` — vertically centered on the message's
outer (end) side, `my-auto` .
- Hidden with `opacity-0` , revealed purely by CSS `group-hover/msg` — **no JS
hover needed for automation**; query the button directly.
- `touch:` variants force it visible on touch devices.
- Width driven by `--hatch-message-compact-action-rail-width: 2.75rem` .
- ⚠️ `getComputedStyle().display/visibility` report the button as visible even
when the rail is `opacity-0` — check `opacity` for true on-screen visibility.
### The single action button: "More options"
Every message (own AND agent) exposes exactly **one ** action button — a Radix
dropdown trigger:
``` html
< button aria-label = "More options" id = "radix-_r_n_"
class = "inline-flex shrink-0 cursor-pointer items-center
justify-center rounded-full" >
<!-- SVG dots icon, no text -->
< / button >
```
``` javascript
msg . querySelector ( 'button[aria-label="More options"]' )
```
- 28× 28 px, `rounded-full` , icon-only (SVG, no text content).
- `id` follows the Radix pattern `radix-_r_<n>_` — **unstable across renders,
do not match on id**; match on `aria-label="More options"` .
- This button is the gateway to ALL message actions (reply, react, copy, delete).
There are no separate always-visible reply/react/copy buttons.
---
## 3. "More options" menu (Radix dropdown)
### Opening it (headless quirk)
A plain `.click()` did **not ** open the menu. The full pointer sequence was required:
``` javascript
const b = msg . querySelector ( 'button[aria-label="More options"]' ) ;
b . scrollIntoView ( { block : 'center' } ) ;
const r = b . getBoundingClientRect ( ) ;
const opts = { bubbles : true , cancelable : true ,
clientX : r . x + r . width / 2 , clientY : r . y + r . height / 2 , button : 0 } ;
b . dispatchEvent ( new PointerEvent ( 'pointerdown' , opts ) ) ;
b . dispatchEvent ( new MouseEvent ( 'mousedown' , opts ) ) ;
b . dispatchEvent ( new PointerEvent ( 'pointerup' , opts ) ) ;
b . dispatchEvent ( new MouseEvent ( 'mouseup' , opts ) ) ;
b . dispatchEvent ( new MouseEvent ( 'click' , opts ) ) ;
```
- When open, the trigger gets `aria-expanded="true"` and `data-state="open"` .
- Opening was flaky in headless (~1 in 4 attempts needed a retry): loop up to 4× ,
checking for `[role="menu"][data-state="open"]` , Escape between attempts.
- Close with: `document.dispatchEvent(new KeyboardEvent('keydown',
{key: 'Escape', keyCode: 27, bubbles: true}))`.
### Menu container
` ``html
<div role="menu" aria-orientation="vertical" data-state="open"
data-side="bottom" data-align="end"
data-radix-menu-content="" data-slot="dropdown-menu-content"
id="radix-_r_nd_" aria-labelledby="radix-_r_nc_" tabindex="-1"
class="bg-fill-blur-thick backdrop-blur-elevation-03 text-text-primary
z-50 min-w-56 rounded-16 p-1.5 …">
` ``
` ``javascript
[...document.querySelectorAll('[role="menu"][data-state="open"]')]
.find(e => /Reply/.test(e.innerText || ''))
` ``
- ` data-side` / ` data-align` describe popper placement (` bottom`/` end` observed).
- ` id` is Radix-generated (` radix-_r_nd_`) — unstable, don't match on it.
2026-10-04 04:28:21 +00:00
### Menu contents — DIFFERS BY OWNERSHIP (corrected 2026-10-04)
Verified on muse (CDP 9410): opening the menu on an **own** message vs an
**other's** (agent) message yields different menus. The earlier doc described
one menu; there are two.
**Own message** — children: header DIV, Reply, Copy, separator, Delete.
*No quick-reactions group at all.*
` ``javascript
[...menu.querySelectorAll('[role="menuitem"]')].map(mi => mi.innerText.trim())
// own message: ["Reply", "Copy", "Delete"]
` ``
**Other's message** — children: header DIV, ` [role="group"][aria-label="Quick reactions"]`,
separator, Reply, Copy. *No Delete item.*
` ``javascript
// other's message: ["Reply", "Copy"] (+ 7 reaction buttons in the group)
` ``
| | Own message menu | Other's message menu |
|---|---|---|
| Timestamp header | yes | yes |
| Quick reactions | **absent** | present (7 buttons) |
| Reply / Copy | yes | yes |
| Delete | yes (2/2 opens) | **absent** |
- **Delete** was present on own messages in both opens this session; still
absent on other's. The earlier "seen once, absent once" observation is
superseded for the ownership axis, but keep a conditional mindset —
render conditions beyond ownership are untested, and clicking Delete was
not tested (destructive).
- **No Edit item exists** on either menu. Message editing UI was not found
anywhere (unchanged).
2026-10-04 03:53:52 +00:00
**a) Timestamp header**
` ``html
<div class="text-caption-2 text-text-secondary px-2 pt-1.5 pb-2">Today at 3:48 AM</div>
` ``
2026-10-04 04:28:21 +00:00
Relative format, **two variants observed**: ` Today at 3:48 AM` (today),
` Oct 3 at 8:01 PM` (older). No ` <time>` element, no ` datetime` attribute.
2026-10-04 03:53:52 +00:00
2026-10-04 04:28:21 +00:00
**b) Quick reactions** — this IS the emoji picker entry point, and it appears
**only on other people's messages** (own-message menus have no reaction UI).
2026-10-04 03:53:52 +00:00
` ``html
<div role="group" aria-label="Quick reactions"
class="flex flex-wrap items-center justify-between gap-0.5 px-1 pt-0.5 pb-1">
` ``
2026-10-04 04:28:21 +00:00
Seven buttons (six emoji + a "More reactions" opener — the 7th was not in the
earlier capture):
2026-10-04 03:53:52 +00:00
` ``html
<button type="button" aria-pressed="false" aria-label="React with thumbs up"
class="flex size-7 shrink-0 cursor-pointer items-center justify-center
rounded-full text-[20px] transition-transform select-none hover:scale-125">👍</button>
` ``
| ` aria-label` | Emoji |
|---|---|
| ` React with thumbs up` | 👍 |
| ` React with heart` | ❤️ |
| ` React with laughing face` | 😂 |
| ` React with surprised face` | 😮 |
| ` React with crying face` | 😢 |
| ` React with folded hands` | 🙏 |
2026-10-04 04:28:21 +00:00
| ` More reactions` | (opens full picker — see below) |
2026-10-04 03:53:52 +00:00
` ``javascript
2026-10-04 04:28:21 +00:00
menu.querySelectorAll('button[aria-label^="React"]') // 7 buttons, other's menu only
2026-10-04 03:53:52 +00:00
` ``
- ` aria-pressed="false"` flips to ` "true"` once applied (state toggle).
2026-10-04 04:28:21 +00:00
- Clicking **"More reactions"** opens a full emoji picker: ` [role="dialog"]`
with category headings (` Smileys & People`, …) — 346 ` React *` buttons
observed in the dialog. Escape closes it cleanly (verified, no draft left).
2026-10-04 03:53:52 +00:00
**c) Separator**
` ``html
<div role="separator" class="bg-fill-divider mx-2 my-1.5 h-[0.5px]"></div>
` ``
**d) Action items**
` ``html
<div role="menuitem"
class="focus-visible:outline-none focus:bg-fill-secondary-elevated
hover:bg-fill-secondary-elevated relative flex cursor-pointer">
<div class="m-0.5 flex w-5 shrink-0 items-center justify-center">
<!-- SVG icon -->
</div>
<div class="flex flex-1 flex-col gap-0 px-1 py-0.5">Reply</div>
</div>
` ``
` ``javascript
[...menu.querySelectorAll('[role="menuitem"]')]
.map(mi => mi.innerText.trim())
// own message: ["Reply", "Copy", "Delete*"]
// other's message: ["Reply", "Copy"]
` ``
- Structure per item: icon slot (` div.m-0.5.flex.w-5…` with SVG) + label
(` div.flex.flex-1…` with the text).
- **Reply** and **Copy** always present.
- **Delete** observed on own messages in one render but absent in another —
treat as conditional (possibly recency/ownership-gated). Clicking it was not
tested (destructive).
- **No Edit item exists.** Message editing UI was not found anywhere.
---
## 4. Reply UI (threaded replies)
Clicking the Reply ` menuitem` (same full pointer sequence as §3) produces a
**reply draft bar above the composer** — replies are quote-style, not inline threads:
` ``html
<div class="bg-fill-secondary-elevated rounded-18 mb-3 flex w-full
flex-col px-3 pt-2 pb-2.5">
<div class="flex min-w-0 flex-1 flex-col">
<div class="flex h-5 w-full items-center gap-0.5">
Replying to operator-main
</div>
…
</div>
</div>
` ``
Parent chain:
` ``
DIV.overflow-clip
└── DIV.origin-top
└── DIV.bg-fill-secondary-elevated.rounded-18.mb-3… ← reply bar (no data-testid)
└── DIV.flex.min-w-0.flex-1.flex-col
└── DIV.flex.h-5.w-full.items-center.gap-0.5 ← "Replying to <name>"
` ``
Facts:
- The bar has **no ` data-testid`** — detect via ` /^Replying to/i` text match.
- Text is ` Replying to <display name>` (e.g. ` Replying to operator-main`).
- ⚠️ **Escape does NOT reliably cancel the draft.** In one test the bar survived
an Escape keydown and was only gone on a later re-check. Automation that opens
Reply must verify cancellation via the text check, and retry (or click the
bar's close control) — otherwise the next ` dm.py` send may go out as a reply.
- No inline threaded-reply display structure was observed (no replies exist in
the inspected threads); the model appears to be composer-quote only.
---
## 5. Timestamps & author display
### Timestamps
2026-10-04 04:28:21 +00:00
- **No ` <time>` elements** exist in the inspected threads (zero hits on all
three nodes, 2026-10-04).
- Message list level: dividers are direct children of ` div[role="log"]`,
interspersed between messages (not per-message nodes):
- Day boundary: ` div.pt-10.pb-4.text-center > span.text-caption-1.text-text-secondary`
— text ` Oct 3, 11:41 PM`.
- Intra-day gap: ` div.py-4.text-center > span.text-caption-1.text-text-secondary`
— text ` 2:38 AM` (bare 12h time).
- Menu header (§3a): relative format with two variants — ` Today at 3:48 AM`
(today), ` Oct 3 at 8:01 PM` (older).
2026-10-04 03:53:52 +00:00
- There is no ` datetime` attribute / ISO timestamp anywhere in the message DOM.
### Author display
2026-10-04 04:28:21 +00:00
Two sr-only layers (verified 2026-10-04):
1. **Wrapper level** — direct child of ` div[data-message-id]`:
` span.sr-only` containing the full accessible label —
` "User message: <entire text>"` / ` "Assistant message: <entire text>"`.
2. **Bubble level** — own messages only: ` span.sr-only` with exactly ` "You:"`,
first child of ` div.hatch-chat-groupable-bubble`. Agent messages have no
bubble-level sr-only span.
2026-10-04 03:53:52 +00:00
` ``javascript
2026-10-04 04:28:21 +00:00
// reliable own-vs-other discriminator (bubble level):
msg.querySelector('.group\\/msg span.sr-only')?.innerText.trim() === 'You:'
// full author+text label (wrapper level):
msg.querySelector(':scope > span.sr-only')?.innerText // "Assistant message: …"
2026-10-04 03:53:52 +00:00
` ``
- Visually, authorship is conveyed by bubble alignment/color only
(right + ` bg-chat-user-bubble` = own; left + agent bubble bg = other).
- No visible author name label exists on either message type in the inspected UI.
---
## 6. Rendered reactions
Searched all messages in main chat for reaction badges:
` ``javascript
document.querySelectorAll('[aria-label*="reacted"], [data-testid*="reaction"]')
// → 0 hits
` ``
No rendered reaction badges were found. Either no inspected message has
reactions, or badges use different markup. Unresolved — re-probe on a thread
with known reactions.
---
## 7. Automation recipes
**List messages with authorship:**
` ``javascript
[...document.querySelectorAll('.group\\/msg')].map(m => ({
own: m.querySelector('span.sr-only')?.innerText.trim() === 'You:',
text: m.innerText.slice(0, 80)
}))
` ``
**Open a message's action menu (headless-safe):**
` ``javascript
function openMsgMenu(msg) {
const b = msg.querySelector('button[aria-label="More options"]');
b.scrollIntoView({block: 'center'});
const r = b.getBoundingClientRect();
const o = {bubbles: true, cancelable: true,
clientX: r.x + r.width/2, clientY: r.y + r.height/2, button: 0};
['pointerdown','mousedown','pointerup','mouseup','click'].forEach(t =>
b.dispatchEvent(new ((t[0]==='p') ? PointerEvent : MouseEvent)(t, o)));
}
// retry loop: check [role="menu"][data-state="open"], Escape, retry (max 4)
` ``
**Click a quick reaction:**
` ``javascript
menu.querySelector('button[aria-label="React with heart"]').click()
// (plain .click() sufficed for menu-internal buttons in testing)
` ``
**Detect + clear a reply draft:**
` ``javascript
const hasDraft = /Replying to/.test(document.body.innerText);
// Escape, then re-check; do not assume it cleared
` ``
---
2026-10-04 04:28:21 +00:00
## 8. Open questions (updated 2026-10-04)
2026-10-04 03:53:52 +00:00
2026-10-04 04:28:21 +00:00
1. ~~Delete menuitem render conditions~~ — resolved on the ownership axis:
present on own messages (2/2 opens), absent on other's. Untested whether
other conditions (age, thread type) also gate it; clicking Delete untested.
2. Rendered reaction badge markup (no badges present to inspect — 0 hits for
` [aria-label*="reacted"]` / ` [data-testid*="reaction"]` on all three nodes).
Re-probe on a thread with known reactions.
3. ~~Full emoji picker~~ — answered: "More reactions" button (other's menu only)
opens ` [role="dialog"]` with categories (` Smileys & People`, …), 346
` React *` buttons observed. Escape closes cleanly.
4. ` aria-pressed` flip verification after applying a reaction (not clicked in
testing — applying a reaction is state-changing; left for a dedicated test).
5. Menu open flakiness varies by node/moment (4/4 attempts failed on pip,
first-try success on muse in the same session — possibly contention with
parallel automation). Keep the retry loop (max 4, Escape between attempts).