---
phase: 05-unified-search
plan: 02
type: execute
wave: 2
depends_on:
  - 05-01
files_modified:
  - client/src/hooks/useCmdKSearch.js
  - client/src/components/CmdKSearch.jsx
  - client/src/components/Layout.jsx
autonomous: false
requirements:
  - SRC-01
  - SRC-03
  - SRC-04

must_haves:
  truths:
    - "Pressing Cmd+K or Ctrl+K from any page opens a search overlay"
    - "Typing 2+ characters triggers debounced search-as-you-type"
    - "Results are grouped by source with section headers, icons, and text snippets"
    - "Keyword matches are highlighted in result snippets"
    - "Arrow keys move selection highlight, Enter navigates to result URL, Escape closes overlay"
    - "Clicking a result navigates to its deep-linked location and closes the overlay"
    - "Clicking the backdrop dismisses the overlay"
    - "Before typing, empty area shows placeholder hint"
    - "No-results state shows centered message"
    - "Input auto-focuses when overlay opens"
    - "Body scroll is locked when overlay is open"
  artifacts:
    - path: "client/src/hooks/useCmdKSearch.js"
      provides: "Global Cmd+K keyboard shortcut hook"
      exports: ["useCmdKSearch"]
    - path: "client/src/components/CmdKSearch.jsx"
      provides: "Search overlay modal component with results display"
    - path: "client/src/components/Layout.jsx"
      provides: "CmdKSearch mounted in layout shell"
      contains: "CmdKSearch"
  key_links:
    - from: "client/src/components/Layout.jsx"
      to: "client/src/components/CmdKSearch.jsx"
      via: "import and render <CmdKSearch />"
      pattern: "CmdKSearch"
    - from: "client/src/components/CmdKSearch.jsx"
      to: "client/src/hooks/useCmdKSearch.js"
      via: "import useCmdKSearch"
      pattern: "useCmdKSearch"
    - from: "client/src/components/CmdKSearch.jsx"
      to: "client/src/services/api.js"
      via: "import { search } from api.js"
      pattern: "search"
    - from: "client/src/components/CmdKSearch.jsx"
      to: "react-router-dom"
      via: "useNavigate() for deep-link navigation"
      pattern: "navigate"
---

<objective>
Build the Cmd+K search overlay UI — a Spotlight/Raycast-style modal that appears on Cmd+K/Ctrl+K from any page, displays categorized search results with keyboard navigation, and navigates to deep-linked locations on selection. The overlay uses a React portal for z-index isolation, debounced search-as-you-type, arrow key navigation, and highlighted keyword matches in snippets.

Purpose: SRC-01 (Cmd+K opens overlay), SRC-03 (categorized results with icons/snippets), SRC-04 (deep-link navigation)
Output: useCmdKSearch.js hook, CmdKSearch.jsx overlay component, Layout.jsx updated with mount point
</objective>

<execution_context>
@/Users/antic/.claude/get-shit-done/workflows/execute-plan.md
@/Users/antic/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/05-unified-search/05-RESEARCH.md
@.planning/phases/05-unified-search/05-01-SUMMARY.md

<interfaces>
<!-- Key contracts from Plan 05-01 that this plan consumes -->

From client/src/services/api.js (added by Plan 05-01):
```js
export const search = (q) => request(`/search?q=${encodeURIComponent(q)}`);
// Returns: { query: string, sources: Source[] }
// Source: { id: string, label: string, icon: string, results: Result[] }
// Result: { id: string, title: string, snippet: string, url: string, type: string }
```

From client/src/components/Layout.jsx (existing):
```jsx
// Layout is the root shell wrapping all pages via <Outlet />
// Import lucide-react icons already present: Brain, Package, History, HeartPulse, Cpu, Search, etc.
// CmdKSearch should be mounted as a sibling to the main content area
// Layout uses Tailwind classes with port-* design tokens
```

From client/src/App.jsx route patterns:
```
/brain/inbox         — Brain captures
/cos/memory          — CoS memory entries
/apps                — App registry
/devtools/history    — Activity history
/meatspace/health    — Health metrics
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create Cmd+K hook and search overlay component</name>
  <files>client/src/hooks/useCmdKSearch.js, client/src/components/CmdKSearch.jsx</files>
  <action>
**1. Create client/src/hooks/useCmdKSearch.js:**

```js
import { useState, useEffect } from 'react';

