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

14 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
23-brainstormer-flow 03 execute 2
23-01
23-02
ui/src/components/ChatMessage.tsx
ui/src/components/ChatMessageList.tsx
ui/src/components/ChatPanel.tsx
ui/src/api/chat.ts
true
AGENT-01
AGENT-02
AGENT-03
AGENT-05
AGENT-06
AGENT-07
CHAT-09
truths artifacts key_links
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
path provides contains
ui/src/components/ChatMessage.tsx messageType dispatch to specialized components messageType
path provides contains
ui/src/components/ChatMessageList.tsx messageType prop propagation to ChatMessage messageType
path provides contains
ui/src/components/ChatPanel.tsx useBrainstormerDefault wiring useBrainstormerDefault
path provides contains
ui/src/api/chat.ts handoffSpec API method handoffSpec
from to via pattern
ui/src/components/ChatMessageList.tsx ui/src/components/ChatMessage.tsx messageType prop passed from message data messageType.*msg.messageType
from to via pattern
ui/src/components/ChatMessage.tsx ui/src/components/ChatSpecCard.tsx conditional render when messageType === spec_card spec_card.*ChatSpecCard
from to via pattern
ui/src/components/ChatPanel.tsx ui/src/hooks/useBrainstormerDefault.ts hook call for default agent selection useBrainstormerDefault
from to via pattern
ui/src/components/ChatSpecCard.tsx ui/src/api/chat.ts handoffSpec call on Send to PM 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.

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

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

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.

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

  1. 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"; ```
  1. Call the hook near the top of the component (after existing state declarations):

    const brainstormerDefaultId = useBrainstormerDefault();
    
  2. 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 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:

    useEffect(() => {
      if (activeAgentId === null && brainstormerDefaultId !== null && !activeConversationId) {
        setActiveAgentId(brainstormerDefaultId);
      }
    }, [activeAgentId, brainstormerDefaultId, activeConversationId]);
    
  3. Add handleHandoff callback 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 chatApi from ../api/chat, toast from sonner (check existing import pattern), and useQueryClient from tanstack react-query.

  4. 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 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
- `pnpm exec tsc --noEmit -p ui/tsconfig.json` passes - `pnpm vitest run --project=ui` passes - `pnpm vitest run` (full suite) passes

<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>
After completion, create `.planning/phases/23-brainstormer-flow/23-03-SUMMARY.md`