---
phase: 24-search-history-branching
plan: 02
type: execute
wave: 2
depends_on: ["24-00"]
files_modified:
  - ui/src/api/chat.ts
  - ui/src/hooks/useChatSearch.ts
  - ui/src/hooks/useChatBookmarks.ts
  - ui/src/components/ChatSearchDialog.tsx
  - ui/src/components/ChatMessageBookmark.tsx
  - ui/src/components/ChatBookmarkList.tsx
  - ui/src/components/ChatBranchSelector.tsx
autonomous: true
requirements:
  - CHAT-07
  - CHAT-13
  - CHAT-14
  - HIST-04
  - HIST-12
  - PERF-04

must_haves:
  truths:
    - "ChatSearchDialog renders search results from the FTS endpoint with conversation context"
    - "ChatMessageBookmark toggles a bookmark icon on any message"
    - "ChatBookmarkList displays all bookmarks with navigation to source message"
    - "ChatBranchSelector shows available branches and allows switching"
    - "chatApi has methods for search, bookmark, branch, and export"
  artifacts:
    - path: "ui/src/api/chat.ts"
      provides: "API client methods for search, bookmark, branch, export"
      contains: "searchMessages"
    - path: "ui/src/hooks/useChatSearch.ts"
      provides: "TanStack Query hook for debounced message search"
      exports: ["useChatSearch"]
    - path: "ui/src/hooks/useChatBookmarks.ts"
      provides: "TanStack Query hooks for bookmark list and toggle mutation"
      exports: ["useChatBookmarks", "useToggleBookmark"]
    - path: "ui/src/components/ChatSearchDialog.tsx"
      provides: "Full-text search overlay using CommandDialog"
      exports: ["ChatSearchDialog"]
    - path: "ui/src/components/ChatMessageBookmark.tsx"
      provides: "Bookmark toggle button for messages"
      exports: ["ChatMessageBookmark"]
    - path: "ui/src/components/ChatBookmarkList.tsx"
      provides: "Filterable list of bookmarked messages"
      exports: ["ChatBookmarkList"]
    - path: "ui/src/components/ChatBranchSelector.tsx"
      provides: "Branch picker shown when conversation has branches"
      exports: ["ChatBranchSelector"]
  key_links:
    - from: "ui/src/hooks/useChatSearch.ts"
      to: "ui/src/api/chat.ts"
      via: "chatApi.searchMessages"
      pattern: "chatApi\\.searchMessages"
    - from: "ui/src/components/ChatSearchDialog.tsx"
      to: "ui/src/hooks/useChatSearch.ts"
      via: "useChatSearch hook"
      pattern: "useChatSearch"
    - from: "ui/src/components/ChatMessageBookmark.tsx"
      to: "ui/src/hooks/useChatBookmarks.ts"
      via: "useToggleBookmark mutation"
      pattern: "useToggleBookmark"
---

<objective>
Create all UI components, hooks, and API client methods for search, bookmarks, branching, and export.

Purpose: Build the UI layer independently from server routes (both depend only on Plan 00 types).
Output: API client extensions, two hooks, four components ready for wiring in Plan 03.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/24-search-history-branching/24-RESEARCH.md
@.planning/phases/24-search-history-branching/24-00-SUMMARY.md

@ui/src/api/chat.ts
@ui/src/components/CommandPalette.tsx
@ui/src/components/ChatMessage.tsx
@ui/src/components/ChatMessageActions.tsx
@ui/src/components/ChatConversationList.tsx
@ui/src/context/ChatPanelContext.tsx
@packages/shared/src/types/chat.ts

<interfaces>
<!-- From packages/shared/src/types/chat.ts (after Plan 00): -->
ChatMessageSearchResult { messageId, conversationId, conversationTitle, content, role, agentId, createdAt, rank }
ChatMessageSearchResponse { items: ChatMessageSearchResult[] }
ChatBookmarkWithMessage extends ChatBookmark { message: ChatMessage, conversationTitle }
ChatBookmarkListResponse { items: ChatBookmarkWithMessage[] }
ChatBookmarkToggleResponse { bookmarked: boolean }
ChatConversation now has: parentConversationId: string | null, branchFromMessageId: string | null

