diff --git a/apps/web/app/task/[id]/page-client.tsx b/apps/web/app/task/[id]/page-client.tsx index eec56f5715..5d1e5d156b 100644 --- a/apps/web/app/task/[id]/page-client.tsx +++ b/apps/web/app/task/[id]/page-client.tsx @@ -6,10 +6,6 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@kandev/ui import { Tabs, TabsContent, TabsList, TabsTrigger } from '@kandev/ui/tabs'; import { TooltipProvider } from '@kandev/ui/tooltip'; import { Textarea } from '@kandev/ui/textarea'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@kandev/ui/dialog'; -import { Button } from '@kandev/ui/button'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select'; -import { Label } from '@kandev/ui/label'; import { IconX } from '@tabler/icons-react'; import { getLocalStorage, setLocalStorage } from '@/lib/local-storage'; import { TaskChatPanel } from '@/components/task/task-chat-panel'; @@ -26,13 +22,6 @@ import { useAppStore, useAppStoreApi } from '@/components/state-provider'; import { useRepositories } from '@/hooks/use-repositories'; import { useRepositoryBranches } from '@/hooks/use-repository-branches'; -type AgentProfile = { - id: string; - name: string; - agent_id: string; - agent_name: string; -}; - const AGENTS = [ { id: 'codex', label: 'Codex' }, { id: 'claude', label: 'Claude Code' }, diff --git a/apps/web/components/kanban-board.tsx b/apps/web/components/kanban-board.tsx index 02df119047..84ef2a2adc 100644 --- a/apps/web/components/kanban-board.tsx +++ b/apps/web/components/kanban-board.tsx @@ -244,6 +244,7 @@ export function KanbanBoard() { }); } }, [ + boardsState.activeId, boardsState.items, commitSettings, setActiveBoard, diff --git a/apps/web/components/task/task-chat-panel.tsx b/apps/web/components/task/task-chat-panel.tsx index f9c3386dc2..2dc6c05358 100644 --- a/apps/web/components/task/task-chat-panel.tsx +++ b/apps/web/components/task/task-chat-panel.tsx @@ -1,6 +1,7 @@ 'use client'; import { FormEvent, useEffect, useRef, useState } from 'react'; +import { useVirtualizer } from '@tanstack/react-virtual'; import { IconBrain, IconCheck, @@ -209,7 +210,7 @@ export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking, taskD const [planModeEnabled, setPlanModeEnabled] = useState(false); const [isSending, setIsSending] = useState(false); const [isAwaitingResponse, setIsAwaitingResponse] = useState(false); - const messagesEndRef = useRef(null); + const messagesContainerRef = useRef(null); const lastAgentMessageCountRef = useRef(0); const commentsState = useAppStore((state) => state.comments); @@ -250,10 +251,20 @@ export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking, taskD // Show typing indicator when awaiting response AND agent session is active const showTypingIndicator = isAwaitingResponse && isAgentWorking; + const itemCount = allMessages.length + (showTypingIndicator ? 1 : 0); + + const virtualizer = useVirtualizer({ + count: itemCount, + getScrollElement: () => messagesContainerRef.current, + estimateSize: () => 96, + overscan: 6, + }); + // Scroll to bottom when new comments arrive or when typing indicator appears useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [visibleComments.length, showTypingIndicator]); + if (itemCount === 0) return; + virtualizer.scrollToIndex(itemCount - 1, { align: 'end', behavior: 'smooth' }); + }, [itemCount, virtualizer]); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); @@ -280,9 +291,57 @@ export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking, taskD return comment.content || '(empty)'; }; + const renderMessage = (comment: Comment) => { + if (comment.type === 'tool_call') { + return ; + } + + const isUser = comment.author_type === 'user'; + const isTaskDescription = comment.id === 'task-description'; + + let label: string; + let containerClass: string; + + if (isTaskDescription) { + label = 'Task'; + containerClass = 'ml-auto bg-primary/20 text-foreground border border-primary/40'; + } else if (isUser) { + label = 'You'; + containerClass = 'ml-auto bg-primary text-primary-foreground'; + } else { + label = 'Agent'; + containerClass = 'bg-muted text-foreground'; + } + + return ( +
+

+ {label} +

+ {isUser ? ( +

{formatContent(comment)}

+ ) : ( +
+ + {formatContent(comment)} + +
+ )} +
+ ); + }; + return ( <> -
+
{(isLoading || commentsLoading) && (
@@ -294,59 +353,27 @@ export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking, taskD No messages yet. Start the conversation!
)} - {allMessages.map((comment) => { - // Tool call comments have a special rendering - if (comment.type === 'tool_call') { - return ; - } - - // Regular message or agent response - render as markdown - const isUser = comment.author_type === 'user'; - const isTaskDescription = comment.id === 'task-description'; - - // Determine label and styling - let label: string; - let containerClass: string; - - if (isTaskDescription) { - label = 'Task'; - containerClass = 'ml-auto bg-primary/20 text-foreground border border-primary/40'; - } else if (isUser) { - label = 'You'; - containerClass = 'ml-auto bg-primary text-primary-foreground'; - } else { - label = 'Agent'; - containerClass = 'bg-muted text-foreground'; - } - - return ( -
-

- {label} -

- {isUser ? ( -

{formatContent(comment)}

- ) : ( -
- - {formatContent(comment)} - + {!isLoading && !commentsLoading && itemCount > 0 && ( +
+ {virtualizer.getVirtualItems().map((virtualRow) => { + const isTypingRow = showTypingIndicator && virtualRow.index === allMessages.length; + const message = allMessages[virtualRow.index]; + return ( +
+
+ {isTypingRow ? : renderMessage(message)} +
- )} -
- ); - })} - {showTypingIndicator && } -
+ ); + })} +
+ )}