# Task Chat Interface - Final Implementation Plan Comprehensive plan to build a high-performance, feature-rich task chat interface with SSR, lazy loading, and command support. **Status:** Updated after rebase from main (Jan 2026) **Key Changes:** - ✅ SSR already implemented - fetches last 10 comments on server - ✅ TanStack Virtual v3.13.18 already integrated - ✅ Basic tool call rendering already exists - ✅ Pagination API implemented (limit/before/after/sort + has_more/cursor) - ✅ Smart auto-scroll added (only scrolls when at bottom) - ✅ Lazy loading added (scroll-to-top fetch for older messages) - ✅ CodeMirror input implemented (markdown, line wrapping, shortcut adapter, Mod-Enter submit) - ✅ Slash + mention autocomplete added (dummy command examples) - ✅ Message rendering refactor with adapter-style components - ✅ Inline diff rendering (metadata diff uses @git-diff-view/react when structured) - ⚠️ Rich content rendering partially implemented (thinking/todos/diff metadata cards, status/error/progress) - ❌ Rich content (thinking, todos, diffs) not fully implemented --- ## Architecture Overview ### Data Flow ``` 1. Initial Load (SSR): Server Component → HTTP GET /api/tasks/:id/comments?limit=10&sort=desc → Hydrate store with last 10 messages → Client renders, scrolled to bottom 2. WebSocket Subscription: Client mounts → ws.subscribe(taskId) → Receives comment.added notifications → Appends new messages to store → Auto-scroll if at bottom 3. Lazy Loading (Scroll Up): User scrolls to top → Detect via TanStack Virtual → If not all messages loaded: HTTP GET /api/tasks/:id/comments?before=:oldestId&limit=20 → Prepend to store → Maintain scroll position ``` ### Tech Stack **Core:** - React 18 + Next.js 14 (SSR) - TanStack Virtual v3 (virtualization) - Zustand + Immer (state) - WebSocket (real-time) **UI:** - Tailwind CSS + shadcn/ui - CodeMirror 6 (input + file editing) - react-markdown + remark-gfm (display) - @git-diff-view/react (existing) **New Dependencies:** ```json { "@codemirror/view": "^6.34.1", "@codemirror/state": "^6.4.1", "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-markdown": "^6.3.1", "@codemirror/autocomplete": "^6.18.1", "@codemirror/commands": "^6.7.0", "@codemirror/language": "^6.10.3", "@codemirror/theme-one-dark": "^6.1.2", "codemirror": "^6.0.1" } ``` **Total:** ~150KB gzipped (unified solution for chat + file editing) **Already Installed:** - `@tanstack/react-virtual`: v3.13.18 ✅ - `react-markdown` + `remark-gfm` ✅ - `@git-diff-view/react` ✅ --- ## Current Implementation Status ### ✅ Already Implemented (After Rebase) 1. **SSR (Server-Side Rendering)** - `app/task/[id]/page.tsx` fetches comments via `listTaskComments()` - State hydration via `StateHydrator` - Fetches last 10 comments on initial load (sorted asc for display) 2. **TanStack Virtual** - Version: 3.13.18 - Location: `task-chat-panel.tsx` (line 256) - Current config: `estimateSize: 96`, `overscan: 6` - Auto-scroll to bottom on new messages 3. **HTTP API Endpoint** - Backend: `GET /api/v1/tasks/:id/comments` (limit/before/after/sort) - Frontend: `listTaskComments(taskId, params)` in `lib/http/index.ts` - Response: `{ comments, total, has_more, cursor }` 4. **Basic Tool Call Rendering** - Expandable tool call cards - Tool icons and status indicators - Args/result display ### ❌ Still Missing (Needs Implementation) 1. **Rich Content Display** - Basic thinking/todos/diff blocks rendered when metadata exists - Error/status/progress cards added - Inline diff viewer now supported for structured metadata - Remaining: tool call enhancements, error card details, thinking toggle, diff extraction from tool results 4. **CodeMirror Input** - Still using basic Textarea - No markdown mode or syntax highlighting - No command/mention autocomplete 5. **Rich Content Display** - No thinking message display - No todo list visualization - No inline diff viewer - Basic error display only --- ## Implementation Phases ### Phase 1: Pagination API + Lazy Loading (Week 1) #### 1.1 Backend API Enhancements **Update Existing Endpoint:** ```go // File: apps/backend/internal/task/dto/requests.go type ListCommentsRequest struct { TaskID string Limit int // default 10, max 100 Before string // comment ID for pagination (older) After string // comment ID for newer messages Sort string // "asc" | "desc", default "desc" } // File: apps/backend/internal/task/dto/dto.go type ListCommentsResponse struct { Comments []*v1.Comment `json:"comments"` Total int `json:"total"` HasMore bool `json:"has_more"` // NEW Cursor string `json:"cursor"` // NEW } ``` **Implementation Steps:** 1. Update `ListCommentsRequest` DTO to accept query params 2. Modify `CommentController.ListComments()` to pass pagination params 3. Update `Service.ListComments()` to support cursor-based queries 4. Modify SQLite repository query with `WHERE created_at < ?` and `LIMIT` 5. Calculate `has_more` by checking if count > limit 6. Return cursor as the last comment's ID **Handler Update:** ```go // File: apps/backend/internal/task/handlers/comment_handlers.go func (h *CommentHandlers) httpListComments(c *gin.Context) { taskID := c.Param("id") limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) before := c.Query("before") sort := c.DefaultQuery("sort", "desc") resp, err := h.commentController.ListComments(c.Request.Context(), dto.ListCommentsRequest{ TaskID: taskID, Limit: limit, Before: before, Sort: sort, }) // ... return resp with has_more and cursor } ``` **Index Check:** ```sql -- Should already exist from schema CREATE INDEX IF NOT EXISTS idx_comments_task_created ON task_comments(task_id, created_at DESC); ``` #### 1.2 Update SSR to Limit to 10 **Server Component:** `app/task/[id]/page.tsx` ```typescript // Update to only fetch last 10 messages comments = await listTaskComments(id, { limit: 10 }, { cache: 'no-store' }); ``` **Frontend HTTP Client:** `lib/http/index.ts` ```typescript export async function listTaskComments( taskId: string, params?: { limit?: number; before?: string; sort?: string }, options?: ApiRequestOptions ) { const query = new URLSearchParams(); if (params?.limit) query.set('limit', params.limit.toString()); if (params?.before) query.set('before', params.before); if (params?.sort) query.set('sort', params.sort); const url = `/api/v1/tasks/${taskId}/comments${query.toString() ? `?${query}` : ''}`; return fetchJson(url, options); } ``` **Types Update:** `lib/types/http.ts` ```typescript export interface ListCommentsResponse { comments: Comment[]; total: number; has_more: boolean; // NEW cursor: string; // NEW } ``` #### 1.3 Store Enhancements **Zustand Store:** ```typescript type CommentsState = { taskId: string | null; items: Comment[]; // Sorted by created_at ASC isLoading: boolean; hasMore: boolean; oldestCursor: string | null; // For lazy loading // Actions setComments(taskId: string, comments: Comment[]); prependComments(comments: Comment[]); // For lazy load addComment(comment: Comment); // For new messages setCommentsMetadata(meta: { hasMore?, isLoading? }); }; ``` #### 1.4 Lazy Loading Hook **`useLazyLoadComments.ts`:** ```typescript export function useLazyLoadComments(taskId: string) { const { hasMore, oldestCursor, isLoading } = useStore(s => s.comments); const prependComments = useStore(s => s.prependComments); const loadMore = useCallback(async () => { if (!hasMore || isLoading) return; setCommentsMetadata({ isLoading: true }); const response = await fetch( `/api/tasks/${taskId}/comments?limit=20&before=${oldestCursor}` ); const { comments, has_more, cursor } = await response.json(); prependComments(comments); setCommentsMetadata({ hasMore: has_more, oldestCursor: cursor, isLoading: false }); }, [taskId, hasMore, oldestCursor, isLoading]); return { loadMore, hasMore, isLoading }; } ``` --- ### Phase 2: Smart Auto-Scroll + Lazy Load Detection (Week 1) #### 2.1 Enhance Existing Virtual List **Update:** `task-chat-panel.tsx` (already has TanStack Virtual at line 256) **Add Lazy Load Detection:** ```typescript // In TaskChatPanel component, after virtualizer setup (line 261) const { loadMore, hasMore, isLoading } = useLazyLoadComments(taskId); // Detect scroll to top for lazy loading useEffect(() => { const [firstItem] = virtualizer.getVirtualItems(); if (!firstItem) return; // If first visible item is index 0 and we have more if (firstItem.index === 0 && hasMore && !isLoading) { loadMore(); } }, [virtualizer.getVirtualItems(), hasMore, isLoading]); // Show loading indicator at top when loading more const showLoadingIndicator = isLoading && hasMore; ``` #### 2.2 Smart Auto-Scroll **Replace Existing Auto-Scroll Logic** (currently at line 263-267 in `task-chat-panel.tsx`): ```typescript // Add ref to track if user was at bottom const wasAtBottomRef = useRef(true); // Check if at bottom on scroll const checkAtBottom = useCallback(() => { const element = messagesContainerRef.current; if (!element) return; const { scrollTop, scrollHeight, clientHeight } = element; wasAtBottomRef.current = scrollHeight - scrollTop - clientHeight < 50; }, []); // Add scroll listener useEffect(() => { const element = messagesContainerRef.current; if (!element) return; element.addEventListener('scroll', checkAtBottom); return () => element.removeEventListener('scroll', checkAtBottom); }, [checkAtBottom]); // REPLACE existing auto-scroll effect (line 264-267) // OLD: Always scrolls to bottom // NEW: Only scroll if user was at bottom useEffect(() => { if (itemCount === 0) return; if (wasAtBottomRef.current) { virtualizer.scrollToIndex(itemCount - 1, { align: 'end', behavior: 'smooth', }); } }, [itemCount, virtualizer]); ``` #### 2.3 Maintain Scroll Position on Prepend **Strategy:** ```typescript // Before prepending new messages const scrollElement = parentRef.current; const oldScrollHeight = scrollElement.scrollHeight; const oldScrollTop = scrollElement.scrollTop; // Prepend messages to store prependComments(newComments); // After render, restore scroll position requestAnimationFrame(() => { const newScrollHeight = scrollElement.scrollHeight; scrollElement.scrollTop = oldScrollTop + (newScrollHeight - oldScrollHeight); }); ``` --- ### Phase 3: Component Architecture (Week 1-2) #### 3.1 Component Hierarchy ``` TaskChatPanel (refactored) ├── TaskChatHeader │ ├── Agent selector │ ├── Settings (thinking, diff view) │ └── Loading indicator ├── TaskChatMessages (TanStack Virtual) │ ├── Lazy load indicator (top) │ └── MessageRenderer (type router) │ ├── UserMessage │ ├── AssistantMessage + ToolCallList │ ├── ThinkingMessage │ ├── ToolCallCard (recursive for sub-agents) │ ├── TodoListCard │ ├── DiffCard │ ├── ErrorCard │ └── SystemMessage └── TaskChatInput (CodeMirror with commands) ``` #### 3.2 Message Components **Key Components:** 1. **MessageRenderer** - Type router based on `comment.type` 2. **UserMessage** - Avatar, timestamp, markdown content 3. **AssistantMessage** - Avatar, duration, tokens, copy button, markdown + tool calls 4. **ToolCallCard** - Collapsible, status badge, nested sub-agents, specialized renderers 5. **TodoListCard** - Progress bar, status icons, animated updates 6. **DiffCard** - File stats, expandable, uses @git-diff-view/react 7. **ErrorCard** - Error icon, message, stack trace 8. **ThinkingMessage** - Italic, bordered, toggle visibility 9. **SystemMessage** - Session info, warnings, status updates **Shared Components:** - `MessageHeader` - Avatar, name, timestamp, metadata - `CopyButton` - Copy message content - `ExpandButton` - Expand/collapse long content - `StatusBadge` - Status indicators with animations #### 3.3 State Management **Expansion State (Zustand):** ```typescript type UIState = { expandedTools: Set; expandedMessages: Set; showThinking: boolean; diffViewMode: 'unified' | 'split'; toggleToolExpanded(id: string): void; toggleMessageExpanded(id: string): void; }; ``` **Persistence:** - Store expansion state in localStorage via Zustand persist - Restore on mount - Clear on task change --- ### Phase 4: CodeMirror Input with Commands (Week 2) #### 4.1 CodeMirror Setup **Chat Input Configuration:** ```typescript import { EditorView, basicSetup } from '@codemirror/basic-setup'; import { markdown } from '@codemirror/lang-markdown'; import { autocompletion } from '@codemirror/autocomplete'; import { keymap } from '@codemirror/view'; const chatExtensions = [ basicSetup({ lineNumbers: false }), // Minimal for chat markdown(), autocompletion({ override: [commandCompletions, mentionCompletions] }), keymap.of([{ key: "Mod-Enter", run: handleSubmit }]), EditorView.theme({ "&": { maxHeight: "300px", minHeight: "60px" }, ".cm-content": { padding: "12px" }, ".cm-scroller": { overflow: "auto" } }) ]; ``` **Features:** - Markdown mode with syntax awareness - Slash commands (/read, /edit, /bash, etc.) - File mentions (@filename.ts) - Syntax highlighting for inline code - Cmd+Enter to submit - Auto-grow up to 300px height #### 4.2 Command Autocomplete **Command Completion Source:** ```typescript import { CompletionContext } from '@codemirror/autocomplete'; const COMMANDS = [ { label: '/read', detail: 'Read a file', info: 'Usage: /read ' }, { label: '/edit', detail: 'Edit a file', info: 'Usage: /edit ' }, { label: '/bash', detail: 'Run bash command', info: 'Usage: /bash ' }, { label: '/grep', detail: 'Search in files', info: 'Usage: /grep ' }, { label: '/help', detail: 'Show commands', info: 'Display all available commands' }, ]; function commandCompletions(context: CompletionContext) { const word = context.matchBefore(/\/\w*/); if (!word || (word.from === word.to && !context.explicit)) return null; return { from: word.from, options: COMMANDS.map(cmd => ({ label: cmd.label, detail: cmd.detail, info: cmd.info, apply: cmd.label + ' ', // Add space after command })) }; } ``` **File Mention Completion:** ```typescript function mentionCompletions(context: CompletionContext) { const word = context.matchBefore(/@[\w\/\-\.]*/); if (!word) return null; // Get available files from git status or file tree const files = getChangedFiles(); // From store or context return { from: word.from, options: files.map(file => ({ label: `@${file.path}`, detail: file.type, info: `Reference ${file.path}`, apply: `@${file.path}`, })) }; } ``` **Keyboard Navigation:** - Built-in: Arrow up/down, Enter, Escape - Tab to accept completion - Fuzzy matching included #### 4.3 Submit Handler **Extract Content and Send:** ```typescript function handleSubmit(view: EditorView): boolean { const content = view.state.doc.toString(); if (!content.trim()) return false; // Parse commands (optional - backend can also parse) const commands = extractCommands(content); // Send to backend client.request('comment.add', { task_id: taskId, content, metadata: commands.length > 0 ? { commands } : undefined }); // Clear editor view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: '' } }); return true; } ``` #### 4.4 File Editing Integration **Unified Editor for Multiple Contexts:** **1. Chat Input (Minimal):** ```typescript const chatExtensions = [ basicSetup({ lineNumbers: false }), markdown(), autocompletion({ override: [commandCompletions, mentionCompletions] }), ]; ``` **2. File Viewer (Read-only):** ```typescript const viewerExtensions = [ basicSetup(), EditorView.editable.of(false), EditorState.readOnly.of(true), languageForFile(filename), syntaxHighlighting(oneDarkHighlightStyle), ]; ``` **3. File Editor (Full-featured):** ```typescript const editorExtensions = [ basicSetup(), languageForFile(filename), syntaxHighlighting(oneDarkHighlightStyle), lintGutter(), autocompletion(), EditorView.updateListener.of((update) => { if (update.docChanged) { debouncedSave(update.state.doc.toString()); } }), ]; ``` **Language Detection:** ```typescript function languageForFile(filename: string) { const ext = filename.split('.').pop(); switch (ext) { case 'js': case 'jsx': case 'ts': case 'tsx': return javascript({ jsx: true, typescript: ext.includes('ts') }); case 'py': return python(); case 'go': return go(); case 'md': return markdown(); // ... 100+ languages supported default: return []; } } ``` **Shared Configuration:** ```typescript // Reusable theme and settings export const baseTheme = EditorView.theme({ "&": { height: "100%" }, ".cm-scroller": { fontFamily: "var(--font-mono)" }, ".cm-gutters": { backgroundColor: "var(--muted)" }, }); export const baseExtensions = [ baseTheme, EditorState.tabSize.of(2), EditorState.allowMultipleSelections.of(true), ]; ``` --- ### Phase 5: Rich Content Rendering (Week 2-3) #### 5.1 Tool Call Enhancements **Features:** - Recursive rendering of sub-agent calls (Task tool) - Tool-specific summaries (Read: filename, Bash: command preview) - Status badges with animations (pending, running, success, error) - Collapsible params and results - Copy button for results - Specialized renderers (Bash output with ANSI colors, JSON pretty-print) **Metadata Structure:** ```typescript type ToolCallMetadata = { tool: { id: string; name: string; args: Record; result: string | null; status: 'pending' | 'running' | 'success' | 'error'; duration_ms?: number; sub_agent_calls?: ToolCallMetadata[]; }; }; ``` #### 5.2 Todo List Visualization **Features:** - Progress bar (completed / total) - Status icons (Circle → Loader → CheckCircle) - Active form vs content based on status - Animated transitions - Group by status (optional) **Backend Integration:** - Agent sends `progress` type comments - Metadata contains todos array - Updates sent as new comments (immutable) - Frontend shows latest state #### 5.3 Diff Integration **Features:** - Extract diffs from tool call results (Edit, Write tools) - Show file stats (additions, deletions) - Expandable diff viewer using @git-diff-view/react - Syntax highlighting per language - "Open in Changes" button → navigate to Changes tab **Diff Sources:** 1. Tool result with `diff` field 2. Tool result with `old_content` and `new_content` 3. Unified diff string in result #### 5.4 Thinking Display **Features:** - Toggle via settings - Italic text with left border - Markdown rendering - Collapsed by default for long thoughts - Visual distinction from regular messages **Backend:** - Agent sends `thinking` type comments - Or `metadata.thinking` in content comments #### 5.5 Error Handling **Features:** - Error cards with red border - Error icon and title - Error message - Collapsible stack trace - Retry button (if applicable) - Copy error details --- ### Phase 6: Performance & Polish (Week 3-4) #### 6.1 Performance Optimizations **Memoization:** ```typescript // Memoize message components const MessageItem = memo(MessageRenderer, (prev, next) => { return prev.comment.id === next.comment.id && prev.comment.updated_at === next.comment.updated_at; }); // Memoize filtered comments const visibleComments = useMemo(() => { return comments.filter(c => { if (c.type === 'thinking' && !showThinking) return false; return true; }); }, [comments, showThinking]); ``` **Debouncing:** - Scroll position checks: 100ms - Lazy load triggers: 200ms - Auto-scroll: 50ms - Input changes: 300ms (for autocomplete) **Code Splitting:** ```typescript // Lazy load heavy components const MonacoDiffEditor = lazy(() => import('./MonacoDiffEditor')); const MermaidRenderer = lazy(() => import('./MermaidRenderer')); ``` **Bundle Size:** - Target: ~150KB increase (CodeMirror core + essential languages) - Use tree-shaking - Import only needed language modes - Lazy load additional languages on demand #### 6.2 UX Polish **Interactions:** - Smooth height transitions (CSS transitions) - Hover states on interactive elements - Loading skeletons for lazy load - Copy feedback (checkmark animation) - Keyboard shortcuts (j/k navigation, / for command palette) **Visual:** - Consistent spacing and alignment - Color-coded message types - Status indicators with meaningful colors - Avatar fallbacks with initials - Timestamp formatting (relative times) - Duration formatting (ms, s, m) **Accessibility:** - Keyboard navigation - Focus management in autocomplete #### 6.3 Error Boundaries **Wrap Critical Components:** ```typescript } onError={(error) => console.error('Message render error:', error)} > ``` **Graceful Degradation:** - If markdown fails, show plain text - If diff parsing fails, show unified diff string - If tool metadata is invalid, show raw JSON --- ## Backend Changes ### API Enhancements **New Endpoints:** ``` GET /api/tasks/:id/comments?limit=10&before=:cursor&sort=desc POST /api/tasks/:id/comments (existing, no changes) ``` **Response Format:** ```json { "comments": [ { "id": "cmt_123", "task_id": "tsk_456", "author_type": "agent", "content": "...", "type": "content", "metadata": {...}, "created_at": "2025-01-14T12:00:00Z" } ], "has_more": true, "cursor": "cmt_100" } ``` **Indexes:** ```sql CREATE INDEX IF NOT EXISTS idx_comments_task_created ON task_comments(task_id, created_at DESC); ``` ### WebSocket (No Changes) Existing `comment.added` notification works as-is. ### Comment Type Usage **Agent Integration:** - `message` - User messages - `content` - Agent text responses - `tool_call` - Tool executions - `progress` - Todo list updates - `thinking` - Agent reasoning - `error` - Error messages - `status` - System notifications --- ## File Structure ``` apps/web/ ├── app/task/[id]/ │ ├── page.tsx (SSR: fetch initial comments) │ └── page-client.tsx (Client: hydrate + subscribe) ├── components/task/ │ ├── task-chat-panel.tsx (Main container, refactored) │ └── chat/ │ ├── TaskChatHeader.tsx │ ├── TaskChatMessages.tsx (TanStack Virtual) │ ├── TaskChatInput.tsx (Lexical + commands) │ ├── messages/ │ │ ├── MessageRenderer.tsx │ │ ├── UserMessage.tsx │ │ ├── AssistantMessage.tsx │ │ ├── ThinkingMessage.tsx │ │ ├── SystemMessage.tsx │ │ └── ErrorCard.tsx │ ├── tools/ │ │ ├── ToolCallCard.tsx │ │ ├── ToolStatusBadge.tsx │ │ └── ToolIcon.tsx │ ├── content/ │ │ ├── TodoListCard.tsx │ │ ├── TodoItem.tsx │ │ ├── DiffCard.tsx │ │ └── CopyButton.tsx │ ├── input/ │ │ ├── CodeMirrorEditor.tsx │ │ ├── commandCompletions.ts │ │ ├── mentionCompletions.ts │ │ └── editorExtensions.ts │ └── hooks/ │ ├── useLazyLoadComments.ts │ ├── useSmartAutoScroll.ts │ └── useChatVirtualizer.ts └── lib/ ├── state/store.ts (Enhanced with pagination) └── api/comments.ts (New: fetch comments API) apps/backend/ └── internal/task/ ├── handlers/ │ └── comment_handlers.go (Add pagination support) └── repository/ └── sqlite.go (Cursor-based queries) ``` **Total new files:** ~25 **Modified files:** 4 (page.tsx, page-client.tsx, task-chat-panel.tsx, store.ts, comment_handlers.go, sqlite.go) --- ## Implementation Timeline ### Week 1: Pagination & Smart Scroll - [x] ~~SSR setup~~ (Already done) - [x] ~~TanStack Virtual~~ (Already done v3.13.18) - [x] ~~Basic tool calls~~ (Already done) - [ ] Backend pagination API (limit, before, cursor) - [ ] Update SSR to limit to 10 messages - [ ] Lazy loading on scroll up - [ ] Smart auto-scroll (only if at bottom) - [ ] Scroll position preservation on prepend ### Week 2: Rich Content - [ ] Enhanced message components (copy, timestamps) - [ ] Nested tool call rendering (sub-agents) - [ ] Todo list visualization with progress bars - [ ] Inline diff viewer integration - [ ] Thinking message display - [ ] Error card enhancements ### Week 3: CodeMirror Input - [ ] Replace Textarea with CodeMirror - [ ] Markdown mode with syntax highlighting - [ ] Slash command autocomplete (/read, /edit, /bash) - [ ] File mention autocomplete (@file.ts) - [ ] Keyboard navigation (arrows, enter, escape) - [ ] Command parsing & execution - [ ] File viewer/editor config (bonus) ### Week 4: Polish & Testing - [ ] Performance optimization (memoization audit) - [ ] Error boundaries for message rendering - [ ] Accessibility (keyboard nav) - [ ] Documentation updates ---