<!-- Existing UI patterns: -->
api.get<T>(path) / api.post<T>(path, body) / api.delete<void>(path) — from ui/src/api/client.ts
CommandDialog, CommandInput, CommandList, CommandItem, CommandEmpty — from ui/src/components/ui/command.tsx
useChatPanel() — { chatOpen, activeConversationId, setChatOpen, setActiveConversationId }
Bookmark icon available from lucide-react
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: API client methods and React Query hooks</name>
  <files>
    ui/src/api/chat.ts,
    ui/src/hooks/useChatSearch.ts,
    ui/src/hooks/useChatBookmarks.ts
  </files>
  <read_first>
    ui/src/api/chat.ts,
    ui/src/hooks/useChatMessages.ts,
    ui/src/context/ChatPanelContext.tsx,
    packages/shared/src/types/chat.ts
  </read_first>
  <action>
    **Add to ui/src/api/chat.ts (chatApi object):**

    ```typescript
    searchMessages(companyId: string, q: string, limit?: number) {
      const params = new URLSearchParams({ q });
      if (limit) params.set("limit", String(limit));
      return api.get<ChatMessageSearchResponse>(
        `/companies/${companyId}/messages/search?${params}`,
      );
    },

    toggleBookmark(conversationId: string, messageId: string) {
      return api.post<ChatBookmarkToggleResponse>(
        `/conversations/${conversationId}/bookmarks`,
        { messageId },
      );
    },

    getBookmarks(companyId: string, conversationId?: string) {
      const params = new URLSearchParams();
      if (conversationId) params.set("conversationId", conversationId);
      const qs = params.toString();
      return api.get<ChatBookmarkListResponse>(
        `/companies/${companyId}/bookmarks${qs ? `?${qs}` : ""}`,
      );
    },

    branchConversation(conversationId: string, branchFromMessageId: string) {
      return api.post<ChatConversation>(
        `/conversations/${conversationId}/branch`,
        { branchFromMessageId },
      );
    },

    listBranches(conversationId: string) {
      return api.get<{ items: ChatConversation[] }>(
        `/conversations/${conversationId}/branches`,
      );
    },

    exportConversation(conversationId: string, format: "markdown" | "json") {
      // Returns a download URL — use window.location.href to trigger
      return `/api/conversations/${conversationId}/export?format=${format}`;
    },
    ```

    Note: `exportConversation` returns a URL string (not a fetch call) since the server sends a file download. Add import for new shared types.

    **Create ui/src/hooks/useChatSearch.ts:**
    - `useChatSearch(companyId: string | null, query: string)` — uses `useQuery` with key `["chat", "search", companyId, query]`
    - `enabled: !!companyId && query.trim().length >= 2`
    - `placeholderData: (prev) => prev` (keeps previous results while loading new)
    - `staleTime: 30_000` (search results stay fresh 30s)
    - Calls `chatApi.searchMessages(companyId!, query)`

    **Create ui/src/hooks/useChatBookmarks.ts:**
    - `useChatBookmarks(companyId: string | null, conversationId?: string)` — uses `useQuery` with key `["chat", "bookmarks", companyId, conversationId]`
    - `enabled: !!companyId`
    - Calls `chatApi.getBookmarks(companyId!, conversationId)`
    - `useToggleBookmark()` — uses `useMutation` calling `chatApi.toggleBookmark`
    - On success: invalidate `["chat", "bookmarks"]` queries
    - Also invalidate `["chat", "search"]` queries (per Pitfall 6 from research)
    - Return `{ data, isLoading, toggleBookmark: mutation.mutate }`
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm --filter @paperclipai/ui build 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "searchMessages" ui/src/api/chat.ts
    - grep -q "toggleBookmark" ui/src/api/chat.ts
    - grep -q "branchConversation" ui/src/api/chat.ts
    - grep -q "exportConversation" ui/src/api/chat.ts
    - grep -q "useChatSearch" ui/src/hooks/useChatSearch.ts
    - grep -q "placeholderData" ui/src/hooks/useChatSearch.ts
    - grep -q "useToggleBookmark" ui/src/hooks/useChatBookmarks.ts
    - grep -q "invalidateQueries" ui/src/hooks/useChatBookmarks.ts
  </acceptance_criteria>
  <done>chatApi has six new methods (searchMessages, toggleBookmark, getBookmarks, branchConversation, listBranches, exportConversation). useChatSearch hook debounces FTS queries. useChatBookmarks/useToggleBookmark manage bookmark state with cache invalidation.</done>
</task>

