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>
This commit is contained in:
Nexus Dev 2026-04-01 21:37:20 +00:00
parent c4c709cd0f
commit 9ed6dd16b3
5 changed files with 1354 additions and 35 deletions

View file

@ -76,15 +76,13 @@ Plans:
3. When the user clicks "Send to PM," a handoff indicator appears in the chat showing "Brainstormer → PM" with the spec content
4. The PM agent creates one or more Nexus issues from the spec; the user can see task IDs referenced in the PM's reply
5. When an Engineer or Generalist completes a task, a status update message appears in the relevant chat conversation
**Plans:** 6 plans
**Plans:** 4 plans
Plans:
- [x] 22-00-PLAN.md — Wave 0: DB migration, shared types, install virtualizer, agent-role-colors, CSS, test stubs
- [x] 22-01-PLAN.md — SSE streaming endpoint + useStreamingChat hook
- [x] 22-02-PLAN.md — Agent identity bar, streaming cursor, agent selector
- [x] 22-03-PLAN.md — Edit/retry/stop message action controls
- [x] 22-04-PLAN.md — Slash commands and @mention popovers
- [ ] 22-05-PLAN.md — Virtualized message list + full ChatPanel integration
- [ ] 23-00-PLAN.md — DB migration (message_type column), shared types/validators, Wave 0 test stubs
- [ ] 23-01-PLAN.md — Server: addSystemMessage helper, handoff route, status-update route
- [ ] 23-02-PLAN.md — UI: ChatSpecCard, ChatHandoffIndicator, ChatTaskCreatedBadge, ChatStatusUpdateBadge, useBrainstormerDefault
- [ ] 23-03-PLAN.md — Wiring: ChatMessage dispatch, ChatMessageList propagation, ChatPanel brainstormer default, chatApi handoff
**UI hint**: yes
### Phase 24: Search, History & Branching
@ -96,15 +94,7 @@ Plans:
2. User can bookmark any message and later filter or navigate to bookmarked messages
3. Editing a message that already has a response creates a new branch; both the original and the new branch are preserved and the user can switch between them
4. User can export any conversation as a Markdown file or as a JSON file containing all messages and metadata
**Plans:** 6 plans
Plans:
- [x] 22-00-PLAN.md — Wave 0: DB migration, shared types, install virtualizer, agent-role-colors, CSS, test stubs
- [x] 22-01-PLAN.md — SSE streaming endpoint + useStreamingChat hook
- [x] 22-02-PLAN.md — Agent identity bar, streaming cursor, agent selector
- [x] 22-03-PLAN.md — Edit/retry/stop message action controls
- [ ] 22-04-PLAN.md — Slash commands and @mention popovers
- [ ] 22-05-PLAN.md — Virtualized message list + full ChatPanel integration
**Plans:** [To be planned]
**UI hint**: yes
### Phase 25: File System
@ -119,15 +109,7 @@ Plans:
5. When an agent generates a placeholder asset, `PLACEHOLDERS.md` is updated in the project directory; when the placeholder is replaced, the DB records the replacement chain and the manifest reflects the change
6. A file uploaded in a conversation linked to a project lives in `files/projects/<slug>/`; a file from an unlinked conversation lives in `files/chat/<conversation-id>/`; the user can promote a chat file to project scope
7. Voice input is available when local AI is enabled: user can hold the record button, speak, see a transcription preview, and confirm to send
**Plans:** 6 plans
Plans:
- [ ] 22-00-PLAN.md — Wave 0: DB migration, shared types, install virtualizer, agent-role-colors, CSS, test stubs
- [ ] 22-01-PLAN.md — SSE streaming endpoint + useStreamingChat hook
- [ ] 22-02-PLAN.md — Agent identity bar, streaming cursor, agent selector
- [ ] 22-03-PLAN.md — Edit/retry/stop message action controls
- [ ] 22-04-PLAN.md — Slash commands and @mention popovers
- [ ] 22-05-PLAN.md — Virtualized message list + full ChatPanel integration
**Plans:** [To be planned]
**UI hint**: yes
### Phase 26: PWA & Performance
@ -141,15 +123,7 @@ Plans:
4. On a phone, the input bar is sticky at the bottom of the screen, touch targets are large enough to tap without errors, and the layout resizes correctly when the software keyboard appears
5. Pulling down on the conversation list on mobile triggers a refresh; push notifications arrive for agent mentions, task completions, and handoff requests where the platform supports them
6. The initial page load on broadband completes in under 2 seconds and on a 3G connection in under 5 seconds; PWA cached load completes in under 1 second
**Plans:** 6 plans
Plans:
- [ ] 22-00-PLAN.md — Wave 0: DB migration, shared types, install virtualizer, agent-role-colors, CSS, test stubs
- [ ] 22-01-PLAN.md — SSE streaming endpoint + useStreamingChat hook
- [ ] 22-02-PLAN.md — Agent identity bar, streaming cursor, agent selector
- [ ] 22-03-PLAN.md — Edit/retry/stop message action controls
- [ ] 22-04-PLAN.md — Slash commands and @mention popovers
- [ ] 22-05-PLAN.md — Virtualized message list + full ChatPanel integration
**Plans:** [To be planned]
**UI hint**: yes
---
@ -232,7 +206,7 @@ All 65 v1 requirements are mapped to exactly one phase. No orphans.
|-------|-----------|----------------|--------|-----------|
| 21. Chat Foundation | v1.3 | 7/7 | Complete | 2026-04-01 |
| 22. Agent Streaming | v1.3 | 6/6 | Complete | 2026-04-01 |
| 23. Brainstormer Flow | v1.3 | 0/? | Not started | - |
| 23. Brainstormer Flow | v1.3 | 0/4 | Planned | - |
| 24. Search, History & Branching | v1.3 | 0/? | Not started | - |
| 25. File System | v1.3 | 0/? | Not started | - |
| 26. PWA & Performance | v1.3 | 0/? | Not started | - |

