fix(21): revise plans based on checker feedback

This commit is contained in:
Nexus Dev 2026-04-01 16:31:43 +00:00
parent 4ee071ddb5
commit 0de520b3b9
8 changed files with 511 additions and 125 deletions

View file

@ -31,9 +31,10 @@
3. Agent messages render with full markdown: code blocks with syntax highlighting and a copy button, tables, lists, headings, links, and inline images
4. Conversations and all messages are stored in libSQL and survive a server restart
5. The chat interface applies Catppuccin Mocha, Tokyo Night, and Catppuccin Latte themes correctly; code block highlighting matches the active theme
**Plans:** 5 plans
**Plans:** 6 plans
Plans:
- [ ] 21-00-PLAN.md — Wave 0 test stubs (chat-service, chat-routes, ChatMarkdownMessage, ChatInput)
- [ ] 21-01-PLAN.md — DB schema (chat_conversations + chat_messages) and shared types/validators
- [ ] 21-02-PLAN.md — Markdown renderer with rehype-highlight, code block copy button, theme CSS
- [ ] 21-03-PLAN.md — Server chat service and REST API routes (CRUD + pagination)
@ -188,7 +189,7 @@ All 65 v1 requirements are mapped to exactly one phase. No orphans.
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 21. Chat Foundation | v1.3 | 0/5 | Planned | - |
| 21. Chat Foundation | v1.3 | 0/6 | Planned | - |
| 22. Agent Streaming | v1.3 | 0/? | Not started | - |
| 23. Brainstormer Flow | v1.3 | 0/? | Not started | - |
| 24. Search, History & Branching | v1.3 | 0/? | Not started | - |

View file

