nexus/.planning/phases/23-brainstormer-flow/23-03-PLAN.md
Nexus Dev 9ed6dd16b3 docs(23-brainstormer-flow): create phase plan — 4 plans across 3 waves
Plan 00 (Wave 0): DB migration for message_type, shared types/validators, test stubs
Plan 01 (Wave 1): Server — addSystemMessage, handoff route, status-update route
Plan 02 (Wave 1): UI — ChatSpecCard, ChatHandoffIndicator, ChatTaskCreatedBadge, ChatStatusUpdateBadge, useBrainstormerDefault
Plan 03 (Wave 2): Wiring — ChatMessage dispatch, ChatMessageList propagation, ChatPanel brainstormer default, chatApi handoff

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:47 +00:00

377 lines
14 KiB
Markdown

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