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