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>
377 lines
14 KiB
Markdown
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>
|