---
phase: 22-agent-streaming
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- ui/src/lib/agent-colors.ts
- ui/src/lib/parseMessageIntent.ts
- ui/src/components/ChatAgentBadge.tsx
- ui/src/components/AgentSelector.tsx
- ui/src/components/ChatAgentBadge.test.tsx
- ui/src/components/ChatInput.slash-mention.test.tsx
- ui/src/lib/parseMessageIntent.test.ts
autonomous: true
requirements: [AGENT-04, THEME-03, INPUT-05, INPUT-06]
must_haves:
truths:
- "ChatAgentBadge renders the agent name and a colored avatar circle based on agent role"
- "Agent avatar colors use --chart-1 through --chart-5 CSS variables, distinguishable across all three themes"
- "Slash commands /brainstorm, /ask-pm, /ask-engineer, /task, /search are parsed with correct target role"
- "@mention syntax @engineer resolves to target agent name"
- "Unknown / prefix passes through as plain text"
- "AgentSelector dropdown shows all agents and triggers onSelect callback"
artifacts:
- path: "ui/src/lib/agent-colors.ts"
provides: "agentRoleColorClass function mapping role to Tailwind class"
exports: ["agentRoleColorClass"]
- path: "ui/src/lib/parseMessageIntent.ts"
provides: "parseMessageIntent function for slash commands and @mentions"
exports: ["parseMessageIntent", "SLASH_COMMANDS"]
- path: "ui/src/components/ChatAgentBadge.tsx"
provides: "Agent badge with colored avatar + name"
exports: ["ChatAgentBadge"]
- path: "ui/src/components/AgentSelector.tsx"
provides: "Dropdown to select active agent per conversation"
exports: ["AgentSelector"]
- path: "ui/src/components/ChatInput.slash-mention.test.tsx"
provides: "Integration tests for slash command and @mention parsing in ChatInput context"
key_links:
- from: "ui/src/components/ChatAgentBadge.tsx"
to: "ui/src/lib/agent-colors.ts"
via: "import { agentRoleColorClass }"
pattern: "agentRoleColorClass"
- from: "ui/src/components/AgentSelector.tsx"
to: "ui/src/lib/agent-colors.ts"
via: "import { agentRoleColorClass }"
pattern: "agentRoleColorClass"
---
UI foundation components: agent color utility, ChatAgentBadge, AgentSelector dropdown, and slash command / @mention parsing logic with full test coverage.
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.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.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:
```typescript
export const agentsApi = {
list: (companyId: string) => api.get(`/companies/${companyId}/agents`),
// ...
};
```
From ui/src/lib/queryKeys.ts:
```typescript
agents: {
list: (companyId: string) => ["agents", companyId] as const,
// ...
}
```
Existing agent pages use this pattern for fetching agents:
```typescript
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):
```typescript
// 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 = {
"/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: ``
- 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 `
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
1. agentRoleColorClass maps all 5 agent roles to chart-1 through chart-5 CSS variables
2. parseMessageIntent correctly parses all 5 slash commands and @mention syntax
3. ChatAgentBadge renders agent name + colored avatar, with fallback for unknown agents
4. AgentSelector provides a dropdown with tooltip and empty state
5. ChatInput.slash-mention.test.tsx has test stubs for INPUT-05 and INPUT-06
6. All UI tests pass and build succeeds