# 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:` | Sidechat thread — **the thread UUID is embedded** | `chat:thread:2e2c90a1-4b17-4590-b8ba-fb17d8018d36` | | `chat:message:assistant-msg-:` | Individual assistant message match inside a thread | `chat:message:assistant-msg-b2a4e27d-4970-8b1e-8863-a96ed0aa1401:20420` | | `chat:message::` | 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
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 ..."> …svg icon… …title + snippet + relative time…
``` 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 ``` ```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:"` 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.