16 KiB
16 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 22-agent-streaming | 02 | execute | 1 |
|
true |
|
|
Purpose: Creates the presentational building blocks and pure parsing logic that Plan 03 (Wave 2) wires into the chat panel. All components are self-contained and testable without streaming infrastructure. Output: 4 new files (agent-colors, parseMessageIntent, ChatAgentBadge, AgentSelector) + 3 test files.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/22-agent-streaming/22-RESEARCH.md @.planning/phases/22-agent-streaming/22-UI-SPEC.md From packages/shared/src/types/agent.ts: ```typescript export interface Agent { id: string; companyId: string; name: string; urlKey: string; role: AgentRole; // "ceo" | "pm" | "engineer" | "general" title: string | null; icon: string | null; status: AgentStatus; // ... other fields } ```From ui/src/api/agents.ts:
export const agentsApi = {
list: (companyId: string) => api.get<Agent[]>(`/companies/${companyId}/agents`),
// ...
};
From ui/src/lib/queryKeys.ts:
agents: {
list: (companyId: string) => ["agents", companyId] as const,
// ...
}
Existing agent pages use this pattern for fetching agents:
const { data: agents } = useQuery({
queryKey: queryKeys.agents.list(selectedCompanyId!),
queryFn: () => agentsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
From ui/src/lib/agent-icons.ts (existing icon system):
// Maps icon string names to lucide icon components
Shadcn components already installed (per UI-SPEC): Avatar, Badge, Select, Tooltip, Command, Popover
Task 1: Agent color utility + parseMessageIntent function + tests (including slash/mention integration stubs) ui/src/lib/agent-colors.ts, ui/src/lib/parseMessageIntent.ts, ui/src/lib/parseMessageIntent.test.ts, ui/src/components/ChatInput.slash-mention.test.tsx ui/src/lib/agent-icons.ts, ui/src/lib/utils.ts, ui/src/index.css (search for --chart-1 through --chart-5 definitions), ui/src/components/ChatInput.tsx, ui/src/components/ChatInput.test.tsx, .planning/phases/22-agent-streaming/22-UI-SPEC.md (Color section, agent role colors table), .planning/phases/22-agent-streaming/22-RESEARCH.md (Pattern 4: slash command parsing, Pattern 6: agent colors) - Test: agentRoleColorClass("ceo") returns "bg-[hsl(var(--chart-1))]" - Test: agentRoleColorClass("pm") returns "bg-[hsl(var(--chart-2))]" - Test: agentRoleColorClass("engineer") returns "bg-[hsl(var(--chart-3))]" - Test: agentRoleColorClass("general") returns "bg-[hsl(var(--chart-4))]" - Test: agentRoleColorClass("brainstormer") returns "bg-[hsl(var(--chart-5))]" - Test: agentRoleColorClass("unknown") returns "bg-muted" - Test: parseMessageIntent("/brainstorm Hello") returns { text: "Hello", targetRole: "brainstormer" } - Test: parseMessageIntent("/ask-pm Can you review?") returns { text: "Can you review?", targetRole: "pm" } - Test: parseMessageIntent("/ask-engineer Fix the bug") returns { text: "Fix the bug", targetRole: "engineer" } - Test: parseMessageIntent("/task Create login page") returns { text: "Create login page", targetRole: "engineer" } - Test: parseMessageIntent("/search old messages") returns { text: "old messages", targetRole: "generalist" } - Test: parseMessageIntent("@engineer Hello") returns { text: "Hello", targetName: "engineer" } - Test: parseMessageIntent("@PM-agent Check this") returns { text: "Check this", targetName: "pm-agent" } - Test: parseMessageIntent("/unknown-command Hello") returns { text: "/unknown-command Hello" } (no targetRole) - Test: parseMessageIntent("Just a normal message") returns { text: "Just a normal message" } - Test: parseMessageIntent("/path/to/file.ts") returns { text: "/path/to/file.ts" } (no targetRole -- not followed by space) 1. **Create `ui/src/lib/agent-colors.ts`:** ```typescript const ROLE_COLOR_CLASS: Record = { ceo: "bg-[hsl(var(--chart-1))]", pm: "bg-[hsl(var(--chart-2))]", engineer: "bg-[hsl(var(--chart-3))]", general: "bg-[hsl(var(--chart-4))]", generalist: "bg-[hsl(var(--chart-4))]", brainstormer: "bg-[hsl(var(--chart-5))]", }; export function agentRoleColorClass(role: string): string {
return ROLE_COLOR_CLASS[role] ?? "bg-muted";
}
```
2. **Create `ui/src/lib/parseMessageIntent.ts`:**
```typescript
export const SLASH_COMMANDS: Record<string, string> = {
"/brainstorm": "brainstormer",
"/ask-pm": "pm",
"/ask-engineer": "engineer",
"/task": "engineer",
"/search": "generalist",
};
export interface MessageIntent {
text: string;
targetRole?: string;
targetName?: string;
}
export function parseMessageIntent(content: string): MessageIntent {
const trimmed = content.trim();
// Slash command: must match known command followed by whitespace or end-of-string
for (const [cmd, role] of Object.entries(SLASH_COMMANDS)) {
if (trimmed.toLowerCase().startsWith(cmd)) {
const rest = trimmed.slice(cmd.length);
// Only match if followed by whitespace or end-of-string (not /path/to/file)
if (rest.length === 0 || /^\s/.test(rest)) {
return { text: rest.trim() || "", targetRole: role };
}
}
}
// @mention: @word followed by whitespace then content
const mentionMatch = trimmed.match(/^@([\w][\w-]*)\s+([\s\S]*)/);
if (mentionMatch) {
return { text: mentionMatch[2]!.trim(), targetName: mentionMatch[1]!.toLowerCase() };
}
return { text: trimmed };
}
```
3. **Create `ui/src/lib/parseMessageIntent.test.ts`:** Write Vitest tests covering all the behaviors listed above. Use `describe("parseMessageIntent", () => { ... })` and `describe("agentRoleColorClass", () => { ... })` blocks. Import from the respective modules.
4. **Create `ui/src/components/ChatInput.slash-mention.test.tsx`:** Create test stubs for INPUT-05 (slash command parsing in ChatInput context) and INPUT-06 (@mention parsing in ChatInput context). These test the integration between ChatInput and parseMessageIntent:
- Use the same jsdom + createRoot + act pattern as `ChatInput.test.tsx`.
- Test stub (INPUT-05): "slash command prefix filters SLASH_COMMANDS and shows popover" -- import `SLASH_COMMANDS`, verify the exported constant has entries for /brainstorm, /ask-pm, /ask-engineer, /task, /search. (Full popover rendering tests will be added in Plan 03 when the popover is wired into ChatInput.)
- Test stub (INPUT-06): "@mention prefix resolves agent name" -- import `parseMessageIntent`, verify `parseMessageIntent("@test-agent hello").targetName` equals "test-agent". (Full popover rendering tests added in Plan 03.)
- Mark any tests that depend on Plan 03 UI changes with `it.todo(...)` so they are tracked but do not block.
pnpm --filter @paperclipai/ui test run -- --reporter=verbose parseMessageIntent ChatInput.slash
- test -f ui/src/lib/agent-colors.ts
- test -f ui/src/lib/parseMessageIntent.ts
- test -f ui/src/lib/parseMessageIntent.test.ts
- test -f ui/src/components/ChatInput.slash-mention.test.tsx
- grep -q "agentRoleColorClass" ui/src/lib/agent-colors.ts returns 0
- grep -q "parseMessageIntent" ui/src/lib/parseMessageIntent.ts returns 0
- grep -q "SLASH_COMMANDS" ui/src/lib/parseMessageIntent.ts returns 0
- grep -q "/brainstorm" ui/src/lib/parseMessageIntent.ts returns 0
- grep -q "@mention" ui/src/lib/parseMessageIntent.ts OR grep -q "targetName" returns 0
- grep -q "INPUT-05" ui/src/components/ChatInput.slash-mention.test.tsx returns 0
- grep -q "INPUT-06" ui/src/components/ChatInput.slash-mention.test.tsx returns 0
- pnpm --filter @paperclipai/ui test run -- parseMessageIntent exits 0
- pnpm --filter @paperclipai/ui test run -- ChatInput.slash exits 0
Agent color mapping utility and message intent parsing with slash commands + @mentions both implemented and fully tested. ChatInput slash/mention integration test stubs created for INPUT-05 and INPUT-06.
Task 2: ChatAgentBadge + AgentSelector components + tests
ui/src/components/ChatAgentBadge.tsx,
ui/src/components/ChatAgentBadge.test.tsx,
ui/src/components/AgentSelector.tsx
ui/src/lib/agent-colors.ts (just created in Task 1),
ui/src/lib/agent-icons.ts,
ui/src/components/ui/select.tsx,
ui/src/components/ui/avatar.tsx,
ui/src/components/ui/tooltip.tsx,
ui/src/components/ui/skeleton.tsx,
.planning/phases/22-agent-streaming/22-UI-SPEC.md (ChatAgentBadge and AgentSelector specs)
1. **Create `ui/src/components/ChatAgentBadge.tsx`:**
Props: `{ agentId: string | null; agents: Agent[] }` where `Agent` is from `@paperclipai/shared`.
Resolve the agent from the `agents` array by matching `agent.id === agentId`. If not found or `agentId` is null, show fallback.
Layout per UI-SPEC:
- Container: `flex items-center gap-2 mb-1`
- Avatar circle: `w-5 h-5 rounded-full flex items-center justify-center` + `agentRoleColorClass(agent.role)` background + `text-white`
- If agent has an `icon` value: use the `AgentIcon` component at 12px (check how `agent-icons.ts` maps icon strings to lucide components -- read that file). If no `AgentIcon` component exists, render the lucide `Bot` icon at 12px.
- If no icon: render first letter of `agent.name` at `text-[10px] font-semibold text-white`
- Agent name: `<span className="text-[13px] text-muted-foreground truncate max-w-[120px]" aria-label={`Agent: ${agent.name}`}>`
- Fallback (agent not found): `Bot` icon (12px) + "Agent" text, `bg-muted` background
- Avatar element: `aria-hidden="true"` (decorative per accessibility contract)
2. **Create `ui/src/components/ChatAgentBadge.test.tsx`:**
Use jsdom + createRoot + act pattern (same as `ChatInput.test.tsx` -- read that file for the testing pattern). NOT `@testing-library/react`.
Tests:
- Renders agent name when agentId matches an agent in the array
- Renders "Agent" when agentId is null
- Renders "Agent" when agentId does not match any agent in the array
- Avatar has aria-hidden="true"
- Agent name span has aria-label containing agent name
3. **Create `ui/src/components/AgentSelector.tsx`:**
Props: `{ agents: Agent[]; currentAgentId: string | null; onSelect: (agentId: string) => void; isLoading?: boolean }`
Implementation per UI-SPEC:
- Use shadcn `<Select>` component
- Trigger: `h-8 px-2 py-1`, shows current agent mini avatar (16px circle, same color mapping) + agent name (13px / truncate)
- Wrap trigger in `<Tooltip>` with content "Active agent for this conversation"
- Trigger has `aria-label="Active agent: ${currentAgent?.name ?? 'None'}"`
- Dropdown items: each agent as `<SelectItem value={agent.id}>` showing:
- 16px colored circle (same `agentRoleColorClass`) + agent name (14px / regular)
- If `agents` is empty: single disabled item "No agents configured"
- If `isLoading`: render `<Skeleton className="h-8 w-28" />`
- On value change: call `onSelect(value)`
No test file needed for AgentSelector (it's a thin UI wrapper over shadcn Select with no logic -- the color mapping is tested via agent-colors, and the integration will be verified in Plan 03's checkpoint).
pnpm --filter @paperclipai/ui test run -- --reporter=verbose ChatAgentBadge
- test -f ui/src/components/ChatAgentBadge.tsx
- test -f ui/src/components/ChatAgentBadge.test.tsx
- test -f ui/src/components/AgentSelector.tsx
- grep -q "agentRoleColorClass" ui/src/components/ChatAgentBadge.tsx returns 0
- grep -q 'aria-hidden="true"' ui/src/components/ChatAgentBadge.tsx returns 0
- grep -q 'aria-label' ui/src/components/ChatAgentBadge.tsx returns 0
- grep -q "AgentSelector" ui/src/components/AgentSelector.tsx returns 0
- grep -q "Active agent for this conversation" ui/src/components/AgentSelector.tsx returns 0
- grep -q "No agents configured" ui/src/components/AgentSelector.tsx returns 0
- pnpm --filter @paperclipai/ui test run -- ChatAgentBadge exits 0
- pnpm --filter @paperclipai/ui build exits 0 (TypeScript compiles)
ChatAgentBadge renders agent identity with role-based colors, AgentSelector provides dropdown to switch agents, badge tests pass, UI builds
- `pnpm --filter @paperclipai/ui test run` -- all UI tests pass
- `pnpm --filter @paperclipai/ui build` -- TypeScript compiles
- Agent color utility tested for all 5 roles + fallback
- parseMessageIntent tested for all 5 slash commands + @mention + plain text + edge cases
- ChatAgentBadge tested for render + fallback + accessibility
- ChatInput.slash-mention.test.tsx exists with INPUT-05 and INPUT-06 stubs
<success_criteria>
- agentRoleColorClass maps all 5 agent roles to chart-1 through chart-5 CSS variables
- parseMessageIntent correctly parses all 5 slash commands and @mention syntax
- ChatAgentBadge renders agent name + colored avatar, with fallback for unknown agents
- AgentSelector provides a dropdown with tooltip and empty state
- ChatInput.slash-mention.test.tsx has test stubs for INPUT-05 and INPUT-06
- All UI tests pass and build succeeds </success_criteria>