345 lines
16 KiB
Markdown
345 lines
16 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
<!-- Agent type from shared package -->
|
|
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<Agent[]>(`/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
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Agent color utility + parseMessageIntent function + tests (including slash/mention integration stubs)</name>
|
|
<files>
|
|
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
|
|
</files>
|
|
<read_first>
|
|
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)
|
|
</read_first>
|
|
<behavior>
|
|
- 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)
|
|
</behavior>
|
|
<action>
|
|
1. **Create `ui/src/lib/agent-colors.ts`:**
|
|
```typescript
|
|
const ROLE_COLOR_CLASS: Record<string, string> = {
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @paperclipai/ui test run -- --reporter=verbose parseMessageIntent ChatInput.slash</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: ChatAgentBadge + AgentSelector components + tests</name>
|
|
<files>
|
|
ui/src/components/ChatAgentBadge.tsx,
|
|
ui/src/components/ChatAgentBadge.test.tsx,
|
|
ui/src/components/AgentSelector.tsx
|
|
</files>
|
|
<read_first>
|
|
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)
|
|
</read_first>
|
|
<action>
|
|
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).
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @paperclipai/ui test run -- --reporter=verbose ChatAgentBadge</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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)
|
|
</acceptance_criteria>
|
|
<done>ChatAgentBadge renders agent identity with role-based colors, AgentSelector provides dropdown to switch agents, badge tests pass, UI builds</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/22-agent-streaming/22-02-SUMMARY.md`
|
|
</output>
|