export function useCmdKSearch() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    const handler = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        setOpen(prev => !prev);
      }
    };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, []);

  return { open, setOpen };
}
```

Note on Pitfall 6 from research: The global Cmd+K handler fires even when an input/textarea is focused. This is the correct behavior — Spotlight and Raycast both override Cmd+K in inputs. The search overlay is the highest-priority action for this shortcut.

**2. Create client/src/components/CmdKSearch.jsx:**

Build a Spotlight-style search overlay using React portal. Key specifications from CONTEXT.md (locked decisions):

**Overlay appearance:**
- Spotlight-style centered modal, ~800px wide (use `max-w-3xl` which is 768px, close to 800)
- Semi-transparent dark backdrop (`bg-black/60`) — click backdrop to dismiss
- Subtle fade+scale animation (~150ms): use Tailwind `transition-all duration-150`
- Use `createPortal(document.body)` to render above sidebar z-index

**Result presentation:**
- Results grouped by source under section headers with icon + label
- Top 3 results per source category initially, with "Show more" button to expand to all
- Each result row: source icon + title (bold) + 1-line text snippet with keyword highlighted
- Categories with zero results hidden (server already omits them)

**Search interaction:**
- Full keyboard navigation: ArrowDown/ArrowUp move highlight, Enter navigates, Escape closes
- Search-as-you-type after 2+ characters with 300ms debounce
- Before typing: empty results area with placeholder hint "Search Brain, Memory, Health..."
- No-results state: subtle centered "No results for 'xyz'" message

**Implementation details:**

Import `{ search }` from `../services/api.js` for the API call.
Import `{ useNavigate }` from `react-router-dom` for deep-link navigation.
Import `{ createPortal }` from `react-dom`.
Import Lucide icons: `Brain, Cpu, Package, History, HeartPulse, Search, Loader2` from `lucide-react`.

**Component state:**
- `query` (string) — current search input
- `results` (array of source objects) — from API response
- `loading` (boolean) — true while fetching
- `focusedIndex` (number) — currently highlighted result in flat list
- `expandedSources` (Set) — source IDs where "Show more" is clicked

**Debounce logic:**
- Use a `useEffect` with `setTimeout`/`clearTimeout` pattern (no external library)
- When `query.length >= 2`, set 300ms timeout, then call `search(query)` and update results
- When `query.length < 2`, clear results immediately

**Flat result list for keyboard navigation:**
- Compute `flatResults` from sources: iterate sources, for each source take first 3 results (or all if source is in expandedSources), flatten into a single array
- Each flat item gets a reference to its source id for rendering

**Reset focusedIndex to 0 whenever results change** (Pitfall 5 from research).

**Auto-focus input on open** (Pitfall 1): `useEffect(() => { if (open) inputRef.current?.focus(); }, [open])`.

**Lock body scroll when open** (Pitfall 2): `useEffect(() => { document.body.style.overflow = open ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [open])`.

**Clear query and results when overlay closes** so it starts fresh each time.

**Keyboard handler** (on the overlay container div, not the input):
- ArrowDown: increment focusedIndex (clamp to flatResults.length - 1)
- ArrowUp: decrement focusedIndex (clamp to 0)
- Enter: if flatResults[focusedIndex] exists, call `navigate(flatResults[focusedIndex].url)` and close overlay
- Escape: close overlay

**Highlight component** (inline helper):
- Split snippet text on the search query (case-insensitive regex with special char escaping)
- Wrap matched parts with `<mark className="bg-port-accent/30 text-white rounded px-0.5">`

**Icon mapping:**
Create a map from icon string to Lucide component:
```js
const ICON_MAP = { Brain, Cpu, Package, History, HeartPulse };
```
Look up `source.icon` in the map; fallback to `Search` icon.

**Rendering structure (inside portal):**

```
backdrop (fixed inset-0, bg-black/60, onClick close)
  container (centered, max-w-3xl, bg-port-card, rounded-xl, border border-port-border, shadow-2xl)
    search input row (flex, border-b border-port-border, p-4)
      Search icon (text-gray-400)
      input (flex-1, bg-transparent, text-white, placeholder, ref=inputRef)
      Cmd+K badge (text-xs text-gray-500 border border-port-border rounded px-1.5)
    results area (max-h-96 overflow-y-auto p-2)
      if loading: centered Loader2 spinner (animate-spin)
      if query.length >= 2 && !loading && results.length === 0: "No results for 'query'"
      if query.length < 2: placeholder hint
      for each source in results:
        source header (flex items-center gap-2, text-xs text-gray-400 uppercase, px-3 py-2)
          SourceIcon (size=14)
          source.label
        for each result (first 3 or all if expanded):
          result row (flex items-start gap-3, px-3 py-2, rounded-lg, cursor-pointer)
            highlight bg if focusedIndex matches this flat index (bg-port-accent/10)
            hover state (hover:bg-white/5)
            title (text-sm font-medium text-white, truncate)
            snippet (text-xs text-gray-400, with Highlight component)
        if source has >3 results and not expanded:
          "Show more" button (text-xs text-port-accent, px-3 py-1)
```

Use Tailwind design tokens from CLAUDE.md: port-bg, port-card, port-border, port-accent.

On result click: `navigate(result.url)` then `setOpen(false)`.

Scroll the focused result into view: on the result row div, use `ref` callback and `scrollIntoView({ block: 'nearest' })` when it becomes focused.
  </action>
  <verify>
    <automated>cd client && node -e "import('./src/hooks/useCmdKSearch.js').then(() => console.log('✅ hook imports ok'))" && node -e "import('./src/components/CmdKSearch.jsx').then(() => console.log('✅ CmdKSearch imports ok'))" 2>&1 || echo "Note: JSX import may fail in plain node — verify via dev server build"</automated>
  </verify>
  <done>useCmdKSearch hook handles Cmd+K/Ctrl+K toggle; CmdKSearch component renders portal overlay with debounced search, grouped results, keyboard navigation, highlighted snippets, and deep-link navigation</done>
</task>

<task type="auto">
  <name>Task 2: Mount CmdKSearch in Layout.jsx</name>
  <files>client/src/components/Layout.jsx</files>
  <action>
Edit `client/src/components/Layout.jsx` to mount the CmdKSearch overlay:

1. Add import at the top with other component imports:
```js
import CmdKSearch from './CmdKSearch';
```

2. Inside the Layout component's return JSX, add `<CmdKSearch />` as a sibling — place it just before the closing `</div>` of the outermost wrapper (or at the end of the return). Since CmdKSearch renders via a portal to document.body, its placement in the JSX tree doesn't affect layout, but placing it in Layout ensures it's always mounted regardless of which page route is active.

The component is self-contained — it uses the useCmdKSearch hook internally for open/close state and renders via portal. No props needed.

Verify the dev server builds without errors after the change.
  </action>
  <verify>
    <automated>cd client && npx vite build 2>&1 | tail -5</automated>
  </verify>
  <done>CmdKSearch is mounted in Layout.jsx; client builds without errors; overlay is available on every page</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 3: Verify Cmd+K search overlay end-to-end</name>
  <files>none</files>
  <action>
Human verifies the complete Cmd+K search overlay:
1. Open PortOS in browser (http://localhost:5555 or Tailscale URL)
2. Press Cmd+K (Mac) or Ctrl+K (other) — search overlay should appear centered with ~800px width, dark backdrop
3. Type "test" (or any 2+ char query) — results should appear grouped by source (Brain, Memory, Apps, History, Health)
4. Verify each result shows: icon, bold title, snippet text with keyword highlighted
5. Press ArrowDown/ArrowUp — selection highlight should move between results
6. Press Enter on a highlighted result — should navigate to the deep-linked page and close overlay
7. Reopen with Cmd+K, type a query, click a result — should navigate and close
8. Click the dark backdrop — overlay should dismiss
9. Press Escape — overlay should dismiss
10. Try a query with no results — should show "No results for 'xyz'" message
11. Verify overlay feels fast and responsive (300ms debounce, no lag)
12. Verify body scroll is locked when overlay is open
  </action>
  <verify>Human approves the overlay appearance, keyboard navigation, result categorization, and deep-link navigation</verify>
  <done>User types "approved" confirming the Cmd+K search overlay works correctly end-to-end</done>
</task>

</tasks>

<verification>
1. Cmd+K/Ctrl+K opens overlay from any page (Dashboard, Brain, Apps, etc.)
2. Search returns results from all data sources that have matching content
3. Results are visually grouped by source with icons and highlighted snippets
4. Keyboard navigation (arrows + Enter + Escape) works fluidly
5. Click navigation closes overlay and lands on correct deep-linked page
6. Empty and loading states render correctly
7. Client build succeeds: `cd client && npx vite build`
</verification>

<success_criteria>
- Cmd+K / Ctrl+K opens search overlay from any page in PortOS
- Typing 2+ chars triggers debounced search, results appear grouped by source
- Each result shows icon, title, snippet with keyword highlighted
- Arrow keys navigate results, Enter opens deep link, Escape closes
- Clicking result navigates to URL and closes overlay
- Clicking backdrop dismisses overlay
- No-results and loading states render appropriately
- Client builds without errors
- Human verification approved
</success_criteria>

<output>
After completion, create `.planning/phases/05-unified-search/05-02-SUMMARY.md`
</output>
