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

454 lines
17 KiB
Markdown
Raw Permalink 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
> **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.
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).
**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.
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
```
**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).
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 — 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).
**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, **two variants observed**: `Today at 3:48 AM` (today),
`Oct 3 at 8:01 PM` (older). No `<time>` element, no `datetime` attribute.
**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).
```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">
```
Seven buttons (six emoji + a "More reactions" opener — the 7th was not in the
earlier capture):
```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` | 🙏 |
| `More reactions` | (opens full picker — see below) |
```javascript
menu.querySelectorAll('button[aria-label^="React"]') // 7 buttons, other's menu only
```
- `aria-pressed="false"` flips to `"true"` once applied (state toggle).
- 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).
**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 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).
- There is no `datetime` attribute / ISO timestamp anywhere in the message DOM.
### Author display
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.
```javascript
// 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: …"
```
- 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 (updated 2026-10-04)
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).