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:
dom-inspector-2
2026-10-04 04:28:21 +00:00
parent bbe1ebb76a
commit c50667b626
2 changed files with 204 additions and 27 deletions
+95 -7
View File
@@ -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 |