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>
14 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 23-brainstormer-flow | 03 | execute | 2 |
|
|
true |
|
|
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.
<execution_context> @.claude/get-shit-done/workflows/execute-plan.md @.claude/get-shit-done/templates/summary.md </execution_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.mdFrom ui/src/components/ChatMessage.tsx (current):
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):
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):
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):
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
Add messageType?: string | null; and conversationId?: string; to ChatMessageProps.
Import the four new components at the top:
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:
// 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.
- Update ChatMessageList.tsx:
Pass messageType and conversationId to ChatMessage:
- In the
<ChatMessage>JSX, add:messageType={msg.messageType}andconversationId={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.
- Update ui/src/api/chat.ts:
Add handoffSpec method to the chatApi object:
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:
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";
```
-
Call the hook near the top of the component (after existing state declarations):
const brainstormerDefaultId = useBrainstormerDefault(); -
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)
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
messagesvariable is available in ChatPanel. It may come fromuseChatMessagesor from theChatMessageListdata. Read the full ChatPanel to find the right variable name and source. If messages aren't directly available in ChatPanel, check ifactiveConversationIdbeing null is a sufficient proxy for "new conversation."Alternative approach (if messages not in ChatPanel scope): Use
activeConversationId === nullas proxy for "new conversation." When a user creates a new conversation, the ID is null until the first message is sent. So:useEffect(() => { if (activeAgentId === null && brainstormerDefaultId !== null && !activeConversationId) { setActiveAgentId(brainstormerDefaultId); } }, [activeAgentId, brainstormerDefaultId, activeConversationId]); -
Add
handleHandoffcallback and pass it through to ChatMessageList: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
chatApifrom../api/chat,toastfrom sonner (check existing import pattern), anduseQueryClientfrom tanstack react-query. -
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
activeConversationIdis managed - How
queryClientis 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
<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> ChatPanel auto-selects brainstormer (general agent) on new conversations; handleHandoff callback calls handoff API and invalidates messages cache; toast shows on failure
<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>