nexus/.planning/phases/23-brainstormer-flow/23-02-PLAN.md
Nexus Dev 9ed6dd16b3 docs(23-brainstormer-flow): create phase plan — 4 plans across 3 waves
Plan 00 (Wave 0): DB migration for message_type, shared types/validators, test stubs
Plan 01 (Wave 1): Server — addSystemMessage, handoff route, status-update route
Plan 02 (Wave 1): UI — ChatSpecCard, ChatHandoffIndicator, ChatTaskCreatedBadge, ChatStatusUpdateBadge, useBrainstormerDefault
Plan 03 (Wave 2): Wiring — ChatMessage dispatch, ChatMessageList propagation, ChatPanel brainstormer default, chatApi handoff

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:47 +00:00

14 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
23-brainstormer-flow 02 execute 1
23-00
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
true
AGENT-01
AGENT-02
AGENT-05
AGENT-06
AGENT-07
truths artifacts key_links
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
path provides exports
ui/src/components/ChatSpecCard.tsx Spec card with What/Why/Constraints/Success fields and action buttons
ChatSpecCard
path provides exports
ui/src/components/ChatHandoffIndicator.tsx Separator-style handoff indicator
ChatHandoffIndicator
path provides exports
ui/src/components/ChatTaskCreatedBadge.tsx Task created inline badge
ChatTaskCreatedBadge
path provides exports
ui/src/components/ChatStatusUpdateBadge.tsx Status update inline badge
ChatStatusUpdateBadge
path provides exports
ui/src/hooks/useBrainstormerDefault.ts Hook returning general agent ID for auto-selection
useBrainstormerDefault
from to via pattern
ui/src/hooks/useBrainstormerDefault.ts ui/src/api/agents.ts useQuery with agents queryKey queryKey.*agents
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.

<execution_context> @.claude/get-shit-done/workflows/execute-plan.md @.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/23-brainstormer-flow/23-RESEARCH.md @.planning/phases/23-brainstormer-flow/23-UI-SPEC.md @.planning/phases/23-brainstormer-flow/23-00-SUMMARY.md

From ui/src/components/ChatMessage.tsx:

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:

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:

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.

Task 1: ChatSpecCard and ChatHandoffIndicator components - 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) ui/src/components/ChatSpecCard.tsx, ui/src/components/ChatHandoffIndicator.tsx 1. Create `ui/src/components/ChatSpecCard.tsx`:

Props interface:

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.
  1. Create ui/src/components/ChatHandoffIndicator.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>
  );
}
cd /opt/nexus && pnpm exec tsc --noEmit -p ui/tsconfig.json 2>&1 | head -20 - 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 ChatSpecCard renders spec sections with edit mode and action buttons; ChatHandoffIndicator renders separator-style indicator with accessibility labels Task 2: ChatTaskCreatedBadge, ChatStatusUpdateBadge, and useBrainstormerDefault - .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) ui/src/components/ChatTaskCreatedBadge.tsx, ui/src/components/ChatStatusUpdateBadge.tsx, ui/src/hooks/useBrainstormerDefault.ts 1. Create `ui/src/components/ChatTaskCreatedBadge.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>
  );
}
  1. Create ui/src/components/ChatStatusUpdateBadge.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>
  );
}
  1. Create ui/src/hooks/useBrainstormerDefault.ts:
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. cd /opt/nexus && pnpm exec tsc --noEmit -p ui/tsconfig.json 2>&1 | head -20 <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> 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

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

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