[nexus] fix(22): revise plans based on checker feedback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikkel Georgsen 2026-04-01 14:45:00 +02:00
parent 1b64970e90
commit e8c70d6c8d
4 changed files with 118 additions and 80 deletions

View file

@ -53,10 +53,17 @@ must_haves:
---
<objective>
Server-side streaming infrastructure: DB schema additions for message editing, SSE streaming endpoint for LLM token delivery, message edit route, agent selection on conversations, and server tests.
Server-side streaming infrastructure: DB schema additions for message editing, SSE streaming endpoint with echo-stream placeholder, message edit route, agent selection on conversations, and server tests.
Purpose: Establishes the entire server-side API surface that the UI plans (02/03) will consume. Every new endpoint is tested.
Output: Working SSE stream endpoint, edit message endpoint, conversation agent update, migration SQL, tests.
Output: Working SSE stream endpoint (echo-stream mode), edit message endpoint, conversation agent update, migration SQL, tests.
NOTE -- Echo-stream scope (CHAT-01 partial): The SSE endpoint uses an echo-stream that replays the
user's last message word-by-word. This fully exercises the streaming pipeline (SSE headers, token
events, done event, abort detection, message persistence) so the UI can be built and tested against
real streaming behavior. Real LLM integration (replacing the echo loop with an adapter call) is
Phase 23 (Brainstormer agent). The echo-stream satisfies CHAT-01's "tokens appear as generated"
contract at the transport level; Phase 23 provides semantic content.
</objective>
<execution_context>
@ -177,30 +184,30 @@ res.write(":ok\n\n");
- Test: PUT /conversations/:id/messages/:messageId with { content: "new" } returns 200 with editedContent set
</behavior>
<action>
1. **DB schema** Add two columns to `chatMessages` in `packages/db/src/schema/chat_messages.ts`:
1. **DB schema** -- Add two columns to `chatMessages` in `packages/db/src/schema/chat_messages.ts`:
```typescript
editedContent: text("edited_content"),
editedAt: timestamp("edited_at", { withTimezone: true }),
```
Then run `pnpm db:generate` to create the migration SQL.
2. **Shared types** Update `ChatMessage` interface in `packages/shared/src/types/chat.ts`:
2. **Shared types** -- Update `ChatMessage` interface in `packages/shared/src/types/chat.ts`:
- Add `editedContent: string | null;`
- Add `editedAt: string | null;`
3. **Validators** In `packages/shared/src/validators/chat.ts`:
3. **Validators** -- In `packages/shared/src/validators/chat.ts`:
- Update `updateConversationSchema` to include `agentId: z.string().uuid().optional().nullable()`
- Add `export const editMessageSchema = z.object({ content: z.string().min(1) });`
- Add `export const streamMessageSchema = z.object({ content: z.string().min(1), agentId: z.string().uuid().optional().nullable() });`
4. **Service methods** Add to `chatService` in `server/src/services/chat.ts`:
- `editMessage(messageId: string, data: { content: string })` sets `editedContent = data.content`, `editedAt = new Date()` on the message row, returns the updated row
- `getMessageHistory(conversationId: string)` selects all messages WHERE conversationId matches, ORDER BY createdAt ASC (ascending, for LLM context window). Returns `ChatMessage[]`. Use `editedContent ?? content` as the effective content field (alias as `effectiveContent` in the return).
4. **Service methods** -- Add to `chatService` in `server/src/services/chat.ts`:
- `editMessage(messageId: string, data: { content: string })` -- sets `editedContent = data.content`, `editedAt = new Date()` on the message row, returns the updated row
- `getMessageHistory(conversationId: string)` -- selects all messages WHERE conversationId matches, ORDER BY createdAt ASC (ascending, for LLM context window). Returns `ChatMessage[]`. Use `editedContent ?? content` as the effective content field (alias as `effectiveContent` in the return).
- Update `updateConversation` to accept and persist `agentId` field: `set({ title: data.title, agentId: data.agentId, updatedAt: new Date() })`. Only set fields that are provided (check `data.agentId !== undefined` before including in set).
5. **Extend existing tests** in `server/src/__tests__/chat-routes.test.ts`:
- Add test: `PATCH /conversations/:id with agentId` create conversation, PATCH with `{ agentId: someAgentId }`, verify response has the agentId set. (Use a dummy UUID string for agentId if the test DB doesn't enforce FK check existing test patterns.)
- Add test: `PUT /conversations/:id/messages/:messageId` create conversation, add message, PUT with `{ content: "edited" }`, verify response has `editedContent: "edited"` and `editedAt` is not null.
- Add test: `PATCH /conversations/:id with agentId` -- create conversation, PATCH with `{ agentId: someAgentId }`, verify response has the agentId set. (Use a dummy UUID string for agentId if the test DB doesn't enforce FK -- check existing test patterns.)
- Add test: `PUT /conversations/:id/messages/:messageId` -- create conversation, add message, PUT with `{ content: "edited" }`, verify response has `editedContent: "edited"` and `editedAt` is not null.
</action>
<verify>
<automated>pnpm --filter @paperclipai/server test run -- --reporter=verbose chat-routes</automated>
@ -221,7 +228,7 @@ res.write(":ok\n\n");
</task>
<task type="auto" tdd="true">
<name>Task 2: SSE streaming endpoint + edit message route + stream tests</name>
<name>Task 2: SSE echo-stream endpoint + edit message route + stream tests</name>
<files>
server/src/routes/chat.ts,
server/src/__tests__/chat-stream-routes.test.ts
@ -241,7 +248,13 @@ res.write(":ok\n\n");
- Test: Client close (req.destroy()) stops the stream loop
</behavior>
<action>
1. **Edit message route** — Add to `server/src/routes/chat.ts`:
**IMPORTANT -- Echo-stream placeholder:** This task implements an echo-stream (replays the user's
last message word-by-word) as a functional placeholder. This is intentional -- it fully exercises
the SSE pipeline so the UI (Plans 02/03) can develop against real streaming behavior. Phase 23
will replace the echo loop body with real LLM adapter calls. The SSE contract (token events,
done event, abort detection, message persistence) is the deliverable here, not LLM content.
1. **Edit message route** -- Add to `server/src/routes/chat.ts`:
```typescript
// PUT /conversations/:id/messages/:messageId
router.put("/conversations/:id/messages/:messageId", validate(editMessageSchema), async (req, res) => {
@ -255,7 +268,7 @@ res.write(":ok\n\n");
});
```
2. **SSE stream endpoint** Add to `server/src/routes/chat.ts`:
2. **SSE stream endpoint** -- Add to `server/src/routes/chat.ts`:
```typescript
// GET /conversations/:id/stream
router.get("/conversations/:id/stream", async (req, res) => {
@ -269,7 +282,7 @@ res.write(":ok\n\n");
return;
}
// Set SSE headers copied from plugins.ts:1146
// Set SSE headers -- copied from plugins.ts:1146
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
@ -288,10 +301,11 @@ res.write(":ok\n\n");
// Get message history for LLM context
const history = await svc.getMessageHistory(conversationId);
// For now: echo-stream mode. The actual LLM call will be wired when a provider
// is configured. This streams tokens from the last user message content one word
// at a time as a functional placeholder that fully exercises the SSE pipeline.
// Phase 23+ will replace this with real LLM calls via the agent's adapterConfig.
// ECHO-STREAM PLACEHOLDER (Phase 22):
// Streams the user's last message back word-by-word to fully exercise the SSE
// pipeline. Phase 23 replaces this block with:
// const adapter = resolveAdapter(agentId);
// for await (const token of adapter.stream(history)) { ... }
const lastUserMsg = history.filter(m => m.role === "user").at(-1);
const echoContent = lastUserMsg
? `Echo from agent: ${lastUserMsg.content}`
@ -325,9 +339,9 @@ res.write(":ok\n\n");
Import `editMessageSchema` and `streamMessageSchema` from `@paperclipai/shared` at the top of the routes file (alongside existing imports).
3. **Stream tests** Create `server/src/__tests__/chat-stream-routes.test.ts`:
3. **Stream tests** -- Create `server/src/__tests__/chat-stream-routes.test.ts`:
- Use the same test DB setup pattern as `chat-routes.test.ts` (read that file for the pattern).
- Test: `GET /conversations/:id/stream?triggerMessageId=X` create conversation, add user message, open stream, collect all SSE data events, verify:
- Test: `GET /conversations/:id/stream?triggerMessageId=X` -- create conversation, add user message, open stream, collect all SSE data events, verify:
- Response status is 200
- Content-Type header contains "text/event-stream"
- X-Accel-Buffering header is "no"
@ -352,26 +366,27 @@ res.write(":ok\n\n");
- grep -q "flushHeaders" server/src/routes/chat.ts returns 0
- grep -q 'type: "done"' server/src/routes/chat.ts returns 0
- grep -q 'type: "token"' server/src/routes/chat.ts returns 0
- grep -q "ECHO-STREAM PLACEHOLDER" server/src/routes/chat.ts returns 0
- test -f server/src/__tests__/chat-stream-routes.test.ts
- pnpm --filter @paperclipai/server test run -- chat-stream exits 0
- pnpm --filter @paperclipai/server test run exits 0 (all server tests green)
</acceptance_criteria>
<done>SSE stream endpoint returns text/event-stream with token+done events, edit message route works, abort detection stops streaming, all server tests pass</done>
<done>SSE echo-stream endpoint returns text/event-stream with token+done events (placeholder for Phase 23 LLM integration), edit message route works, abort detection stops streaming, all server tests pass</done>
</task>
</tasks>
<verification>
- `pnpm --filter @paperclipai/server test run` all server tests pass
- `pnpm --filter @paperclipai/server test run` -- all server tests pass
- `pnpm db:generate` has been run and migration exists
- SSE endpoint tested with token + done events
- SSE endpoint tested with token + done events (echo-stream mode)
- Edit message route tested with editedContent persistence
- PATCH conversation with agentId tested
</verification>
<success_criteria>
1. New migration SQL exists and applies the editedContent + editedAt columns
2. GET /conversations/:id/stream returns text/event-stream with token events then done event
2. GET /conversations/:id/stream returns text/event-stream with token events then done event (echo-stream placeholder -- Phase 23 replaces with real LLM)
3. PUT /conversations/:id/messages/:messageId updates editedContent and editedAt
4. PATCH /conversations/:id with { agentId } persists the agent selection
5. All server tests pass (both chat-routes and chat-stream-routes)

View file

@ -36,6 +36,8 @@ must_haves:
- path: "ui/src/components/AgentSelector.tsx"
provides: "Dropdown to select active agent per conversation"
exports: ["AgentSelector"]
- path: "ui/src/components/ChatInput.slash-mention.test.tsx"
provides: "Integration tests for slash command and @mention parsing in ChatInput context"
key_links:
- from: "ui/src/components/ChatAgentBadge.tsx"
to: "ui/src/lib/agent-colors.ts"
@ -120,16 +122,19 @@ Shadcn components already installed (per UI-SPEC): Avatar, Badge, Select, Toolti
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Agent color utility + parseMessageIntent function + tests</name>
<name>Task 1: Agent color utility + parseMessageIntent function + tests (including slash/mention integration stubs)</name>
<files>
ui/src/lib/agent-colors.ts,
ui/src/lib/parseMessageIntent.ts,
ui/src/lib/parseMessageIntent.test.ts
ui/src/lib/parseMessageIntent.test.ts,
ui/src/components/ChatInput.slash-mention.test.tsx
</files>
<read_first>
ui/src/lib/agent-icons.ts,
ui/src/lib/utils.ts,
ui/src/index.css (search for --chart-1 through --chart-5 definitions),
ui/src/components/ChatInput.tsx,
ui/src/components/ChatInput.test.tsx,
.planning/phases/22-agent-streaming/22-UI-SPEC.md (Color section, agent role colors table),
.planning/phases/22-agent-streaming/22-RESEARCH.md (Pattern 4: slash command parsing, Pattern 6: agent colors)
</read_first>
@ -149,7 +154,7 @@ Shadcn components already installed (per UI-SPEC): Avatar, Badge, Select, Toolti
- Test: parseMessageIntent("@PM-agent Check this") returns { text: "Check this", targetName: "pm-agent" }
- Test: parseMessageIntent("/unknown-command Hello") returns { text: "/unknown-command Hello" } (no targetRole)
- Test: parseMessageIntent("Just a normal message") returns { text: "Just a normal message" }
- Test: parseMessageIntent("/path/to/file.ts") returns { text: "/path/to/file.ts" } (no targetRole not followed by space)
- Test: parseMessageIntent("/path/to/file.ts") returns { text: "/path/to/file.ts" } (no targetRole -- not followed by space)
</behavior>
<action>
1. **Create `ui/src/lib/agent-colors.ts`:**
@ -209,22 +214,32 @@ Shadcn components already installed (per UI-SPEC): Avatar, Badge, Select, Toolti
```
3. **Create `ui/src/lib/parseMessageIntent.test.ts`:** Write Vitest tests covering all the behaviors listed above. Use `describe("parseMessageIntent", () => { ... })` and `describe("agentRoleColorClass", () => { ... })` blocks. Import from the respective modules.
4. **Create `ui/src/components/ChatInput.slash-mention.test.tsx`:** Create test stubs for INPUT-05 (slash command parsing in ChatInput context) and INPUT-06 (@mention parsing in ChatInput context). These test the integration between ChatInput and parseMessageIntent:
- Use the same jsdom + createRoot + act pattern as `ChatInput.test.tsx`.
- Test stub (INPUT-05): "slash command prefix filters SLASH_COMMANDS and shows popover" -- import `SLASH_COMMANDS`, verify the exported constant has entries for /brainstorm, /ask-pm, /ask-engineer, /task, /search. (Full popover rendering tests will be added in Plan 03 when the popover is wired into ChatInput.)
- Test stub (INPUT-06): "@mention prefix resolves agent name" -- import `parseMessageIntent`, verify `parseMessageIntent("@test-agent hello").targetName` equals "test-agent". (Full popover rendering tests added in Plan 03.)
- Mark any tests that depend on Plan 03 UI changes with `it.todo(...)` so they are tracked but do not block.
</action>
<verify>
<automated>pnpm --filter @paperclipai/ui test run -- --reporter=verbose parseMessageIntent</automated>
<automated>pnpm --filter @paperclipai/ui test run -- --reporter=verbose parseMessageIntent ChatInput.slash</automated>
</verify>
<acceptance_criteria>
- test -f ui/src/lib/agent-colors.ts
- test -f ui/src/lib/parseMessageIntent.ts
- test -f ui/src/lib/parseMessageIntent.test.ts
- test -f ui/src/components/ChatInput.slash-mention.test.tsx
- grep -q "agentRoleColorClass" ui/src/lib/agent-colors.ts returns 0
- grep -q "parseMessageIntent" ui/src/lib/parseMessageIntent.ts returns 0
- grep -q "SLASH_COMMANDS" ui/src/lib/parseMessageIntent.ts returns 0
- grep -q "/brainstorm" ui/src/lib/parseMessageIntent.ts returns 0
- grep -q "@mention" ui/src/lib/parseMessageIntent.ts OR grep -q "targetName" returns 0
- grep -q "INPUT-05" ui/src/components/ChatInput.slash-mention.test.tsx returns 0
- grep -q "INPUT-06" ui/src/components/ChatInput.slash-mention.test.tsx returns 0
- pnpm --filter @paperclipai/ui test run -- parseMessageIntent exits 0
- pnpm --filter @paperclipai/ui test run -- ChatInput.slash exits 0
</acceptance_criteria>
<done>Agent color mapping utility and message intent parsing with slash commands + @mentions both implemented and fully tested</done>
<done>Agent color mapping utility and message intent parsing with slash commands + @mentions both implemented and fully tested. ChatInput slash/mention integration test stubs created for INPUT-05 and INPUT-06.</done>
</task>
<task type="auto">
@ -253,14 +268,14 @@ Shadcn components already installed (per UI-SPEC): Avatar, Badge, Select, Toolti
Layout per UI-SPEC:
- Container: `flex items-center gap-2 mb-1`
- Avatar circle: `w-5 h-5 rounded-full flex items-center justify-center` + `agentRoleColorClass(agent.role)` background + `text-white`
- If agent has an `icon` value: use the `AgentIcon` component at 12px (check how `agent-icons.ts` maps icon strings to lucide components read that file). If no `AgentIcon` component exists, render the lucide `Bot` icon at 12px.
- If agent has an `icon` value: use the `AgentIcon` component at 12px (check how `agent-icons.ts` maps icon strings to lucide components -- read that file). If no `AgentIcon` component exists, render the lucide `Bot` icon at 12px.
- If no icon: render first letter of `agent.name` at `text-[10px] font-semibold text-white`
- Agent name: `<span className="text-[13px] text-muted-foreground truncate max-w-[120px]" aria-label={`Agent: ${agent.name}`}>`
- Fallback (agent not found): `Bot` icon (12px) + "Agent" text, `bg-muted` background
- Avatar element: `aria-hidden="true"` (decorative per accessibility contract)
2. **Create `ui/src/components/ChatAgentBadge.test.tsx`:**
Use jsdom + createRoot + act pattern (same as `ChatInput.test.tsx` read that file for the testing pattern). NOT `@testing-library/react`.
Use jsdom + createRoot + act pattern (same as `ChatInput.test.tsx` -- read that file for the testing pattern). NOT `@testing-library/react`.
Tests:
- Renders agent name when agentId matches an agent in the array
@ -284,7 +299,7 @@ Shadcn components already installed (per UI-SPEC): Avatar, Badge, Select, Toolti
- If `isLoading`: render `<Skeleton className="h-8 w-28" />`
- On value change: call `onSelect(value)`
No test file needed for AgentSelector (it's a thin UI wrapper over shadcn Select with no logic the color mapping is tested via agent-colors, and the integration will be verified in Plan 03's checkpoint).
No test file needed for AgentSelector (it's a thin UI wrapper over shadcn Select with no logic -- the color mapping is tested via agent-colors, and the integration will be verified in Plan 03's checkpoint).
</action>
<verify>
<automated>pnpm --filter @paperclipai/ui test run -- --reporter=verbose ChatAgentBadge</automated>
@ -308,11 +323,12 @@ Shadcn components already installed (per UI-SPEC): Avatar, Badge, Select, Toolti
</tasks>
<verification>
- `pnpm --filter @paperclipai/ui test run` all UI tests pass
- `pnpm --filter @paperclipai/ui build` TypeScript compiles
- `pnpm --filter @paperclipai/ui test run` -- all UI tests pass
- `pnpm --filter @paperclipai/ui build` -- TypeScript compiles
- Agent color utility tested for all 5 roles + fallback
- parseMessageIntent tested for all 5 slash commands + @mention + plain text + edge cases
- ChatAgentBadge tested for render + fallback + accessibility
- ChatInput.slash-mention.test.tsx exists with INPUT-05 and INPUT-06 stubs
</verification>
<success_criteria>
@ -320,7 +336,8 @@ Shadcn components already installed (per UI-SPEC): Avatar, Badge, Select, Toolti
2. parseMessageIntent correctly parses all 5 slash commands and @mention syntax
3. ChatAgentBadge renders agent name + colored avatar, with fallback for unknown agents
4. AgentSelector provides a dropdown with tooltip and empty state
5. All UI tests pass and build succeeds
5. ChatInput.slash-mention.test.tsx has test stubs for INPUT-05 and INPUT-06
6. All UI tests pass and build succeeds
</success_criteria>
<output>

View file

@ -202,7 +202,7 @@ import { VList } from "virtua";
pnpm --filter @paperclipai/ui add virtua
```
1. **Extend `ui/src/api/chat.ts`** Add these methods to the `chatApi` object:
1. **Extend `ui/src/api/chat.ts`** -- Add these methods to the `chatApi` object:
```typescript
editMessage: (conversationId: string, messageId: string, data: { content: string }) =>
api.put<ChatMessage>(`/api/conversations/${conversationId}/messages/${messageId}`, data),
@ -210,7 +210,7 @@ import { VList } from "virtua";
api.patch<ChatConversation>(`/api/conversations/${id}`, { agentId }),
```
2. **Extend `ui/src/hooks/useChatMessages.ts`** Add `useStreamMessage` hook:
2. **Extend `ui/src/hooks/useChatMessages.ts`** -- Add `useStreamMessage` hook:
```typescript
export function useStreamMessage(conversationId: string | null) {
const queryClient = useQueryClient();
@ -265,7 +265,7 @@ import { VList } from "virtua";
esRef.current = null;
setStreaming(false);
setPartialContent("");
// Toast would go here for now log
// Toast would go here -- for now log
console.error("Stream error:", parsed.message);
}
} catch {
@ -283,7 +283,7 @@ import { VList } from "virtua";
const retry = useCallback(async (agentId?: string | null) => {
if (!conversationId || streaming) return;
// Retry: open stream without posting a new message server re-generates from last user message
// Retry: open stream without posting a new message -- server re-generates from last user message
setStreaming(true);
setPartialContent("");
@ -342,7 +342,7 @@ import { VList } from "virtua";
Add required imports at top: `useState, useCallback, useRef, useEffect` from react.
3. **Extend `ui/src/hooks/useChatConversations.ts`** Add `useUpdateConversationAgent` hook:
3. **Extend `ui/src/hooks/useChatConversations.ts`** -- Add `useUpdateConversationAgent` hook:
```typescript
export function useUpdateConversationAgent() {
const queryClient = useQueryClient();
@ -359,7 +359,7 @@ import { VList } from "virtua";
Add import for `chatApi` (should already be imported; if not, add `import { chatApi } from "../api/chat";`).
</action>
<verify>
<automated>pnpm --filter @paperclipai/ui build</automated>
<automated>pnpm --filter @paperclipai/ui build && pnpm --filter @paperclipai/ui test run</automated>
</verify>
<acceptance_criteria>
- grep -q "virtua" ui/package.json returns 0
@ -372,8 +372,9 @@ import { VList } from "virtua";
- grep -q "partialContent" ui/src/hooks/useChatMessages.ts returns 0
- grep -q "streaming" ui/src/hooks/useChatMessages.ts returns 0
- pnpm --filter @paperclipai/ui build exits 0
- pnpm --filter @paperclipai/ui test run exits 0
</acceptance_criteria>
<done>virtua installed, API client extended with editMessage + updateConversationAgent, useStreamMessage hook with EventSource streaming + stop + retry, useEditMessage mutation, useUpdateConversationAgent mutation, build passes</done>
<done>virtua installed, API client extended with editMessage + updateConversationAgent, useStreamMessage hook with EventSource streaming + stop + retry, useEditMessage mutation, useUpdateConversationAgent mutation, build and tests pass. Note: useStreamMessage is tested via the full integration in Plan 04's visual checkpoint rather than unit tests, since EventSource requires complex browser mocking -- the hook's logic is straightforward state management over a well-tested SSE endpoint.</done>
</task>
<task type="auto">
@ -397,6 +398,11 @@ import { VList } from "virtua";
.planning/phases/22-agent-streaming/22-UI-SPEC.md (full Interaction Contract + Component Inventory)
</read_first>
<action>
NOTE: This task touches 3 tightly-coupled components that share streaming state. They are kept
in one task because splitting would create artificial seams -- ChatPanel owns the state that
ChatMessageList and ChatInput consume. Implement in order: A (ChatMessageList), B (ChatInput),
C (ChatPanel wiring).
**A. Rewrite `ChatMessageList.tsx`** to use virtua VList with agent badges and action buttons:
Replace the entire component. New props interface:
@ -449,7 +455,7 @@ import { VList } from "virtua";
```
3. Track `isAtBottom` state: use VList's `onScroll` callback. Virtua's VList provides `onScroll` with the scroll offset. Calculate: `isAtBottom = (event.scrollOffset + event.viewportSize >= event.scrollSize - 80)`. Initialize `isAtBottom` to `true`.
4. Auto-scroll during streaming: when `streaming` is true and `isAtBottom`, after each partialContent change, call `listRef.current?.scrollToIndex(allMessages.length, { smooth: false })` via a useEffect.
5. Keep `role="log"` and `aria-live="polite"` on an outer wrapper div (not the VList itself VList is the scroll container).
5. Keep `role="log"` and `aria-live="polite"` on an outer wrapper div (not the VList itself -- VList is the scroll container).
**MessageItem** (inline component or extracted):
```tsx
@ -508,7 +514,7 @@ import { VList } from "virtua";
{message.editedAt && " (edited)"}
</span>
{/* Action buttons visible on hover, hidden during streaming */}
{/* Action buttons -- visible on hover, hidden during streaming */}
{!streaming && !editing && (
<div className="flex justify-end gap-1 mt-1 opacity-0 group-hover:opacity-100 transition-opacity duration-150">
{message.role === "user" && (
@ -544,7 +550,7 @@ import { VList } from "virtua";
Imports needed: `VList` from `virtua`, `ChatAgentBadge` from `./ChatAgentBadge`, `ChatMarkdownMessage` from `./ChatMarkdownMessage`, `Button` from `@/components/ui/button`, `Pencil, RotateCcw, ChevronDown` from `lucide-react`, `Agent` from `@paperclipai/shared`, `useState, useRef, useEffect, useCallback` from `react`, `cn` from `../lib/utils`.
**B. Update `ChatInput.tsx`** Add Stop button, slash command popover, @mention popover:
**B. Update `ChatInput.tsx`** -- Add Stop button, slash command popover, @mention popover:
New props interface:
```typescript
@ -603,9 +609,9 @@ import { VList } from "virtua";
- Render same `<Popover>` + `<Command>` pattern
- On item select: replace input with `@{agentName} `, close popover
The popover trigger is the textarea container itself (invisible trigger use `<PopoverAnchor>` on the textarea wrapper div).
The popover trigger is the textarea container itself (invisible trigger -- use `<PopoverAnchor>` on the textarea wrapper div).
**C. Update `ChatPanel.tsx`** Wire everything together:
**C. Update `ChatPanel.tsx`** -- Wire everything together:
1. Import `AgentSelector` from `./AgentSelector`.
2. Import `useStreamMessage, useEditMessage` from `../hooks/useChatMessages`.
@ -650,7 +656,7 @@ import { VList } from "virtua";
try {
const conversation = await createConversation.mutateAsync(undefined);
setActiveConversationId(conversation.id);
// Can't stream yet conversation just created, need to wait for state update
// Can't stream yet -- conversation just created, need to wait for state update
// Queue the send for after state settles
setTimeout(() => stream.send(content, resolveAgentId(intent, agents, conversation.agentId)), 50);
} catch { /* ignore */ }
@ -704,7 +710,7 @@ import { VList } from "virtua";
}, [editMessage, stream, activeConversation]);
```
16. Add `AgentSelector` to the panel header. Modify the inner layout add a header bar above the message area:
16. Add `AgentSelector` to the panel header. Modify the inner layout -- add a header bar above the message area:
```tsx
{/* Message area */}
<div className="flex flex-1 flex-col min-w-0 overflow-hidden">
@ -778,9 +784,9 @@ import { VList } from "virtua";
</tasks>
<verification>
- `pnpm --filter @paperclipai/ui build` TypeScript compiles
- `pnpm --filter @paperclipai/ui test run` all UI tests pass
- `pnpm test run` full suite green
- `pnpm --filter @paperclipai/ui build` -- TypeScript compiles
- `pnpm --filter @paperclipai/ui test run` -- all UI tests pass
- `pnpm test run` -- full suite green
- ChatMessageList uses VList from virtua
- ChatInput shows Stop button during streaming
- ChatPanel has AgentSelector in header

View file

@ -10,9 +10,14 @@ requirements: [CHAT-01, CHAT-08, CHAT-10, CHAT-11, CHAT-12, INPUT-05, INPUT-06,
must_haves:
truths:
- "Full streaming chat flow works end-to-end"
- "All three themes render agent colors correctly"
- "All six success criteria from ROADMAP are met"
- "pnpm test run exits 0 with all chat-routes, chat-stream-routes, parseMessageIntent, ChatAgentBadge, and ChatInput.slash-mention tests passing"
- "pnpm --filter @paperclipai/ui build and pnpm --filter @paperclipai/server build both exit 0"
- "User sends a message and sees echo-stream tokens appear word-by-word in a streaming assistant bubble (CHAT-01 transport-level, echo-stream placeholder)"
- "Stop button (red square) appears during streaming and cancels the stream on click"
- "Agent selector dropdown in chat header shows agents with colored avatars and persists selection across page reload"
- "Agent badge with colored circle and name appears above each assistant message"
- "Slash command popover appears when typing / prefix, @mention popover appears when typing @ prefix"
- "Agent colors are visually distinguishable across all three themes (Catppuccin Mocha, Tokyo Night, Catppuccin Latte)"
artifacts: []
key_links: []
---
@ -41,12 +46,15 @@ Output: Verified, working Phase 22.
<task type="auto">
<name>Task 1: Full test suite verification and build check</name>
<files></files>
<files>
(run-only verification task -- no source changes expected; reads test files for diagnostics if failures occur)
</files>
<read_first>
server/src/__tests__/chat-stream-routes.test.ts,
server/src/__tests__/chat-routes.test.ts,
ui/src/lib/parseMessageIntent.test.ts,
ui/src/components/ChatAgentBadge.test.tsx
ui/src/components/ChatAgentBadge.test.tsx,
ui/src/components/ChatInput.slash-mention.test.tsx
</read_first>
<action>
Run the full test suite and verify all tests pass:
@ -70,42 +78,34 @@ Output: Verified, working Phase 22.
<verify>
<automated>pnpm test run && pnpm --filter @paperclipai/ui build && pnpm --filter @paperclipai/server build</automated>
</verify>
<acceptance_criteria>
- pnpm test run exits 0
- pnpm --filter @paperclipai/ui build exits 0
- pnpm --filter @paperclipai/server build exits 0
</acceptance_criteria>
<done>All tests pass and both UI and server build cleanly</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Visual and functional verification of streaming chat</name>
<files></files>
<files>
(checkpoint -- no files modified; visual/functional verification only)
</files>
<action>
Present the verification checklist to the user. The user will manually test the streaming chat experience across all features built in Phase 22:
Present the verification checklist to the user. Complete Phase 22 agent streaming has been built:
SSE echo-stream endpoint, virtualized message list with agent badges, edit/retry actions,
Stop button, AgentSelector, slash command and @mention popovers.
The user will manually test:
1. Start the dev server: `pnpm dev`
2. Open the chat panel (MessageSquare icon in sidebar)
3. Create a new conversation and send a message — verify tokens stream in word-by-word
3. Create a new conversation and send a message -- verify tokens stream in word-by-word (echo-stream: you will see your own message echoed back, this is the Phase 22 placeholder; real LLM responses come in Phase 23)
4. While streaming: verify the Stop button (red square) appears; click it to cancel
5. Hover over an assistant message verify Retry button (rotate icon) appears; click it
6. Hover over a user message verify Edit button (pencil icon) appears; click to enter edit mode, modify text, click Regenerate
7. Open the Agent Selector dropdown in the header verify agents appear with colored avatars
8. Select a different agent verify it persists (reload page, re-open conversation)
9. Type `/ask-pm ` verify slash command popover appears with matching commands
10. Type `@` followed by an agent name verify mention popover appears
11. Switch between all three themes (Catppuccin Mocha, Tokyo Night, Catppuccin Latte) verify agent badge colors are distinguishable
5. Hover over an assistant message -- verify Retry button (rotate icon) appears; click it
6. Hover over a user message -- verify Edit button (pencil icon) appears; click to enter edit mode, modify text, click Regenerate
7. Open the Agent Selector dropdown in the header -- verify agents appear with colored avatars
8. Select a different agent -- verify it persists (reload page, re-open conversation)
9. Type `/ask-pm ` -- verify slash command popover appears with matching commands
10. Type `@` followed by an agent name -- verify mention popover appears
11. Switch between all three themes (Catppuccin Mocha, Tokyo Night, Catppuccin Latte) -- verify agent badge colors are distinguishable
12. (Optional) If you have a conversation with many messages, scroll rapidly to check smoothness
</action>
<verify>User types "approved" or describes issues to fix</verify>
<acceptance_criteria>
- User confirms streaming tokens appear as they are generated
- User confirms Stop button cancels in-progress stream
- User confirms agent badge shows on assistant messages with colored avatar
- User confirms agent selector changes the conversation's agent
- User confirms slash command and @mention popovers appear
- User confirms agent colors are distinguishable across all three themes
</acceptance_criteria>
<done>User has approved the complete Phase 22 streaming chat experience</done>
</task>