Files
box/docs/DOM-SEARCH.md
T

300 lines
11 KiB
Markdown
Raw 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: Search (`docs/DOM-SEARCH.md`)
> **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.
Muse.ai global search (quick-search / command palette). Inspected live on bl via CDP
against the opm browser (warp-opm netns, CDP 127.0.0.1:9440) on 2026-10-04.
All selectors verified against the live DOM.
Re-verified end-to-end 2026-10-04 (open → type → results → empty state → Escape).
Related: `docs/DOM-CHAT-PANEL.md` (chat panel), `docs/DOM-MESSAGES.md`,
`docs/DOM-PAGE-STRUCTURE.md`.
---
## 1. Search trigger — nav rail button
```javascript
document.querySelector('[data-testid="hatch-nav-search"]')
```
| Property | Value |
|---|---|
| Tag | `BUTTON` |
| `data-testid` | `hatch-nav-search` |
| `aria-label` | `"Search"` (stable — unlike `hatch-nav-chat`, no notification count) |
| Inner text | none (icon-only, SVG magnifier) |
| Geometry | x:0, y:136, 72×44 (left nav rail, second button) |
| Visibility | always present and visible on chat pages |
Parent chain (4 levels):
```
BUTTON[data-testid="hatch-nav-search"].group.relative.flex
└── DIV.hatch-dock-reveal-item
└── DIV.my-auto.flex.shrink-0
└── DIV.scrollbar-hide.flex.min-h-0 (nav rail container)
```
Nav rail button order (siblings, via `closest("div.my-auto")`):
```
hatch-nav-chat
hatch-nav-search <-- search
hatch-nav-system-item-66656564 ("feed", hex)
hatch-nav-system-item-6964656173 ("ideas", hex)
hatch-nav-system-item-676f616c73 ("goals", hex)
hatch-nav-system-item-6c696272617279 ("library", hex)
```
### Opening the dialog
| Method | Result |
|---|---|
| Real mouse click on the button (CDP `Input.dispatchMouseEvent` press+release at button center) | **Reliable — opens every time** |
| `Ctrl+K` via CDP `Input.dispatchKeyEvent` | Opens (verified multiple times; one flaky miss observed — prefer mouse click) |
| JS `button.click()` | **Unreliable** — Radix trigger appears to require trusted events; often silently does nothing |
---
## 2. Search dialog — quick-search frame
Opens as a Radix popover portal (direct child of a plain `DIV` under `BODY`):
```javascript
document.querySelector('[data-testid="hatch-quick-search-frame"]')
```
| Property | Value |
|---|---|
| Tag | `DIV` |
| `role` | `dialog` |
| `data-testid` | `hatch-quick-search-frame` |
| `data-slot` | `popover-content` |
| `data-state` | `"open"` when visible |
| `data-side` / `data-align` | `"bottom"` / `"center"` |
| `id` | `radix-_r_NN_` (unstable — generated per mount, do not match) |
| `tabindex` | `-1` |
Parent chain:
```
DIV#radix-_r_44_[data-testid="hatch-quick-search-frame"][role="dialog"]
└── DIV (Radix portal wrapper)
└── BODY
└── HTML
```
Lifecycle:
- **On close (Escape): the dialog is fully removed from the DOM** — `querySelector`
returns `null` (`ABSENT`), not merely hidden. Detection must handle absence, not
just `offsetParent`.
- No `PRESENT_HIDDEN` state was observed; it is either open or gone.
---
## 3. Search input
```javascript
document.querySelector('[data-testid="command-palette-search-input"]')
```
| Property | Value |
|---|---|
| Tag | `INPUT` `type="text"` |
| `data-testid` | `command-palette-search-input` |
| `data-slot` | `command-input` |
| `cmdk-input` | `""` (boolean attribute — this is a cmdk command palette) |
| `role` | `combobox` |
| `aria-label` | `"Search Muse"` |
| `placeholder` | `"Search"` |
| `aria-autocomplete` | `"list"` |
| `aria-expanded` | `"true"` while open |
| `aria-controls` | `radix-_r_NN_` (unstable) |
| `aria-activedescendant` | set to the highlighted item's id on ArrowDown |
| `autocomplete` / `autocorrect` / `spellcheck` | `off` / `off` / `false` |
Behavior:
- **Auto-focuses when the dialog opens** (verified: `document.activeElement === input`
immediately after open).
- **Do not touch the input via JS** (`focus()`, `click()`, native value setter):
synthetic interaction on the input **closes the dialog** (verified twice).
Type with trusted CDP key events instead (rawKeyDown → char → keyUp per char).
---
## 4. Results structure
### Listbox
```javascript
document.querySelector('[data-testid="hatch-quick-search-frame"] [role="listbox"]')
```
- `DIV[role="listbox"]`, cmdk-managed (class contains `cmdk-list-sizer`).
- Contains group headings + items.
### Result items
```javascript
document.querySelectorAll('[data-testid="hatch-quick-search-frame"] [data-testid="command-palette-item"]')
```
| Property | Value |
|---|---|
| Tag | `DIV` |
| `data-testid` | `command-palette-item` |
| `data-value` | **encodes the target** (see below) — the automation gold |
| Inner | `SPAN.bg-fill-secondary.flex` (icon, contains SVG) + `SPAN.flex.min-w-0` (text) |
| Text | thread title + snippet + relative time, `|`-separated |
### `data-value` formats (the important part)
| Prefix | Meaning | Example |
|---|---|---|
| `chat:main` | Main chat | `chat:main` |
| `chat:thread:<uuid>` | Sidechat thread — **the thread UUID is embedded** | `chat:thread:2e2c90a1-4b17-4590-b8ba-fb17d8018d36` |
| `chat:message:assistant-msg-<uuid>:<n>` | Individual assistant message match inside a thread | `chat:message:assistant-msg-b2a4e27d-4970-8b1e-8863-a96ed0aa1401:20420` |
| `chat:message:<uuid>:<n>` | Individual message match (non-assistant id form) | `chat:message:08a44cdd-91a7-408e-9073-743e7e15d50f:20890` |
Search matches **both threads and individual messages**. A query like `"heartbeat"`
returned 12 results on 2026-10-04 re-verify: 6 `chat:thread:*` + 6 `chat:message:*`,
mixing both message id forms.
This is a ready-made thread-lookup mechanism: search for a sidechat name, read
`data-value`, extract the UUID — no title-matching or URL polling needed.
### Full result-item markup (verified 2026-10-04)
```html
<div data-slot="command-item"
data-testid="command-palette-item"
id="radix-_r_1e9_" <!-- random per item per render — do not match -->
cmdk-item=""
role="option"
aria-disabled="false" aria-selected="true"
data-disabled="false" data-selected="true"
data-value="chat:thread:778fbbfa-3bf8-4163-bfe6-92c0f88657e8"
class="... h-12 items-center gap-2 p-1.5 ...">
<span class="bg-fill-secondary flex size-9 ...rounded-full">…svg icon…</span>
<span class="flex min-w-0">…title + snippet + relative time…</span>
</div>
```
Notes:
- The **first result is auto-highlighted** on render (`aria-selected="true"` /
`data-selected="true"`); the input's `aria-activedescendant` points at it.
Enter would activate it (navigation not tested).
- `data-value` is the stable automation hook; `id` is random per render.
---
## 5. Groups
Initial (empty query) view:
```javascript
document.querySelectorAll('[data-testid="hatch-quick-search-frame"] [cmdk-group-heading]')
// => ["Recents"]
```
- One group heading: `"Recents"` (6 items observed).
- **After typing a query, group headings disappear** (`headings: []`) — filtered
results render flat.
## 6. Filters / scopes
**None.** The dialog has no scope tabs, filter chips, or `role="tab"` elements:
```javascript
document.querySelectorAll('[data-testid="hatch-quick-search-frame"] [role="tab"], [data-testid*="tab"], [data-testid*="scope"], [data-testid*="filter"]')
// => []
```
Search is global over chats/messages with a single Recents group.
---
## 7. Empty / no-results state
For a query matching nothing (e.g. `"zzzznonexistentqueryzzzz"`):
- Dialog stays **open**, item count drops to `0`.
- Empty marker:
```html
<div data-slot="command-empty"
class="text-footnote text-text-secondary flex h-12 items-center justify-center p-1.5 text-center"
cmdk-empty=""
role="presentation">No results found</div>
```
```javascript
document.querySelector('[data-testid="hatch-quick-search-frame"] [cmdk-empty]')
// innerText === "No results found"
```
Detection: `querySelector('[cmdk-empty]')` non-null, or item count `=== 0`.
---
## 8. Keyboard shortcuts
| Keys | Action | Verified |
|---|---|---|
| `Ctrl+K` | Open search dialog | Yes (CDP key events; one flaky miss — mouse click preferred) |
| `Cmd+K` | (likely macOS equivalent) | Not isolated — dialog was already open in test |
| `Escape` | Close dialog (removes it from DOM) | Yes |
| `ArrowDown` / `ArrowUp` | Move highlight; sets `aria-activedescendant` on input | Yes (`aria-activedescendant="radix-_r_19o_"`) |
| `Enter` | Activate highlighted result (navigate) | Expected combobox behavior — **not tested** (would navigate the browser away) |
| `/` | — | No effect on dialog state |
---
## 9. Testid inventory (search-related)
| `data-testid` | Tag | Purpose |
|---|---|---|
| `hatch-nav-search` | `BUTTON` | Nav rail trigger (`aria-label="Search"`) |
| `hatch-quick-search-frame` | `DIV` | Dialog root (`role="dialog"`, Radix popover) |
| `command-palette-search-input` | `INPUT` | Search box (`role="combobox"`, cmdk) |
| `command-palette-item` | `DIV` | Result row (`data-value` encodes target) |
| `hatch-chat-search-toolbar` | `DIV` | Seen once alongside an open dialog (context unclear — absent on main chat page; possibly panel inline search in some states) |
| `hatch-chat-search-field` | `DIV` | Same as above — observed but not mapped; treat as contextual |
---
## 10. Automation notes
1. **Open with a real mouse click**, not JS:
```javascript
const b = document.querySelector('[data-testid="hatch-nav-search"]');
const r = b.getBoundingClientRect();
// CDP: Input.dispatchMouseEvent mousePressed + mouseReleased at (r.x + r.width/2, r.y + r.height/2)
```
JS `.click()` on the trigger is unreliable (Radix wants trusted events).
2. **Type with CDP `Input.insertText`** (verified 2026-10-04 — simplest reliable
path). The raw `rawKeyDown` → `char` → `keyUp` sequence also works but is
unnecessary. **Never JS-touch the input** (`focus()`, `click()`, native value
setter): synthetic interaction on the input **closes the dialog** (verified
twice). One exception observed once: `input.select()` on the already-open
dialog did *not* close it — usable for clearing, but Escape-and-reopen
remains the safest clear.
3. **Closed === absent.** After Escape the dialog node is gone; check
`querySelector(...) === null`, with optional chaining (`?.offsetParent`) —
without `?.` a missing node throws and a truthy error string can false-positive
an "open" check.
4. **Thread UUIDs for free.** `data-value="chat:thread:<uuid>"` on result items is
the most reliable thread-identity source found so far — better than
title-matching or post-send URL polling.
5. **No scopes to set.** One global search; filter client-side on `data-value`
prefix (`chat:thread:` vs `chat:message:` vs `chat:main`).
6. **Ctrl+A via synthetic CDP events did not select input text** in one test
(subsequent typing appended). For clearing, prefer Escape-and-reopen or
repeated Backspace over select-all.
7. Dialog `id` (`radix-_r_NN_`) and `aria-activedescendant` values are generated
per mount — never match on them.