DOM message surface re-verified 2026-10-04 (3 nodes): .group/msg nesting fix, ownership-split menus, More reactions dialog, dividers, virtualization, DM text formats
Session: sidechat/dom-messages
This commit is contained in:
+109
-20
@@ -5,6 +5,11 @@ 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`.
|
||||
|
||||
---
|
||||
@@ -25,6 +30,27 @@ Selector:
|
||||
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):
|
||||
|
||||
```
|
||||
@@ -153,7 +179,41 @@ b.dispatchEvent(new MouseEvent('click', opts));
|
||||
- `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)
|
||||
### 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**
|
||||
|
||||
@@ -161,16 +221,19 @@ b.dispatchEvent(new MouseEvent('click', opts));
|
||||
<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.
|
||||
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 (no separate picker UI exists)
|
||||
**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">
|
||||
```
|
||||
|
||||
Six buttons, each:
|
||||
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"
|
||||
@@ -186,14 +249,16 @@ Six buttons, each:
|
||||
| `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"]') // 6 buttons
|
||||
menu.querySelectorAll('button[aria-label^="React"]') // 7 buttons, other's menu only
|
||||
```
|
||||
|
||||
- `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.
|
||||
- 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**
|
||||
|
||||
@@ -275,20 +340,34 @@ Facts:
|
||||
|
||||
### 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`.
|
||||
- **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
|
||||
|
||||
- Own messages: `<span class="sr-only">You:</span>` — **screen-reader-only**,
|
||||
first child of `div.hatch-chat-groupable-bubble`. Not visually rendered.
|
||||
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:
|
||||
msg.querySelector('span.sr-only')?.innerText.trim() === 'You:'
|
||||
// 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
|
||||
@@ -354,9 +433,19 @@ const hasDraft = /Replying to/.test(document.body.innerText);
|
||||
|
||||
---
|
||||
|
||||
## 8. Open questions
|
||||
## 8. Open questions (updated 2026-10-04)
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
+95
-7
@@ -5,6 +5,11 @@ 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.
|
||||
@@ -174,20 +179,46 @@ div[data-message-id] ← one per message, class "flex flex-col gap-2"
|
||||
Selector for all messages:
|
||||
|
||||
```js
|
||||
document.querySelectorAll('[data-message-id]')
|
||||
document.querySelectorAll('div[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"):
|
||||
**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: <full text>" / "Assistant message: <full text>"
|
||||
```
|
||||
|
||||
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 `<article>` / `<time>` / `data-testid`-containing-"message"
|
||||
elements, and no *visible* author name on either message type.
|
||||
|
||||
Author is encoded in the id prefix:
|
||||
|
||||
| Author | `data-message-id` format | `innerText` prefix |
|
||||
|-----------|--------------------------------|--------------------------|
|
||||
| assistant | `assistant-msg-<uuid>` | `Assistant message: ...` |
|
||||
| user | `<uuid>` (bare, no prefix) | `User 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.
|
||||
The `User message: ...` prefix **is** observed (verified 2026-10-04 on pip's
|
||||
main chat); id-prefix detection remains the most robust signal.
|
||||
|
||||
⚠️ `data-message-id` is **not messages-only**: non-message cards share the
|
||||
attribute, e.g. `data-message-id="presentation-carrier:8917…"` (media/
|
||||
presentation card, empty innerText). For real messages filter to
|
||||
`/^[0-9a-f-]{36}$/` (user) or `/^assistant-msg-/` (assistant).
|
||||
|
||||
### Extracting text / author
|
||||
|
||||
@@ -212,6 +243,63 @@ 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.
|
||||
|
||||
### Virtualization (verified 2026-10-04)
|
||||
|
||||
The message list is **virtualized**: only viewport-near messages exist in the
|
||||
DOM. Observed `[data-message-id]` counts on live main chats: 5, 33, 40, 110
|
||||
across nodes and moments on the same threads. Consequences for automation:
|
||||
|
||||
- DOM readback is **windowed** — you only see what's rendered. Scroll
|
||||
`#hatch-chat-scroll` to load older messages; re-query after scrolling.
|
||||
- Never assert "message absent" from a single DOM snapshot; assert
|
||||
"not in rendered window".
|
||||
- `.group/msg` count always equals the rendered message count (every rendered
|
||||
message gets the interactive wrapper + its `More options` rail button —
|
||||
verified rails == groups on all three nodes).
|
||||
|
||||
### Date / time dividers
|
||||
|
||||
`div[role="log"]` children are not all messages. Date and time dividers are
|
||||
direct children of the log, interspersed between `div[data-message-id]`:
|
||||
|
||||
```html
|
||||
<!-- day boundary -->
|
||||
<div class="pt-10 pb-4 text-center">
|
||||
<span class="text-caption-1 text-text-secondary">Oct 3, 11:41 PM</span>
|
||||
</div>
|
||||
<!-- intra-day gap -->
|
||||
<div class="py-4 text-center">
|
||||
<span class="text-caption-1 text-text-secondary">2:38 AM</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
- Day divider: `div.pt-10.pb-4.text-center`, text `Oct 3, 11:41 PM`.
|
||||
- Time divider: `div.py-4.text-center`, text `2:38 AM` (bare 12h time).
|
||||
- Zero `<time>` elements anywhere in the message DOM (verified on all nodes).
|
||||
|
||||
### Truncation
|
||||
|
||||
No truncation observed: a 2110-char message rendered in full, and no
|
||||
"show more" / "expand" / "read more" buttons were found in any inspected
|
||||
thread. Long messages are fully present in `innerText`.
|
||||
|
||||
### DMs vs chat messages
|
||||
|
||||
**No structural difference.** DMs arrive as ordinary user messages in main
|
||||
chat; sender attribution is text-only, with two formats observed live:
|
||||
|
||||
- Legacy: `[from opm] [7fce46e0] <text>`
|
||||
- Signed: `[from:operator-main] [id:fd553ef5] <text>` (+ `-----BEGIN SSH SIGNATURE-----` block)
|
||||
|
||||
Anything parsing DMs must match the text, not the markup. (Textual `[from:X]`
|
||||
is unauthenticated — see DM trust notes elsewhere.)
|
||||
|
||||
### Unread signals
|
||||
|
||||
- `hatch-nav-chat` aria-label: `Chat, N notifications` (already documented).
|
||||
- Page title prefix: `(1) Chat — <agent>` — the count also appears in
|
||||
`document.title` (observed `(1)` on pip).
|
||||
|
||||
### Message-related testids seen in the wild
|
||||
|
||||
| testid | Meaning | Present when |
|
||||
|
||||
Reference in New Issue
Block a user