--- phase: 24-search-history-branching verified: 2026-04-01T00:00:00Z status: gaps_found score: 3/4 success criteria verified gaps: - truth: "User can export any conversation as a Markdown file or as a JSON file" status: partial reason: "Server supports both formats (markdown + json) and chatApi.exportConversation accepts format param, but ChatPanel only renders a single 'Export as Markdown' button (calls handleExport('markdown')). No UI control triggers JSON export." artifacts: - path: "ui/src/components/ChatPanel.tsx" issue: "Only one export button at line 261: onClick={() => handleExport('markdown')}. JSON export path (handleExport('json')) is never called from any UI element." missing: - "Add a second export button (or dropdown) in ChatPanel header to trigger handleExport('json') — the handler, API client method, server route, and service all exist and work; only the UI trigger is missing" human_verification: - test: "Confirm Cmd+K -> 'Search chat messages' -> type query -> click result scrolls to the correct message" expected: "Search overlay opens, results appear for matching terms within ~500ms, clicking a result switches to that conversation and the virtualizer scrolls to the target message" why_human: "Cannot test keyboard shortcuts, overlay rendering, or virtualizer scroll behavior programmatically without running the app" - test: "Confirm bookmark toggle persists: hover a message, click the bookmark icon (fills solid), refresh page, verify icon is still filled" expected: "Bookmark state survives page reload, indicating the POST /conversations/:id/bookmarks endpoint is being called and the DB write is real" why_human: "Requires live browser interaction and server running" - test: "Confirm branch-on-edit: send two messages, click edit on the first, submit — verify a new branch conversation appears in the sidebar with a GitBranch icon" expected: "Branch conversation is created, sidebar shows it indented under the original with a GitBranch indicator, branch selector bar appears above the message list" why_human: "Requires live server, active agent streaming session, and visual inspection of the sidebar grouping" - test: "Confirm Markdown export downloads a .md file with agent names (not UUIDs) in the headers" expected: "Browser downloads a file named -.md; assistant messages show agent name (e.g. 'Brainstormer') not a UUID" why_human: "Requires browser file download and manual inspection of content" --- # Phase 24: Search, History & Branching — Verification Report **Phase Goal:** Users can find any message across all conversations in under 500ms, export conversations, bookmark key messages, and branch from any point in a conversation **Verified:** 2026-04-01 **Status:** gaps_found — 1 gap blocking complete goal achievement **Re-verification:** No — initial verification --- ## Goal Achievement ### Success Criteria (from ROADMAP.md) | # | Criterion | Status | Evidence | |---|-----------|--------|----------| | 1 | Cmd+K opens search overlay; results returned in <500ms across 10,000+ messages | ? HUMAN NEEDED | FTS pipeline fully wired (tsvector GIN index → plainto_tsquery → ts_rank → ChatSearchDialog → CommandPalette custom event); runtime perf requires human test | | 2 | User can bookmark any message and navigate to bookmarked messages | ✓ VERIFIED | ChatMessageBookmark renders on every message via ChatMessageActions; useToggleBookmark mutation calls POST /bookmarks; ChatBookmarkList with onNavigate wired into ChatPanel | | 3 | Editing a message with a response creates a branch; user can switch branches | ✓ VERIFIED | handleEdit checks editedIdx < messages.length - 1 then calls chatApi.branchConversation; ChatBranchSelector rendered when branches.length > 0; ChatConversationList shows GitBranch icon + pl-4 indent | | 4 | User can export as Markdown **or** JSON | ✗ FAILED | Server route and service support both formats; chatApi.exportConversation accepts format param; ChatPanel only exposes markdown button — JSON export has no UI trigger | **Score:** 3/4 success criteria verified (criterion 1 needs human runtime check; criterion 4 has a confirmed code gap) --- ## Required Artifacts ### Plan 00 — DB Migrations, Schema, Shared Types | Artifact | Expected | Status | Details | |----------|----------|--------|---------| | `packages/db/src/migrations/0050_add_branch_columns.sql` | Branch columns migration | ✓ VERIFIED | Contains `parent_conversation_id` FK + `branch_from_message_id` + index | | `packages/db/src/migrations/0051_add_message_search_vector.sql` | tsvector + GIN index migration | ✓ VERIFIED | `content_search` generated column + GIN index present | | `packages/db/src/migrations/0052_create_chat_message_bookmarks.sql` | Bookmarks table migration | ✓ VERIFIED | Table + two compound indexes for company+message and company+conversation | | `packages/db/src/schema/chat_message_bookmarks.ts` | Drizzle schema for bookmarks | ✓ VERIFIED | `chatMessageBookmarks` exported; compound indexes defined | | `packages/db/src/schema/chat_conversations.ts` | Branch columns added | ✓ VERIFIED | `parentConversationId` with `AnyPgColumn` for self-referential FK; `branchFromMessageId`; `parentIdx` | | `packages/db/src/schema/index.ts` | Re-exports chatMessageBookmarks | ✓ WIRED | Line 61: `export { chatMessageBookmarks } from "./chat_message_bookmarks.js"` | | `packages/shared/src/types/chat.ts` | Search, bookmark, branch types | ✓ VERIFIED | `ChatMessageSearchResult`, `ChatBookmark`, `ChatBookmarkWithMessage`, `ChatBookmarkToggleResponse`; `parentConversationId` on `ChatConversation` | | `packages/shared/src/validators/chat.ts` | searchMessagesSchema, branchConversationSchema | ✓ VERIFIED | Both validators present with correct shapes | | `packages/shared/src/index.ts` | Re-exports new types and validators | ✓ WIRED | Lines 565–566, 581: all new symbols exported | | `server/src/__tests__/chat-service.test.ts` | Wave 0 test stubs | ✓ VERIFIED | 4 describe blocks with it.todo entries (searchMessages, toggleBookmark, branchConversation, exportConversation) | ### Plan 01 — Server Service Methods and Routes | Artifact | Expected | Status | Details | |----------|----------|--------|---------| | `server/src/services/chat.ts` | Six service methods | ✓ VERIFIED | searchMessages (tsvector FTS), toggleBookmark (transactional), getBookmarks (join), branchConversation (message copy), listBranches, exportConversation (agent LEFT JOIN) | | `server/src/routes/chat.ts` | Six route handlers | ✓ VERIFIED | GET /messages/search, POST /bookmarks, GET /bookmarks, POST /branch, GET /branches, GET /export — all with assertBoard guard | ### Plan 02 — UI API Client, Hooks, Components | Artifact | Expected | Status | Details | |----------|----------|--------|---------| | `ui/src/api/chat.ts` | Six new chatApi methods | ✓ VERIFIED | searchMessages, toggleBookmark, getBookmarks, branchConversation, listBranches, exportConversation (returns URL string) | | `ui/src/hooks/useChatSearch.ts` | Debounced FTS query hook | ✓ VERIFIED | placeholderData, staleTime: 30_000, enabled when query >= 2 chars | | `ui/src/hooks/useChatBookmarks.ts` | Bookmark query + mutation hooks | ✓ VERIFIED | useChatBookmarks + useToggleBookmark; invalidates both `["chat","bookmarks"]` and `["chat","search"]` | | `ui/src/components/ChatSearchDialog.tsx` | CommandDialog FTS overlay | ✓ VERIFIED | Uses CommandDialog, shouldFilter={false}, useChatSearch, HighlightedText (XSS-safe) | | `ui/src/components/ChatMessageBookmark.tsx` | Bookmark toggle button | ✓ VERIFIED | fill-current on isBookmarked; aria-label toggles; ghost h-6 w-6 sizing | | `ui/src/components/ChatBookmarkList.tsx` | Scrollable bookmark list | ✓ VERIFIED | useChatBookmarks; skeleton loading; empty state; onNavigate callback | | `ui/src/components/ChatBranchSelector.tsx` | Horizontal branch picker | ✓ VERIFIED | GitBranch icon; bg-accent for active; renders null when no branches | ### Plan 03 — Integration Wiring | Artifact | Expected | Status | Details | |----------|----------|--------|---------| | `ui/src/context/ChatPanelContext.tsx` | scrollToMessageId state | ✓ VERIFIED | State + setter in interface, provider value, and useState | | `ui/src/components/ChatPanel.tsx` | Full feature integration | ✓ VERIFIED | ChatSearchDialog, ChatBranchSelector, export (markdown only — see gap), bookmarks panel, branch-on-edit, useToggleBookmark, bookmarkedMessageIds Set | | `ui/src/components/ChatMessageList.tsx` | Scroll-to-message support | ✓ VERIFIED | useEffect on scrollToMessageId; virtualizer.scrollToIndex(index, {align:"center"}); resets to null after scroll | | `ui/src/components/ChatMessageActions.tsx` | Bookmark button on messages | ✓ VERIFIED | ChatMessageBookmark rendered as last action; onBookmark + isBookmarked props | | `ui/src/components/ChatMessage.tsx` | Bookmark prop threading | ✓ VERIFIED | onBookmark={id && onBookmark ? () => onBookmark(id) : undefined} — real messageId passed | | `ui/src/components/CommandPalette.tsx` | "Search chat messages" command item | ✓ VERIFIED | value="search-chat"; dispatches `nexus:open-chat-search` custom event | | `ui/src/components/ChatConversationList.tsx` | Branch indicators | ✓ VERIFIED | GitBranch icon + pl-4 indent when parentConversationId is non-null | --- ## Key Link Verification | From | To | Via | Status | Details | |------|----|-----|--------|---------| | `packages/db/src/schema/chat_message_bookmarks.ts` | `packages/db/src/schema/index.ts` | re-export | ✓ WIRED | Line 61 of schema/index.ts | | `packages/shared/src/types/chat.ts` | `packages/shared/src/index.ts` | re-export | ✓ WIRED | Lines 565–581 | | `server/src/routes/chat.ts` | `server/src/services/chat.ts` | svc.searchMessages, svc.toggleBookmark, svc.branchConversation, svc.exportConversation | ✓ WIRED | All four service calls confirmed in route handlers | | `server/src/services/chat.ts` | `packages/db/src/schema/chat_message_bookmarks.ts` | import chatMessageBookmarks | ✓ WIRED | Line 3 of services/chat.ts | | `ui/src/hooks/useChatSearch.ts` | `ui/src/api/chat.ts` | chatApi.searchMessages | ✓ WIRED | Line 7 of useChatSearch.ts | | `ui/src/components/ChatSearchDialog.tsx` | `ui/src/hooks/useChatSearch.ts` | useChatSearch | ✓ WIRED | Line 3 import + line 78 usage | | `ui/src/components/ChatMessageBookmark.tsx` | `ui/src/hooks/useChatBookmarks.ts` | useToggleBookmark | ✓ WIRED | Indirectly — onToggle is wired at ChatPanel → handleBookmark → toggleBookmark; ChatMessageBookmark itself only needs onToggle callback | | `ui/src/components/CommandPalette.tsx` | `ui/src/components/ChatSearchDialog.tsx` | nexus:open-chat-search custom event | ✓ WIRED | CommandPalette dispatches; ChatPanel listens and sets searchOpen(true) | | `ui/src/context/ChatPanelContext.tsx` | `ui/src/components/ChatMessageList.tsx` | scrollToMessageId | ✓ WIRED | useChatPanel() in ChatMessageList; useEffect scrolls virtualizer | | `ui/src/components/ChatPanel.tsx` | `ui/src/components/ChatBranchSelector.tsx` | branches data from useQuery | ✓ WIRED | listBranches query feeds ChatBranchSelector; onSelectBranch calls setActiveConversationId | | `ui/src/components/ChatMessage.tsx` | `ui/src/components/ChatMessageBookmark.tsx` (via ChatMessageActions) | onBookmark prop chain | ✓ WIRED | ChatPanel → handleBookmark → ChatMessageList → ChatMessage → ChatMessageActions → ChatMessageBookmark.onToggle | | `ui/src/components/ChatPanel.tsx` | JSON export | handleExport("json") | ✗ NOT_WIRED | handleExport callback accepts "json" but no UI element calls it; only markdown button rendered | --- ## Data-Flow Trace (Level 4) | Artifact | Data Variable | Source | Produces Real Data | Status | |----------|---------------|--------|--------------------|--------| | `server/src/services/chat.ts` :: searchMessages | rows | Drizzle query with tsvector @@ plainto_tsquery, ts_rank ORDER BY, LIMIT | Yes — real DB join + FTS WHERE clause | ✓ FLOWING | | `server/src/services/chat.ts` :: toggleBookmark | existing | SELECT then INSERT or DELETE in transaction | Yes — real DB read-modify-write | ✓ FLOWING | | `server/src/services/chat.ts` :: branchConversation | messagesToCopy | SELECT lte(createdAt) + INSERT returning | Yes — real DB copy + returns new conversation | ✓ FLOWING | | `server/src/services/chat.ts` :: exportConversation | rows | SELECT + LEFT JOIN agents | Yes — all messages + agent names from DB | ✓ FLOWING | | `ui/src/hooks/useChatSearch.ts` | data | chatApi.searchMessages → GET /companies/:id/messages/search | Yes — enabled when query >= 2 chars, real endpoint | ✓ FLOWING | | `ui/src/hooks/useChatBookmarks.ts` | data | chatApi.getBookmarks → GET /companies/:id/bookmarks | Yes — real endpoint | ✓ FLOWING | | `ui/src/components/ChatPanel.tsx` :: bookmarkedMessageIds | Set | useChatBookmarks(companyId, activeConversationId).data | Yes — rebuilt per-conversation from server data | ✓ FLOWING | | `ui/src/components/ChatPanel.tsx` :: branches | ChatConversation[] | useQuery → chatApi.listBranches → GET /conversations/:id/branches | Yes — real endpoint | ✓ FLOWING | | `ui/src/components/ChatPanel.tsx` :: JSON export | — | handleExport("json") | N/A — UI trigger missing | ✗ DISCONNECTED (no UI entry point) | --- ## Behavioral Spot-Checks | Behavior | Method | Result | Status | |----------|--------|--------|--------| | All 8 Phase 24 commits exist in git | git log for 8 commit hashes | All 8 found (430bbbb8 through 2b526e78) | ✓ PASS | | Migration journal has 0050/0051/0052 entries | grep in _journal.json | All three tags present | ✓ PASS | | Shared package exports key symbols | node -e require check | ChatMessageSearchResult, ChatBookmark, searchMessagesSchema, branchConversationSchema — all FOUND | ✓ PASS | | searchMessages has real FTS WHERE clause | grep in service | `"content_search" @@ plainto_tsquery` with ts_rank ORDER BY found | ✓ PASS | | export route sets Content-Disposition header | grep in routes | Line 273: res.setHeader("Content-Disposition", ...) confirmed | ✓ PASS | | JSON export has UI trigger in ChatPanel | grep for handleExport("json") | Only markdown button at line 261; no JSON trigger | ✗ FAIL | --- ## Requirements Coverage | Requirement | Source Plan(s) | Description | Status | Evidence | |-------------|---------------|-------------|--------|----------| | CHAT-07 | 00, 01, 02, 03 | Full-text search across all conversations | ✓ SATISFIED | tsvector GIN index, plainto_tsquery service, ChatSearchDialog, Cmd+K integration all wired | | CHAT-13 | 00, 01, 02, 03 | Message bookmarks: mark important messages | ✓ SATISFIED | ChatMessageBookmark on every message, toggleBookmark service+route, ChatBookmarkList navigation | | CHAT-14 | 01, 03 | Conversation branching on edit | ✓ SATISFIED | branchConversation service, POST /branch route, handleEdit branch-on-edit logic, ChatBranchSelector | | HIST-04 | 00, 01, 02, 03 | Conversation export as Markdown or JSON | ✗ BLOCKED | Server and API client support both formats; ChatPanel only exposes Markdown button — JSON has no UI trigger | | PERF-04 | 01 | FTS returns results <500ms across 10,000+ messages | ? NEEDS HUMAN | GIN index in place; ts_rank ordering correct; runtime perf requires load test or browser timing | ### Phantom Requirement IDs in Plan Frontmatter The following requirement IDs appear in plan frontmatter (`24-00-PLAN.md` through `24-03-PLAN.md`) but **do not exist** in `REQUIREMENTS.md`: - `HIST-07`, `HIST-08`, `HIST-09`, `HIST-10`, `HIST-11`, `HIST-12` `REQUIREMENTS.md` defines only HIST-01 through HIST-06. These IDs were invented in the plan documents but have no corresponding requirement definitions. The authoritative requirement set for Phase 24 per ROADMAP.md is: **CHAT-07, CHAT-13, CHAT-14, HIST-04, PERF-04** — all of which are accounted for above. --- ## Anti-Patterns Found | File | Line | Pattern | Severity | Impact | |------|------|---------|----------|--------| | `ui/src/components/ChatMessageActions.tsx` | 37, 71 | `messageId=""` and `conversationId=""` passed to `ChatMessageBookmark` | ℹ️ Info | Not a functional stub — `ChatMessageBookmark` does not use these props in its body (destructures only `isBookmarked` and `onToggle`); real messageId flows via `onToggle` callback. Props are dead interface noise, not a bug. | | `ui/src/components/ChatMessageList.tsx` | (comment) | `// TODO: if message not found, best-effort only` | ℹ️ Info | Intentional — plan specified this as acceptable for scroll-to on unpaginated messages. Does not block any success criterion. | No blocking stubs found. All core implementations contain real DB queries, real HTTP calls, and real state management. --- ## Human Verification Required ### 1. Full-text search round-trip with scroll-to-message **Test:** Press Cmd+K in the Nexus UI. Select "Search chat messages". Type a search term that exists in a past conversation message. Verify results appear with conversation title and message snippet. Click a result. **Expected:** ChatSearchDialog closes; the chat panel switches to the target conversation; the virtualizer scrolls to the target message (centered in view) within ~500ms total. **Why human:** Keyboard shortcut dispatch, dialog rendering, virtualizer scroll, and sub-500ms timing cannot be verified without a running browser session. ### 2. Bookmark persistence across page reload **Test:** Open a conversation. Hover a message. Click the bookmark icon (should fill solid). Reload the page. Return to the same conversation and hover the same message. **Expected:** The bookmark icon is still filled, confirming the DB write via POST /conversations/:id/bookmarks persisted and the GET /companies/:id/bookmarks query on reload returns it. **Why human:** Requires live server, DB write, and page reload cycle. ### 3. Branch-on-edit creates visible branch in sidebar **Test:** In a conversation with at least two messages (one user, one assistant reply), click edit on the user message, change the text, and submit. **Expected:** A new branch conversation appears in the sidebar with a GitBranch icon and pl-4 indent under the original. The branch selector bar appears above the message list. Clicking "Original" in the branch selector switches back to the original conversation. **Why human:** Requires an active agent session, streaming, and visual sidebar inspection. ### 4. Markdown export produces correct agent names (not UUIDs) **Test:** In a conversation that involved a named agent, click the Download icon in the chat header. Open the downloaded .md file. **Expected:** The file is named `-.md`; assistant messages show the agent's name (e.g. "Brainstormer") not a UUID; user messages show "You"; format matches `**Speaker** (timestamp)\ncontent\n\n---`. **Why human:** Requires browser file download and manual inspection of content. --- ## Gaps Summary **1 gap blocks full goal achievement:** **JSON export has no UI trigger (Success Criterion 4, HIST-04)** The complete JSON export pipeline exists — the server service (`exportConversation`), the Express route (`GET /conversations/:id/export?format=json`), and the API client method (`chatApi.exportConversation(id, "json")`) all work correctly. The `handleExport` callback in ChatPanel also accepts "json" as a format. However, `ChatPanel.tsx` only renders one export button (line 261) that hardcodes `handleExport("markdown")`. No UI element triggers `handleExport("json")`. **Fix required:** Add a second export button (or a dropdown with two options) in the ChatPanel header that calls `handleExport("json")`. The server, service, route, API client, and handler are all already correct — only the UI trigger is missing. --- _Verified: 2026-04-01_ _Verifier: Claude (gsd-verifier)_