--- phase: 22-agent-streaming plan: "00" type: execute wave: 0 depends_on: [] files_modified: - packages/db/src/schema/chat_messages.ts - packages/db/src/migrations/0048_add_chat_messages_updated_at.sql - packages/shared/src/types/chat.ts - ui/src/lib/agent-role-colors.ts - ui/src/lib/agent-role-colors.test.ts - ui/src/hooks/useStreamingChat.test.ts - ui/src/components/ChatAgentSelector.test.tsx - ui/src/components/ChatMessage.test.tsx - ui/src/components/ChatSlashCommandPopover.test.tsx - ui/src/components/ChatMentionPopover.test.tsx - ui/src/components/ChatMessageIdentityBar.test.tsx - ui/src/components/ChatMessageList.test.tsx - ui/src/index.css - ui/package.json autonomous: true requirements: - THEME-03 must_haves: truths: - "agent-role-colors.ts exports a color class for every AgentRole value from AGENT_ROLES" - "chat_messages table has an updated_at column" - "ChatMessage shared type includes updatedAt field" - "@tanstack/react-virtual is installed in ui workspace" - "Cursor blink animation is declared in index.css" - "All Wave 0 test stubs exist and run without error" - "All 11 agent roles have visually distinct color assignments (no two roles share the same color)" artifacts: - path: "ui/src/lib/agent-role-colors.ts" provides: "AgentRole to Tailwind class map" exports: ["agentRoleColors", "agentRoleColorDefault"] - path: "packages/db/src/schema/chat_messages.ts" provides: "updatedAt column on chat_messages" contains: "updatedAt" - path: "packages/shared/src/types/chat.ts" provides: "updatedAt on ChatMessage type" contains: "updatedAt" key_links: - from: "ui/src/lib/agent-role-colors.ts" to: "@paperclipai/shared constants" via: "import AgentRole" pattern: "import.*AgentRole" --- Wave 0 foundation: DB migration adding `updated_at` to `chat_messages`, shared type update, install `@tanstack/react-virtual`, create `agent-role-colors.ts` utility (THEME-03), cursor-blink CSS animation, and all test stubs for Phase 22. Purpose: Provide the schema, types, dependencies, and test scaffolds that all subsequent plans need. Output: Migration file, updated schema, shared types, agent-role-colors utility, CSS animation, test stubs, installed virtualizer package. @$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 @.planning/phases/22-agent-streaming/22-VALIDATION.md From packages/shared/src/constants.ts: ```typescript export const AGENT_ROLES = [ "pm", "engineer", "ceo", "general", "designer", "qa", "researcher", "devops", "cto", "cmo", "cfo", ] as const; export type AgentRole = (typeof AGENT_ROLES)[number]; ``` From packages/db/src/schema/chat_messages.ts: ```typescript export const chatMessages = pgTable("chat_messages", { id: uuid("id").primaryKey().defaultRandom(), conversationId: uuid("conversation_id").notNull().references(() => chatConversations.id, { onDelete: "cascade" }), role: text("role").notNull(), content: text("content").notNull(), agentId: uuid("agent_id"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ conversationCreatedIdx: index("chat_messages_conversation_created_idx").on(table.conversationId, table.createdAt), })); ``` From packages/shared/src/types/chat.ts: ```typescript export interface ChatMessage { id: string; conversationId: string; role: "user" | "assistant" | "system"; content: string; agentId: string | null; createdAt: string; } ``` Task 1: DB migration, shared types, install virtualizer, agent-role-colors, CSS animation - packages/db/src/schema/chat_messages.ts - packages/shared/src/types/chat.ts - packages/shared/src/constants.ts - ui/src/index.css - ui/src/lib/status-colors.ts - ui/package.json packages/db/src/schema/chat_messages.ts, packages/db/src/migrations/0048_add_chat_messages_updated_at.sql, packages/shared/src/types/chat.ts, ui/src/lib/agent-role-colors.ts, ui/src/lib/agent-role-colors.test.ts, ui/src/index.css, ui/package.json 1. Add `updatedAt` column to `chat_messages` Drizzle schema: In `packages/db/src/schema/chat_messages.ts`, add after `createdAt`: ``` updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(), ``` Note: NOT `.notNull()` -- existing rows will have null until updated. 2. Create migration `packages/db/src/migrations/0048_add_chat_messages_updated_at.sql`: ```sql ALTER TABLE "chat_messages" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now(); ``` 3. Update `packages/shared/src/types/chat.ts` -- add `updatedAt` to `ChatMessage` interface: ``` updatedAt: string | null; ``` 4. Install `@tanstack/react-virtual`: ```bash pnpm add @tanstack/react-virtual --filter @paperclipai/ui ``` 5. Create `ui/src/lib/agent-role-colors.ts` with DISTINCT colors for all 11 roles (THEME-03): ```typescript import type { AgentRole } from "@paperclipai/shared"; export const agentRoleColors: Record = { pm: "text-blue-600 dark:text-blue-400", engineer: "text-violet-600 dark:text-violet-400", ceo: "text-amber-600 dark:text-amber-400", general: "text-slate-600 dark:text-slate-400", designer: "text-pink-600 dark:text-pink-400", qa: "text-orange-600 dark:text-orange-400", researcher: "text-teal-600 dark:text-teal-400", devops: "text-emerald-600 dark:text-emerald-400", cto: "text-indigo-600 dark:text-indigo-400", cmo: "text-rose-600 dark:text-rose-400", cfo: "text-cyan-600 dark:text-cyan-400", }; export const agentRoleColorDefault = "text-muted-foreground"; ``` CRITICAL (THEME-03): Each of the 11 roles MUST have a unique color. The previous plan had ceo+general sharing yellow, devops+cto sharing green, and cmo+cfo sharing neutral. This is corrected above: - pm=blue, engineer=violet, ceo=amber, general=slate, designer=pink - qa=orange, researcher=teal, devops=emerald, cto=indigo, cmo=rose, cfo=cyan 6. Create `ui/src/lib/agent-role-colors.test.ts`: ```typescript import { describe, it, expect } from "vitest"; import { AGENT_ROLES } from "@paperclipai/shared"; import { agentRoleColors, agentRoleColorDefault } from "./agent-role-colors"; describe("agentRoleColors", () => { it("has an entry for every AGENT_ROLES value", () => { for (const role of AGENT_ROLES) { expect(agentRoleColors[role]).toBeDefined(); expect(agentRoleColors[role]).toContain("text-"); } }); it("each entry has both light and dark variant", () => { for (const role of AGENT_ROLES) { expect(agentRoleColors[role]).toContain("dark:"); } }); it("exports a default fallback color", () => { expect(agentRoleColorDefault).toBe("text-muted-foreground"); }); it("all 11 roles have distinct color classes", () => { const colors = Object.values(agentRoleColors); const unique = new Set(colors); expect(unique.size).toBe(colors.length); }); }); ``` 7. Add cursor-blink animation to `ui/src/index.css` (append before the closing of the file): ```css @keyframes cursor-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .animate-cursor-blink { animation: cursor-blink 800ms step-start infinite; } @media (prefers-reduced-motion: reduce) { .animate-cursor-blink { animation: none; opacity: 1; } } ``` pnpm --filter @paperclipai/ui vitest run src/lib/agent-role-colors.test.ts --reporter=verbose - grep -q "updatedAt" packages/db/src/schema/chat_messages.ts - grep -q "updated_at" packages/db/src/migrations/0048_add_chat_messages_updated_at.sql - grep -q "updatedAt" packages/shared/src/types/chat.ts - grep -q "agentRoleColors" ui/src/lib/agent-role-colors.ts - grep -q "agentRoleColorDefault" ui/src/lib/agent-role-colors.ts - grep -q "cursor-blink" ui/src/index.css - grep -q "@tanstack/react-virtual" ui/package.json - grep -q "prefers-reduced-motion" ui/src/index.css - agent-role-colors.test.ts "all 11 roles have distinct color classes" test passes - chat_messages schema has updatedAt column - Migration 0048 exists with ALTER TABLE - ChatMessage shared type has updatedAt: string | null - @tanstack/react-virtual installed in ui workspace - agent-role-colors.ts exports map for all 11 AgentRole values with DISTINCT light+dark variants (no duplicates) - agent-role-colors.test.ts passes (4 tests including uniqueness check) - cursor-blink CSS animation in index.css with reduced-motion guard Task 2: Wave 0 test stubs for all Phase 22 components/hooks - ui/src/hooks/useChatMessages.ts - ui/src/components/ChatMessage.tsx - ui/src/components/ChatMessageList.tsx - ui/src/components/ChatInput.tsx ui/src/hooks/useStreamingChat.test.ts, ui/src/components/ChatAgentSelector.test.tsx, ui/src/components/ChatMessage.test.tsx, ui/src/components/ChatSlashCommandPopover.test.tsx, ui/src/components/ChatMentionPopover.test.tsx, ui/src/components/ChatMessageIdentityBar.test.tsx, ui/src/components/ChatMessageList.test.tsx Create test stub files using `it.todo()` pattern (established in Phase 21 Wave 0). Minimal imports, no service mocks. 1. `ui/src/hooks/useStreamingChat.test.ts`: ```typescript import { describe, it } from "vitest"; describe("useStreamingChat", () => { it.todo("accumulates tokens from SSE data events into streamingContent"); it.todo("sets isStreaming=true when stream starts, false when done"); it.todo("clears streamingContent and invalidates query cache on done event"); it.todo("stop() closes the EventSource and sets isStreaming=false"); it.todo("handles SSE error event by closing connection"); }); ``` 2. `ui/src/components/ChatAgentSelector.test.tsx`: ```typescript import { describe, it } from "vitest"; describe("ChatAgentSelector", () => { it.todo("renders active agent icon and name when agentId is set"); it.todo("renders 'Select agent' placeholder when no agent selected"); it.todo("lists all workspace agents in dropdown"); it.todo("calls onAgentChange with new agentId on selection"); it.todo("shows 'No agents configured' when agent list is empty"); }); ``` 3. `ui/src/components/ChatMessage.test.tsx`: ```typescript import { describe, it } from "vitest"; describe("ChatMessage", () => { it.todo("renders user message as right-aligned bubble with plain text"); it.todo("renders assistant message with ChatMarkdownMessage"); it.todo("renders ChatMessageIdentityBar for assistant messages when agentName is provided"); it.todo("shows edit pencil on hover for user messages"); it.todo("shows retry button on hover for assistant messages"); it.todo("hides retry button when isStreaming is true"); it.todo("switches to inline edit textarea on pencil click"); it.todo("renders ChatStreamingCursor when isStreaming is true"); }); ``` 4. `ui/src/components/ChatSlashCommandPopover.test.tsx`: ```typescript import { describe, it } from "vitest"; describe("ChatSlashCommandPopover", () => { it.todo("renders 5 slash command items when open"); it.todo("filters commands by typed query"); it.todo("calls onSelect with command string on item click"); it.todo("closes on Escape key"); it.todo("shows /search as greyed out with 'Coming soon' suffix"); }); ``` 5. `ui/src/components/ChatMentionPopover.test.tsx`: ```typescript import { describe, it } from "vitest"; describe("ChatMentionPopover", () => { it.todo("renders agent list filtered by query string"); it.todo("shows agent icon, name, and role for each item"); it.todo("calls onSelect with @agentName on item click"); it.todo("shows 'No agents found' when filter matches nothing"); }); ``` 6. `ui/src/components/ChatMessageIdentityBar.test.tsx`: ```typescript import { describe, it } from "vitest"; describe("ChatMessageIdentityBar", () => { it.todo("renders agent icon at 16x16px"); it.todo("renders agent name in semibold text"); it.todo("renders timestamp in muted text"); it.todo("applies role-specific color from agentRoleColors"); }); ``` 7. `ui/src/components/ChatMessageList.test.tsx`: ```typescript import { describe, it } from "vitest"; describe("ChatMessageList", () => { it.todo("renders messages using virtualizer"); it.todo("auto-scrolls to bottom when new messages arrive"); it.todo("shows loading skeleton when isLoading"); it.todo("shows empty state when no messages"); it.todo("appends streaming message as synthetic entry"); }); ``` pnpm --filter @paperclipai/ui vitest run --reporter=verbose 2>&1 | tail -30 - grep -q "it.todo" ui/src/hooks/useStreamingChat.test.ts - grep -q "it.todo" ui/src/components/ChatAgentSelector.test.tsx - grep -q "it.todo" ui/src/components/ChatMessage.test.tsx - grep -q "it.todo" ui/src/components/ChatSlashCommandPopover.test.tsx - grep -q "it.todo" ui/src/components/ChatMentionPopover.test.tsx - grep -q "it.todo" ui/src/components/ChatMessageIdentityBar.test.tsx - grep -q "it.todo" ui/src/components/ChatMessageList.test.tsx - 7 test stub files exist with it.todo() entries - All test stubs run without error (todos are not failures) - Full UI test suite passes - `pnpm --filter @paperclipai/ui vitest run --reporter=verbose` passes (all existing + new tests green, todos listed) - `pnpm --filter @paperclipai/ui exec -- tsc --noEmit` passes (type check) - `grep -q "@tanstack/react-virtual" ui/package.json` confirms install - DB migration 0048 adds updated_at to chat_messages - ChatMessage type includes updatedAt - @tanstack/react-virtual installed - agent-role-colors.ts covers all 11 roles with DISTINCT themed classes (no duplicate colors) - Cursor-blink CSS animation declared with reduced-motion guard - All 7 Wave 0 test stub files exist and run After completion, create `.planning/phases/22-agent-streaming/22-00-SUMMARY.md`