15 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 22-agent-streaming | 00 | execute | 0 |
|
true |
|
|
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.
-
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(); -
Update
packages/shared/src/types/chat.ts-- addupdatedAttoChatMessageinterface:updatedAt: string | null; -
Install
@tanstack/react-virtual:pnpm add @tanstack/react-virtual --filter @paperclipai/ui -
Create
ui/src/lib/agent-role-colors.tswith 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
-
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); }); }); -
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; } }
-
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"); }); -
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"); }); -
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"); }); -
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"); }); -
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"); }); -
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"); }); -
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"); });
<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>