docs(21): create gap closure plan for HIST-02 search + INPUT-07 Cmd+K
This commit is contained in:
parent
64b2302f93
commit
3b8857cc9d
2 changed files with 332 additions and 3 deletions
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
## Phases
|
||||
|
||||
- [x] **Phase 21: Chat Foundation** — Persistent conversation storage, sidebar, CRUD, markdown rendering, theme integration, keyboard shortcuts (completed 2026-04-01)
|
||||
- [ ] **Phase 21: Chat Foundation** — Persistent conversation storage, sidebar, CRUD, markdown rendering, theme integration, keyboard shortcuts
|
||||
- [ ] **Phase 22: Agent Streaming** — Real-time streaming via SSE/WebSocket, agent selector, agent identity on messages, stop/edit/regenerate, slash commands and @mentions
|
||||
- [ ] **Phase 23: Brainstormer Flow** — Brainstormer agent persona, structured questioning flow, spec generation, PM handoff, task creation from chat, agent status updates in chat
|
||||
- [ ] **Phase 24: Search, History & Branching** — Full-text search across all conversations, export, conversation branching, message bookmarks
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
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:** 6/6 plans complete
|
||||
**Plans:** 7 plans (6 complete + 1 gap closure)
|
||||
|
||||
Plans:
|
||||
- [x] 21-00-PLAN.md — Wave 0 test stubs (chat-service, chat-routes, ChatMarkdownMessage, ChatInput)
|
||||
|
|
@ -40,6 +40,7 @@ Plans:
|
|||
- [x] 21-03-PLAN.md — Server chat service and REST API routes (CRUD + pagination)
|
||||
- [x] 21-04-PLAN.md — ChatPanel shell, ChatPanelContext, ChatInput, Layout integration
|
||||
- [x] 21-05-PLAN.md — Full UI wiring: API client, conversation list, message thread, infinite scroll
|
||||
- [ ] 21-06-PLAN.md — Gap closure: conversation search/filter (HIST-02) + Cmd+K shortcut (INPUT-07)
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
|
|
@ -189,7 +190,7 @@ All 65 v1 requirements are mapped to exactly one phase. No orphans.
|
|||
|
||||
| Phase | Milestone | Plans Complete | Status | Completed |
|
||||
|-------|-----------|----------------|--------|-----------|
|
||||
| 21. Chat Foundation | v1.3 | 6/6 | Complete | 2026-04-01 |
|
||||
| 21. Chat Foundation | v1.3 | 6/7 | Gap closure | - |
|
||||
| 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 | - |
|
||||
|
|
|
|||
328
.planning/phases/21-chat-foundation/21-06-PLAN.md
Normal file
328
.planning/phases/21-chat-foundation/21-06-PLAN.md
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
---
|
||||
phase: 21-chat-foundation
|
||||
plan: 06
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- server/src/services/chat.ts
|
||||
- server/src/routes/chat.ts
|
||||
- ui/src/api/chat.ts
|
||||
- ui/src/hooks/useChatConversations.ts
|
||||
- ui/src/components/ChatConversationList.tsx
|
||||
- ui/src/hooks/useKeyboardShortcuts.ts
|
||||
- ui/src/components/Layout.tsx
|
||||
autonomous: true
|
||||
gap_closure: true
|
||||
requirements:
|
||||
- HIST-02
|
||||
- INPUT-07
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Typing in the search input filters the conversation list to only matching titles"
|
||||
- "Cmd+K (Mac) or Ctrl+K (non-Mac) focuses the conversation search input when the chat panel is open"
|
||||
- "Passing an agentId query parameter to GET /companies/:companyId/conversations returns only conversations for that agent"
|
||||
- "Clearing the search input restores the full conversation list"
|
||||
artifacts:
|
||||
- path: "server/src/services/chat.ts"
|
||||
provides: "listConversations with search and agentId filter params"
|
||||
contains: "ilike"
|
||||
- path: "server/src/routes/chat.ts"
|
||||
provides: "GET route reads search and agentId query params"
|
||||
contains: "req.query"
|
||||
- path: "ui/src/components/ChatConversationList.tsx"
|
||||
provides: "Search input above conversation list"
|
||||
contains: "search"
|
||||
- path: "ui/src/hooks/useKeyboardShortcuts.ts"
|
||||
provides: "Cmd+K handler"
|
||||
contains: "onSearch"
|
||||
key_links:
|
||||
- from: "ui/src/components/ChatConversationList.tsx"
|
||||
to: "ui/src/hooks/useChatConversations.ts"
|
||||
via: "search param passed to hook which passes to chatApi"
|
||||
pattern: "useChatConversations.*search"
|
||||
- from: "ui/src/hooks/useKeyboardShortcuts.ts"
|
||||
to: "ui/src/components/Layout.tsx"
|
||||
via: "onSearch callback focuses chat search input"
|
||||
pattern: "onSearch"
|
||||
- from: "ui/src/api/chat.ts"
|
||||
to: "server/src/routes/chat.ts"
|
||||
via: "search query param in URL"
|
||||
pattern: "search"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Close two verification gaps from Phase 21: conversation search/filtering (HIST-02) and Cmd+K keyboard shortcut (INPUT-07).
|
||||
|
||||
Purpose: Complete the two remaining PARTIAL requirements that prevent Phase 21 from fully passing verification.
|
||||
Output: Searchable conversation list with server-side filtering, Cmd+K shortcut to focus search.
|
||||
</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/21-chat-foundation/21-VERIFICATION.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
From packages/shared/src/types/chat.ts:
|
||||
```typescript
|
||||
export interface ChatConversationListItem {
|
||||
id: string;
|
||||
companyId: string;
|
||||
title: string | null;
|
||||
agentId: string | null;
|
||||
pinnedAt: string | null;
|
||||
archivedAt: string | null;
|
||||
updatedAt: string;
|
||||
lastMessagePreview: string | null;
|
||||
}
|
||||
|
||||
export interface ChatConversationListResponse {
|
||||
items: ChatConversationListItem[];
|
||||
hasMore: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
From server/src/services/chat.ts:
|
||||
```typescript
|
||||
export function chatService(db: Db) {
|
||||
return {
|
||||
async listConversations(
|
||||
companyId: string,
|
||||
opts: { cursor?: string; limit?: number; includeArchived?: boolean },
|
||||
) { ... },
|
||||
// ... other methods
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
From ui/src/hooks/useKeyboardShortcuts.ts:
|
||||
```typescript
|
||||
interface ShortcutHandlers {
|
||||
onNewIssue?: () => void;
|
||||
onToggleSidebar?: () => void;
|
||||
onTogglePanel?: () => void;
|
||||
}
|
||||
export function useKeyboardShortcuts({ onNewIssue, onToggleSidebar, onTogglePanel }: ShortcutHandlers) { ... }
|
||||
```
|
||||
|
||||
From ui/src/api/chat.ts:
|
||||
```typescript
|
||||
export const chatApi = {
|
||||
listConversations(companyId: string, opts?: { cursor?: string; limit?: number }) { ... },
|
||||
// ... other methods
|
||||
};
|
||||
```
|
||||
|
||||
From ui/src/hooks/useChatConversations.ts:
|
||||
```typescript
|
||||
export function useChatConversations(companyId: string | null) {
|
||||
// useInfiniteQuery with queryKey: ["chat", "conversations", companyId]
|
||||
// queryFn calls chatApi.listConversations(companyId!, { cursor: pageParam })
|
||||
}
|
||||
```
|
||||
|
||||
From ui/src/components/Layout.tsx (lines 159-163):
|
||||
```typescript
|
||||
useKeyboardShortcuts({
|
||||
onNewIssue: () => openNewIssue(),
|
||||
onToggleSidebar: toggleSidebar,
|
||||
onTogglePanel: togglePanel,
|
||||
});
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add search and agentId filter to server service, route, and API client</name>
|
||||
<files>server/src/services/chat.ts, server/src/routes/chat.ts, ui/src/api/chat.ts, ui/src/hooks/useChatConversations.ts</files>
|
||||
<read_first>
|
||||
- server/src/services/chat.ts (full file — understand listConversations signature and condition-building pattern)
|
||||
- server/src/routes/chat.ts (full file — understand how query params are extracted)
|
||||
- ui/src/api/chat.ts (full file — understand how query params are serialized)
|
||||
- ui/src/hooks/useChatConversations.ts (full file — understand queryKey and queryFn)
|
||||
- packages/shared/src/types/chat.ts (ChatConversationListResponse type)
|
||||
</read_first>
|
||||
<action>
|
||||
**server/src/services/chat.ts** — Extend `listConversations` opts type and query:
|
||||
1. Add `import { ilike } from "drizzle-orm"` to the existing import (add `ilike` alongside `and, desc, eq, isNull, lt`)
|
||||
2. Extend the opts parameter type: `opts: { cursor?: string; limit?: number; includeArchived?: boolean; search?: string; agentId?: string }`
|
||||
3. In the conditions array, after the existing conditions, add:
|
||||
- If `opts.search` is truthy: `conditions.push(ilike(chatConversations.title, \`%${opts.search}%\`))`
|
||||
- If `opts.agentId` is truthy: `conditions.push(eq(chatConversations.agentId, opts.agentId))`
|
||||
|
||||
**server/src/routes/chat.ts** — Pass search and agentId from query params:
|
||||
1. In the GET `/companies/:companyId/conversations` handler, destructure `search` and `agentId` from `req.query` alongside the existing `cursor, limit, includeArchived`
|
||||
2. Pass them to `svc.listConversations`: `search: search as string | undefined, agentId: agentId as string | undefined`
|
||||
|
||||
**ui/src/api/chat.ts** — Add search and agentId to the API client:
|
||||
1. Extend `listConversations` opts type: `opts?: { cursor?: string; limit?: number; search?: string; agentId?: string }`
|
||||
2. In the URLSearchParams builder, add: `if (opts?.search) params.set("search", opts.search)` and `if (opts?.agentId) params.set("agentId", opts.agentId)`
|
||||
|
||||
**ui/src/hooks/useChatConversations.ts** — Accept search param and pass through:
|
||||
1. Change signature: `export function useChatConversations(companyId: string | null, opts?: { search?: string })`
|
||||
2. Add `opts?.search` to the queryKey: `queryKey: ["chat", "conversations", companyId, opts?.search ?? ""]`
|
||||
3. In the queryFn, pass search: `chatApi.listConversations(companyId!, { cursor: pageParam as string | undefined, search: opts?.search || undefined })`
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus && pnpm --filter @paperclipai/server exec -- tsc --noEmit 2>&1 | grep -i chat; pnpm --filter @paperclipai/ui exec -- tsc --noEmit 2>&1 | grep -i chat; echo "TypeScript check done"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `server/src/services/chat.ts` listConversations accepts `search?: string` and `agentId?: string` in opts
|
||||
- When `search` is provided, the query includes `ilike(chatConversations.title, '%search%')`
|
||||
- When `agentId` is provided, the query includes `eq(chatConversations.agentId, agentId)`
|
||||
- `server/src/routes/chat.ts` extracts `search` and `agentId` from `req.query` and passes to service
|
||||
- `ui/src/api/chat.ts` serializes `search` and `agentId` as URL query params
|
||||
- `ui/src/hooks/useChatConversations.ts` accepts `opts?: { search?: string }` and includes search in queryKey
|
||||
- TypeScript compilation passes for both server and ui packages (no new errors in chat files)
|
||||
</acceptance_criteria>
|
||||
<done>Server-side search and agent filtering works end-to-end from API client through route to database query. The hook re-fetches when search changes due to updated queryKey.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add search input to ChatConversationList and Cmd+K shortcut</name>
|
||||
<files>ui/src/components/ChatConversationList.tsx, ui/src/hooks/useKeyboardShortcuts.ts, ui/src/components/Layout.tsx</files>
|
||||
<read_first>
|
||||
- ui/src/components/ChatConversationList.tsx (full file — understand structure, imports, existing state)
|
||||
- ui/src/hooks/useKeyboardShortcuts.ts (full file — understand ShortcutHandlers interface and handler pattern)
|
||||
- ui/src/components/Layout.tsx (lines 1-30 for imports, lines 155-165 for useKeyboardShortcuts call, and lines 440-470 for ChatPanel render area)
|
||||
- ui/src/context/ChatPanelContext.tsx (full file — understand useChatPanel exports)
|
||||
</read_first>
|
||||
<action>
|
||||
**ui/src/components/ChatConversationList.tsx** — Add search input with debounced filtering:
|
||||
1. Add `import { Search, X } from "lucide-react"` (add Search and X to existing lucide imports; Plus is already imported)
|
||||
2. Add `import { Input } from "@/components/ui/input"` (shadcn input component)
|
||||
3. Add a `useRef<HTMLInputElement>(null)` for the search input ref — name it `searchInputRef`
|
||||
4. Add `const [searchTerm, setSearchTerm] = useState("")` state
|
||||
5. Add a debounced search value with a simple useEffect + setTimeout pattern:
|
||||
```
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedSearch(searchTerm), 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchTerm]);
|
||||
```
|
||||
6. Pass debouncedSearch to the hook: change `useChatConversations(companyId)` to `useChatConversations(companyId, { search: debouncedSearch || undefined })`
|
||||
7. Add a search input between the "New conversation" button div and the ScrollArea:
|
||||
```tsx
|
||||
<div className="px-2 pb-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search conversations..."
|
||||
className="h-7 pl-7 pr-7 text-xs"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchTerm("")}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
8. Export the searchInputRef via an imperative handle: Add `useImperativeHandle` from React. Change the component to use `forwardRef<ChatConversationListHandle, ChatConversationListProps>`. Define the handle interface:
|
||||
```typescript
|
||||
export interface ChatConversationListHandle {
|
||||
focusSearch: () => void;
|
||||
}
|
||||
```
|
||||
In `useImperativeHandle(ref, () => ({ focusSearch: () => searchInputRef.current?.focus() }))`.
|
||||
|
||||
**ui/src/hooks/useKeyboardShortcuts.ts** — Add onSearch handler for Cmd+K:
|
||||
1. Add `onSearch?: () => void` to the `ShortcutHandlers` interface
|
||||
2. Add a new handler block BEFORE the existing shortcut checks (Cmd+K uses metaKey/ctrlKey, so it won't conflict with the input-guard since Cmd+K is a global shortcut that should work even from inputs):
|
||||
```typescript
|
||||
// Cmd+K / Ctrl+K → Search (global, works even from inputs)
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
onSearch?.();
|
||||
return;
|
||||
}
|
||||
```
|
||||
Place this check BEFORE the `if (target.tagName === "INPUT" ...)` early return, so Cmd+K fires even when focused in an input/textarea.
|
||||
3. Add `onSearch` to the useEffect dependency array
|
||||
|
||||
**ui/src/components/Layout.tsx** — Wire Cmd+K to focus chat search:
|
||||
1. Add `import { useRef } from "react"` (add useRef to the existing React import)
|
||||
2. Add `import type { ChatConversationListHandle } from "./ChatConversationList"`
|
||||
3. Create a ref: `const chatSearchRef = useRef<ChatConversationListHandle>(null)`
|
||||
4. Add `onSearch` to the `useKeyboardShortcuts` call:
|
||||
```typescript
|
||||
useKeyboardShortcuts({
|
||||
onNewIssue: () => openNewIssue(),
|
||||
onToggleSidebar: toggleSidebar,
|
||||
onTogglePanel: togglePanel,
|
||||
onSearch: () => {
|
||||
if (!chatOpen) setChatOpen(true);
|
||||
// Use requestAnimationFrame to ensure panel is visible before focusing
|
||||
requestAnimationFrame(() => chatSearchRef.current?.focusSearch());
|
||||
},
|
||||
});
|
||||
```
|
||||
5. Pass the ref down: The ChatConversationList is rendered inside ChatPanel, which is rendered inside Layout. The simplest approach is to expose a `searchRef` prop on ChatPanel and pass it through.
|
||||
|
||||
ALTERNATIVE (simpler): Instead of threading refs, add a dedicated `useEffect` in `ChatConversationList` that listens for a custom event:
|
||||
- In ChatConversationList, add: `useEffect(() => { const handler = () => searchInputRef.current?.focus(); window.addEventListener("nexus:focus-chat-search", handler); return () => window.removeEventListener("nexus:focus-chat-search", handler); }, []);`
|
||||
- In Layout's onSearch callback: `if (!chatOpen) setChatOpen(true); requestAnimationFrame(() => window.dispatchEvent(new Event("nexus:focus-chat-search")));`
|
||||
- This avoids drilling refs through ChatPanel. Use this approach.
|
||||
|
||||
With the custom event approach, you do NOT need chatSearchRef, ChatConversationListHandle, or forwardRef. Simplify:
|
||||
- ChatConversationList: do NOT use forwardRef or useImperativeHandle. Just add the event listener useEffect with searchInputRef.
|
||||
- Layout: just dispatch the event in onSearch.
|
||||
- useKeyboardShortcuts: add onSearch to interface and handler as described above.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus && pnpm --filter @paperclipai/ui exec -- tsc --noEmit 2>&1 | tail -5; echo "---"; pnpm vitest run ui/src/components/ChatInput.test.tsx 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- ChatConversationList renders an `<input>` with placeholder "Search conversations..."
|
||||
- The search input has a Search icon on the left and a clear (X) button when non-empty
|
||||
- Typing in the search input debounces at 300ms then passes the search term to useChatConversations
|
||||
- useKeyboardShortcuts has an `onSearch` handler that fires on Cmd+K (metaKey+k) or Ctrl+K (ctrlKey+k)
|
||||
- The Cmd+K handler fires even when focus is in an input or textarea (it is checked before the input-guard early return)
|
||||
- Layout.tsx wires onSearch to open the chat panel (if closed) and dispatch "nexus:focus-chat-search" event
|
||||
- ChatConversationList listens for "nexus:focus-chat-search" and focuses the search input
|
||||
- TypeScript compilation passes with no new errors
|
||||
- Existing ChatInput tests still pass
|
||||
</acceptance_criteria>
|
||||
<done>Users can type in the search input to filter conversations by title. Cmd+K (Mac) or Ctrl+K (other) opens the chat panel if needed and focuses the search input. Clearing the search restores the full list.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. TypeScript: `pnpm --filter @paperclipai/server exec -- tsc --noEmit` and `pnpm --filter @paperclipai/ui exec -- tsc --noEmit` — no new errors in chat files
|
||||
2. Existing tests: `pnpm vitest run 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` — all pass
|
||||
3. Search input visible in ChatConversationList (grep for `Search conversations` in ChatConversationList.tsx)
|
||||
4. Cmd+K handler present (grep for `metaKey.*ctrlKey.*k` in useKeyboardShortcuts.ts)
|
||||
5. Server route passes search param (grep for `search` in server/src/routes/chat.ts GET handler)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- HIST-02 gap closed: conversation list is searchable via a search input with server-side ilike filtering on title; agentId filter parameter accepted by service and route
|
||||
- INPUT-07 gap closed: Cmd+K / Ctrl+K keyboard shortcut opens chat panel and focuses the search input
|
||||
- All existing tests continue to pass
|
||||
- TypeScript compilation clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/21-chat-foundation/21-06-SUMMARY.md`
|
||||
</output>
|
||||
Loading…
Add table
Reference in a new issue