404 lines
15 KiB
Markdown
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>
|