nexus/.planning/milestones/v1.3-phases/24-search-history-branching/24-02-PLAN.md
Nexus Dev ffc7b130e4 chore: archive v1.3 phase directories to milestones/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:48 +00:00

13 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
24-search-history-branching 02 execute 2
24-00
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
true
CHAT-07
CHAT-13
CHAT-14
HIST-04
HIST-12
PERF-04
truths artifacts key_links
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
path provides contains
ui/src/api/chat.ts API client methods for search, bookmark, branch, export searchMessages
path provides exports
ui/src/hooks/useChatSearch.ts TanStack Query hook for debounced message search
useChatSearch
path provides exports
ui/src/hooks/useChatBookmarks.ts TanStack Query hooks for bookmark list and toggle mutation
useChatBookmarks
useToggleBookmark
path provides exports
ui/src/components/ChatSearchDialog.tsx Full-text search overlay using CommandDialog
ChatSearchDialog
path provides exports
ui/src/components/ChatMessageBookmark.tsx Bookmark toggle button for messages
ChatMessageBookmark
path provides exports
ui/src/components/ChatBookmarkList.tsx Filterable list of bookmarked messages
ChatBookmarkList
path provides exports
ui/src/components/ChatBranchSelector.tsx Branch picker shown when conversation has branches
ChatBranchSelector
from to via pattern
ui/src/hooks/useChatSearch.ts ui/src/api/chat.ts chatApi.searchMessages chatApi.searchMessages
from to via pattern
ui/src/components/ChatSearchDialog.tsx ui/src/hooks/useChatSearch.ts useChatSearch hook useChatSearch
from to via pattern
ui/src/components/ChatMessageBookmark.tsx ui/src/hooks/useChatBookmarks.ts useToggleBookmark mutation 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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

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<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 }`
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)

<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>

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