View file

@ -0,0 +1,313 @@
---
phase: 23-brainstormer-flow
plan: 00
type: execute
wave: 0
depends_on: []
files_modified:
- packages/db/src/schema/chat_messages.ts
- packages/db/src/migrations/0049_add_message_type.sql
- packages/db/src/migrations/meta/_journal.json
- packages/shared/src/types/chat.ts
- packages/shared/src/validators/chat.ts
- packages/shared/src/index.ts
- ui/src/components/ChatSpecCard.test.tsx
- ui/src/components/ChatHandoffIndicator.test.tsx
- ui/src/components/ChatTaskCreatedBadge.test.tsx
- ui/src/components/ChatStatusUpdateBadge.test.tsx
- ui/src/hooks/useBrainstormerDefault.test.ts
autonomous: true
requirements:
- AGENT-01
- AGENT-02
- AGENT-03
- AGENT-05
- AGENT-06
- AGENT-07
- CHAT-09
must_haves:
truths:
- "chat_messages table has a message_type text column"
- "ChatMessage shared type includes messageType field"
- "createMessageSchema accepts optional messageType"
- "handoffSchema validates spec content and targetRole"
- "Test stubs exist for all new Phase 23 components and hooks"
artifacts:
- path: "packages/db/src/schema/chat_messages.ts"
provides: "messageType column definition"
contains: "messageType"
- path: "packages/db/src/migrations/0049_add_message_type.sql"
provides: "SQL migration for message_type column"
contains: "ADD COLUMN"
- path: "packages/shared/src/types/chat.ts"
provides: "ChatMessage.messageType field"
contains: "messageType"
- path: "packages/shared/src/validators/chat.ts"
provides: "handoffSchema and messageType in createMessageSchema"
contains: "handoffSchema"
key_links:
- from: "packages/db/src/schema/chat_messages.ts"
to: "packages/shared/src/types/chat.ts"
via: "messageType field must match"
pattern: "messageType"
---
<objective>
Foundation: DB migration for message_type column, shared types/validators extension, and Wave 0 test stubs for all Phase 23 components.
Purpose: Every subsequent plan depends on the message_type column existing in the DB schema, the ChatMessage type having a messageType field, and test stubs being in place for TDD-style development.
Output: Migration file, updated schema/types/validators, test stub files.
</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
<interfaces>
<!-- Existing types and schemas the executor needs -->
From packages/shared/src/types/chat.ts:
```typescript
export interface ChatMessage {
id: string;
conversationId: string;
role: "user" | "assistant" | "system";
content: string;
agentId: string | null;
createdAt: string;
updatedAt: string | null;
}
```
From packages/shared/src/validators/chat.ts:
```typescript
export const createMessageSchema = z.object({
role: z.enum(["user", "assistant", "system"]),
content: z.string().min(1).max(100_000),
agentId: z.string().uuid().optional(),
});
```
From packages/db/src/schema/chat_messages.ts:
```typescript
export const chatMessages = pgTable(
"chat_messages",
{
id: uuid("id").primaryKey().defaultRandom(),
conversationId: uuid("conversation_id").notNull()
.references(() => chatConversations.id, { onDelete: "cascade" }),
role: text("role").notNull(),
content: text("content").notNull(),
agentId: uuid("agent_id"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
},
(table) => ({
conversationCreatedIdx: index("chat_messages_conversation_created_idx").on(table.conversationId, table.createdAt),
}),
);
```
Migration journal: last entry is idx 46, tag "0046_smooth_sentinels". Files on disk go up to 0048. Next migration must be 0049.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: DB migration and shared types for message_type</name>
<read_first>
- packages/db/src/schema/chat_messages.ts
- packages/db/src/migrations/meta/_journal.json
- packages/shared/src/types/chat.ts
- packages/shared/src/validators/chat.ts
- packages/shared/src/index.ts
</read_first>
<files>
packages/db/src/schema/chat_messages.ts,
packages/db/src/migrations/0049_add_message_type.sql,
packages/db/src/migrations/meta/_journal.json,
packages/shared/src/types/chat.ts,
packages/shared/src/validators/chat.ts,
packages/shared/src/index.ts
</files>
<action>
1. Create migration file `packages/db/src/migrations/0049_add_message_type.sql`:
```sql
ALTER TABLE "chat_messages" ADD COLUMN "message_type" text;
```
2. Update `packages/db/src/migrations/meta/_journal.json`: append a new entry after idx 46 with idx 49, version "7", tag "0049_add_message_type", breakpoints true. Use `when: Date.now()` (current epoch ms).
IMPORTANT: The journal has entries up to idx 46 but disk has files 0047 and 0048 (added manually in Phases 21/22 without journal entries). Add ONLY the idx 49 entry. Do NOT add entries for 0047 or 0048 — those were applied outside the journal.
Wait — re-read the journal. If _journal.json only goes to idx 46, but files go to 0048, we must add entries for 47, 48, AND 49 to keep the journal consistent. Read the actual SQL files 0047 and 0048 to determine their tags.
Actually, looking at the research notes: "The migration numbering... The next migration must be named 0049_*.sql with idx 49 added to _journal.json." Follow this guidance — add idx 47 (tag "0047_nebulous_klaw"), idx 48 (tag "0048_add_chat_messages_updated_at"), and idx 49 (tag "0049_add_message_type") entries.
3. Update `packages/db/src/schema/chat_messages.ts`: add `messageType: text("message_type"),` after the `agentId` field. This is a nullable text column. Values: null (normal), "handoff", "spec_card", "task_created", "status_update".
4. Update `packages/shared/src/types/chat.ts`:
- Add `messageType: string | null;` to the `ChatMessage` interface (after `agentId`).
5. Update `packages/shared/src/validators/chat.ts`:
- Add `messageType: z.string().optional(),` to `createMessageSchema`.
- Add new `handoffSchema`:
```typescript
export const handoffSchema = z.object({
spec: z.object({
what: z.string().min(1),
why: z.string().min(1),
constraints: z.string().optional().default(""),
success: z.string().optional().default(""),
}),
targetRole: z.enum(["pm", "engineer", "general"]),
});
export type Handoff = z.infer<typeof handoffSchema>;
```
- Add `handoffSchema` and `Handoff` to the file's exports.
6. Update `packages/shared/src/index.ts`: add re-exports for `handoffSchema` and `Handoff` type from validators/chat.ts. Follow the existing pattern of re-exporting from `./validators/chat.js`.
</action>
<verify>
<automated>cd /opt/nexus && pnpm exec tsc --noEmit -p packages/shared/tsconfig.json 2>&1 | head -20 && pnpm exec tsc --noEmit -p packages/db/tsconfig.json 2>&1 | head -20</automated>
</verify>
<acceptance_criteria>
- grep -q "messageType" packages/db/src/schema/chat_messages.ts
- grep -q "message_type" packages/db/src/migrations/0049_add_message_type.sql
- grep -q "messageType" packages/shared/src/types/chat.ts
- grep -q "handoffSchema" packages/shared/src/validators/chat.ts
- grep -q "handoffSchema" packages/shared/src/index.ts
- grep -q "0049_add_message_type" packages/db/src/migrations/meta/_journal.json
</acceptance_criteria>
<done>message_type column defined in Drizzle schema and SQL migration; ChatMessage type and createMessageSchema include messageType; handoffSchema exported from shared package</done>
</task>
<task type="auto">
<name>Task 2: Wave 0 test stubs for Phase 23 components and hooks</name>
<read_first>
- ui/src/components/ChatMessage.test.tsx
- ui/src/components/ChatMessageList.test.tsx
</read_first>
<files>
ui/src/components/ChatSpecCard.test.tsx,
ui/src/components/ChatHandoffIndicator.test.tsx,
ui/src/components/ChatTaskCreatedBadge.test.tsx,
ui/src/components/ChatStatusUpdateBadge.test.tsx,
ui/src/hooks/useBrainstormerDefault.test.ts
</files>
<action>
Create test stub files using it.todo() pattern (matching Phase 21 convention — NOT it.skip()):
1. `ui/src/components/ChatSpecCard.test.tsx`:
```tsx
// @vitest-environment jsdom
import { describe, it } from "vitest";
describe("ChatSpecCard", () => {
it.todo("renders four spec sections: What, Why, Constraints, Success");
it.todo("parses JSON content and displays field values");
it.todo("shows error fallback on JSON parse failure");
it.todo("Send to PM button calls onHandoff with spec content");
it.todo("Edit button switches to textarea edit mode");
it.todo("Save changes button disabled when all fields empty");
it.todo("Discard button reverts to read-only mode");
it.todo("Save as Draft button adds [Draft] badge");
it.todo("renders with role=region and aria-label=Specification");
});
```
2. `ui/src/components/ChatHandoffIndicator.test.tsx`:
```tsx
// @vitest-environment jsdom
import { describe, it } from "vitest";
describe("ChatHandoffIndicator", () => {
it.todo("renders content text between two hr elements");
it.todo("has aria-label for agent handoff");
it.todo("hr elements have aria-hidden=true");
});
```
3. `ui/src/components/ChatTaskCreatedBadge.test.tsx`:
```tsx
// @vitest-environment jsdom
import { describe, it } from "vitest";
describe("ChatTaskCreatedBadge", () => {
it.todo("shows Creating task... when taskId is not provided");
it.todo("renders taskId, taskTitle, and View task link when resolved");
it.todo("View task link has correct aria-label");
it.todo("has role=status on container");
});
```
4. `ui/src/components/ChatStatusUpdateBadge.test.tsx`:
```tsx
// @vitest-environment jsdom
import { describe, it } from "vitest";
describe("ChatStatusUpdateBadge", () => {
it.todo("renders CheckCircle2 icon, agent name, and task reference");
it.todo("View task link navigates to issue detail");
it.todo("has role=status on container");
});
```
5. `ui/src/hooks/useBrainstormerDefault.test.ts`:
```ts
// @vitest-environment jsdom
import { describe, it } from "vitest";
describe("useBrainstormerDefault", () => {
it.todo("returns general role agent ID when available");
it.todo("returns first by createdAt when multiple general agents exist");
it.todo("returns null when no agents loaded");
it.todo("returns null when no general agent exists");
});
```
</action>
<verify>
<automated>cd /opt/nexus && pnpm vitest run --project=ui -- ChatSpecCard ChatHandoffIndicator ChatTaskCreatedBadge ChatStatusUpdateBadge useBrainstormerDefault 2>&1 | tail -10</automated>
</verify>
<acceptance_criteria>
- test -f ui/src/components/ChatSpecCard.test.tsx
- test -f ui/src/components/ChatHandoffIndicator.test.tsx
- test -f ui/src/components/ChatTaskCreatedBadge.test.tsx
- test -f ui/src/components/ChatStatusUpdateBadge.test.tsx
- test -f ui/src/hooks/useBrainstormerDefault.test.ts
- grep -q "it.todo" ui/src/components/ChatSpecCard.test.tsx
- grep -q "it.todo" ui/src/hooks/useBrainstormerDefault.test.ts
</acceptance_criteria>
<done>All 5 test stub files exist with it.todo() entries covering every Phase 23 behavior; vitest run finds them without errors</done>
</task>
</tasks>
<verification>
- `pnpm exec tsc --noEmit` passes for shared and db packages
- Migration file 0049 exists with correct SQL
- Journal updated with idx 49 entry
- All 5 test stub files parseable by vitest
</verification>
<success_criteria>
- messageType column in Drizzle schema and SQL migration
- ChatMessage type has messageType field
- handoffSchema exported from shared
- createMessageSchema accepts optional messageType
- 5 test stub files with it.todo() entries
</success_criteria>
<output>
After completion, create `.planning/phases/23-brainstormer-flow/23-00-SUMMARY.md`
</output>

