---
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"
---

<objective>
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.
</objective>

<execution_context>
@.claude/get-shit-done/workflows/execute-plan.md
@.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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

<interfaces>
<!-- Current file states the executor needs to understand -->

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<string, { name: string; icon: string | null; role: AgentRole | null }>;
}
// displayMessages type: Array<ChatMessageType & { isStreamingEntry?: boolean }>
// The streaming synthetic entry needs messageType: null
```

From ui/src/components/ChatPanel.tsx (current):
```typescript
const [activeAgentId, setActiveAgentId] = useState<string | null>(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`
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: ChatMessage dispatch, ChatMessageList propagation, and chatApi handoff method</name>
  <read_first>
    - 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
  </read_first>
  <files>
    ui/src/components/ChatMessage.tsx,
    ui/src/components/ChatMessageList.tsx,
    ui/src/api/chat.ts
  </files>
  <action>
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 (
      <ChatSpecCard
        content={content}
        messageId={id}
        conversationId={conversationId}
        onHandoff={onHandoff}
      />
    );
  }
  if (messageType === "handoff") {
    return <ChatHandoffIndicator content={content} />;
  }
  if (messageType === "task_created") {
    // Parse JSON content for task badge props
    try {
      const data = JSON.parse(content);
      return <ChatTaskCreatedBadge taskId={data.taskId} taskTitle={data.taskTitle} taskUrl={data.taskUrl} />;
    } catch {
      return <ChatTaskCreatedBadge />;
    }
  }
  if (messageType === "status_update") {
    try {
      const data = JSON.parse(content);
      return <ChatStatusUpdateBadge agentName={data.agentName} taskId={data.taskId} taskTitle={data.taskTitle} taskUrl={data.taskUrl} />;
    } 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 `<ChatMessage>` 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);
},
```
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm exec tsc --noEmit -p ui/tsconfig.json 2>&1 | head -20</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>ChatMessage dispatches to specialized components based on messageType; ChatMessageList passes messageType from stored messages; chatApi has handoffSpec and postStatusUpdate methods</done>
</task>

<task type="auto">
  <name>Task 2: ChatPanel brainstormer default wiring and handoff callback</name>
  <read_first>
    - ui/src/components/ChatPanel.tsx
    - ui/src/hooks/useBrainstormerDefault.ts
    - ui/src/hooks/useChatMessages.ts
    - ui/src/api/chat.ts
  </read_first>
  <files>ui/src/components/ChatPanel.tsx</files>
  <action>
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 `<ChatMessageList>` (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
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm exec tsc --noEmit -p ui/tsconfig.json 2>&1 | head -20 && pnpm vitest run --project=ui 2>&1 | tail -15</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>ChatPanel auto-selects brainstormer (general agent) on new conversations; handleHandoff callback calls handoff API and invalidates messages cache; toast shows on failure</done>
</task>

</tasks>

<verification>
- `pnpm exec tsc --noEmit -p ui/tsconfig.json` passes
- `pnpm vitest run --project=ui` passes
- `pnpm vitest run` (full suite) passes
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.planning/phases/23-brainstormer-flow/23-03-SUMMARY.md`
</output>
