---
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"
---

<objective>
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.
</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
@.planning/phases/22-agent-streaming/22-VALIDATION.md

<interfaces>
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;
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: DB migration, shared types, install virtualizer, agent-role-colors, CSS animation</name>
  <read_first>
    - 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
  </read_first>
  <files>
    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
  </files>
  <action>
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<AgentRole, string> = {
     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;
     }
   }
   ```
  </action>
  <verify>
    <automated>pnpm --filter @paperclipai/ui vitest run src/lib/agent-role-colors.test.ts --reporter=verbose</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>
    - 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
  </done>
</task>

<task type="auto">
  <name>Task 2: Wave 0 test stubs for all Phase 22 components/hooks</name>
  <read_first>
    - ui/src/hooks/useChatMessages.ts
    - ui/src/components/ChatMessage.tsx
    - ui/src/components/ChatMessageList.tsx
    - ui/src/components/ChatInput.tsx
  </read_first>
  <files>
    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
  </files>
  <action>
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");
   });
   ```
  </action>
  <verify>
    <automated>pnpm --filter @paperclipai/ui vitest run --reporter=verbose 2>&1 | tail -30</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>
    - 7 test stub files exist with it.todo() entries
    - All test stubs run without error (todos are not failures)
    - Full UI test suite passes
  </done>
</task>

</tasks>

<verification>
- `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
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.planning/phases/22-agent-streaming/22-00-SUMMARY.md`
</output>
