nexus/.planning/phases/24-search-history-branching/24-02-PLAN.md

298 lines
13 KiB
Markdown

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