--- 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" --- 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. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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 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 api.get(path) / api.post(path, body) / api.delete(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 Task 1: API client methods and React Query hooks ui/src/api/chat.ts, ui/src/hooks/useChatSearch.ts, ui/src/hooks/useChatBookmarks.ts ui/src/api/chat.ts, ui/src/hooks/useChatMessages.ts, ui/src/context/ChatPanelContext.tsx, packages/shared/src/types/chat.ts **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( `/companies/${companyId}/messages/search?${params}`, ); }, toggleBookmark(conversationId: string, messageId: string) { return api.post( `/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( `/companies/${companyId}/bookmarks${qs ? `?${qs}` : ""}`, ); }, branchConversation(conversationId: string, branchFromMessageId: string) { return api.post( `/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 }` cd /opt/nexus && pnpm --filter @paperclipai/ui build 2>&1 | tail -10 - 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 chatApi has six new methods (searchMessages, toggleBookmark, getBookmarks, branchConversation, listBranches, exportConversation). useChatSearch hook debounces FTS queries. useChatBookmarks/useToggleBookmark manage bookmark state with cache invalidation. Task 2: UI components — ChatSearchDialog, ChatMessageBookmark, ChatBookmarkList, ChatBranchSelector ui/src/components/ChatSearchDialog.tsx, ui/src/components/ChatMessageBookmark.tsx, ui/src/components/ChatBookmarkList.tsx, ui/src/components/ChatBranchSelector.tsx 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 **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 `` 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)` cd /opt/nexus && pnpm --filter @paperclipai/ui build 2>&1 | tail -10 - 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 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. - `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) 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. After completion, create `.planning/phases/24-search-history-branching/24-02-SUMMARY.md`