DOM reference: page structure, states, and approvals\n\n- 538-line map of 5 page states with detection logic\n- 57 data-testid values inventoried and categorized\n- check_approvals IP filtering documented
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
# muse.ai DOM Page Structure Reference
|
||||
|
||||
**Generated:** 2026-10-04 via live CDP inspection (opm browser, warp-opm netns, CDP 9440)
|
||||
**Purpose:** Reference for future automation development. All selectors verified against live DOM.
|
||||
|
||||
---
|
||||
|
||||
## 1. Page States / Modes
|
||||
|
||||
The muse.ai SPA has distinct DOM states. **Detect state before acting** — selectors valid in one state are absent in another.
|
||||
|
||||
### 1.1 Main Chat
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| URL | `https://muse.ai/` |
|
||||
| Title | `Chat — <agent-name>` (e.g., `Chat — operator-main`) |
|
||||
| Switcher `[data-testid="hatch-chat-switcher-trigger"]` | ✅ Present |
|
||||
| Compose `[data-testid="hatch-chat-compose"]` | ❌ Absent (panel closed) |
|
||||
| Panel content `[data-testid="hatch-side-chats-panel-content"]` | ❌ Absent (panel closed) |
|
||||
| Panel shell `[data-testid="hatch-side-chats-panel-shell"]` | ✅ Present (empty/collapsed) |
|
||||
| Message input `[aria-label="Message"]` | ✅ Present, visible |
|
||||
|
||||
**Detection:**
|
||||
```javascript
|
||||
// Reliable main-chat detection
|
||||
const isMainChat =
|
||||
window.location.href === 'https://muse.ai/' &&
|
||||
document.title.startsWith('Chat —') &&
|
||||
!!document.querySelector('[data-testid="hatch-chat-switcher-trigger"]');
|
||||
```
|
||||
|
||||
### 1.2 Panel Open (Chat Navigation Panel Visible)
|
||||
|
||||
Reached by clicking the switcher on main chat. This is the **required precondition** for sidechat creation.
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| URL | `https://muse.ai/` (unchanged) |
|
||||
| Title | `Chat — <agent-name>` (unchanged) |
|
||||
| Switcher | ✅ Present |
|
||||
| Compose `[data-testid="hatch-chat-compose"]` | ✅ Present **and visible** |
|
||||
| Panel content | ✅ Present |
|
||||
| Thread rows `[data-testid="hatch-thread-row"]` | ✅ Present (26 rows observed) |
|
||||
|
||||
**Detection:**
|
||||
```javascript
|
||||
// Panel is open and ready for sidechat creation
|
||||
const isPanelOpen =
|
||||
!!document.querySelector('[data-testid="hatch-side-chats-panel-content"]') &&
|
||||
!!document.querySelector('[data-testid="hatch-chat-compose"]');
|
||||
|
||||
// Verify compose button is actually clickable
|
||||
const composeBtn = document.querySelector('[data-testid="hatch-chat-compose"]');
|
||||
const isClickable = composeBtn && composeBtn.getBoundingClientRect().width > 0;
|
||||
```
|
||||
|
||||
**Reliable open sequence:**
|
||||
```javascript
|
||||
// 1. Ensure on main chat (Ctrl+J via CDP Input.dispatchKeyEvent)
|
||||
// 2. Check if panel already open
|
||||
if (!document.querySelector('[data-testid="hatch-chat-compose"]')) {
|
||||
// 3. Click switcher to open panel
|
||||
document.querySelector('[data-testid="hatch-chat-switcher-trigger"]').click();
|
||||
// 4. Wait 2s, re-check for compose button
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 `/thread/new` (Fresh Sidechat / Poisoned State)
|
||||
|
||||
**This is the "poisoned" state.** After creating a sidechat, the browser lands here with a **stripped DOM** that lacks the switcher and compose button. The next sidechat create will fail unless you navigate back to main first.
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| URL | `https://muse.ai/thread/new` |
|
||||
| Title | `Muse` (just "Muse", **not** "Chat — ...") |
|
||||
| Switcher | ❌ **Absent** |
|
||||
| Compose | ❌ **Absent** |
|
||||
| Panel shell | ✅ Present but stripped (no panel-content) |
|
||||
| Message input | ✅ Present |
|
||||
| Button count | ~45 (vs ~21 on partially-loaded states) |
|
||||
|
||||
**Detection:**
|
||||
```javascript
|
||||
const isThreadNew =
|
||||
window.location.href.includes('/thread/new') ||
|
||||
(document.title === 'Muse' &&
|
||||
!document.querySelector('[data-testid="hatch-chat-switcher-trigger"]'));
|
||||
```
|
||||
|
||||
**Recovery:** Send Ctrl+J via CDP to return to main chat, then open panel via switcher click.
|
||||
|
||||
**Distinct testid set** (no chat-switcher testids, dock-rail based):
|
||||
```
|
||||
hatch-dock-rail, hatch-dock-rail-divider, hatch-nav-chat,
|
||||
hatch-dock-chat-notification-badge, hatch-nav-search,
|
||||
hatch-nav-system-item-*, app-download-dock-mark, hatch-dock-more,
|
||||
hatch-side-chats-open-drag-handle, hatch-side-chats-panel-shell,
|
||||
hatch-chat-nav-fade, hatch-chat-leading-controls,
|
||||
hatch-status-panel-sliding-surface, hatch-composer-placeholder-overlay,
|
||||
hatch-browser-task-banner-host
|
||||
```
|
||||
|
||||
### 1.4 Sidechat Thread (`/thread/<uuid>`)
|
||||
|
||||
A real sidechat with a UUID. DOM structure is similar to main chat (switcher present, panel available). Thread rows in the panel show the sidechat title.
|
||||
|
||||
**Note:** After sending the first message in a `/thread/new` chat, the URL transitions to `/thread/<uuid>`. Poll `window.location.href` after send to capture the real thread ID.
|
||||
|
||||
### 1.5 Landing Page (Not Logged In)
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Title | `muse.ai` (just the domain) |
|
||||
| Indicators | Body contains "Log in" button, no chat UI |
|
||||
|
||||
**Detection:**
|
||||
```javascript
|
||||
const isLanding = document.title === 'muse.ai';
|
||||
// or
|
||||
const isLoggedOut = document.body.innerText.includes('Log in') &&
|
||||
!document.querySelector('[data-testid="hatch-chat-switcher-trigger"]');
|
||||
```
|
||||
|
||||
### 1.6 State Summary Table
|
||||
|
||||
| State | URL | Title | Switcher | Compose | Panel Content |
|
||||
|---|---|---|---|---|---|
|
||||
| Main chat | `muse.ai/` | `Chat — ...` | ✅ | ❌ | ❌ |
|
||||
| Panel open | `muse.ai/` | `Chat — ...` | ✅ | ✅ | ✅ |
|
||||
| `/thread/new` | `muse.ai/thread/new` | `Muse` | ❌ | ❌ | ❌ |
|
||||
| `/thread/<uuid>` | `muse.ai/thread/<uuid>` | `Chat — ...` | ✅ | ❌ | ❌ |
|
||||
| Landing | `muse.ai/` | `muse.ai` | ❌ | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 2. Key Selectors
|
||||
|
||||
### 2.1 Chat Switcher (Panel Toggle)
|
||||
|
||||
```javascript
|
||||
document.querySelector('[data-testid="hatch-chat-switcher-trigger"]')
|
||||
```
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| Tag | `BUTTON` |
|
||||
| `data-testid` | `hatch-chat-switcher-trigger` |
|
||||
| Inner text | `Chats\nOpen chat and side chats` (or `Chats\nUnread chats\nOpen chat and side chats` when unreads exist) |
|
||||
| `aria-expanded` | Not used (always `null`) |
|
||||
| Class (partial) | `bg-fill-blur-thick shadow-blur-elevation-01 text-body-medium` |
|
||||
|
||||
**Parent chain** (3-4 levels):
|
||||
```
|
||||
BUTTON[data-testid="hatch-chat-switcher-trigger"]
|
||||
└── DIV[data-testid="hatch-chat-leading-controls"] (cls: pointer-events-auto absolute start-3)
|
||||
└── DIV (cls: pointer-events-none absolute inset-x-0)
|
||||
└── DIV (cls: absolute inset-0 overflow-hidden)
|
||||
└── DIV (cls: relative flex min-h-0)
|
||||
```
|
||||
|
||||
**Visibility:** Present on main chat and `/thread/<uuid>`. **Absent** on `/thread/new` and landing page.
|
||||
|
||||
**Note:** The old detection `document.body.innerText.includes('Side chats')` is **invalid** — it matches the switcher's "Open chat and side chats" text and DM content mentioning "Side chats". Use the exact header check or `hatch-side-chats-panel-content` presence instead.
|
||||
|
||||
### 2.2 Compose Button (New Side Chat `+`)
|
||||
|
||||
```javascript
|
||||
document.querySelector('[data-testid="hatch-chat-compose"]')
|
||||
```
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| Tag | `BUTTON` |
|
||||
| `data-testid` | `hatch-chat-compose` |
|
||||
| `aria-label` | `New side chat` |
|
||||
| `title` | `New side chat` |
|
||||
| Inner text | *(empty — SVG icon only)* |
|
||||
| Class (partial) | `inline-flex shrink-0 cursor-pointer` |
|
||||
|
||||
**Parent chain:**
|
||||
```
|
||||
BUTTON[data-testid="hatch-chat-compose"]
|
||||
└── SPAN (cls: contents)
|
||||
└── DIV (cls: group/nav-section flex w-full)
|
||||
└── SECTION (cls: flex flex-col)
|
||||
└── DIV (cls: flex flex-col relative)
|
||||
```
|
||||
|
||||
**Visibility conditions:**
|
||||
- ✅ Present and visible **only when panel is open**
|
||||
- ❌ Absent when panel is closed (even on main chat)
|
||||
- ❌ Absent on `/thread/new` (stripped DOM)
|
||||
- ❌ Absent on landing page
|
||||
|
||||
**Critical:** This button contains only an SVG icon. Do NOT search by text `"+"` — it will never match.
|
||||
|
||||
### 2.3 Sidechat Panel Shell
|
||||
|
||||
```javascript
|
||||
document.querySelector('[data-testid="hatch-side-chats-panel-shell"]')
|
||||
```
|
||||
|
||||
Container for the entire chat navigation panel. Present in most states but may be empty/collapsed.
|
||||
|
||||
**Children when panel is open:**
|
||||
```
|
||||
hatch-side-chats-panel-sliding-surface
|
||||
hatch-side-chats-panel-content
|
||||
hatch-chat-search-toolbar
|
||||
hatch-chat-search-field
|
||||
hatch-side-chats-options
|
||||
hatch-thread-row (× N)
|
||||
hatch-chat-compose
|
||||
hatch-side-chat-unread-indicator
|
||||
```
|
||||
|
||||
### 2.4 Sidechat Panel Content
|
||||
|
||||
```javascript
|
||||
document.querySelector('[data-testid="hatch-side-chats-panel-content"]')
|
||||
```
|
||||
|
||||
**This is the reliable "panel is open" indicator.** When present, the compose button should also be present.
|
||||
|
||||
### 2.5 Thread Rows (Chat List Items)
|
||||
|
||||
```javascript
|
||||
document.querySelectorAll('[data-testid="hatch-thread-row"]')
|
||||
```
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| Tag | `DIV` |
|
||||
| `data-testid` | `hatch-thread-row` |
|
||||
| `role` | `button` |
|
||||
| `tabindex` | `0` |
|
||||
| Active row | `aria-current="page"` |
|
||||
| Class (partial) | `group/nav-row relative flex h-9 w-full shrink-0 items-center` |
|
||||
|
||||
**Not links** — they are `div[role="button"]` with click handlers. No `href`. To navigate, click the row.
|
||||
|
||||
**Text format:** `<title>\n<unread-indicator>\n<timestamp>` (e.g., `"Main chat\n\njust now"`)
|
||||
|
||||
**Exact header check** (reliable "Side chats" panel header detection):
|
||||
```javascript
|
||||
// Find leaf element with exact text "Side chats"
|
||||
const header = [...document.querySelectorAll('*')].find(e =>
|
||||
e.children.length === 0 && (e.innerText || '').trim() === 'Side chats'
|
||||
);
|
||||
// Returns SPAN element when panel is open, null otherwise
|
||||
```
|
||||
|
||||
### 2.6 Message Input (Composer)
|
||||
|
||||
```javascript
|
||||
document.querySelector('[aria-label="Message"]')
|
||||
```
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| Tag | `TEXTAREA` |
|
||||
| `aria-label` | `Message` |
|
||||
| Class (partial) | `text-text-primary block max-h-48` |
|
||||
|
||||
**Parent chain:**
|
||||
```
|
||||
TEXTAREA[aria-label="Message"]
|
||||
└── DIV (cls: relative flex min-h-8)
|
||||
└── DIV (cls: flex shrink-0 flex-col)
|
||||
└── DIV (cls: backdrop-blur-elevation-01 bg-fill-blur-thick shadow-blur-elevation-01)
|
||||
```
|
||||
|
||||
### 2.7 "Main Chat" Navigation Button
|
||||
|
||||
To navigate to main chat from a sidechat, find the button with **exact** text `"Main chat"`:
|
||||
|
||||
```javascript
|
||||
const mainBtn = [...document.querySelectorAll('button, [role="button"]')]
|
||||
.find(e => (e.innerText || '').trim() === 'Main chat');
|
||||
```
|
||||
|
||||
**Must use exact match** (`=== 'Main chat'`), not substring — thread rows may contain "Main chat" as part of longer text.
|
||||
|
||||
**Alternative:** Ctrl+J via CDP `Input.dispatchKeyEvent` (proven reliable for main-chat navigation):
|
||||
```javascript
|
||||
// Via CDP websocket:
|
||||
{"id": 30, "method": "Input.dispatchKeyEvent",
|
||||
"params": {"type": "keyDown", "key": "j", "code": "KeyJ", "ctrlKey": True, "modifiers": 2}}
|
||||
{"id": 31, "method": "Input.dispatchKeyEvent",
|
||||
"params": {"type": "keyUp", "key": "j", "code": "KeyJ", "ctrlKey": True, "modifiers": 2}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Approval / Permission Dialogs
|
||||
|
||||
### 3.1 How `check_approvals` Works
|
||||
|
||||
Location: `/home/super/Projects/NetVM/bin/muse-chat-api.py`, `check_approvals(ws)`
|
||||
|
||||
**Detection logic:**
|
||||
1. Scans `document.body.innerText` for `"Allow"` + `"to share"` pattern
|
||||
2. Finds elements containing both strings with text length < 500 chars
|
||||
3. Fallback: looks for buttons with text containing `allow`/`deny`/`block` (if ≥2 found and no dialogs yet)
|
||||
|
||||
**Critical filter** (added 2026-10-04 to fix false positives):
|
||||
```python
|
||||
# Only block for IP-based permission dialogs.
|
||||
# Dialogs without IPs are likely false positives (chat content, UI text).
|
||||
ips = re.findall(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', dialog_text)
|
||||
if not ips:
|
||||
continue # Skip — don't block
|
||||
```
|
||||
|
||||
**Trusted IPs** (auto-approve "Allow"):
|
||||
```python
|
||||
TRUSTED_IPS = {
|
||||
"34.139.37.135", # VM (gateway)
|
||||
"100.123.153.75", # bl (main compute)
|
||||
"100.81.31.9", # VM tailnet
|
||||
}
|
||||
```
|
||||
|
||||
**Auto-approve action:** Clicks button with text `"allow once"` or exact `"allow"`.
|
||||
|
||||
**Exit code 2** with `APPROVAL_NEEDED` if untrusted IP dialog is found.
|
||||
|
||||
### 3.2 Dialog Selectors
|
||||
|
||||
No `[role="dialog"]` or `[role="alertdialog"]` elements were observed in the current DOM state. Permission dialogs appear as overlay elements when triggered (e.g., by download attempts, screen sharing requests).
|
||||
|
||||
**Patterns to watch for:**
|
||||
```javascript
|
||||
// Dialog containers (when present)
|
||||
'[role="dialog"]'
|
||||
'[role="alertdialog"]'
|
||||
'[data-testid*="dialog"]'
|
||||
'[data-testid*="approval"]'
|
||||
'[data-testid*="permission"]'
|
||||
|
||||
// Permission buttons (when dialog is active)
|
||||
'button' // with text: /allow|deny|block/i
|
||||
```
|
||||
|
||||
**Known false positive:** Task/checklist buttons in chat content may contain "Confirm" in their text (e.g., "Confirm 03:45 heartbeat in main thread"). The IP filter prevents these from blocking automation.
|
||||
|
||||
### 3.3 React DOM Selectors
|
||||
|
||||
Per AGENTS.md (2026-10-03): *"The Muse platform is a React app. DOM selectors for dialogs exist identically in headful and headless environments."* The `check_approvals` selectors work in both.
|
||||
|
||||
---
|
||||
|
||||
## 4. Overlays and Interaction Blockers
|
||||
|
||||
### 4.1 Observed Overlays
|
||||
|
||||
One fixed-position overlay with `z-index: 26` was observed:
|
||||
```javascript
|
||||
{
|
||||
tag: "DIV",
|
||||
id: null,
|
||||
testid: null,
|
||||
role: null,
|
||||
z: "26",
|
||||
cls: "absolute start-0 top-0"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Potential Blockers
|
||||
|
||||
| Element | Impact |
|
||||
|---|---|
|
||||
| Permission dialogs | Block sends until approved/dismissed |
|
||||
| `hatch-browser-task-banner-host` | Browser task notification banner |
|
||||
| `hatch-composer-placeholder-overlay` | Composer placeholder (not a blocker) |
|
||||
| `hatch-status-panel-sliding-surface` | Status panel (may overlay content) |
|
||||
|
||||
**Detection:**
|
||||
```javascript
|
||||
const blockers = [...document.querySelectorAll('*')].filter(e => {
|
||||
const cs = getComputedStyle(e);
|
||||
const r = e.getBoundingClientRect();
|
||||
return (cs.position === 'fixed' || cs.position === 'absolute') &&
|
||||
r.width > 100 && r.height > 100 &&
|
||||
parseInt(cs.zIndex || '0') > 10;
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. URL Patterns
|
||||
|
||||
| URL Pattern | Meaning | DOM State |
|
||||
|---|---|---|
|
||||
| `https://muse.ai/` | Main chat | Full DOM, switcher present |
|
||||
| `https://muse.ai/thread/new` | New/unsent sidechat | **Stripped DOM**, no switcher/compose |
|
||||
| `https://muse.ai/thread/<uuid>` | Existing sidechat | Full DOM, switcher present |
|
||||
|
||||
**UUID format:** Standard UUID v4 (e.g., `550e8400-e29b-41d4-a716-446655440000`).
|
||||
|
||||
**Navigation notes:**
|
||||
- After `sidechat create`, URL goes to `/thread/new` (placeholder)
|
||||
- After first message send, URL transitions to `/thread/<uuid>` (real ID)
|
||||
- Poll `window.location.href` after send to capture the real thread ID
|
||||
- `/thread/new` **poisons** subsequent creates — always navigate to main first
|
||||
|
||||
---
|
||||
|
||||
## 6. Full `data-testid` Inventory
|
||||
|
||||
57 unique `data-testid` values observed on main chat with panel open:
|
||||
|
||||
### Chat Core
|
||||
```
|
||||
hatch-chat-switcher-trigger # Panel toggle button
|
||||
hatch-chat-switcher-icon-knockout
|
||||
hatch-chat-switcher-navigation-icon
|
||||
hatch-chat-compose # New side chat (+) button
|
||||
hatch-chat-leading-controls # Switcher container
|
||||
hatch-chat-nav-fade
|
||||
hatch-chat-search-field # Panel search input
|
||||
hatch-chat-search-toolbar
|
||||
```
|
||||
|
||||
### Sidechat Panel
|
||||
```
|
||||
hatch-side-chats-panel-shell
|
||||
hatch-side-chats-panel-sliding-surface
|
||||
hatch-side-chats-panel-content
|
||||
hatch-side-chats-options
|
||||
hatch-side-chat-unread-indicator
|
||||
hatch-thread-row # Individual chat rows (× N)
|
||||
active-side-chat-load-more-sentinel
|
||||
```
|
||||
|
||||
### Navigation Dock
|
||||
```
|
||||
hatch-dock-rail
|
||||
hatch-dock-more
|
||||
hatch-nav-chat
|
||||
hatch-nav-search
|
||||
hatch-nav-system-item-66656564 # (hex-encoded IDs)
|
||||
hatch-nav-system-item-676f616c73
|
||||
hatch-nav-system-item-6964656173
|
||||
hatch-nav-system-item-6c696272617279
|
||||
hatch-panel-resize-divider-line
|
||||
```
|
||||
|
||||
### Status/Other
|
||||
```
|
||||
hatch-status-panel-close
|
||||
hatch-status-panel-close-drag-handle
|
||||
hatch-status-panel-close-drag-handle-divider-line
|
||||
hatch-status-panel-close-toolbar
|
||||
hatch-status-panel-sliding-surface
|
||||
hatch-composer-placeholder-overlay
|
||||
hatch-browser-task-banner-host
|
||||
hatch-invite-friends-button
|
||||
app-download-dock-mark
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Reliable Automation Sequences
|
||||
|
||||
### 7.1 Sidechat Creation (Robust)
|
||||
|
||||
```python
|
||||
# 1. Navigate to main chat (recover from any state)
|
||||
send_ctrl_j() # CDP Input.dispatchKeyEvent
|
||||
time.sleep(3)
|
||||
|
||||
# 2. Verify we're on main chat
|
||||
assert window.location.href == 'https://muse.ai/'
|
||||
assert document.querySelector('[data-testid="hatch-chat-switcher-trigger"]')
|
||||
|
||||
# 3. Open panel if compose button not present
|
||||
if not document.querySelector('[data-testid="hatch-chat-compose"]'):
|
||||
document.querySelector('[data-testid="hatch-chat-switcher-trigger"]').click()
|
||||
time.sleep(2)
|
||||
|
||||
# 4. Verify compose button is present AND visible
|
||||
btn = document.querySelector('[data-testid="hatch-chat-compose"]')
|
||||
assert btn and btn.getBoundingClientRect().width > 0
|
||||
|
||||
# 5. Click to create
|
||||
btn.click()
|
||||
|
||||
# 6. Poll for /thread/ URL (up to 15s)
|
||||
# 7. Send message directly (browser is on the new sidechat)
|
||||
# 8. Poll URL for /thread/<uuid> to capture real thread ID
|
||||
```
|
||||
|
||||
### 7.2 State Detection (One-Liner)
|
||||
|
||||
```javascript
|
||||
(() => {
|
||||
const url = window.location.href;
|
||||
const title = document.title;
|
||||
const hasSwitcher = !!document.querySelector('[data-testid="hatch-chat-switcher-trigger"]');
|
||||
const hasCompose = !!document.querySelector('[data-testid="hatch-chat-compose"]');
|
||||
const hasPanelContent = !!document.querySelector('[data-testid="hatch-side-chats-panel-content"]');
|
||||
|
||||
if (title === 'muse.ai') return 'LANDING';
|
||||
if (url.includes('/thread/new')) return 'THREAD_NEW';
|
||||
if (url.includes('/thread/')) return hasSwitcher ? 'SIDECHAT' : 'THREAD_NEW_STRIPPED';
|
||||
if (url === 'https://muse.ai/') {
|
||||
if (!hasSwitcher) return 'UNKNOWN_NO_SWITCHER';
|
||||
if (hasCompose && hasPanelContent) return 'MAIN_PANEL_OPEN';
|
||||
return 'MAIN_PANEL_CLOSED';
|
||||
}
|
||||
return 'UNKNOWN';
|
||||
})()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Body Landmarks (Top-Level Structure)
|
||||
|
||||
```html
|
||||
<body>
|
||||
<div> <!-- React root (has_react: true) -->
|
||||
<!-- App content -->
|
||||
</div>
|
||||
<iframe class="hidden"> × 4 <!-- Hidden utility iframes -->
|
||||
<script id="hatch-early-gateway-bootstrap">
|
||||
<div class="min-w-0 flex group/sidebar-wrapper min-h-svh"> <!-- Sidebar wrapper -->
|
||||
<section aria-label="Notifications alt+T">
|
||||
<script> × N <!-- App scripts -->
|
||||
</body>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Change Log
|
||||
|
||||
- **2026-10-04:** Initial mapping via live CDP inspection. Documented 5 page states, 57 testids, approval dialog logic, and the `/thread/new` poisoning behavior.
|
||||
Reference in New Issue
Block a user