nexus/.planning/milestones/v1.3-phases/23-brainstormer-flow/23-02-PLAN.md
Nexus Dev 832b95e07d chore: archive v1.3 phase directories to milestones/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:08:51 +00:00

370 lines
14 KiB
Markdown

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