298 lines
13 KiB
Markdown
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>
|