` (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.
+
+### Extracting text / author
+
+```js
+const msgs = [...document.querySelectorAll('[data-message-id]')].map(m => {
+ const id = m.getAttribute('data-message-id');
+ return {
+ id,
+ author: id.startsWith('assistant-msg') ? 'assistant' : 'user',
+ text: m.innerText
+ };
+});
+```
+
+### Simple read-back (what `muse-chat-api.py messages` does)
+
+```js
+[...document.querySelectorAll('p')].slice(-n * 2).map(p => p.innerText.slice(0, width))
+```
+
+Works because message bodies render as `` elements; the last `2n`
+paragraphs cover roughly the last `n` messages. Crude but effective for
+"did my text land?" verification.
+
+### Message-related testids seen in the wild
+
+| testid | Meaning | Present when |
+|---------------------------------------|--------------------------------------|--------------|
+| `hatch-chat-typing-indicator` | assistant is typing/generating | generating only |
+| `hatch-composer-stop-button` | stop-generation button | generating only |
+| `hatch-composer-placeholder-overlay` | "Message" ghost text | composer empty |
+| `hatch-chat-switcher-unread-indicator`| unread badge on the chat switcher | unread chats exist |
+
+---
+
+## 4. Chat activation button (speech-bubble / nav)
+
+This is the dock-rail button that activates the Chat section. The user's
+finding: sidechat create only works when the chat function is active, and
+this button (not Ctrl+J) is the reliable activator.
+
+### Selector
+
+```js
+document.querySelector('[data-testid="hatch-nav-chat"]')
+```
+
+### Observed attributes
+
+| Attribute | Value |
+|----------------|------------------------------------------------------------|
+| tag | `A` (anchor, not button) |
+| `data-testid` | `hatch-nav-chat` |
+| `href` | `/` |
+| `aria-label` | dynamic: `Chat` or `Chat, N notifications` (e.g. `Chat, 2 notifications`) |
+| `aria-current` | `page` when Chat is the active section (absent otherwise) |
+| visible | `true` (dock rail is always rendered) |
+
+### Parent chain (4 levels)
+
+```
+a[data-testid="hatch-nav-chat"][aria-label="Chat, 2 notifications"][aria-current="page"]
+└── div
+ └── div
+ └── div
+ └── div[data-testid="hatch-dock-rail"]
+```
+
+### States
+
+| State | Signal |
+|-------|--------|
+| Chat section active | `aria-current="page"` present |
+| Chat section inactive | `aria-current` absent (button still visible/clickable) |
+| Unread messages | `aria-label` contains `N notifications` |
+
+### Usage
+
+```js
+// Activate chat reliably (replaces Ctrl+J):
+document.querySelector('[data-testid="hatch-nav-chat"]').click();
+```
+
+`cmd_sidechat_main` already falls back to this click when the "Main chat"
+element lookup fails. Sibling nav buttons in the same rail:
+`hatch-nav-search` (Search), plus `div[role="button"]` items for Feed, Ideas,
+Goals, Library, and `hatch-dock-more` (Settings).
+
+---
+
+## 5. Loading / empty states
+
+| State | How to detect |
+|-------|---------------|
+| New empty thread (`/thread/new`) | URL is `/thread/new`, `document.title === "Muse"`, zero `[data-message-id]` elements, composer present |
+| Main chat loaded | URL is `/`, title is `Chat — `, `[data-message-id]` elements present |
+| Assistant generating | `[data-testid="hatch-chat-typing-indicator"]` exists; send button replaced by `[data-testid="hatch-composer-stop-button"]` |
+| Idle, ready to send | typing indicator absent, stop button absent, `textarea[aria-label="Message"]` visible and enabled |
+| Composer empty | `[data-testid="hatch-composer-placeholder-overlay"]` visible with text `Message` |
+| Side panel open (threads visible) | `[data-testid="hatch-chat-compose"]` ("New side chat") present |
+
+### Page-title signals
+
+| Title | Meaning |
+|-------|---------|
+| `Chat — ` | logged in, chat UI loaded |
+| `Muse` | partial load or `/thread/new` stripped state — treat as not-ready, navigate to `/` first |
+
+---
+
+## Quick-reference selector list
+
+```js
+// Composer
+'textarea[aria-label="Message"]'
+'[data-testid="hatch-composer-placeholder-overlay"]' // empty-state ghost text
+
+// Send / stop
+'button[aria-label="Send"]' // via aria-label includes 'send'
+'[data-testid="hatch-composer-stop-button"]' // while generating
+
+// Messages
+'#hatch-chat-scroll' // scroll container
+'div[role="log"][aria-label="Chat messages"]' // list
+'[data-message-id]' // each message
+'[data-message-id^="assistant-msg"]' // assistant messages only
+
+// Chat activation
+'[data-testid="hatch-nav-chat"]' // dock-rail Chat link
+'[data-testid="hatch-chat-typing-indicator"]' // generating indicator
+```
+
+## Caveats
+
+- Selectors observed on the `opm` account; class names are Tailwind-ish and
+ may shift with deploys — prefer `data-testid`, `aria-label`, `role`, and
+ `id` selectors over classes.
+- `aria-label` on `hatch-nav-chat` embeds a live notification count; match
+ with `startsWith('Chat')` or the testid, never exact equality.
+- During this capture another agent was driving the same browser; page URL
+ flipped between `/` and `/thread/new` between probes. Element identities
+ above were each verified at least once against the live DOM.