nexus/.planning/phases/22-agent-streaming/22-00-PLAN.md

15 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
22-agent-streaming 00 execute 0
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
true
THEME-03
truths artifacts key_links
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)
path provides exports
ui/src/lib/agent-role-colors.ts AgentRole to Tailwind class map
agentRoleColors
agentRoleColorDefault
path provides contains
packages/db/src/schema/chat_messages.ts updatedAt column on chat_messages updatedAt
path provides contains
packages/shared/src/types/chat.ts updatedAt on ChatMessage type updatedAt
from to via pattern
ui/src/lib/agent-role-colors.ts @paperclipai/shared constants import AgentRole 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.

<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 @.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:

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:

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.
  1. Create migration packages/db/src/migrations/0048_add_chat_messages_updated_at.sql:

    ALTER TABLE "chat_messages" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now();
    
  2. Update packages/shared/src/types/chat.ts -- add updatedAt to ChatMessage interface:

    updatedAt: string | null;
    
  3. Install @tanstack/react-virtual:

    pnpm add @tanstack/react-virtual --filter @paperclipai/ui
    
  4. Create ui/src/lib/agent-role-colors.ts with DISTINCT colors for all 11 roles (THEME-03):

    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
  5. Create ui/src/lib/agent-role-colors.test.ts:

    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);
      });
    });
    
  6. Add cursor-blink animation to ui/src/index.css (append before the closing of the file):

    @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:

    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:

    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:

    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:

    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:

    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:

    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:

    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

<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>
After completion, create `.planning/phases/22-agent-streaming/22-00-SUMMARY.md`