282 lines
15 KiB
Markdown
282 lines
15 KiB
Markdown
---
|
|
phase: 24-search-history-branching
|
|
plan: 03
|
|
type: execute
|
|
wave: 3
|
|
depends_on: ["24-01", "24-02"]
|
|
files_modified:
|
|
- ui/src/context/ChatPanelContext.tsx
|
|
- ui/src/components/ChatPanel.tsx
|
|
- ui/src/components/ChatMessage.tsx
|
|
- ui/src/components/ChatMessageActions.tsx
|
|
- ui/src/components/ChatMessageList.tsx
|
|
- ui/src/components/ChatConversationList.tsx
|
|
- ui/src/components/CommandPalette.tsx
|
|
autonomous: false
|
|
requirements:
|
|
- CHAT-07
|
|
- CHAT-13
|
|
- CHAT-14
|
|
- HIST-04
|
|
- HIST-07
|
|
- HIST-08
|
|
- HIST-09
|
|
- HIST-10
|
|
- HIST-11
|
|
- HIST-12
|
|
- PERF-04
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Cmd+K opens CommandPalette which has a 'Search chat messages' item that opens ChatSearchDialog"
|
|
- "Clicking a search result navigates to the conversation and scrolls to the message"
|
|
- "Every message shows a bookmark icon; clicking it toggles the bookmark"
|
|
- "Editing a message that has responses creates a branch conversation"
|
|
- "Branch selector appears above messages when a conversation has branches"
|
|
- "Export button in conversation header allows downloading as Markdown or JSON"
|
|
- "Bookmarked messages are accessible from a bookmarks panel"
|
|
artifacts:
|
|
- path: "ui/src/context/ChatPanelContext.tsx"
|
|
provides: "scrollToMessageId state for cross-component message navigation"
|
|
contains: "scrollToMessageId"
|
|
- path: "ui/src/components/ChatPanel.tsx"
|
|
provides: "Integration of search, bookmarks, branching, export into the chat panel"
|
|
contains: "ChatSearchDialog"
|
|
- path: "ui/src/components/ChatMessageActions.tsx"
|
|
provides: "Bookmark button on each message"
|
|
contains: "ChatMessageBookmark"
|
|
- path: "ui/src/components/CommandPalette.tsx"
|
|
provides: "Search chat messages command item"
|
|
contains: "Search chat"
|
|
key_links:
|
|
- from: "ui/src/components/CommandPalette.tsx"
|
|
to: "ui/src/components/ChatSearchDialog.tsx"
|
|
via: "opens ChatSearchDialog from command item"
|
|
pattern: "ChatSearchDialog"
|
|
- from: "ui/src/context/ChatPanelContext.tsx"
|
|
to: "ui/src/components/ChatMessageList.tsx"
|
|
via: "scrollToMessageId triggers virtualizer scrollToIndex"
|
|
pattern: "scrollToMessageId"
|
|
- from: "ui/src/components/ChatPanel.tsx"
|
|
to: "ui/src/components/ChatBranchSelector.tsx"
|
|
via: "renders branch selector above message list"
|
|
pattern: "ChatBranchSelector"
|
|
- from: "ui/src/components/ChatMessage.tsx"
|
|
to: "ui/src/components/ChatMessageBookmark.tsx"
|
|
via: "renders bookmark icon in message actions"
|
|
pattern: "ChatMessageBookmark"
|
|
---
|
|
|
|
<objective>
|
|
Wire all Phase 24 components into the existing ChatPanel, ChatMessage, CommandPalette, and ChatPanelContext. Connect search navigation, bookmark toggle, branch creation on edit, export triggers, and scroll-to-message.
|
|
|
|
Purpose: Final integration — turns standalone components into a working user experience.
|
|
Output: Fully functional search, bookmarks, branching, and export features.
|
|
</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
|
|
@.planning/phases/24-search-history-branching/24-01-SUMMARY.md
|
|
@.planning/phases/24-search-history-branching/24-02-SUMMARY.md
|
|
|
|
@ui/src/context/ChatPanelContext.tsx
|
|
@ui/src/components/ChatPanel.tsx
|
|
@ui/src/components/ChatMessage.tsx
|
|
@ui/src/components/ChatMessageActions.tsx
|
|
@ui/src/components/ChatMessageList.tsx
|
|
@ui/src/components/ChatConversationList.tsx
|
|
@ui/src/components/CommandPalette.tsx
|
|
@ui/src/hooks/useKeyboardShortcuts.ts
|
|
|
|
<interfaces>
|
|
<!-- From Plan 02 components: -->
|
|
ChatSearchDialog: { open, onOpenChange, companyId, onNavigate: (conversationId, messageId) => void }
|
|
ChatMessageBookmark: { messageId, conversationId, isBookmarked, onToggle }
|
|
ChatBookmarkList: { companyId, onNavigate: (conversationId, messageId) => void }
|
|
ChatBranchSelector: { conversationId, branches, activeBranchId, onSelectBranch }
|
|
|
|
<!-- From Plan 02 hooks: -->
|
|
useChatSearch(companyId, query) => { data, isLoading }
|
|
useChatBookmarks(companyId, conversationId?) => { data, isLoading }
|
|
useToggleBookmark() => { mutate(vars), isPending }
|
|
|
|
<!-- From Plan 02 API: -->
|
|
chatApi.branchConversation(conversationId, branchFromMessageId) => ChatConversation
|
|
chatApi.listBranches(conversationId) => { items: ChatConversation[] }
|
|
chatApi.exportConversation(conversationId, format) => URL string
|
|
|
|
<!-- From Plan 01 routes: -->
|
|
POST /conversations/:id/branch { branchFromMessageId }
|
|
GET /conversations/:id/branches
|
|
GET /conversations/:id/export?format=markdown|json
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: ChatPanelContext + ChatPanel wiring (search, branch, export, scroll-to)</name>
|
|
<files>
|
|
ui/src/context/ChatPanelContext.tsx,
|
|
ui/src/components/ChatPanel.tsx,
|
|
ui/src/components/ChatMessageList.tsx,
|
|
ui/src/components/CommandPalette.tsx,
|
|
ui/src/components/ChatConversationList.tsx
|
|
</files>
|
|
<read_first>
|
|
ui/src/context/ChatPanelContext.tsx,
|
|
ui/src/components/ChatPanel.tsx,
|
|
ui/src/components/ChatMessageList.tsx,
|
|
ui/src/components/CommandPalette.tsx,
|
|
ui/src/components/ChatConversationList.tsx,
|
|
ui/src/hooks/useKeyboardShortcuts.ts,
|
|
ui/src/hooks/useChatSearch.ts,
|
|
ui/src/hooks/useChatBookmarks.ts,
|
|
ui/src/components/ChatSearchDialog.tsx,
|
|
ui/src/components/ChatBranchSelector.tsx,
|
|
ui/src/components/ChatBookmarkList.tsx
|
|
</read_first>
|
|
<action>
|
|
**ChatPanelContext.tsx — add scrollToMessageId:**
|
|
- Add to interface: `scrollToMessageId: string | null; setScrollToMessageId: (id: string | null) => void;`
|
|
- Add state: `const [scrollToMessageId, setScrollToMessageId] = useState<string | null>(null);`
|
|
- Add to provider value object
|
|
|
|
**CommandPalette.tsx — add "Search chat messages" item:**
|
|
- Add a new `CommandItem` in the existing command list: value "search-chat", label "Search chat messages", icon `Search` from lucide-react
|
|
- On select: dispatch custom event `window.dispatchEvent(new CustomEvent("nexus:open-chat-search"))` and close the palette
|
|
- This avoids Cmd+K conflict (Pitfall 3 from research) — routes through existing CommandPalette
|
|
|
|
**ChatPanel.tsx — integrate search dialog, branch selector, export, bookmarks:**
|
|
- Add state: `const [searchOpen, setSearchOpen] = useState(false)`
|
|
- Listen for `nexus:open-chat-search` custom event: `useEffect` that adds event listener, sets `setSearchOpen(true)`, returns cleanup
|
|
- Render `<ChatSearchDialog>` with `onNavigate` that: sets `setActiveConversationId(conversationId)`, then `setScrollToMessageId(messageId)`, closes dialog
|
|
- Fetch branches for active conversation: `useQuery(["chat", "branches", activeConversationId], () => chatApi.listBranches(activeConversationId!), { enabled: !!activeConversationId })`
|
|
- Render `<ChatBranchSelector>` above `ChatMessageList` when branches exist — `onSelectBranch` calls `setActiveConversationId(branchId)`
|
|
- Add export buttons (Markdown/JSON) in the conversation header area (next to title). Use `Download` icon. On click: `window.location.href = chatApi.exportConversation(activeConversationId!, format)`
|
|
- Add a bookmarks panel toggle (show/hide `ChatBookmarkList`): small `Bookmark` icon button in header. When active, shows `ChatBookmarkList` in a side panel or below the header
|
|
- `ChatBookmarkList` `onNavigate` wired same as search: `setActiveConversationId` + `setScrollToMessageId`
|
|
- Modify `handleEdit` callback: When editing a message that already has subsequent messages (assistant reply exists after the edited message), call `chatApi.branchConversation(activeConversationId, messageId)` FIRST, then switch to the new branch via `setActiveConversationId(newConv.id)`, then proceed with the edit on the new branch. This is the branching trigger per CHAT-14.
|
|
- After any edit/retry, invalidate `["chat", "search"]` queries (per Pitfall 6)
|
|
|
|
**ChatMessageList.tsx — scroll-to-message support:**
|
|
- Get `scrollToMessageId` and `setScrollToMessageId` from `useChatPanel()`
|
|
- When `scrollToMessageId` changes (useEffect): find the message index in the flattened messages array, call `virtualizer.scrollToIndex(index, { align: "center" })`, then `setScrollToMessageId(null)` to reset
|
|
- If message not found in current page, this is a best-effort scroll (message may not be loaded yet). Add a TODO comment for infinite-scroll-then-scroll-to in a future iteration.
|
|
|
|
**ChatConversationList.tsx — branch indicators:**
|
|
- Read `parentConversationId` and `branchFromMessageId` from each conversation item
|
|
- If `parentConversationId` is set, render a small `GitBranch` icon next to the conversation title
|
|
- Optionally indent branch conversations under their parent (client-side grouping: group by parentConversationId, then render parent followed by its children)
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus && pnpm --filter @paperclipai/ui build 2>&1 | tail -10</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- grep -q "scrollToMessageId" ui/src/context/ChatPanelContext.tsx
|
|
- grep -q "setScrollToMessageId" ui/src/context/ChatPanelContext.tsx
|
|
- grep -q "nexus:open-chat-search" ui/src/components/CommandPalette.tsx
|
|
- grep -q "Search chat" ui/src/components/CommandPalette.tsx
|
|
- grep -q "ChatSearchDialog" ui/src/components/ChatPanel.tsx
|
|
- grep -q "ChatBranchSelector" ui/src/components/ChatPanel.tsx
|
|
- grep -q "exportConversation" ui/src/components/ChatPanel.tsx
|
|
- grep -q "branchConversation" ui/src/components/ChatPanel.tsx
|
|
- grep -q "scrollToMessageId" ui/src/components/ChatMessageList.tsx
|
|
- grep -q "scrollToIndex" ui/src/components/ChatMessageList.tsx
|
|
- grep -q "GitBranch" ui/src/components/ChatConversationList.tsx
|
|
</acceptance_criteria>
|
|
<done>ChatPanelContext has scrollToMessageId. CommandPalette has "Search chat messages" item that dispatches custom event. ChatPanel integrates search dialog, branch selector, export buttons, and bookmark panel. ChatMessageList scrolls to a target message. ChatConversationList shows branch indicator icons. Edit-with-responses triggers branch creation.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: ChatMessage + ChatMessageActions bookmark integration</name>
|
|
<files>
|
|
ui/src/components/ChatMessage.tsx,
|
|
ui/src/components/ChatMessageActions.tsx
|
|
</files>
|
|
<read_first>
|
|
ui/src/components/ChatMessage.tsx,
|
|
ui/src/components/ChatMessageActions.tsx,
|
|
ui/src/components/ChatMessageBookmark.tsx,
|
|
ui/src/hooks/useChatBookmarks.ts
|
|
</read_first>
|
|
<action>
|
|
**ChatMessageActions.tsx:**
|
|
- Add props: `onBookmark?: () => void; isBookmarked?: boolean`
|
|
- Import `ChatMessageBookmark` component
|
|
- Add `<ChatMessageBookmark>` as the LAST action button (after edit/retry), visible for both user and assistant messages (not system)
|
|
- Pass `isBookmarked` and `onToggle={onBookmark}` from props
|
|
- Only render bookmark button when `onBookmark` is provided (same pattern as onEdit/onRetry)
|
|
|
|
**ChatMessage.tsx:**
|
|
- Add props: `onBookmark?: (messageId: string) => void; isBookmarked?: boolean`
|
|
- Pass `onBookmark={() => id && onBookmark?.(id)}` and `isBookmarked` to `ChatMessageActions`
|
|
- Do NOT render bookmark for system messages (messageType checks)
|
|
|
|
The actual `onBookmark` callback wiring from ChatPanel (calling `useToggleBookmark`) and the `isBookmarked` state (from `useChatBookmarks` data) will be set up in ChatPanel.tsx (Task 1 handles ChatPanel, but the bookmark prop threading from ChatPanel -> ChatMessageList -> ChatMessage needs to be connected).
|
|
|
|
**In ChatPanel.tsx (addendum to Task 1 wiring):**
|
|
- Use `useChatBookmarks(companyId, activeConversationId)` to get bookmark data for active conversation
|
|
- Use `useToggleBookmark()` mutation
|
|
- Create `bookmarkedMessageIds` Set from bookmark data for O(1) lookup
|
|
- Pass `onBookmark={(messageId) => toggleBookmark({ conversationId: activeConversationId!, messageId })}` and `isBookmarked={bookmarkedMessageIds.has(messageId)}` through ChatMessageList to each ChatMessage
|
|
- This means ChatMessageList also needs `onBookmark` and `bookmarkedMessageIds` props threaded through
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus && pnpm --filter @paperclipai/ui build 2>&1 | tail -10</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- grep -q "onBookmark" ui/src/components/ChatMessageActions.tsx
|
|
- grep -q "isBookmarked" ui/src/components/ChatMessageActions.tsx
|
|
- grep -q "ChatMessageBookmark" ui/src/components/ChatMessageActions.tsx
|
|
- grep -q "onBookmark" ui/src/components/ChatMessage.tsx
|
|
- grep -q "isBookmarked" ui/src/components/ChatMessage.tsx
|
|
- grep -q "useToggleBookmark" ui/src/components/ChatPanel.tsx
|
|
- grep -q "bookmarkedMessageIds" ui/src/components/ChatPanel.tsx
|
|
</acceptance_criteria>
|
|
<done>ChatMessageActions renders a bookmark toggle button for user and assistant messages. ChatMessage threads onBookmark and isBookmarked props. ChatPanel manages bookmark state via useChatBookmarks and useToggleBookmark, passing bookmarkedMessageIds set down through ChatMessageList to each ChatMessage.</done>
|
|
</task>
|
|
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
|
<name>Task 3: Verify complete Phase 24 functionality</name>
|
|
<files>none</files>
|
|
<action>
|
|
Human verifies all Phase 24 features end-to-end in the browser:
|
|
1. Open Nexus in browser. Open the chat panel.
|
|
2. Search: Press Cmd+K, find "Search chat messages", select it. Type a term. Verify results with snippets. Click a result — verify navigation and scroll-to-message.
|
|
3. Bookmarks: Hover a message, click bookmark icon (fills in). Check bookmarks list in header. Navigate from bookmark. Un-bookmark.
|
|
4. Branching: Edit a mid-conversation message. Verify branch created, branch icon in list, branch selector appears. Switch branches.
|
|
5. Export: Click export in header. Download Markdown (agent names, not UUIDs). Download JSON (all messages).
|
|
</action>
|
|
<verify>Human confirms all 4 features work: search, bookmarks, branching, export.</verify>
|
|
<done>User types "approved" confirming all Phase 24 success criteria are met.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `pnpm --filter @paperclipai/ui build` passes
|
|
- Cmd+K > "Search chat messages" opens ChatSearchDialog
|
|
- Search results navigate to conversation + scroll to message
|
|
- Bookmark toggle works on user and assistant messages
|
|
- Edit-with-responses creates branch conversation
|
|
- Branch selector switches between original and branch
|
|
- Export downloads Markdown/JSON with agent names
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
All four Phase 24 features are integrated and functional: (1) FTS search via Cmd+K with scroll-to-message navigation, (2) bookmark toggle on messages with bookmark list panel, (3) conversation branching on edit with branch selector UI, (4) export as Markdown/JSON with agent names. Human verification confirms all flows work end-to-end.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/24-search-history-branching/24-03-SUMMARY.md`
|
|
</output>
|