--- phase: 23-brainstormer-flow plan: 03 type: execute wave: 2 depends_on: ["23-01", "23-02"] files_modified: - ui/src/components/ChatMessage.tsx - ui/src/components/ChatMessageList.tsx - ui/src/components/ChatPanel.tsx - ui/src/api/chat.ts autonomous: true requirements: - AGENT-01 - AGENT-02 - AGENT-03 - AGENT-05 - AGENT-06 - AGENT-07 - CHAT-09 must_haves: truths: - "Spec card renders inline in chat when message has messageType spec_card" - "Handoff indicator renders inline when message has messageType handoff" - "Task created badge renders inline when message has messageType task_created" - "Status update badge renders inline when message has messageType status_update" - "New conversations auto-select the Brainstormer (general agent) by default" - "Send to PM triggers handoff API call and optimistic UI insertion" artifacts: - path: "ui/src/components/ChatMessage.tsx" provides: "messageType dispatch to specialized components" contains: "messageType" - path: "ui/src/components/ChatMessageList.tsx" provides: "messageType prop propagation to ChatMessage" contains: "messageType" - path: "ui/src/components/ChatPanel.tsx" provides: "useBrainstormerDefault wiring" contains: "useBrainstormerDefault" - path: "ui/src/api/chat.ts" provides: "handoffSpec API method" contains: "handoffSpec" key_links: - from: "ui/src/components/ChatMessageList.tsx" to: "ui/src/components/ChatMessage.tsx" via: "messageType prop passed from message data" pattern: "messageType.*msg\\.messageType" - from: "ui/src/components/ChatMessage.tsx" to: "ui/src/components/ChatSpecCard.tsx" via: "conditional render when messageType === spec_card" pattern: "spec_card.*ChatSpecCard" - from: "ui/src/components/ChatPanel.tsx" to: "ui/src/hooks/useBrainstormerDefault.ts" via: "hook call for default agent selection" pattern: "useBrainstormerDefault" - from: "ui/src/components/ChatSpecCard.tsx" to: "ui/src/api/chat.ts" via: "handoffSpec call on Send to PM" pattern: "handoffSpec" --- Wire all Phase 23 components into the existing chat pipeline: messageType dispatch in ChatMessage, prop propagation in ChatMessageList, brainstormer default in ChatPanel, and handoff API in chatApi. Purpose: This is the integration plan that connects the server routes (Plan 01) and UI components (Plan 02) to the existing chat infrastructure from Phases 21-22. Without this wiring, the new components are isolated and unreachable. Output: Extended ChatMessage, ChatMessageList, ChatPanel, and chatApi with full Phase 23 functionality. @.claude/get-shit-done/workflows/execute-plan.md @.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/23-brainstormer-flow/23-RESEARCH.md @.planning/phases/23-brainstormer-flow/23-UI-SPEC.md @.planning/phases/23-brainstormer-flow/23-01-SUMMARY.md @.planning/phases/23-brainstormer-flow/23-02-SUMMARY.md From ui/src/components/ChatMessage.tsx (current): ```typescript interface ChatMessageProps { id?: string; role: "user" | "assistant" | "system"; content: string; agentName?: string | null; agentIcon?: string | null; agentRole?: AgentRole | null; timestamp?: string; isStreaming?: boolean; isAnyStreaming?: boolean; onEdit?: (messageId: string, newContent: string) => void; onRetry?: (messageId: string) => void; } // Currently: role === "user" renders right-aligned bubble // Otherwise: renders assistant/system with ChatMarkdownMessage ``` From ui/src/components/ChatMessageList.tsx (current): ```typescript interface ChatMessageListProps { conversationId: string; streamingContent?: string; isStreaming?: boolean; streamingAgentName?: string | null; streamingAgentIcon?: string | null; streamingAgentRole?: AgentRole | null; onEdit?: (messageId: string, newContent: string) => void; onRetry?: (messageId: string) => void; agentMap?: Map; } // displayMessages type: Array // The streaming synthetic entry needs messageType: null ``` From ui/src/components/ChatPanel.tsx (current): ```typescript const [activeAgentId, setActiveAgentId] = useState(null); // agentMap = useMemo(() => new Map from agents list) // No useBrainstormerDefault wiring yet ``` From packages/shared/src/types/chat.ts (after Plan 00): ```typescript export interface ChatMessage { id: string; conversationId: string; role: "user" | "assistant" | "system"; content: string; agentId: string | null; messageType: string | null; // NEW in Plan 00 createdAt: string; updatedAt: string | null; } ``` New components from Plan 02: - ChatSpecCard: `({ content, messageId, conversationId, onHandoff }) => JSX` - ChatHandoffIndicator: `({ content }) => JSX` - ChatTaskCreatedBadge: `({ taskId, taskTitle, taskUrl }) => JSX` - ChatStatusUpdateBadge: `({ agentName, taskId, taskTitle, taskUrl }) => JSX` - useBrainstormerDefault: `() => string | null` Task 1: ChatMessage dispatch, ChatMessageList propagation, and chatApi handoff method - ui/src/components/ChatMessage.tsx - ui/src/components/ChatMessageList.tsx - ui/src/api/chat.ts - ui/src/components/ChatSpecCard.tsx - ui/src/components/ChatHandoffIndicator.tsx - ui/src/components/ChatTaskCreatedBadge.tsx - ui/src/components/ChatStatusUpdateBadge.tsx ui/src/components/ChatMessage.tsx, ui/src/components/ChatMessageList.tsx, ui/src/api/chat.ts 1. **Update ChatMessage.tsx:** Add `messageType?: string | null;` and `conversationId?: string;` to `ChatMessageProps`. Import the four new components at the top: ```typescript import { ChatSpecCard } from "./ChatSpecCard"; import { ChatHandoffIndicator } from "./ChatHandoffIndicator"; import { ChatTaskCreatedBadge } from "./ChatTaskCreatedBadge"; import { ChatStatusUpdateBadge } from "./ChatStatusUpdateBadge"; ``` Add a messageType dispatch block BEFORE the existing `if (role === "user")` check. This goes right after the `const [editValue, setEditValue] = ...` line: ```typescript // Dispatch to specialized system message components (Phase 23) if (role === "system" || messageType) { if (messageType === "spec_card") { return ( ); } if (messageType === "handoff") { return ; } if (messageType === "task_created") { // Parse JSON content for task badge props try { const data = JSON.parse(content); return ; } catch { return ; } } if (messageType === "status_update") { try { const data = JSON.parse(content); return ; } catch { return null; } } // Fall through to default system message rendering (plain markdown) } ``` Also add `onHandoff?: (spec: { what: string; why: string; constraints: string; success: string }) => void;` and `conversationId?: string;` to the props interface and destructuring. 2. **Update ChatMessageList.tsx:** Pass `messageType` and `conversationId` to `ChatMessage`: - In the `` JSX, add: `messageType={msg.messageType}` and `conversationId={conversationId}` - In the synthetic streaming entry object, add: `messageType: null,` so that the streaming message does not trigger the system message dispatch Also add `onHandoff` prop to `ChatMessageListProps` and pass it through to `ChatMessage`. 3. **Update ui/src/api/chat.ts:** Add `handoffSpec` method to the `chatApi` object: ```typescript handoffSpec( conversationId: string, spec: { what: string; why: string; constraints: string; success: string }, targetRole: string = "pm", ) { return api.post<{ handoffMessageId: string; issues: Array<{ id: string; identifier: string; title: string }> }>( `/conversations/${conversationId}/handoff`, { spec, targetRole }, ); }, ``` Also add `postStatusUpdate` method: ```typescript postStatusUpdate( conversationId: string, data: { agentName: string; taskId: string; taskTitle?: string; taskUrl?: string }, ) { return api.post<{ id: string }>(`/conversations/${conversationId}/status-update`, data); }, ``` cd /opt/nexus && pnpm exec tsc --noEmit -p ui/tsconfig.json 2>&1 | head -20 - grep -q "messageType" ui/src/components/ChatMessage.tsx - grep -q "ChatSpecCard" ui/src/components/ChatMessage.tsx - grep -q "ChatHandoffIndicator" ui/src/components/ChatMessage.tsx - grep -q "ChatTaskCreatedBadge" ui/src/components/ChatMessage.tsx - grep -q "ChatStatusUpdateBadge" ui/src/components/ChatMessage.tsx - grep -q "messageType" ui/src/components/ChatMessageList.tsx - grep -q "handoffSpec" ui/src/api/chat.ts - grep -q "postStatusUpdate" ui/src/api/chat.ts ChatMessage dispatches to specialized components based on messageType; ChatMessageList passes messageType from stored messages; chatApi has handoffSpec and postStatusUpdate methods Task 2: ChatPanel brainstormer default wiring and handoff callback - ui/src/components/ChatPanel.tsx - ui/src/hooks/useBrainstormerDefault.ts - ui/src/hooks/useChatMessages.ts - ui/src/api/chat.ts ui/src/components/ChatPanel.tsx 1. Import `useBrainstormerDefault` from `../hooks/useBrainstormerDefault`: ```typescript import { useBrainstormerDefault } from "../hooks/useBrainstormerDefault"; ``` 2. Call the hook near the top of the component (after existing state declarations): ```typescript const brainstormerDefaultId = useBrainstormerDefault(); ``` 3. Add a useEffect for auto-selection of the brainstormer default agent. This fires ONLY when: - `activeAgentId === null` (no agent manually selected) - `brainstormerDefaultId !== null` (a general agent exists) - The conversation has no messages (new conversation) ```typescript useEffect(() => { if (activeAgentId === null && brainstormerDefaultId !== null) { // Only auto-select for new conversations with no messages const hasMessages = messages && messages.length > 0; if (!hasMessages) { setActiveAgentId(brainstormerDefaultId); } } }, [activeAgentId, brainstormerDefaultId, messages]); ``` IMPORTANT: Check what `messages` variable is available in ChatPanel. It may come from `useChatMessages` or from the `ChatMessageList` data. Read the full ChatPanel to find the right variable name and source. If messages aren't directly available in ChatPanel, check if `activeConversationId` being null is a sufficient proxy for "new conversation." Alternative approach (if messages not in ChatPanel scope): Use `activeConversationId === null` as proxy for "new conversation." When a user creates a new conversation, the ID is null until the first message is sent. So: ```typescript useEffect(() => { if (activeAgentId === null && brainstormerDefaultId !== null && !activeConversationId) { setActiveAgentId(brainstormerDefaultId); } }, [activeAgentId, brainstormerDefaultId, activeConversationId]); ``` 4. Add `handleHandoff` callback and pass it through to ChatMessageList: ```typescript const handleHandoff = useCallback(async (spec: { what: string; why: string; constraints: string; success: string }) => { if (!activeConversationId) return; try { await chatApi.handoffSpec(activeConversationId, spec, "pm"); // Invalidate messages to show the new handoff + task_created messages queryClient.invalidateQueries({ queryKey: ["chat", "messages", activeConversationId] }); // Toast or other success feedback can be added here } catch { toast.error("Could not send to PM. Try again."); } }, [activeConversationId, queryClient]); ``` Import `chatApi` from `../api/chat`, `toast` from sonner (check existing import pattern), and `useQueryClient` from tanstack react-query. 5. Pass `onHandoff={handleHandoff}` to `` (which passes it through to ChatMessage per Task 1). NOTE: Read the full ChatPanel.tsx to understand the existing patterns for: - How `activeConversationId` is managed - How `queryClient` is accessed (useQueryClient or from context) - How toast is imported (sonner pattern from Phase 21) - Where to place the new useEffect relative to existing effects cd /opt/nexus && pnpm exec tsc --noEmit -p ui/tsconfig.json 2>&1 | head -20 && pnpm vitest run --project=ui 2>&1 | tail -15 - grep -q "useBrainstormerDefault" ui/src/components/ChatPanel.tsx - grep -q "brainstormerDefaultId" ui/src/components/ChatPanel.tsx - grep -q "handleHandoff" ui/src/components/ChatPanel.tsx - grep -q "onHandoff" ui/src/components/ChatPanel.tsx ChatPanel auto-selects brainstormer (general agent) on new conversations; handleHandoff callback calls handoff API and invalidates messages cache; toast shows on failure - `pnpm exec tsc --noEmit -p ui/tsconfig.json` passes - `pnpm vitest run --project=ui` passes - `pnpm vitest run` (full suite) passes - Opening a new conversation auto-selects the general (brainstormer) agent - Messages with messageType render specialized components instead of markdown - Spec card "Send to PM" calls handoff API and shows handoff indicator + task badges - Streaming synthetic messages have messageType: null (no false dispatch) - All existing tests still pass After completion, create `.planning/phases/23-brainstormer-flow/23-03-SUMMARY.md`