<task type="auto">
  <name>Task 2: UI components — ChatSearchDialog, ChatMessageBookmark, ChatBookmarkList, ChatBranchSelector</name>
  <files>
    ui/src/components/ChatSearchDialog.tsx,
    ui/src/components/ChatMessageBookmark.tsx,
    ui/src/components/ChatBookmarkList.tsx,
    ui/src/components/ChatBranchSelector.tsx
  </files>
  <read_first>
    ui/src/components/CommandPalette.tsx,
    ui/src/components/ChatMessage.tsx,
    ui/src/components/ChatMessageActions.tsx,
    ui/src/components/ChatConversationList.tsx,
    ui/src/components/ui/command.tsx,
    ui/src/context/ChatPanelContext.tsx
  </read_first>
  <action>
    **ChatSearchDialog.tsx:**
    - Props: `{ open: boolean; onOpenChange: (open: boolean) => void; companyId: string | null; onNavigate: (conversationId: string, messageId: string) => void }`
    - Uses `CommandDialog` from `ui/src/components/ui/command.tsx` (same as CommandPalette)
    - Local state: `query` string, controlled by `CommandInput`
    - Uses `useChatSearch(companyId, query)` hook
    - Set `shouldFilter={false}` on the `Command` component — server-side search, not client-side filtering (per research State of the Art: cmdk v1.x)
    - `CommandList` renders search results: each `CommandItem` shows conversationTitle (dim, small), message content snippet (truncated to ~100 chars), role badge, relative timestamp
    - `CommandEmpty` shows "No results found" when query >= 2 and no results
    - Placeholder text: "Search all messages..."
    - On select: call `onNavigate(result.conversationId, result.messageId)` and close dialog
    - Content snippet: strip markdown, truncate to 120 chars, highlight matching terms with `<mark>` tag
    - Use `Search` icon from lucide-react in the input

    **ChatMessageBookmark.tsx:**
    - Props: `{ messageId: string; conversationId: string; isBookmarked: boolean; onToggle: () => void }`
    - Renders a ghost icon button (same sizing as ChatMessageActions buttons: `h-6 w-6` button, `h-3.5 w-3.5` icon)
    - Uses `Bookmark` icon from lucide-react
    - When `isBookmarked`, add `fill-current` class to icon (filled bookmark)
    - `aria-label`: "Remove bookmark" / "Bookmark message" based on state
    - On click: call `onToggle()`

    **ChatBookmarkList.tsx:**
    - Props: `{ companyId: string; onNavigate: (conversationId: string, messageId: string) => void }`
    - Uses `useChatBookmarks(companyId)` hook
    - Renders a scrollable list of bookmarked messages
    - Each item shows: conversation title (small, muted), message content preview (truncated), timestamp
    - Click navigates to the message: `onNavigate(bookmark.conversationId, bookmark.message.id)`
    - Empty state: "No bookmarks yet" with `Bookmark` icon
    - Loading state: skeleton placeholders (match ChatConversationList pattern)

    **ChatBranchSelector.tsx:**
    - Props: `{ conversationId: string; branches: ChatConversation[]; activeBranchId: string | null; onSelectBranch: (id: string) => void }`
    - Only renders when `branches.length > 0`
    - Shows a compact horizontal bar: "Branch: [Original] [Branch 1] [Branch 2]..."
    - "Original" is the parent conversation (activeBranchId === null or matches parent)
    - Each branch shows its title or "Branch {n}" fallback, creation date
    - Active branch has a highlighted/selected style (bg-accent)
    - Uses `GitBranch` icon from lucide-react
    - Clicking a branch calls `onSelectBranch(branchId)`
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm --filter @paperclipai/ui build 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "ChatSearchDialog" ui/src/components/ChatSearchDialog.tsx
    - grep -q "CommandDialog" ui/src/components/ChatSearchDialog.tsx
    - grep -q "shouldFilter" ui/src/components/ChatSearchDialog.tsx
    - grep -q "useChatSearch" ui/src/components/ChatSearchDialog.tsx
    - grep -q "ChatMessageBookmark" ui/src/components/ChatMessageBookmark.tsx
    - grep -q "fill-current" ui/src/components/ChatMessageBookmark.tsx
    - grep -q "ChatBookmarkList" ui/src/components/ChatBookmarkList.tsx
    - grep -q "useChatBookmarks" ui/src/components/ChatBookmarkList.tsx
    - grep -q "ChatBranchSelector" ui/src/components/ChatBranchSelector.tsx
    - grep -q "GitBranch" ui/src/components/ChatBranchSelector.tsx
  </acceptance_criteria>
  <done>Four UI components created: ChatSearchDialog uses CommandDialog with server-side FTS, ChatMessageBookmark is a toggle icon button, ChatBookmarkList renders bookmarked messages with navigation, ChatBranchSelector shows a horizontal branch picker bar. All components use existing UI primitives and lucide icons.</done>
</task>

</tasks>

<verification>
- `pnpm --filter @paperclipai/ui build` passes
- ChatSearchDialog uses `shouldFilter={false}` for server-side search
- ChatMessageBookmark follows ChatMessageActions button sizing
- All components accept callback props for navigation (not internal routing)
</verification>

<success_criteria>
UI builds cleanly. All four components render standalone. API client has six new methods. Hooks manage query/mutation state. Components are ready for wiring into ChatPanel in Plan 03.
</success_criteria>

<output>
After completion, create `.planning/phases/24-search-history-branching/24-02-SUMMARY.md`
</output>