@ -0,0 +1,268 @@
---
phase: 21-chat-foundation
plan: 00
type: execute
wave: 0
depends_on: []
files_modified:
- server/src/__tests__/chat-service.test.ts
- server/src/__tests__/chat-routes.test.ts
- ui/src/components/ChatMarkdownMessage.test.tsx
- ui/src/components/ChatInput.test.tsx
autonomous: true
requirements: [HIST-01, CHAT-02, CHAT-03, CHAT-04, CHAT-05, CHAT-06, INPUT-07]
must_haves:
truths:
- "Test stubs exist and can be executed by vitest without errors"
- "Each stub has describe blocks with placeholder tests that skip or pass trivially"
artifacts:
- path: "server/src/__tests__/chat-service.test.ts"
provides: "Test scaffold for chat service (HIST-01, CHAT-04, CHAT-05, CHAT-06)"
contains: "describe.*chatService"
- path: "server/src/__tests__/chat-routes.test.ts"
provides: "Test scaffold for chat routes (POST conversation, GET list, POST message)"
contains: "describe.*chatRoutes"
- path: "ui/src/components/ChatMarkdownMessage.test.tsx"
provides: "Test scaffold for markdown rendering (CHAT-02, CHAT-03)"
contains: "describe.*ChatMarkdownMessage"
- path: "ui/src/components/ChatInput.test.tsx"
provides: "Test scaffold for keyboard shortcuts (INPUT-07)"
contains: "describe.*ChatInput"
key_links: []
---
<objective>
Create Wave 0 test stubs for the four key test files needed by Plans 01-05.
Purpose: Satisfy the Nyquist rule — every implementation task must have a pre-existing test file with describe blocks and placeholder expectations. Plans 01 and 02 depend on these stubs existing before they execute.
Output: Four test files with describe/it blocks that vitest can discover and run.
</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/phases/21-chat-foundation/21-RESEARCH.md
<interfaces>
From server/src/__tests__/activity-routes.test.ts (reference pattern for server tests):
```typescript
import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { errorHandler } from "../middleware/index.js";
const mockService = vi.hoisted(() => ({ list: vi.fn(), create: vi.fn() }));
vi.mock("../services/activity.js", () => ({ activityService: () => mockService }));
function createApp() {
const app = express();
app.use(express.json());
// ... mock actor middleware
}
```
From ui/src/components/MarkdownBody.test.tsx (reference pattern for UI component tests):
```typescript
// @vitest-environment node
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { ThemeProvider } from "../context/ThemeContext";
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create server-side test stubs (chat-service + chat-routes)</name>
<files>server/src/__tests__/chat-service.test.ts, server/src/__tests__/chat-routes.test.ts</files>
<read_first>
- server/src/__tests__/activity-routes.test.ts (full file — reference for mock pattern, createApp, supertest usage)
</read_first>
<action>
Create `server/src/__tests__/chat-service.test.ts`:
```typescript
import { describe, it, expect } from "vitest";
describe("chatService", () => {
describe("createConversation", () => {
it.todo("creates a conversation row with companyId");
it.todo("returns the created conversation with id and timestamps");
});
describe("listConversations", () => {
it.todo("returns conversations sorted by updatedAt DESC");
it.todo("excludes soft-deleted conversations");
it.todo("supports cursor-based pagination with hasMore");
it.todo("limits results to max 100");
});
describe("getConversation", () => {
it.todo("returns conversation by id");
it.todo("throws notFound for non-existent conversation");
it.todo("throws notFound for soft-deleted conversation");
});
describe("updateConversation", () => {
it.todo("updates title");
it.todo("sets pinnedAt timestamp");
it.todo("clears pinnedAt when set to null");
it.todo("sets archivedAt timestamp");
it.todo("bumps updatedAt on every update");
});
describe("softDeleteConversation", () => {
it.todo("sets deletedAt timestamp");
it.todo("throws notFound if already deleted");
});
describe("addMessage", () => {
it.todo("inserts a message row with conversationId and role");
it.todo("bumps conversation updatedAt after insert");
it.todo("auto-sets title from first user message when title is null");
it.todo("does not overwrite existing title on subsequent messages");
});
describe("listMessages", () => {
it.todo("returns messages for conversation sorted by createdAt DESC");
it.todo("supports cursor-based pagination");
});
});
```
Create `server/src/__tests__/chat-routes.test.ts`:
```typescript
import { describe, it, expect } from "vitest";
describe("chatRoutes", () => {
describe("POST /companies/:companyId/conversations", () => {
it.todo("creates a conversation and returns 201");
it.todo("accepts optional title and agentId");
});
describe("GET /companies/:companyId/conversations", () => {
it.todo("returns paginated conversation list");
it.todo("supports cursor query param");
});
describe("GET /conversations/:id", () => {
it.todo("returns conversation by id");
it.todo("returns 404 for non-existent conversation");
});
describe("PATCH /conversations/:id", () => {
it.todo("updates conversation fields");
});
describe("DELETE /conversations/:id", () => {
it.todo("soft-deletes and returns 204");
});
describe("POST /conversations/:id/messages", () => {
it.todo("creates a message and returns 201");
it.todo("rejects invalid role");
});
describe("GET /conversations/:id/messages", () => {
it.todo("returns paginated message list");
});
});
```
Both files use `it.todo()` which vitest marks as skipped — they run without error and serve as scaffolds for implementation tasks to fill in.
</action>
<verify>
<automated>cd /opt/nexus && pnpm vitest run server/src/__tests__/chat-service.test.ts server/src/__tests__/chat-routes.test.ts --reporter=verbose 2>&1 | tail -5</automated>
</verify>
<done>Server test stubs exist with describe/it.todo blocks covering all chatService methods and chatRoutes endpoints. Vitest runs them without error.</done>
</task>
<task type="auto">
<name>Task 2: Create UI test stubs (ChatMarkdownMessage + ChatInput)</name>
<files>ui/src/components/ChatMarkdownMessage.test.tsx, ui/src/components/ChatInput.test.tsx</files>
<read_first>
- ui/src/components/MarkdownBody.test.tsx (reference for UI component test pattern — renderToStaticMarkup, ThemeProvider wrapper)
- ui/src/components/IssueRow.test.tsx (reference for component test with RTL if used)
</read_first>
<action>
Create `ui/src/components/ChatMarkdownMessage.test.tsx`:
```tsx
// @vitest-environment node
import { describe, it, expect } from "vitest";
describe("ChatMarkdownMessage", () => {
describe("markdown rendering (CHAT-02)", () => {
it.todo("renders plain text as paragraph");
it.todo("renders code blocks with hljs classes for syntax highlighting");
it.todo("renders GFM tables");
it.todo("renders headings, lists, and links");
});
describe("code block features (CHAT-03)", () => {
it.todo("renders language label from code fence");
it.todo("renders copy button with aria-label");
it.todo("extracts code text content for clipboard");
});
});
```
Create `ui/src/components/ChatInput.test.tsx`:
```tsx
// @vitest-environment node
import { describe, it, expect } from "vitest";
describe("ChatInput", () => {
describe("keyboard shortcuts (INPUT-07)", () => {
it.todo("calls onSend when Enter is pressed without Shift");
it.todo("inserts newline when Shift+Enter is pressed");
it.todo("clears input when Escape is pressed");
it.todo("does not send when input is empty");
});
describe("auto-resize (INPUT-01)", () => {
it.todo("textarea has max-height constraint");
});
describe("submit state", () => {
it.todo("disables send button when isSubmitting is true");
});
});
```
Both files use `it.todo()` for the same reason as the server stubs.
</action>
<verify>
<automated>cd /opt/nexus && pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx ui/src/components/ChatInput.test.tsx --reporter=verbose 2>&1 | tail -5</automated>
</verify>
<done>UI test stubs exist with describe/it.todo blocks covering ChatMarkdownMessage rendering and ChatInput keyboard shortcuts. Vitest runs them without error.</done>
</task>
</tasks>
<verification>
- All four test files exist and vitest discovers them
- `pnpm vitest run server/src/__tests__/chat-service.test.ts` exits 0
- `pnpm vitest run server/src/__tests__/chat-routes.test.ts` exits 0
- `pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx` exits 0
- `pnpm vitest run ui/src/components/ChatInput.test.tsx` exits 0
</verification>
<success_criteria>
- Four test stub files created with describe blocks matching the requirements they cover
- Vitest runs all four files without errors (todo tests are skipped, not failed)
- Implementation plans (01-05) can reference these files in their verify commands
</success_criteria>
<output>
After completion, create `.planning/phases/21-chat-foundation/21-00-SUMMARY.md`
</output>

View file

@ -3,7 +3,7 @@ phase: 21-chat-foundation
plan: 01
type: execute
wave: 1
depends_on: []
depends_on: ["21-00"]
files_modified:
- packages/db/src/schema/chat_conversations.ts
- packages/db/src/schema/chat_messages.ts
@ -13,13 +13,13 @@ files_modified:
- packages/shared/src/types/index.ts
- packages/shared/src/validators/index.ts
autonomous: true
requirements: [HIST-01, HIST-05, HIST-06]
requirements: [HIST-01, HIST-06]
must_haves:
truths:
- "chat_conversations and chat_messages tables exist in PostgreSQL after migration"
- "Conversations persist across server restarts (DB-backed, not in-memory)"
- "Conversations and messages written to the database survive a server restart"
- "Shared types and Zod validators are importable from @paperclipai/shared"
- "A new migration SQL file exists that can create the chat tables on first server start"
artifacts:
- path: "packages/db/src/schema/chat_conversations.ts"
provides: "chatConversations Drizzle table"

View file

@ -3,7 +3,7 @@ phase: 21-chat-foundation
plan: 02
type: execute
wave: 1
depends_on: []
depends_on: ["21-00"]
files_modified:
- ui/src/components/ChatMarkdownMessage.tsx
- ui/src/components/ChatCodeBlock.tsx
@ -93,9 +93,9 @@ cd /opt/nexus && pnpm --filter @paperclipai/ui add rehype-highlight
Then add highlight.js theme CSS overrides to `ui/src/index.css`. Do NOT import highlight.js CSS files directly — instead, add CSS custom property overrides scoped to each theme class. Add the following AFTER the existing theme variable blocks (after the `.theme-tokyo-night.dark` block), but BEFORE any component-specific styles:
```css
/* ── highlight.js syntax theme overrides (chat code blocks) ───────────── */
/* -- highlight.js syntax theme overrides (chat code blocks) ------------- */
/* Base hljs reset ensure code blocks use our themed variables */
/* Base hljs reset -- ensure code blocks use our themed variables */
.hljs {
background: var(--code-block-bg, hsl(var(--card))) !important;
color: var(--code-block-fg, hsl(var(--card-foreground))) !important;
@ -168,22 +168,35 @@ IMPORTANT: The `.dark` selector matches Catppuccin Mocha. The `.theme-tokyo-nigh
<done>rehype-highlight is installed, and highlight.js theme CSS overrides exist in index.css for all three Nexus themes (Catppuccin Mocha, Tokyo Night, Catppuccin Latte).</done>
</task>
<task type="auto">
<task type="auto" tdd="true">
<name>Task 2: Create ChatCodeBlock and ChatMarkdownMessage components</name>
<files>ui/src/components/ChatCodeBlock.tsx, ui/src/components/ChatMarkdownMessage.tsx</files>
<files>ui/src/components/ChatCodeBlock.tsx, ui/src/components/ChatMarkdownMessage.tsx, ui/src/components/ChatMarkdownMessage.test.tsx</files>
<behavior>
- ChatMarkdownMessage renders plain text as a paragraph
- ChatMarkdownMessage renders fenced code blocks with hljs classes applied by rehype-highlight
- ChatCodeBlock extracts language from className "language-xxx" and displays it as a label
- ChatCodeBlock renders a copy button with aria-label="Copy code"
</behavior>
<read_first>
- ui/src/components/MarkdownBody.tsx (existing markdown component — understand the Components override pattern, mermaid handling, and remark-gfm usage)
- ui/src/components/MarkdownBody.tsx (existing markdown component -- understand the Components override pattern, mermaid handling, and remark-gfm usage)
- ui/src/context/ThemeContext.tsx (useTheme, THEME_META exports)
- ui/src/lib/utils.ts (cn utility)
- ui/src/components/ChatMarkdownMessage.test.tsx (test stub from Plan 00 -- fill in test expectations)
</read_first>
<action>
Create `ui/src/components/ChatCodeBlock.tsx`:
First, update `ui/src/components/ChatMarkdownMessage.test.tsx` to replace the `.todo` stubs with real test expectations. Use `renderToStaticMarkup` (same pattern as MarkdownBody.test.tsx) to verify:
- Plain text renders as `<p>` tag
- A fenced code block (` ```typescript ... ``` `) produces output containing `hljs` class
- The rendered output contains a copy button element with appropriate aria-label
- Language label is extracted from the code fence
Then create `ui/src/components/ChatCodeBlock.tsx`:
A `pre` component override for react-markdown that wraps code blocks with:
1. A toolbar bar at the top showing the language label (extracted from the `className` on the child `<code>` element, e.g., `language-typescript` -> `typescript`)
2. A copy button in the toolbar that calls `navigator.clipboard.writeText(codeText)` where `codeText` is extracted by recursively flattening the children's text content
3. Copy button shows `Copy` icon (lucide-react) by default, switches to `Check` icon for 1500ms after a successful copy, then reverts
4. Toolbar background: `bg-card` — same as code block background, with `border-b border-border`
4. Toolbar background: `bg-card` -- same as code block background, with `border-b border-border`
5. Language label: `text-xs text-muted-foreground font-mono`
6. Copy button: `Button variant="ghost" size="icon"` with `className="h-6 w-6"`, `aria-label="Copy code"` (changes to `"Copied!"` during success state)
7. For `pre` blocks without a code child (plain preformatted text), render a plain `<pre>` without the toolbar
@ -198,7 +211,7 @@ interface ChatCodeBlockProps {
export function ChatCodeBlock({ children, className, ...props }: ChatCodeBlockProps): JSX.Element;
```
Create `ui/src/components/ChatMarkdownMessage.tsx`:
Then create `ui/src/components/ChatMarkdownMessage.tsx`:
Builds on the existing `MarkdownBody` pattern but uses `rehype-highlight` for syntax highlighting and the custom `ChatCodeBlock` for code block rendering.
@ -233,10 +246,15 @@ export function ChatMarkdownMessage({ content, className }: ChatMarkdownMessageP
The `paperclip-markdown` class ensures existing markdown prose styles from index.css apply (font-size 0.9375rem, line-height 1.6, heading styles, table styles, etc.).
Do NOT duplicate mermaid handling from MarkdownBody — mermaid diagrams are not expected in chat responses for Phase 21. If needed later, it can be added.
Do NOT duplicate mermaid handling from MarkdownBody -- mermaid diagrams are not expected in chat responses for Phase 21. If needed later, it can be added.
Run tests after implementation to verify:
```bash
pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx
```
</action>
<verify>
<automated>cd /opt/nexus && grep -q "rehypeHighlight" ui/src/components/ChatMarkdownMessage.tsx && grep -q "ChatCodeBlock" ui/src/components/ChatMarkdownMessage.tsx && grep -q "navigator.clipboard.writeText" ui/src/components/ChatCodeBlock.tsx && echo "OK"</automated>
<automated>cd /opt/nexus && pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- ui/src/components/ChatMarkdownMessage.tsx contains `import rehypeHighlight from "rehype-highlight"`
@ -247,14 +265,16 @@ Do NOT duplicate mermaid handling from MarkdownBody — mermaid diagrams are not
- ui/src/components/ChatCodeBlock.tsx contains `aria-label` with "Copy code" or "Copied!"
- ui/src/components/ChatCodeBlock.tsx extracts language from className pattern `language-`
- ui/src/components/ChatCodeBlock.tsx uses `Check` and `Copy` icons from lucide-react
- ChatMarkdownMessage tests pass via vitest
</acceptance_criteria>
<done>ChatMarkdownMessage renders markdown with syntax-highlighted code blocks via rehype-highlight. ChatCodeBlock shows a language label and copy button on every code block, with a 1500ms success state on copy.</done>
<done>ChatMarkdownMessage renders markdown with syntax-highlighted code blocks via rehype-highlight. ChatCodeBlock shows a language label and copy button on every code block, with a 1500ms success state on copy. Tests pass.</done>
</task>
</tasks>
<verification>
- `pnpm --filter @paperclipai/ui exec -- tsc --noEmit` passes (type checks)
- `pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx` passes
- Code blocks in ChatMarkdownMessage render with .hljs classes
- Copy button wires to navigator.clipboard.writeText
- Theme CSS in index.css covers all three themes
@ -265,6 +285,7 @@ Do NOT duplicate mermaid handling from MarkdownBody — mermaid diagrams are not
- ChatMarkdownMessage renders markdown with syntax highlighting
- ChatCodeBlock provides language label + copy button
- highlight.js theme overrides in index.css for Mocha, Tokyo Night, and Latte
- ChatMarkdownMessage tests pass
</success_criteria>
<output>

View file

@ -3,16 +3,17 @@ phase: 21-chat-foundation
plan: 03
type: execute
wave: 2
depends_on: ["21-01"]
depends_on: ["21-00", "21-01"]
files_modified:
- server/src/services/chat.ts
- server/src/routes/chat.ts
- server/src/app.ts
autonomous: true
requirements: [CHAT-04, CHAT-05, CHAT-06]
requirements: [CHAT-04, CHAT-05, CHAT-06, HIST-05]
must_haves:
truths:
- "A user on any device on the network can create a conversation and retrieve it from another browser session"
- "POST /api/companies/:companyId/conversations creates a conversation row in DB"
- "GET /api/companies/:companyId/conversations returns paginated list sorted by updatedAt DESC"
- "POST /api/conversations/:id/messages creates a message and bumps conversation updatedAt"
@ -44,7 +45,7 @@ must_haves:
<objective>
Build the server-side chat service and REST API routes.
Purpose: Provide the backend for conversation CRUD (create, list, update, pin, archive, soft-delete) and message CRUD (create, list) with cursor-based pagination. This is the data layer the UI consumes.
Purpose: Provide the backend for conversation CRUD (create, list, update, pin, archive, soft-delete) and message CRUD (create, list) with cursor-based pagination. This is the data layer the UI consumes. HIST-05 (cross-device sync) is satisfied by these API endpoints being network-accessible.
Output: Working API endpoints mounted on the Express app.
</objective>
@ -110,17 +111,33 @@ export function unprocessable(message: string, issues?: unknown): HttpError;
<tasks>
<task type="auto">
<task type="auto" tdd="true">
<name>Task 1: Create chat service with CRUD operations</name>
<files>server/src/services/chat.ts</files>
<files>server/src/services/chat.ts, server/src/__tests__/chat-service.test.ts</files>
<behavior>
- createConversation inserts a row and returns it with id + timestamps
- listConversations returns items sorted by updatedAt DESC, excludes deleted, supports cursor pagination with hasMore
- getConversation returns row by id, throws notFound for missing/deleted
- updateConversation sets provided fields and bumps updatedAt
- softDeleteConversation sets deletedAt, throws notFound if already deleted
- addMessage inserts message, bumps conversation updatedAt, auto-sets title on first user message when title IS NULL
- listMessages returns items sorted by createdAt DESC with cursor pagination
</behavior>
<read_first>
- server/src/services/documents.ts (reference for service factory pattern, Drizzle query patterns)
- server/src/services/activity.ts (reference for simpler service pattern)
- packages/db/src/schema/chat_conversations.ts (table definition — will exist from Plan 01)
- packages/db/src/schema/chat_messages.ts (table definition — will exist from Plan 01)
- server/src/__tests__/chat-service.test.ts (test stub from Plan 00 -- fill in test implementations)
- packages/db/src/schema/chat_conversations.ts (table definition -- will exist from Plan 01)
- packages/db/src/schema/chat_messages.ts (table definition -- will exist from Plan 01)
</read_first>
<action>
Create `server/src/services/chat.ts` following the `function chatService(db: Db)` factory pattern:
First, update `server/src/__tests__/chat-service.test.ts` to replace `.todo` stubs with real test implementations using the vi.mock pattern from activity-routes.test.ts. Mock the db object and verify:
- createConversation calls db.insert with correct table and returns result
- listConversations calls db.select with correct where/orderBy/limit
- addMessage calls db.insert for the message AND db.update for conversation updatedAt
- addMessage calls db.update with `isNull(title)` condition for auto-title
Then create `server/src/services/chat.ts` following the `function chatService(db: Db)` factory pattern:
```typescript
import { and, asc, desc, eq, isNull, lt, sql, count } from "drizzle-orm";
@ -198,7 +215,7 @@ Implementation details for each method:
.set({ updatedAt: new Date() })
.where(eq(chatConversations.id, conversationId));
```
- CRITICAL (Pitfall 5 from RESEARCH.md): Auto-title generation if this is the first message (role === "user") and the conversation has no title:
- CRITICAL (Pitfall 5 from RESEARCH.md): Auto-title generation -- if this is the first message (role === "user") and the conversation has no title:
```typescript
await db.update(chatConversations)
.set({ title: data.content.slice(0, 60), updatedAt: new Date() })
@ -206,9 +223,14 @@ Implementation details for each method:
```
Use `WHERE title IS NULL` to make it idempotent.
- Return the inserted message row
Run tests after implementation:
```bash
pnpm vitest run server/src/__tests__/chat-service.test.ts
```
</action>
<verify>
<automated>cd /opt/nexus && grep -q "export function chatService" server/src/services/chat.ts && grep -q "listConversations" server/src/services/chat.ts && grep -q "addMessage" server/src/services/chat.ts && grep -q "isNull(chatConversations.title)" server/src/services/chat.ts && echo "OK"</automated>
<automated>cd /opt/nexus && pnpm vitest run server/src/__tests__/chat-service.test.ts 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- server/src/services/chat.ts contains `export function chatService(db: Db)`
@ -218,21 +240,39 @@ Implementation details for each method:
- listConversations filters `isNull(chatConversations.deletedAt)`
- listConversations uses `desc(chatConversations.updatedAt)` ordering
- Pagination uses `limit + 1` pattern with `hasMore` boolean
- chat-service tests pass via vitest
</acceptance_criteria>
<done>Chat service exports a factory function with 7 CRUD methods covering conversation lifecycle (create, list, get, update, soft-delete) and message operations (list, add), including auto-title generation and updatedAt bumping.</done>
<done>Chat service exports a factory function with 7 CRUD methods covering conversation lifecycle (create, list, get, update, soft-delete) and message operations (list, add), including auto-title generation and updatedAt bumping. Tests pass.</done>
</task>
<task type="auto">
<task type="auto" tdd="true">
<name>Task 2: Create chat routes and mount in app.ts</name>
<files>server/src/routes/chat.ts, server/src/app.ts</files>
<files>server/src/routes/chat.ts, server/src/app.ts, server/src/__tests__/chat-routes.test.ts</files>
<behavior>
- POST /companies/:companyId/conversations creates a conversation and returns 201
- GET /companies/:companyId/conversations returns paginated list
- GET /conversations/:id returns conversation, 404 for missing
- PATCH /conversations/:id updates fields
- DELETE /conversations/:id soft-deletes and returns 204
- POST /conversations/:id/messages creates message and returns 201
- GET /conversations/:id/messages returns paginated messages
</behavior>
<read_first>
- server/src/routes/activity.ts (reference route factory pattern: function activityRoutes(db: Db): Router)
- server/src/routes/authz.ts (assertBoard, assertCompanyAccess imports)
- server/src/app.ts (existing route mounting pattern: api.use(fooRoutes(db)))
- server/src/middleware/validate.ts (if exists — validation middleware pattern)
- server/src/middleware/validate.ts (if exists -- validation middleware pattern)
- server/src/__tests__/chat-routes.test.ts (test stub from Plan 00 -- fill in test implementations)
</read_first>
<action>
Create `server/src/routes/chat.ts`:
First, update `server/src/__tests__/chat-routes.test.ts` to replace `.todo` stubs with real test implementations using the supertest + vi.mock pattern from activity-routes.test.ts. Mock chatService, create an express app with mock actor middleware, and verify:
- POST /companies/:companyId/conversations returns 201 with created conversation
- GET /companies/:companyId/conversations returns 200 with list
- GET /conversations/:id returns 200
- DELETE /conversations/:id returns 204
- POST /conversations/:id/messages returns 201
Then create `server/src/routes/chat.ts`:
```typescript
import { Router } from "express";
@ -317,31 +357,33 @@ NOTE: Check if existing routes wrap async handlers with a try/catch or rely on E
Mount in `server/src/app.ts`:
1. Add import at the top with the other route imports: `import { chatRoutes } from "./routes/chat.js";`
2. Add `api.use(chatRoutes(db));` in the route mounting section, after the `activityRoutes` line (around line 158).
Run tests after implementation:
```bash
pnpm vitest run server/src/__tests__/chat-routes.test.ts
```
</action>
<verify>
<automated>cd /opt/nexus && grep -q "chatRoutes" server/src/routes/chat.ts && grep -q "chatRoutes" server/src/app.ts && grep -q 'companies/:companyId/conversations' server/src/routes/chat.ts && grep -q 'conversations/:id/messages' server/src/routes/chat.ts && echo "OK"</automated>
<automated>cd /opt/nexus && pnpm vitest run server/src/__tests__/chat-routes.test.ts 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- server/src/routes/chat.ts contains `export function chatRoutes(db: Db): Router`
- server/src/routes/chat.ts contains route `GET /companies/:companyId/conversations`
- server/src/routes/chat.ts contains route `POST /companies/:companyId/conversations`
- server/src/routes/chat.ts contains route `GET /conversations/:id`
- server/src/routes/chat.ts contains route `PATCH /conversations/:id`
- server/src/routes/chat.ts contains route `DELETE /conversations/:id`
- server/src/routes/chat.ts contains route `GET /conversations/:id/messages`
- server/src/routes/chat.ts contains route `POST /conversations/:id/messages`
- server/src/routes/chat.ts contains all 7 route handlers
- server/src/routes/chat.ts calls `assertBoard(req)` on every route
- server/src/routes/chat.ts calls `assertCompanyAccess(req, req.params.companyId!)` on company-scoped routes
- server/src/app.ts contains `import { chatRoutes } from "./routes/chat.js"`
- server/src/app.ts contains `chatRoutes(db)`
- chat-routes tests pass via vitest
</acceptance_criteria>
<done>Chat API routes are mounted on the Express app with 7 endpoints covering conversation CRUD and message CRUD, all gated by assertBoard auth.</done>
<done>Chat API routes are mounted on the Express app with 7 endpoints covering conversation CRUD and message CRUD, all gated by assertBoard auth. Tests pass.</done>
</task>
</tasks>
<verification>
- `cd /opt/nexus && pnpm --filter @paperclipai/server exec -- tsc --noEmit` passes
- `pnpm vitest run server/src/__tests__/chat-service.test.ts` passes
- `pnpm vitest run server/src/__tests__/chat-routes.test.ts` passes
- Routes follow the factory pattern used by all other route files
- Service correctly handles all pitfalls from RESEARCH.md (updatedAt bump, auto-title, soft delete)
</verification>
@ -351,6 +393,7 @@ Mount in `server/src/app.ts`:
- Chat routes mounted in app.ts with proper auth
- Pagination uses cursor-based approach with hasMore
- Auto-title and updatedAt bump implemented per RESEARCH.md pitfalls
- Both server test files pass
</success_criteria>
<output>

View file

@ -3,7 +3,7 @@ phase: 21-chat-foundation
plan: 04
type: execute
wave: 2
depends_on: ["21-02"]
depends_on: ["21-00", "21-02"]
files_modified:
- ui/src/context/ChatPanelContext.tsx
- ui/src/components/ChatPanel.tsx
@ -53,7 +53,7 @@ must_haves:
<objective>
Create the chat panel shell, context provider, input component, and Layout integration.
Purpose: Wire the chat UI skeleton into the app a toggle button in the Layout, a right-side drawer with open/close animation, and an auto-resizing input with keyboard shortcuts. This gives users a visible, interactive chat panel.
Purpose: Wire the chat UI skeleton into the app -- a toggle button in the Layout, a right-side drawer with open/close animation, and an auto-resizing input with keyboard shortcuts. This gives users a visible, interactive chat panel.
Output: Functional chat drawer that opens/closes, with working text input.
</objective>
@ -110,15 +110,32 @@ export function ChatMarkdownMessage({ content, className }: { content: string; c
<tasks>
<task type="auto">
<task type="auto" tdd="true">
<name>Task 1: Create ChatPanelContext and ChatInput components</name>
<files>ui/src/context/ChatPanelContext.tsx, ui/src/components/ChatInput.tsx, ui/src/components/ChatMessage.tsx</files>
<files>ui/src/context/ChatPanelContext.tsx, ui/src/components/ChatInput.tsx, ui/src/components/ChatMessage.tsx, ui/src/components/ChatInput.test.tsx</files>
<behavior>
- ChatInput calls onSend when Enter is pressed without Shift
- ChatInput inserts newline when Shift+Enter is pressed
- ChatInput clears content when Escape is pressed
- ChatInput does not call onSend when input is empty
- ChatInput disables send button when isSubmitting is true
</behavior>
<read_first>
- ui/src/context/PanelContext.tsx (mirror this pattern for localStorage persistence)
- ui/src/context/ThemeContext.tsx (context + hook export pattern)
- ui/src/components/MarkdownBody.tsx (understand existing markdown rendering approach)
- ui/src/components/ChatInput.test.tsx (test stub from Plan 00 -- fill in test implementations)
</read_first>
<action>
First, update `ui/src/components/ChatInput.test.tsx` to replace `.todo` stubs with real test implementations. Use `@testing-library/react` (if available) or `renderToStaticMarkup` to verify:
- Enter key triggers onSend callback with the textarea content
- Shift+Enter does NOT trigger onSend
- Escape clears the textarea value
- Empty textarea does not trigger onSend on Enter
- Send button has disabled state when isSubmitting=true
Then create the components:
**ChatPanelContext.tsx:**
Create `ui/src/context/ChatPanelContext.tsx` mirroring the PanelContext pattern:
@ -199,9 +216,9 @@ Create `ui/src/components/ChatInput.tsx`:
- `className`: use shadcn textarea base classes (`flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ...`) plus `resize-none min-h-[40px] max-h-[160px]`
- `rows={1}` initially
- `onKeyDown` handler:
- `e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing` `e.preventDefault(); submit()`
- `e.key === "Escape"` clear textarea value, call `e.preventDefault()`
- `Shift+Enter` default behavior (inserts newline)
- `e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing` -> `e.preventDefault(); submit()`
- `e.key === "Escape"` -> clear textarea value, call `e.preventDefault()`
- `Shift+Enter` -> default behavior (inserts newline)
- Send button: `Button variant="ghost" size="icon"` with `Send` icon from lucide-react
- `disabled` when textarea is empty (after trim) or `isSubmitting` is true
- `aria-label="Send message"`
@ -249,9 +266,14 @@ export function ChatMessage({ role, content }: ChatMessageProps) {
User messages: right-aligned bubble with `bg-secondary`, plain text (no markdown).
Assistant messages: left-aligned, full width, rendered via `ChatMarkdownMessage`.
Run tests after implementation:
```bash
pnpm vitest run ui/src/components/ChatInput.test.tsx
```
</action>
<verify>
<automated>cd /opt/nexus && grep -q "nexus:chat-panel-open" ui/src/context/ChatPanelContext.tsx && grep -q "export function useChatPanel" ui/src/context/ChatPanelContext.tsx && grep -q "onSend" ui/src/components/ChatInput.tsx && grep -q "Shift" ui/src/components/ChatInput.tsx && grep -q "ChatMarkdownMessage" ui/src/components/ChatMessage.tsx && echo "OK"</automated>
<automated>cd /opt/nexus && pnpm vitest run ui/src/components/ChatInput.test.tsx 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- ui/src/context/ChatPanelContext.tsx contains `const STORAGE_KEY = "nexus:chat-panel-open"`
@ -264,22 +286,23 @@ Assistant messages: left-aligned, full width, rendered via `ChatMarkdownMessage`
- ui/src/components/ChatInput.tsx contains `max-h-[160px]` or equivalent max-height
- ui/src/components/ChatMessage.tsx renders `ChatMarkdownMessage` for assistant role
- ui/src/components/ChatMessage.tsx renders `bg-secondary` bubble for user role
- ChatInput tests pass via vitest
</acceptance_criteria>
<done>ChatPanelContext provides open/close state with localStorage persistence. ChatInput auto-resizes and handles Enter/Shift+Enter/Escape keyboard shortcuts. ChatMessage renders user bubbles and assistant markdown.</done>
<done>ChatPanelContext provides open/close state with localStorage persistence. ChatInput auto-resizes and handles Enter/Shift+Enter/Escape keyboard shortcuts. ChatMessage renders user bubbles and assistant markdown. Tests pass.</done>
</task>
<task type="auto">
<name>Task 2: Create ChatPanel shell and wire into Layout + main.tsx</name>
<files>ui/src/components/ChatPanel.tsx, ui/src/components/Layout.tsx, ui/src/main.tsx</files>
<read_first>
- ui/src/components/Layout.tsx (full file understand the flex row structure, PropertiesPanel placement, existing imports, and the mobile/desktop branching)
- ui/src/main.tsx (full file understand provider nesting order)
- ui/src/components/Layout.tsx (full file -- understand the flex row structure, PropertiesPanel placement, existing imports, and the mobile/desktop branching)
- ui/src/main.tsx (full file -- understand provider nesting order)
- ui/src/components/PropertiesPanel.tsx (understand how it reads panelVisible, its width, and its rendering pattern)
</read_first>
<action>
**ChatPanel.tsx:**
Create `ui/src/components/ChatPanel.tsx` the right-side drawer shell:
Create `ui/src/components/ChatPanel.tsx` -- the right-side drawer shell:
```typescript
import { useChatPanel } from "../context/ChatPanelContext";
@ -313,7 +336,7 @@ export function ChatPanel() {
{/* Two-column layout: conversation list (left) + thread (right) */}
<div className="flex flex-1 min-h-0 min-w-[380px]">
{/* Left column: conversation list placeholder for Plan 05 */}
{/* Left column: conversation list -- placeholder for Plan 05 */}
<div className="w-[160px] flex-shrink-0 border-r border-border bg-card overflow-hidden">
<div className="p-3 text-center text-xs text-muted-foreground">
No conversations yet
@ -323,7 +346,7 @@ export function ChatPanel() {
{/* Right column: message thread + input */}
<div className="flex flex-1 flex-col min-w-0">
<ScrollArea className="flex-1 p-3">
{/* Messages placeholder wired in Plan 05 */}
{/* Messages placeholder -- wired in Plan 05 */}
<div className="flex items-center justify-center h-full">
<p className="text-sm text-muted-foreground">Send a message to start this conversation.</p>
</div>
@ -348,7 +371,7 @@ export function ChatPanel() {
Key specs per UI-SPEC:
- Width: 380px when open, 0 when closed
- `transition-[width] duration-100 ease-out` matches sidebar
- `hidden md:flex` desktop only
- `hidden md:flex` -- desktop only
- Two-column: left 160px for conversation list, right flex-1 for messages+input
- `min-w-[380px]` on inner elements prevents content collapsing during width animation
@ -413,7 +436,7 @@ Key specs per UI-SPEC:
**main.tsx modifications:**
Add `ChatPanelProvider` in the provider stack. Insert it as a sibling of `PanelProvider` AFTER `PanelProvider` (since ChatPanel needs to call `setPanelVisible` from PanelContext):
Add `ChatPanelProvider` in the provider stack. Insert it as a sibling of `PanelProvider` -- AFTER `PanelProvider` (since ChatPanel needs to call `setPanelVisible` from PanelContext):
```tsx
import { ChatPanelProvider } from "./context/ChatPanelContext";
@ -454,6 +477,7 @@ In the render tree, wrap after PanelProvider:
<verification>
- `cd /opt/nexus && pnpm --filter @paperclipai/ui exec -- tsc --noEmit` passes
- `pnpm vitest run ui/src/components/ChatInput.test.tsx` passes
- Chat panel open state persists in localStorage under "nexus:chat-panel-open"
- Layout flex row: main + ChatPanel + PropertiesPanel
- ChatInput handles Enter/Shift+Enter/Escape
@ -465,6 +489,7 @@ In the render tree, wrap after PanelProvider:
- PropertiesPanel closes when chat opens
- Input auto-resizes, keyboard shortcuts work
- All theme CSS variables used (no hardcoded colors)
- ChatInput tests pass
</success_criteria>
<output>

View file

@ -67,7 +67,7 @@ must_haves:
Wire the full chat UI: API client, TanStack Query hooks, conversation list with infinite scroll, message thread, and ChatPanel integration.
Purpose: Connect the UI shell (Plan 04) to the server API (Plan 03), enabling users to create conversations, send messages, and manage their conversation list. This is the integration plan that brings the chat feature to life.
Output: Fully functional chat experience create, read, update, delete conversations; send and view messages.
Output: Fully functional chat experience -- create, read, update, delete conversations; send and view messages.
</objective>
<execution_context>
@ -133,7 +133,7 @@ export function ChatMessage({ role, content }: { role: "user" | "assistant" | "s
- ui/src/api/client.ts (api.get, api.post, api.patch, api.delete patterns)
- ui/src/api/activity.ts (reference for a simple API module pattern)
- ui/src/hooks/useKeyboardShortcuts.ts (hook file pattern)
- ui/src/lib/queryKeys.ts (if exists check for existing query key patterns)
- ui/src/lib/queryKeys.ts (if exists -- check for existing query key patterns)
</read_first>
<action>
**chat.ts API client:**
@ -212,7 +212,7 @@ export function useChatConversations(companyId: string | null) {
getNextPageParam: (lastPage: ChatConversationListResponse) =>
lastPage.hasMore ? lastPage.items.at(-1)?.updatedAt : undefined,
enabled: !!companyId,
placeholderData: (prev) => prev, // keepPreviousData equivalent prevents flicker (Pitfall 6)
placeholderData: (prev) => prev, // keepPreviousData equivalent -- prevents flicker (Pitfall 6)
});
const createMutation = useMutation({
@ -283,7 +283,7 @@ export function useChatMessages(conversationId: string | null) {
Note: Messages come from API in `desc(createdAt)` order (most recent first). Reversing gives chronological order for display.
</action>
<verify>
<automated>cd /opt/nexus && grep -q "export const chatApi" ui/src/api/chat.ts && grep -q "useInfiniteQuery" ui/src/hooks/useChatConversations.ts && grep -q "useInfiniteQuery" ui/src/hooks/useChatMessages.ts && grep -q "sendMutation" ui/src/hooks/useChatMessages.ts && echo "OK"</automated>
<automated>cd /opt/nexus && pnpm --filter @paperclipai/ui exec -- tsc --noEmit 2>&1 | tail -3</automated>
</verify>
<acceptance_criteria>
- ui/src/api/chat.ts exports `chatApi` object with methods: listConversations, createConversation, getConversation, updateConversation, deleteConversation, listMessages, postMessage
@ -293,6 +293,7 @@ Note: Messages come from API in `desc(createdAt)` order (most recent first). Rev
- ui/src/hooks/useChatMessages.ts exports `useChatMessages` using `useInfiniteQuery`
- ui/src/hooks/useChatMessages.ts exports `sendMutation` and `messages` (flattened+reversed)
- Both hooks have `enabled: !!conversationId` or `enabled: !!companyId` guards
- TypeScript compilation passes
</acceptance_criteria>
<done>Chat API client provides 7 fetch methods. useChatConversations provides infinite scroll + CRUD mutations. useChatMessages provides paginated messages + send mutation with optimistic invalidation.</done>
</task>
@ -301,9 +302,9 @@ Note: Messages come from API in `desc(createdAt)` order (most recent first). Rev
<name>Task 2: Create ChatConversationList, ChatConversationItem, ChatMessageList, and wire ChatPanel</name>
<files>ui/src/components/ChatConversationList.tsx, ui/src/components/ChatConversationItem.tsx, ui/src/components/ChatMessageList.tsx, ui/src/components/ChatPanel.tsx</files>
<read_first>
- ui/src/components/ChatPanel.tsx (current placeholder state from Plan 04 will be updated)
- ui/src/components/ChatPanel.tsx (current placeholder state from Plan 04 -- will be updated)
- ui/src/components/ChatMessage.tsx (message rendering component from Plan 04)
- ui/src/context/ChatPanelContext.tsx (useChatPanel hook activeConversationId, setActiveConversationId)
- ui/src/context/ChatPanelContext.tsx (useChatPanel hook -- activeConversationId, setActiveConversationId)
- ui/src/context/CompanyContext.tsx (useCompany for selectedCompanyId)
- ui/src/components/ui/dropdown-menu.tsx (shadcn dropdown component for action menu)
- ui/src/components/ui/skeleton.tsx (shadcn skeleton for loading state)
@ -337,10 +338,10 @@ Renders a row with:
- Preview text below title: `lastMessagePreview` truncated, `text-xs text-muted-foreground truncate`
- Active state: `bg-accent/60` when `isActive`, otherwise `hover:bg-accent`
- On hover: reveal a `MoreHorizontal` icon button (lucide-react) that opens a `DropdownMenu` with items:
- "Rename" triggers inline rename (for simplicity in Phase 21, use `window.prompt("Rename conversation", currentTitle)` and call `onRename` a proper inline editor can be added later)
- "Pin" / "Unpin" calls `onPin(id, !isPinned)` where `isPinned = !!conversation.pinnedAt`
- "Archive" calls `onArchive(id)`
- "Delete" calls `onDelete(id)` (the parent handles the confirmation dialog)
- "Rename" -- triggers inline rename (for simplicity in Phase 21, use `window.prompt("Rename conversation", currentTitle)` and call `onRename` -- a proper inline editor can be added later)
- "Pin" / "Unpin" -- calls `onPin(id, !isPinned)` where `isPinned = !!conversation.pinnedAt`
- "Archive" -- calls `onArchive(id)`
- "Delete" -- calls `onDelete(id)` (the parent handles the confirmation dialog)
- Pin indicator: if `conversation.pinnedAt`, show a small `Pin` icon (lucide-react, `h-3 w-3 text-muted-foreground`) before the title
- Click on the row (outside dropdown) calls `onSelect(conversation.id)`
@ -358,7 +359,7 @@ interface ChatConversationListProps {
Implementation:
- Uses `useChatConversations(companyId)` hook
- Renders a `ScrollArea` container
- At the top: a "New conversation" button with `Plus` icon, `text-xs`, full width calls `createMutation.mutateAsync()` then `setActiveConversationId(newConvo.id)`
- At the top: a "New conversation" button with `Plus` icon, `text-xs`, full width -- calls `createMutation.mutateAsync()` then `setActiveConversationId(newConvo.id)`
- Separate pinned conversations from unpinned: render pinned first (sorted by `pinnedAt`), then unpinned (sorted by `updatedAt`)
- Map conversations to `<ChatConversationItem />` entries
- Loading state: 5 `Skeleton` elements (`h-10 w-full rounded`)
@ -394,34 +395,48 @@ Implementation:
Replace the placeholder content in `ChatPanel.tsx` (from Plan 04) with the real components:
- Import `ChatConversationList`, `ChatMessageList`, `useCompany`, `useChatMessages`
- Import `ChatConversationList`, `ChatMessageList`, `useCompany`, `useChatMessages`, `chatApi`, `useQueryClient`
- Get `selectedCompanyId` from `useCompany()`
- Get `activeConversationId`, `setActiveConversationId` from `useChatPanel()`
- Wire `useChatMessages(activeConversationId)` for the send handler
- Left column: `<ChatConversationList companyId={selectedCompanyId!} />` (guard: only render if `selectedCompanyId`)
- Right column:
- If `activeConversationId`: render `<ChatMessageList conversationId={activeConversationId} />`
- If no `activeConversationId`: show empty state "Send a message to start this conversation."
- Wire ChatInput's `onSend` to: if no activeConversationId, first create a conversation, then send message. If activeConversationId exists, just send message:
```typescript
const handleSend = async (content: string) => {
let convId = activeConversationId;
if (!convId) {
const newConvo = await chatApi.createConversation(selectedCompanyId!, {});
convId = newConvo.id;
setActiveConversationId(convId);
}
await chatApi.postMessage(convId, { role: "user", content });
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ["chat", "messages", convId] });
queryClient.invalidateQueries({ queryKey: ["chat", "conversations"] });
};
```
Use `useMutation` or direct api calls with `useQueryClient` for invalidation.
- Pass `isSubmitting` to ChatInput from the mutation state
**Message send flow -- two distinct paths in handleSend:**
The `handleSend` function in ChatPanel handles two cases:
1. **No active conversation (activeConversationId is null):** Call `chatApi.createConversation(selectedCompanyId!, {})` directly to create a new conversation, then set it as active via `setActiveConversationId(newConvo.id)`, then call `chatApi.postMessage(newConvo.id, { role: "user", content })`. This path uses `chatApi` directly (NOT `useChatMessages.sendMutation`) because `sendMutation` requires a non-null `conversationId` which does not exist yet when the mutation is configured.
2. **Active conversation exists (activeConversationId is set):** Call `useChatMessages(activeConversationId).sendMutation.mutateAsync({ content })`. This path uses the hook's mutation which handles query invalidation automatically.
Both paths invalidate the conversation list query after completion to update sort order and lastMessagePreview.
```typescript
const { sendMutation } = useChatMessages(activeConversationId);
const queryClient = useQueryClient();
const handleSend = async (content: string) => {
if (!selectedCompanyId) return;
if (!activeConversationId) {
// Path 1: No active conversation -- create one first via direct API call
const newConvo = await chatApi.createConversation(selectedCompanyId, {});
setActiveConversationId(newConvo.id);
await chatApi.postMessage(newConvo.id, { role: "user", content });
queryClient.invalidateQueries({ queryKey: ["chat"] });
} else {
// Path 2: Active conversation -- use hook mutation for automatic invalidation
await sendMutation.mutateAsync({ content });
}
};
```
Pass `isSubmitting` to ChatInput: derive from `sendMutation.isPending` for path 2, or manage a local `isSending` state that covers both paths.
</action>
<verify>
<automated>cd /opt/nexus && grep -q "ChatConversationList" ui/src/components/ChatConversationList.tsx && grep -q "ChatConversationItem" ui/src/components/ChatConversationItem.tsx && grep -q "ChatMessageList" ui/src/components/ChatMessageList.tsx && grep -q "ChatConversationList" ui/src/components/ChatPanel.tsx && grep -q "ChatMessageList" ui/src/components/ChatPanel.tsx && grep -q "postMessage" ui/src/components/ChatPanel.tsx && echo "OK"</automated>
<automated>cd /opt/nexus && pnpm --filter @paperclipai/ui exec -- tsc --noEmit 2>&1 | tail -3</automated>
</verify>
<acceptance_criteria>
- ui/src/components/ChatConversationList.tsx uses `useChatConversations` hook
@ -436,17 +451,18 @@ Replace the placeholder content in `ChatPanel.tsx` (from Plan 04) with the real
- ui/src/components/ChatMessageList.tsx auto-scrolls to bottom on new messages
- ui/src/components/ChatPanel.tsx renders `ChatConversationList` in the left column
- ui/src/components/ChatPanel.tsx renders `ChatMessageList` when `activeConversationId` is set
- ui/src/components/ChatPanel.tsx creates a conversation on first send if none active
- ui/src/components/ChatPanel.tsx handleSend creates conversation on first send (path 1: direct chatApi)
- ui/src/components/ChatPanel.tsx handleSend uses sendMutation for existing conversation (path 2: hook mutation)
- ui/src/components/ChatPanel.tsx invalidates queries after sending a message
</acceptance_criteria>
<done>Full chat UI wired: conversation list with infinite scroll, CRUD actions (rename, pin, archive, delete with confirmation), message thread with auto-scroll, and send flow that creates conversations on first message.</done>
<done>Full chat UI wired: conversation list with infinite scroll, CRUD actions (rename, pin, archive, delete with confirmation), message thread with auto-scroll, and send flow with two documented paths (direct API for new conversations, hook mutation for existing ones).</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Verify complete chat flow</name>
<files>none</files>
<action>
Human verification checkpoint. No automated work all implementation was completed in Tasks 1 and 2. The user follows the verification steps below to confirm the complete Phase 21 chat feature works end-to-end.
Human verification checkpoint. No automated work -- all implementation was completed in Tasks 1 and 2. The user follows the verification steps below to confirm the complete Phase 21 chat feature works end-to-end.
</action>
<verify>
<automated>cd /opt/nexus && pnpm --filter @paperclipai/ui exec -- tsc --noEmit && pnpm --filter @paperclipai/server exec -- tsc --noEmit && echo "TYPE CHECK OK"</automated>
@ -469,9 +485,9 @@ Complete Phase 21 Chat Foundation: database persistence, server API, and full ch
<how-to-verify>
1. Start the server: `cd /opt/nexus && pnpm dev`
2. Open the app in a browser
3. Click the MessageSquare (chat) icon in the top-right area the chat panel should slide open from the right
3. Click the MessageSquare (chat) icon in the top-right area -- the chat panel should slide open from the right
4. Click the "+" button to create a new conversation
5. Type a message and press Enter the message should appear as a right-aligned bubble
5. Type a message and press Enter -- the message should appear as a right-aligned bubble
6. Type a message with a code block:
````
Here is some code:
@ -490,14 +506,14 @@ Complete Phase 21 Chat Foundation: database persistence, server API, and full ch
- Syntax highlighting (colors matching the active theme)
- Language label ("typescript")
- Copy button (hover over the code block)
8. Switch themes (cycle button in top-right) verify code block colors change
8. Switch themes (cycle button in top-right) -- verify code block colors change
9. Test conversation management:
- Hover a conversation row, click "...", try Rename, Pin, Archive, Delete
- Pin a conversation verify it moves to the top
- Delete a conversation verify confirmation dialog appears
10. Reload the page verify conversations and messages persist
11. Press Shift+Enter in the input verify newline is inserted
12. Press Escape in the input verify content is cleared
- Pin a conversation -- verify it moves to the top
- Delete a conversation -- verify confirmation dialog appears
10. Reload the page -- verify conversations and messages persist
11. Press Shift+Enter in the input -- verify newline is inserted
12. Press Escape in the input -- verify content is cleared
</how-to-verify>
<resume-signal>Type "approved" or describe issues to fix</resume-signal>
<done>User has verified the complete Phase 21 chat flow: panel toggle, conversation CRUD, message persistence, markdown rendering, syntax highlighting, theme integration, and keyboard shortcuts.</done>

View file

@ -2,7 +2,7 @@
phase: 21
slug: chat-foundation
status: draft
nyquist_compliant: false
nyquist_compliant: true
wave_0_complete: false
created: 2026-04-01
---
@ -18,17 +18,19 @@ created: 2026-04-01
| Property | Value |
|----------|-------|
| **Framework** | vitest |
| **Config file** | `ui/vitest.config.ts` (if exists) or created in Wave 0 |
| **Quick run command** | `cd ui && pnpm test --run` |
| **Full suite command** | `cd ui && pnpm test --run --coverage` |
| **Server test dir** | `server/src/__tests__/` |
| **UI test colocation** | `ui/src/components/*.test.tsx` |
| **Quick run (server)** | `pnpm vitest run server/src/__tests__/chat-service.test.ts server/src/__tests__/chat-routes.test.ts` |
| **Quick run (UI)** | `pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx ui/src/components/ChatInput.test.tsx` |
| **Full suite command** | `pnpm test:run` |
| **Estimated runtime** | ~15 seconds |
---
## Sampling Rate
- **After every task commit:** Run `cd ui && pnpm test --run`
- **After every plan wave:** Run `cd ui && pnpm test --run --coverage`
- **After every task commit:** Run relevant test file(s) per task verify command
- **After every plan wave:** Run `pnpm test:run`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 15 seconds
@ -38,24 +40,32 @@ created: 2026-04-01
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 21-01-01 | 01 | 1 | CHAT-02, CHAT-03 | unit | `cd ui && pnpm test --run` | ❌ W0 | ⬜ pending |
| 21-01-02 | 01 | 1 | HIST-01, HIST-02 | unit | `cd ui && pnpm test --run` | ❌ W0 | ⬜ pending |
| 21-02-01 | 02 | 1 | CHAT-04, CHAT-05, CHAT-06 | unit | `cd ui && pnpm test --run` | ❌ W0 | ⬜ pending |
| 21-02-02 | 02 | 1 | INPUT-01, INPUT-07 | unit | `cd ui && pnpm test --run` | ❌ W0 | ⬜ pending |
| 21-03-01 | 03 | 2 | HIST-03, HIST-05, HIST-06 | unit | `cd ui && pnpm test --run` | ❌ W0 | ⬜ pending |
| 21-03-02 | 03 | 2 | THEME-01, THEME-02 | visual | manual | N/A | ⬜ pending |
| 21-00-01 | 00 | 0 | (scaffolds) | stub | `pnpm vitest run server/src/__tests__/chat-service.test.ts server/src/__tests__/chat-routes.test.ts` | Created in W0 | pending |
| 21-00-02 | 00 | 0 | (scaffolds) | stub | `pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx ui/src/components/ChatInput.test.tsx` | Created in W0 | pending |
| 21-01-01 | 01 | 1 | HIST-01, HIST-06 | schema | grep + migration file check | N/A | pending |
| 21-01-02 | 01 | 1 | HIST-01 | types | import check | N/A | pending |
| 21-02-01 | 02 | 1 | CHAT-02, CHAT-03, THEME-02 | unit | `pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx` | Wave 0 | pending |
| 21-03-01 | 03 | 2 | CHAT-04, CHAT-05, CHAT-06, HIST-05 | unit | `pnpm vitest run server/src/__tests__/chat-service.test.ts` | Wave 0 | pending |
| 21-03-02 | 03 | 2 | CHAT-04, CHAT-05, CHAT-06, HIST-05 | unit | `pnpm vitest run server/src/__tests__/chat-routes.test.ts` | Wave 0 | pending |
| 21-04-01 | 04 | 2 | INPUT-01, INPUT-07, THEME-01 | unit | `pnpm vitest run ui/src/components/ChatInput.test.tsx` | Wave 0 | pending |
| 21-05-01 | 05 | 3 | HIST-02, HIST-03 | tsc | `pnpm --filter @paperclipai/ui exec -- tsc --noEmit` | N/A | pending |
| 21-05-02 | 05 | 3 | HIST-02, HIST-03 | tsc | `pnpm --filter @paperclipai/ui exec -- tsc --noEmit` | N/A | pending |
| 21-05-03 | 05 | 3 | (all) | visual | manual | N/A | pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
*Status: pending / green / red / flaky*
---
## Wave 0 Requirements
- [ ] `ui/src/__tests__/chat/` — test directory structure
- [ ] Vitest config if not present
- [ ] Test utilities for rendering with providers (theme, panel context)
Plan 21-00 creates these four test stubs:
*Test stubs will be created during Wave 0 of execution if needed.*
- [x] `server/src/__tests__/chat-service.test.ts` — covers HIST-01, CHAT-04, CHAT-05, CHAT-06
- [x] `server/src/__tests__/chat-routes.test.ts` — covers route-level integration (POST conversation, GET list, POST message)
- [x] `ui/src/components/ChatMarkdownMessage.test.tsx` — covers CHAT-02/03 (code block render + copy button)
- [x] `ui/src/components/ChatInput.test.tsx` — covers INPUT-07 keyboard shortcuts
*Test stubs are created by Plan 21-00 (Wave 0). Plans 01-04 fill in real test implementations.*
---
@ -64,17 +74,19 @@ created: 2026-04-01
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Theme visual correctness | THEME-01, THEME-02 | Visual appearance cannot be tested with unit tests | Switch between Catppuccin Mocha, Tokyo Night, and Catppuccin Latte; verify code block highlighting matches active theme |
| Markdown rendering fidelity | CHAT-04 | Complex rendering output hard to unit test fully | Send messages with code blocks, tables, lists, headings, links, inline images; verify each renders correctly |
| Markdown rendering fidelity | CHAT-02 | Complex rendering output hard to unit test fully | Send messages with code blocks, tables, lists, headings, links, inline images; verify each renders correctly |
| Infinite scroll behavior | HIST-03 | Requires browser scroll events | Create 30+ conversations, scroll to bottom of list, verify more load |
| Full end-to-end flow | All | Integration across all plans | Plan 05 Task 3 checkpoint covers this |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 15s
- [ ] `nyquist_compliant: true` set in frontmatter
- [x] All tasks have `<automated>` verify or Wave 0 dependencies
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
- [x] Wave 0 covers all MISSING references
- [x] No watch-mode flags
- [x] Feedback latency < 15s
- [x] `nyquist_compliant: true` set in frontmatter
**Approval:** pending
**Approval:** approved