` at the bottom of the list. When it enters the viewport and `hasNextPage` is true, call `fetchNextPage()`
+- Delete confirmation: maintain a `deletingId` state. When set, render a shadcn `Dialog` with title "Delete conversation?", body "This conversation and all its messages will be permanently deleted.", and "Delete" (destructive) + "Keep conversation" (outline) buttons
+- Rename handler: `updateMutation.mutate({ id, title: newTitle })`
+- Pin handler: `updateMutation.mutate({ id, pinnedAt: pinned ? new Date().toISOString() : null })`
+- Archive handler: `updateMutation.mutate({ id, archivedAt: new Date().toISOString() })`
+- Delete handler: `deleteMutation.mutate(id)` then clear `deletingId` and if the deleted conversation was active, set `activeConversationId` to null
+
+**ChatMessageList.tsx:**
+
+Create `ui/src/components/ChatMessageList.tsx`:
+
+Props:
+```typescript
+interface ChatMessageListProps {
+ conversationId: string;
+}
+```
+
+Implementation:
+- Uses `useChatMessages(conversationId)` hook
+- Renders messages in a container with `space-y-4`
+- Maps `messages` array (already chronological from the hook) to `
`
+- Auto-scroll: use a `useRef` on a bottom sentinel div and `useEffect` that scrolls it into view when `messages.length` changes
+- Empty state: "Send a message to start this conversation." centered
+- Wrap in a `ScrollArea` or use a plain `div` with `overflow-auto flex-1`
+- The parent (`ChatPanel`) wraps this in the scroll region
+
+**ChatPanel.tsx update:**
+
+Replace the placeholder content in `ChatPanel.tsx` (from Plan 04) with the real components:
+
+- Import `ChatConversationList`, `ChatMessageList`, `useCompany`, `useChatMessages`
+- Get `selectedCompanyId` from `useCompany()`
+- Get `activeConversationId`, `setActiveConversationId` from `useChatPanel()`
+- Wire `useChatMessages(activeConversationId)` for the send handler
+- Left column: `
` (guard: only render if `selectedCompanyId`)
+- Right column:
+ - If `activeConversationId`: render `
`
+ - If no `activeConversationId`: show empty state "Send a message to start this conversation."
+- Wire ChatInput's `onSend` to: if no activeConversationId, first create a conversation, then send message. If activeConversationId exists, just send message:
+ ```typescript
+ const handleSend = async (content: string) => {
+ let convId = activeConversationId;
+ if (!convId) {
+ const newConvo = await chatApi.createConversation(selectedCompanyId!, {});
+ convId = newConvo.id;
+ setActiveConversationId(convId);
+ }
+ await chatApi.postMessage(convId, { role: "user", content });
+ // Invalidate queries
+ queryClient.invalidateQueries({ queryKey: ["chat", "messages", convId] });
+ queryClient.invalidateQueries({ queryKey: ["chat", "conversations"] });
+ };
+ ```
+ Use `useMutation` or direct api calls with `useQueryClient` for invalidation.
+- Pass `isSubmitting` to ChatInput from the mutation state
+
+
+ cd /opt/nexus && grep -q "ChatConversationList" ui/src/components/ChatConversationList.tsx && grep -q "ChatConversationItem" ui/src/components/ChatConversationItem.tsx && grep -q "ChatMessageList" ui/src/components/ChatMessageList.tsx && grep -q "ChatConversationList" ui/src/components/ChatPanel.tsx && grep -q "ChatMessageList" ui/src/components/ChatPanel.tsx && grep -q "postMessage" ui/src/components/ChatPanel.tsx && echo "OK"
+
+
+ - ui/src/components/ChatConversationList.tsx uses `useChatConversations` hook
+ - ui/src/components/ChatConversationList.tsx renders `Plus` icon button for new conversation
+ - ui/src/components/ChatConversationList.tsx has IntersectionObserver or sentinel div for infinite scroll
+ - ui/src/components/ChatConversationList.tsx shows 5 Skeleton elements during loading
+ - ui/src/components/ChatConversationList.tsx has delete confirmation Dialog with "Delete conversation?" title
+ - ui/src/components/ChatConversationItem.tsx renders `DropdownMenu` with Rename, Pin/Unpin, Archive, Delete items
+ - ui/src/components/ChatConversationItem.tsx applies `bg-accent/60` when `isActive`
+ - ui/src/components/ChatMessageList.tsx uses `useChatMessages` hook
+ - ui/src/components/ChatMessageList.tsx renders `ChatMessage` components
+ - ui/src/components/ChatMessageList.tsx auto-scrolls to bottom on new messages
+ - ui/src/components/ChatPanel.tsx renders `ChatConversationList` in the left column
+ - ui/src/components/ChatPanel.tsx renders `ChatMessageList` when `activeConversationId` is set
+ - ui/src/components/ChatPanel.tsx creates a conversation on first send if none active
+ - ui/src/components/ChatPanel.tsx invalidates queries after sending a message
+
+
Full chat UI wired: conversation list with infinite scroll, CRUD actions (rename, pin, archive, delete with confirmation), message thread with auto-scroll, and send flow that creates conversations on first message.
+
+
+
+ Task 3: Verify complete chat flow
+ none
+
+Human verification checkpoint. No automated work — all implementation was completed in Tasks 1 and 2. The user follows the verification steps below to confirm the complete Phase 21 chat feature works end-to-end.
+
+
+ cd /opt/nexus && pnpm --filter @paperclipai/ui exec -- tsc --noEmit && pnpm --filter @paperclipai/server exec -- tsc --noEmit && echo "TYPE CHECK OK"
+
+
+ - ui/src/components/ChatPanel.tsx
+ - server/src/routes/chat.ts
+
+
+ - TypeScript compilation passes for both ui and server packages
+ - User confirms: chat panel opens/closes from Layout toggle button
+ - User confirms: conversations can be created, renamed, pinned, archived, deleted
+ - User confirms: messages persist across page reload
+ - User confirms: code blocks show syntax highlighting and copy button
+ - User confirms: theme switch changes code block colors
+
+
+Complete Phase 21 Chat Foundation: database persistence, server API, and full chat UI with conversation management, markdown rendering, syntax highlighting, and theme integration.
+
+
+1. Start the server: `cd /opt/nexus && pnpm dev`
+2. Open the app in a browser
+3. Click the MessageSquare (chat) icon in the top-right area — the chat panel should slide open from the right
+4. Click the "+" button to create a new conversation
+5. Type a message and press Enter — the message should appear as a right-aligned bubble
+6. Type a message with a code block:
+ ````
+ Here is some code:
+ ```typescript
+ const x: number = 42;
+ console.log(x);
+ ```
+ ````
+ Send it. The assistant message area will not auto-reply (no streaming in Phase 21), but you can manually POST an assistant message via curl to verify rendering:
+ ```bash
+ curl -X POST http://localhost:3100/api/conversations/CONVERSATION_ID/messages \
+ -H 'Content-Type: application/json' \
+ -d '{"role":"assistant","content":"Here is code:\n```typescript\nconst x: number = 42;\nconsole.log(x);\n```"}'
+ ```
+7. Verify the code block has:
+ - Syntax highlighting (colors matching the active theme)
+ - Language label ("typescript")
+ - Copy button (hover over the code block)
+8. Switch themes (cycle button in top-right) — verify code block colors change
+9. Test conversation management:
+ - Hover a conversation row, click "...", try Rename, Pin, Archive, Delete
+ - Pin a conversation — verify it moves to the top
+ - Delete a conversation — verify confirmation dialog appears
+10. Reload the page — verify conversations and messages persist
+11. Press Shift+Enter in the input — verify newline is inserted
+12. Press Escape in the input — verify content is cleared
+
+ Type "approved" or describe issues to fix
+ User has verified the complete Phase 21 chat flow: panel toggle, conversation CRUD, message persistence, markdown rendering, syntax highlighting, theme integration, and keyboard shortcuts.
+
+
+
+
+
+- All API endpoints respond correctly (conversation CRUD + message CRUD)
+- Conversation list uses infinite scroll (TanStack Query useInfiniteQuery)
+- Messages render with markdown + syntax highlighting
+- Theme switch updates code block colors
+- Data persists across page reload
+- Keyboard shortcuts work (Enter, Shift+Enter, Escape)
+
+
+
+- User can create, rename, pin, archive, and delete conversations
+- User can send messages and see them in the thread
+- Code blocks in messages have syntax highlighting, language label, and copy button
+- Conversation list supports infinite scroll
+- All data persists in PostgreSQL across server restarts
+- Chat panel respects all three themes
+
+
+