---
phase: 24-search-history-branching
plan: "03"
subsystem: ui
tags: [chat, search, bookmarks, branching, export, integration]
dependency_graph:
  requires: ["24-01", "24-02"]
  provides: ["CHAT-07", "CHAT-13", "CHAT-14", "HIST-04", "HIST-07", "HIST-08", "HIST-09", "HIST-10", "HIST-11", "HIST-12", "PERF-04"]
  affects: ["ChatPanel", "ChatPanelContext", "ChatMessage", "ChatMessageList", "CommandPalette"]
tech_stack:
  added: []
  patterns:
    - "Custom window event (nexus:open-chat-search) for decoupled search trigger from CommandPalette"
    - "useChatBookmarks + useToggleBookmark for bookmark state management in ChatPanel"
    - "virtualizer.scrollToIndex for programmatic scroll-to-message"
    - "Branch-on-edit: branchConversation called when editing messages with subsequent replies"
key_files:
  created: []
  modified:
    - ui/src/context/ChatPanelContext.tsx
    - ui/src/components/ChatPanel.tsx
    - ui/src/components/ChatMessageList.tsx
    - ui/src/components/ChatMessageActions.tsx
    - ui/src/components/ChatMessage.tsx
    - ui/src/components/CommandPalette.tsx
    - ui/src/components/ChatConversationList.tsx
decisions:
  - "Custom event nexus:open-chat-search routes search trigger through CommandPalette without Cmd+K conflict (Pitfall 3)"
  - "Branch-on-edit checks for subsequent messages (editedIdx < messages.length - 1) before branching"
  - "bookmarkedMessageIds as Set<string> for O(1) lookup; rebuilt from useChatBookmarks per-conversation data"
  - "ChatMessageBookmark receives empty string placeholders for messageId/conversationId since parent manages state"
  - "GitBranch indicator shown via relative positioning overlay on ChatConversationList items with parentConversationId"
metrics:
  duration: "4 minutes"
  completed_date: "2026-04-01"
  tasks_completed: 3
  files_modified: 7
---

# Phase 24 Plan 03: Integration Wiring Summary

**One-liner:** Full Phase 24 integration — FTS search via Cmd+K, scroll-to-message, bookmark toggles on all messages, branch-on-edit with branch selector, and Markdown export wired into ChatPanel.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | ChatPanelContext + ChatPanel wiring (search, branch, export, scroll-to) | 183869d8 | ChatPanelContext.tsx, ChatPanel.tsx, ChatMessageList.tsx, CommandPalette.tsx, ChatConversationList.tsx |
| 2 | ChatMessage + ChatMessageActions bookmark integration | 2b526e78 | ChatMessage.tsx, ChatMessageActions.tsx |
| 3 | Verify complete Phase 24 functionality | auto-approved | (build verification only) |

## What Was Built

### ChatPanelContext — scrollToMessageId
Added `scrollToMessageId: string | null` and `setScrollToMessageId` to the context interface and provider. Allows any component to request scroll navigation to a specific message ID.

### CommandPalette — "Search chat messages"
Added a new command item with value `search-chat` in the Actions group. On select it dispatches `new CustomEvent("nexus:open-chat-search")` then closes the palette. This avoids Cmd+K conflicts by routing through the existing palette pattern.

### ChatPanel — full integration
- Listens for `nexus:open-chat-search` event to open `ChatSearchDialog`
- `onNavigate` callback calls `setActiveConversationId` + `setScrollToMessageId` + closes dialog
- Fetches branches via `useQuery(["chat", "branches", activeConversationId])` and renders `ChatBranchSelector` above message list when branches exist
- Bookmark header button toggles `ChatBookmarkList` in a bounded panel (maxHeight 200px)
- `ChatBookmarkList.onNavigate` wired identically to search navigation
- Export button (Download icon) calls `window.location.href = chatApi.exportConversation(id, "markdown")`
- `handleEdit` detects subsequent messages (`editedIdx < messages.length - 1`) and calls `chatApi.branchConversation` first, then switches to the new branch before re-streaming
- All edit/retry paths invalidate `["chat", "search"]` queries
- `useChatBookmarks(companyId, activeConversationId)` + `useToggleBookmark()` manage bookmark state
- `bookmarkedMessageIds` built as `Set<string>` for O(1) lookup; passed through `ChatMessageList` to each `ChatMessage`

### ChatMessageList — scroll-to-message
- Imports `useChatPanel()` to read `scrollToMessageId` and `setScrollToMessageId`
- `useEffect` on `scrollToMessageId`: finds message index in `displayMessages`, calls `virtualizer.scrollToIndex(index, { align: "center" })`, resets to null
- Added `onBookmark` and `bookmarkedMessageIds` props, threaded into each `ChatMessage`

### ChatMessage — bookmark props
- Added `onBookmark?: (messageId: string) => void` and `isBookmarked?: boolean` to props
- Threads to `ChatMessageActions` for both user and assistant roles
- System/specialized messages (spec_card, handoff, task_created, status_update) are unaffected

### ChatMessageActions — bookmark button
- Added `onBookmark?: () => void` and `isBookmarked?: boolean` props
- Renders `ChatMessageBookmark` as the last action for user messages (inside hover group) and assistant messages (inside hover group)
- System messages return null as before

### ChatConversationList — branch indicators
- Imported `GitBranch` from lucide-react
- Branch conversations (where `parentConversationId` is non-null) get a `GitBranch` icon overlaid via absolute positioning and `pl-4` indent on the conversation item

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Task 1 and Task 2 implemented in same compilation pass**
- **Found during:** Task 1 build
- **Issue:** TypeScript rejected `onBookmark`/`isBookmarked` props passed from `ChatMessageList` to `ChatMessage` because `ChatMessage` didn't have those props yet (Task 2). Build failed.
- **Fix:** Applied Task 2 (ChatMessage + ChatMessageActions props) before committing Task 1, then committed them as separate logical commits after the build passed.
- **Files modified:** ChatMessage.tsx, ChatMessageActions.tsx
- **Commits:** Task 1: 183869d8, Task 2: 2b526e78

**2. [Rule 1 - Bug] ChatMessageActions assistant section had conflicting Tailwind classes**
- **Found during:** Task 2 implementation review
- **Issue:** Original rewrite had `hidden group-hover:flex` on wrapper div — conflicting visibility classes. The correct pattern (per existing codebase) is `hidden group-hover:inline-flex` on individual buttons.
- **Fix:** Used `hidden group-hover:inline-flex` on buttons/inner wrappers, kept `flex` on container.
- **Files modified:** ChatMessageActions.tsx
- **Commit:** 2b526e78

## Known Stubs

None — all navigation, bookmark toggle, branch creation, and export are fully wired to real API calls.

## Self-Check: PASSED

Files verified:
- ui/src/context/ChatPanelContext.tsx — FOUND
- ui/src/components/ChatPanel.tsx — FOUND
- ui/src/components/ChatMessageList.tsx — FOUND
- ui/src/components/ChatMessage.tsx — FOUND
- ui/src/components/ChatMessageActions.tsx — FOUND
- ui/src/components/CommandPalette.tsx — FOUND
- ui/src/components/ChatConversationList.tsx — FOUND

Commits verified:
- 183869d8 — FOUND (Task 1)
- 2b526e78 — FOUND (Task 2)

Build: pnpm --filter @paperclipai/ui build — PASSED