View file

@ -0,0 +1,285 @@
---
phase: 23-brainstormer-flow
plan: 01
type: execute
wave: 1
depends_on: ["23-00"]
files_modified:
- server/src/services/chat.ts
- server/src/routes/chat.ts
autonomous: true
requirements:
- AGENT-03
- AGENT-06
- AGENT-07
- CHAT-09
must_haves:
truths:
- "POST /conversations/:id/handoff inserts a handoff system message and creates issues"
- "POST /conversations/:id/status-update inserts a status_update system message"
- "addMessage accepts optional messageType and persists it"
- "addSystemMessage helper creates system role messages with messageType"
artifacts:
- path: "server/src/services/chat.ts"
provides: "addSystemMessage helper and messageType support in addMessage"
contains: "addSystemMessage"
- path: "server/src/routes/chat.ts"
provides: "handoff and status-update routes"
contains: "handoff"
key_links:
- from: "server/src/routes/chat.ts"
to: "server/src/services/chat.ts"
via: "svc.addSystemMessage call"
pattern: "svc\\.addSystemMessage"
- from: "server/src/routes/chat.ts"
to: "server/src/routes/issues.ts"
via: "issueService for task creation from handoff"
pattern: "issueSvc\\.create"
---
<objective>
Server-side: extend chat service with addSystemMessage helper, messageType support in addMessage, and add handoff + status-update routes.
Purpose: The handoff route is the backbone of the Brainstormer-to-PM flow. The status-update route enables agent completion notifications in chat. Both insert typed system messages.
Output: Extended chat service, two new routes on chatRoutes.
</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-00-SUMMARY.md
<interfaces>
<!-- Key server interfaces the executor needs -->
From server/src/services/chat.ts:
```typescript
export function chatService(db: Db) {
return {
async addMessage(conversationId: string, data: { role: string; content: string; agentId?: string }) { ... },
async getConversation(id: string) { ... }, // returns full row including companyId
async editMessage(messageId: string, content: string) { ... },
// ... other methods
};
}
```
From server/src/routes/chat.ts:
```typescript
export function chatRoutes(db: Db): Router {
const router = Router();
const svc = chatService(db);
// Routes: GET/POST conversations, GET/PATCH/DELETE conversations/:id,
// GET/POST messages, POST stream, PATCH messages/:msgId, DELETE messages/after/:msgId
}
```
From packages/shared/src/validators/chat.ts (after Plan 00):
```typescript
export const handoffSchema = z.object({
spec: z.object({ what: z.string().min(1), why: z.string().min(1), constraints: z.string().optional().default(""), success: z.string().optional().default("") }),
targetRole: z.enum(["pm", "engineer", "general"]),
});
```
From packages/shared/src/validators/issue.ts:
```typescript
export const createIssueSchema = z.object({
title: z.string().min(1),
description: z.string().optional().nullable(),
status: z.enum(ISSUE_STATUSES).optional().default("backlog"),
priority: z.enum(ISSUE_PRIORITIES).optional().default("medium"),
// ... many optional fields
});
```
Issue creation pattern: `issueService(db).create(companyId, { title, description, ... })` returns `{ id, identifier, title, ... }`.
From server/src/routes/issues.ts (line 964):
```typescript
router.post("/companies/:companyId/issues", validate(createIssueSchema), async (req, res) => {
const issue = await svc.create(companyId, { ...req.body, createdByAgentId: actor.agentId, createdByUserId: ... });
res.status(201).json(issue);
});
```
The chatRoutes function currently receives only `db: Db`. To call issueService, either:
(a) Import and instantiate issueService inside chatRoutes, or
(b) Add issueService as a parameter to chatRoutes.
Option (a) is simplest and matches how heartbeat.ts instantiates issueService locally.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Extend chat service with messageType support and addSystemMessage</name>
<read_first>
- server/src/services/chat.ts
- packages/db/src/schema/chat_messages.ts
</read_first>
<files>server/src/services/chat.ts</files>
<action>
1. Extend `addMessage` to accept optional `messageType?: string` in its data parameter. Pass `messageType: data.messageType ?? null` in the `.values()` call to `chatMessages`. This requires the schema from Plan 00 to include the messageType column.
2. Add `addSystemMessage` helper method to the returned service object:
```typescript
async addSystemMessage(
conversationId: string,
data: { content: string; messageType: string; agentId?: string },
) {
const [message] = await db
.insert(chatMessages)
.values({
conversationId,
role: "system",
content: data.content,
agentId: data.agentId ?? null,
messageType: data.messageType,
})
.returning();
// Bump conversation updatedAt (same pattern as addMessage Pitfall 3)
await db
.update(chatConversations)
.set({ updatedAt: new Date() })
.where(eq(chatConversations.id, conversationId));
return message!;
},
```
3. Also extend `listMessages` return — the `messageType` field will automatically be included in `select()` results since the Drizzle schema now defines it. No change needed in listMessages itself, but verify the return rows will include `messageType`.
</action>
<verify>
<automated>cd /opt/nexus && pnpm exec tsc --noEmit -p server/tsconfig.json 2>&1 | head -20</automated>
</verify>
<acceptance_criteria>
- grep -q "addSystemMessage" server/src/services/chat.ts
- grep -q "messageType" server/src/services/chat.ts
</acceptance_criteria>
<done>addMessage accepts messageType; addSystemMessage creates system messages with typed messageType; both bump conversation updatedAt</done>
</task>
<task type="auto">
<name>Task 2: Add handoff and status-update routes</name>
<read_first>
- server/src/routes/chat.ts
- server/src/services/issues.ts (first 20 lines + the create method signature)
- server/src/routes/issues.ts (lines 964-1001 for create pattern)
- packages/shared/src/validators/chat.ts
</read_first>
<files>server/src/routes/chat.ts</files>
<action>
1. Import `handoffSchema` from `@paperclipai/shared` at the top of chat.ts.
2. Import `issueService` from `../services/issues.js` at the top.
3. Inside `chatRoutes(db)`, instantiate: `const issueSvc = issueService(db);`
4. Add `POST /conversations/:id/handoff` route (before the `return router` line):
```typescript
router.post("/conversations/:id/handoff", async (req, res) => {
assertBoard(req);
const data = handoffSchema.parse(req.body);
// Resolve companyId from conversation (Pitfall 4)
const conversation = await svc.getConversation(req.params.id!);
const companyId = conversation.companyId;
// 1. Insert handoff system message
const handoffMsg = await svc.addSystemMessage(req.params.id!, {
content: `Brainstormer \u2192 PM: spec handed off`,
messageType: "handoff",
});
// 2. Create issue from spec
const specDescription = [
`**What:** ${data.spec.what}`,
`**Why:** ${data.spec.why}`,
data.spec.constraints ? `**Constraints:** ${data.spec.constraints}` : "",
data.spec.success ? `**Success:** ${data.spec.success}` : "",
].filter(Boolean).join("\n\n");
const issue = await issueSvc.create(companyId, {
title: data.spec.what.slice(0, 100),
description: specDescription,
status: "backlog",
priority: "medium",
});
// 3. Insert task_created system message
await svc.addSystemMessage(req.params.id!, {
content: JSON.stringify({
taskId: issue.identifier,
taskTitle: issue.title,
taskUrl: `/issues/${issue.id}`,
}),
messageType: "task_created",
});
res.json({ handoffMessageId: handoffMsg.id, issues: [issue] });
});
```
5. Add `POST /conversations/:id/status-update` route:
```typescript
router.post("/conversations/:id/status-update", async (req, res) => {
assertBoard(req);
const { agentName, taskId, taskTitle, taskUrl } = req.body;
if (!agentName || !taskId) {
res.status(400).json({ error: "agentName and taskId are required" });
return;
}
const message = await svc.addSystemMessage(req.params.id!, {
content: JSON.stringify({ agentName, taskId, taskTitle, taskUrl }),
messageType: "status_update",
});
res.status(201).json(message);
});
```
IMPORTANT: The `issueService` import path uses `.js` extension (ESM convention in this codebase). Check the existing imports in chat.ts and issues.ts for the exact pattern. The server uses `"../services/issues.js"` style imports.
</action>
<verify>
<automated>cd /opt/nexus && pnpm exec tsc --noEmit -p server/tsconfig.json 2>&1 | head -20</automated>
</verify>
<acceptance_criteria>
- grep -q "handoff" server/src/routes/chat.ts
- grep -q "status-update" server/src/routes/chat.ts
- grep -q "issueService" server/src/routes/chat.ts
- grep -q "handoffSchema" server/src/routes/chat.ts
- grep -q "addSystemMessage" server/src/routes/chat.ts
</acceptance_criteria>
<done>POST /conversations/:id/handoff creates handoff message + issue + task_created message; POST /conversations/:id/status-update creates status_update message; both routes use assertBoard for auth</done>
</task>
</tasks>
<verification>
- `pnpm exec tsc --noEmit -p server/tsconfig.json` passes
- `pnpm vitest run --project=server` passes (existing tests not broken)
</verification>
<success_criteria>
- addSystemMessage helper exists in chat service
- addMessage accepts optional messageType
- Handoff route creates handoff + task_created system messages and an issue
- Status-update route creates status_update system message
- TypeScript compilation clean
</success_criteria>
<output>
After completion, create `.planning/phases/23-brainstormer-flow/23-01-SUMMARY.md`
</output>

