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