13 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 24-search-history-branching | 02 | execute | 2 |
|
|
true |
|
|
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 | nullapi.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`