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

404 lines
15 KiB
Markdown

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