View file

@ -0,0 +1,370 @@
---
phase: 23-brainstormer-flow
plan: 02
type: execute
wave: 1
depends_on: ["23-00"]
files_modified:
- ui/src/components/ChatSpecCard.tsx
- ui/src/components/ChatHandoffIndicator.tsx
- ui/src/components/ChatTaskCreatedBadge.tsx
- ui/src/components/ChatStatusUpdateBadge.tsx
- ui/src/hooks/useBrainstormerDefault.ts
autonomous: true
requirements:
- AGENT-01
- AGENT-02
- AGENT-05
- AGENT-06
- AGENT-07
must_haves:
truths:
- "ChatSpecCard renders spec sections and action buttons"
- "ChatSpecCard edit mode allows editing all four fields"
- "ChatHandoffIndicator renders as separator with flanking hr elements"
- "ChatTaskCreatedBadge shows loading state and resolved state"
- "ChatStatusUpdateBadge shows completion icon and task reference"
- "useBrainstormerDefault returns general role agent ID"
artifacts:
- path: "ui/src/components/ChatSpecCard.tsx"
provides: "Spec card with What/Why/Constraints/Success fields and action buttons"
exports: ["ChatSpecCard"]
- path: "ui/src/components/ChatHandoffIndicator.tsx"
provides: "Separator-style handoff indicator"
exports: ["ChatHandoffIndicator"]
- path: "ui/src/components/ChatTaskCreatedBadge.tsx"
provides: "Task created inline badge"
exports: ["ChatTaskCreatedBadge"]
- path: "ui/src/components/ChatStatusUpdateBadge.tsx"
provides: "Status update inline badge"
exports: ["ChatStatusUpdateBadge"]
- path: "ui/src/hooks/useBrainstormerDefault.ts"
provides: "Hook returning general agent ID for auto-selection"
exports: ["useBrainstormerDefault"]
key_links:
- from: "ui/src/hooks/useBrainstormerDefault.ts"
to: "ui/src/api/agents.ts"
via: "useQuery with agents queryKey"
pattern: 'queryKey.*agents'
---
<objective>
Build all five new UI components and the useBrainstormerDefault hook for Phase 23.
Purpose: These components render the four structured message types (spec_card, handoff, task_created, status_update) and provide the brainstormer default agent selection. Plan 03 wires them into ChatMessage dispatch.
Output: 4 new components + 1 new hook, all independently testable.
</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-00-SUMMARY.md
<interfaces>
<!-- Existing UI patterns the executor needs -->
From ui/src/components/ChatMessage.tsx:
```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;
}
```
From ui/src/api/chat.ts:
```typescript
export const chatApi = {
editMessage(conversationId: string, messageId: string, content: string) { ... },
// Will need: handoffSpec(conversationId, spec, targetRole) — added in Plan 03
};
```
From ui/src/api/issues.ts:
```typescript
export const issuesApi = {
create: (companyId: string, data: Record<string, unknown>) => api.post<Issue>(`/companies/${companyId}/issues`, data),
};
```
Existing shadcn components available: button, card, textarea (all installed).
Lucide icons needed: CheckCircle2, Brain (from lucide-react ^0.574.0, already installed).
From agent-role-colors.ts: general role maps to text-slate-600 dark:text-slate-400.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: ChatSpecCard and ChatHandoffIndicator components</name>
<read_first>
- ui/src/components/ChatMessage.tsx
- ui/src/components/ChatMessageIdentityBar.tsx
- .planning/phases/23-brainstormer-flow/23-UI-SPEC.md (Spec Card Layout section and Handoff Indicator section)
</read_first>
<files>
ui/src/components/ChatSpecCard.tsx,
ui/src/components/ChatHandoffIndicator.tsx
</files>
<action>
1. Create `ui/src/components/ChatSpecCard.tsx`:
Props interface:
```typescript
interface ChatSpecCardProps {
content: string; // JSON string of SpecContent
messageId?: string;
conversationId?: string;
onHandoff?: (spec: SpecContent) => void;
}
interface SpecContent {
what: string;
why: string;
constraints: string;
success: string;
}
```
Implementation:
- Parse `content` via `JSON.parse` in a try/catch. On failure, render: `<div className="text-destructive text-[13px]">Could not render spec.</div>`
- Container: `role="region" aria-label="Specification"` with `className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-2 rounded-lg border border-border bg-card p-4 max-w-[480px]"`
- Four sections, each with:
- Label: `<p className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">What</p>` (and Why, Constraints, Success)
- Content: `<p className="text-[15px] font-normal text-foreground leading-relaxed">{spec.what}</p>`
- Sections wrapped in `<div className="space-y-4">`
- Action row: `<div className="flex gap-2 pt-4 border-t border-border mt-4">`
- "Send to PM" button: `variant="default" size="sm"` — calls `onHandoff?.(spec)`, disables during submission with `aria-disabled="true"` and `aria-busy="true"` on container
- "Edit" button: `variant="outline" size="sm"` — toggles local `isEditing` state
- "Save as Draft" button: `variant="ghost" size="sm"` — sets local `isDraft` state, adds "[Draft]" badge
- Edit mode (when `isEditing === true`):
- Each field becomes a `<textarea>` with explicit `aria-label` ("What to build", "Why it matters", "Constraints", "Success criteria") and placeholder text per UI-SPEC copywriting contract
- Tab order: What -> Why -> Constraints -> Success -> Save changes -> Discard
- "Save changes" button: `variant="default" size="sm"`, disabled when all four fields are empty. Uses `chatApi.editMessage(conversationId, messageId, JSON.stringify(editedSpec))` if conversationId and messageId are available
- "Discard" button: `variant="ghost" size="sm"`, reverts local state
- Escape key discards (add keydown handler)
- Draft mode: When `isDraft` is true, show `<span className="text-[11px] text-muted-foreground ml-2">[Draft]</span>` in the header area
- "Send to PM" disabled state while in-flight: Use local `isSubmitting` state. Set true before calling onHandoff, caller resets via success/failure.
2. Create `ui/src/components/ChatHandoffIndicator.tsx`:
```tsx
import { cn } from "../lib/utils";
interface ChatHandoffIndicatorProps {
content: string;
}
export function ChatHandoffIndicator({ content }: ChatHandoffIndicatorProps) {
return (
<div
className={cn(
"flex items-center gap-3 py-2 text-[13px] text-muted-foreground",
"motion-safe:animate-in motion-safe:fade-in"
)}
aria-label="Agent handoff from Brainstormer to PM"
>
<hr className="flex-1 border-border" aria-hidden="true" />
<span className="whitespace-nowrap">{content}</span>
<hr className="flex-1 border-border" aria-hidden="true" />
</div>
);
}
```
</action>
<verify>
<automated>cd /opt/nexus && pnpm exec tsc --noEmit -p ui/tsconfig.json 2>&1 | head -20</automated>
</verify>
<acceptance_criteria>
- grep -q "ChatSpecCard" ui/src/components/ChatSpecCard.tsx
- grep -q "role=\"region\"" ui/src/components/ChatSpecCard.tsx
- grep -q "Send to PM" ui/src/components/ChatSpecCard.tsx
- grep -q "ChatHandoffIndicator" ui/src/components/ChatHandoffIndicator.tsx
- grep -q "aria-label" ui/src/components/ChatHandoffIndicator.tsx
- grep -q "aria-hidden" ui/src/components/ChatHandoffIndicator.tsx
</acceptance_criteria>
<done>ChatSpecCard renders spec sections with edit mode and action buttons; ChatHandoffIndicator renders separator-style indicator with accessibility labels</done>
</task>
<task type="auto">
<name>Task 2: ChatTaskCreatedBadge, ChatStatusUpdateBadge, and useBrainstormerDefault</name>
<read_first>
- .planning/phases/23-brainstormer-flow/23-UI-SPEC.md (Task Created and Status Update sections)
- ui/src/hooks/useStreamingChat.ts (first 20 lines for hook pattern)
- ui/src/context/CompanyContext.tsx (for useCompany import path)
- ui/src/api/agents.ts (for agentsApi.list pattern)
</read_first>
<files>
ui/src/components/ChatTaskCreatedBadge.tsx,
ui/src/components/ChatStatusUpdateBadge.tsx,
ui/src/hooks/useBrainstormerDefault.ts
</files>
<action>
1. Create `ui/src/components/ChatTaskCreatedBadge.tsx`:
```tsx
import { Link } from "react-router-dom";
import { cn } from "../lib/utils";
interface ChatTaskCreatedBadgeProps {
taskId?: string | null;
taskTitle?: string | null;
taskUrl?: string | null;
}
export function ChatTaskCreatedBadge({ taskId, taskTitle, taskUrl }: ChatTaskCreatedBadgeProps) {
if (!taskId) {
return (
<div className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-1 inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1 text-[13px] text-muted-foreground">
Creating task...
</div>
);
}
return (
<div
className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-1 inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1 text-[13px]"
role="status"
>
<span className="text-[11px] font-semibold text-muted-foreground">{taskId}</span>
<span className="text-foreground">{taskTitle}</span>
{taskUrl && (
<Link
to={taskUrl}
className="text-primary underline-offset-2 hover:underline"
aria-label={`View task ${taskId}`}
>
View task
</Link>
)}
</div>
);
}
```
2. Create `ui/src/components/ChatStatusUpdateBadge.tsx`:
```tsx
import { Link } from "react-router-dom";
import { CheckCircle2 } from "lucide-react";
interface ChatStatusUpdateBadgeProps {
agentName: string;
taskId: string;
taskTitle?: string;
taskUrl?: string;
}
export function ChatStatusUpdateBadge({ agentName, taskId, taskTitle, taskUrl }: ChatStatusUpdateBadgeProps) {
const displayTitle = taskTitle && taskTitle.length > 40
? taskTitle.slice(0, 40) + "..."
: taskTitle;
return (
<div
className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-1 inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1 text-[13px]"
role="status"
>
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 dark:text-green-400" />
<span className="text-foreground">
{agentName} completed {taskId}{displayTitle ? `: ${displayTitle}` : ""}
</span>
{taskUrl && (
<Link
to={taskUrl}
className="text-primary underline-offset-2 hover:underline"
aria-label={`View task ${taskId}`}
>
View task
</Link>
)}
</div>
);
}
```
3. Create `ui/src/hooks/useBrainstormerDefault.ts`:
```typescript
import { useQuery } from "@tanstack/react-query";
import { agentsApi } from "../api/agents";
import { useCompany } from "../context/CompanyContext";
export function useBrainstormerDefault(): string | null {
const { selectedCompanyId } = useCompany();
const { data: agents = [] } = useQuery({
queryKey: ["agents", selectedCompanyId],
queryFn: () => agentsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
// Reuses same queryKey as ChatPanel's agent list — React Query deduplicates
const generalAgent = agents
.filter((a: { role: string }) => a.role === "general")
.sort((a: { createdAt: string }, b: { createdAt: string }) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)[0];
return generalAgent?.id ?? null;
}
```
IMPORTANT: Check the exact import paths for `useCompany` and `agentsApi` by reading existing hooks (like useStreamingChat.ts or ChatPanel.tsx). The `agentsApi.list` return type may need a type assertion — check the actual API client.
NOTE: The `Link` component import — check if this project uses `react-router-dom` or `wouter` or another router. Read an existing component that has a `Link` or `<a>` with client-side navigation to confirm the import pattern.
</action>
<verify>
<automated>cd /opt/nexus && pnpm exec tsc --noEmit -p ui/tsconfig.json 2>&1 | head -20</automated>
</verify>
<acceptance_criteria>
- grep -q "ChatTaskCreatedBadge" ui/src/components/ChatTaskCreatedBadge.tsx
- grep -q "Creating task" ui/src/components/ChatTaskCreatedBadge.tsx
- grep -q "role=\"status\"" ui/src/components/ChatTaskCreatedBadge.tsx
- grep -q "ChatStatusUpdateBadge" ui/src/components/ChatStatusUpdateBadge.tsx
- grep -q "CheckCircle2" ui/src/components/ChatStatusUpdateBadge.tsx
- grep -q "useBrainstormerDefault" ui/src/hooks/useBrainstormerDefault.ts
- grep -q "general" ui/src/hooks/useBrainstormerDefault.ts
</acceptance_criteria>
<done>ChatTaskCreatedBadge renders loading and resolved states with View task link; ChatStatusUpdateBadge shows CheckCircle2 + agent completion text; useBrainstormerDefault returns general role agent ID with cache deduplication</done>
</task>
</tasks>
<verification>
- `pnpm exec tsc --noEmit -p ui/tsconfig.json` passes
- All 5 new files exist and export their named components/hooks
- `pnpm vitest run --project=ui` passes (existing tests not broken)
</verification>
<success_criteria>
- ChatSpecCard renders spec card with 4 sections, edit mode, and 3 action buttons
- ChatHandoffIndicator renders separator-style with flanking hr and aria-label
- ChatTaskCreatedBadge shows "Creating task..." or resolved badge
- ChatStatusUpdateBadge shows CheckCircle2 + agent + task reference
- useBrainstormerDefault returns general agent ID or null
- All components use CSS variables for theme compatibility
- All components respect prefers-reduced-motion via motion-safe: prefix
</success_criteria>
<output>
After completion, create `.planning/phases/23-brainstormer-flow/23-02-SUMMARY.md`
</output>

View file

@ -0,0 +1,377 @@
---
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>