Files
box/docs/DOM-MESSAGE-ACTIONS.md
T

363 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.
# DOM Reference: Message Actions & Thread Interactions
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).
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
```
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.
### Menu contents (in order)
**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>
```
Relative format ("Today at …"). No `<time>` element, no `datetime` attribute.
**b) Quick reactions** — this IS the emoji picker (no separate picker UI exists)
```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">
```
Six buttons, each:
```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` | 🙏 |
```javascript
menu.querySelectorAll('button[aria-label^="React"]') // 6 buttons
```
- `aria-pressed="false"` flips to `"true"` once applied (state toggle).
- Followed by an empty `<span class="contents">` — likely a conditional slot
(e.g. "more emojis" expander); empty in all observed renders.
**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
- **No `<time>` elements** exist in the inspected threads (zero hits).
- Message list level: relative plain text, e.g. `3:47 AM`, rendered as a text node
at the `div[role="log"]` level (not wrapped in its own element per message).
- Menu header (§3a): relative format `Today at 3:48 AM`.
- There is no `datetime` attribute / ISO timestamp anywhere in the message DOM.
### Author display
- Own messages: `<span class="sr-only">You:</span>` — **screen-reader-only**,
first child of `div.hatch-chat-groupable-bubble`. Not visually rendered.
```javascript
// reliable own-vs-other discriminator:
msg.querySelector('span.sr-only')?.innerText.trim() === 'You:'
```
- 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
```
---
## 8. Open questions
1. Delete menuitem render conditions (seen on own message once, absent once).
2. Rendered reaction badge markup (no badges present to inspect).
3. Whether the empty `span.contents` after quick reactions becomes a full emoji picker.
4. `aria-pressed` flip verification after applying a reaction (not clicked in testing).