|
1 | | - |
2 | 1 | import { MessageItem, type MessageType } from "./message" |
3 | 2 | import React from "react" |
| 3 | +import { useVirtualizer } from "@tanstack/react-virtual" |
4 | 4 | import { Button } from "@/components/ui/button" |
5 | 5 | import { ChevronsDownUp, ChevronsUpDown } from "lucide-react" |
6 | 6 | import { Label } from "@/components/ui/label" |
@@ -73,12 +73,43 @@ export const TaskChatPanel = ({ messages, cli, streamStatus, disabled, thinkingM |
73 | 73 | } |
74 | 74 | }, [streamStatus]) |
75 | 75 |
|
| 76 | + const displayMessages = React.useMemo( |
| 77 | + () => messages.filter((message) => message.type !== 'agent_thought_chunk'), |
| 78 | + [messages] |
| 79 | + ) |
| 80 | + |
| 81 | + const virtualRows = React.useMemo(() => { |
| 82 | + const rows: Array<{ type: 'message'; message: MessageType } | { type: 'taskStatus' } | { type: 'fileChanges' }> = displayMessages.map((m) => ({ type: 'message' as const, message: m })) |
| 83 | + if (streamStatus !== 'waiting') { |
| 84 | + rows.push({ type: 'taskStatus' }) |
| 85 | + } |
| 86 | + if (!disabled && fileChanges.length > 0 && showSubmitButton) { |
| 87 | + rows.push({ type: 'fileChanges' }) |
| 88 | + } |
| 89 | + return rows |
| 90 | + }, [displayMessages, streamStatus, disabled, fileChanges.length, showSubmitButton]) |
| 91 | + |
| 92 | + const virtualizer = useVirtualizer({ |
| 93 | + count: virtualRows.length, |
| 94 | + getScrollElement: () => scrollContainerRef.current, |
| 95 | + estimateSize: (index) => { |
| 96 | + const row = virtualRows[index] |
| 97 | + if (row.type === 'taskStatus') return 40 |
| 98 | + if (row.type === 'fileChanges') return 50 |
| 99 | + return 120 |
| 100 | + }, |
| 101 | + overscan: 5, |
| 102 | + gap: 4, |
| 103 | + }) |
| 104 | + |
| 105 | + const virtualItems = virtualizer.getVirtualItems() |
| 106 | + |
76 | 107 | // 自动滚动到底部 |
77 | 108 | React.useEffect(() => { |
78 | | - if (scrollContainerRef.current) { |
| 109 | + if (scrollContainerRef.current && streamStatus !== 'waiting') { |
79 | 110 | scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight |
80 | 111 | } |
81 | | - }, [streamStatus]) |
| 112 | + }, [streamStatus, virtualRows.length]) |
82 | 113 |
|
83 | 114 | const renderTaskStatus = () => { |
84 | 115 | if (streamStatus === 'inited') { |
@@ -198,34 +229,52 @@ export const TaskChatPanel = ({ messages, cli, streamStatus, disabled, thinkingM |
198 | 229 | </div>} |
199 | 230 |
|
200 | 231 | <div ref={scrollContainerRef} className="h-full overflow-y-auto p-2 border rounded-md"> |
201 | | - <div className="flex flex-col justify-end min-h-full gap-1"> |
202 | | - {messages.filter((message) => message.type !== 'agent_thought_chunk').map((message) => ( |
203 | | - <div key={message.id} id={`message-${message.id}`} className="scroll-mt-4"> |
204 | | - <MessageItem message={message as MessageType} cli={cli} /> |
205 | | - </div> |
206 | | - ))} |
207 | | - {renderTaskStatus()} |
208 | | - {!disabled && fileChanges.length > 0 && showSubmitButton ? ( |
209 | | - <div className="flex flex-row px-3 py-2 border rounded-md items-center bg-muted/50 mt-2"> |
210 | | - <div |
211 | | - className="flex-1 text-xs cursor-pointer hover:text-primary transition-colors" |
212 | | - onClick={() => setFileChangesDialogOpen(true)} |
| 232 | + <div |
| 233 | + style={{ |
| 234 | + height: virtualizer.getTotalSize(), |
| 235 | + position: 'relative', |
| 236 | + }} |
| 237 | + > |
| 238 | + {virtualItems.map((virtualRow) => { |
| 239 | + const row = virtualRows[virtualRow.index] |
| 240 | + return ( |
| 241 | + <div |
| 242 | + key={virtualRow.key} |
| 243 | + data-index={virtualRow.index} |
| 244 | + ref={virtualizer.measureElement} |
| 245 | + style={{ |
| 246 | + position: 'absolute', |
| 247 | + top: 0, |
| 248 | + left: 0, |
| 249 | + width: '100%', |
| 250 | + transform: `translateY(${virtualRow.start}px)`, |
| 251 | + }} |
213 | 252 | > |
214 | | - {fileChanges.length} 个文件被修改,是否提交保存 |
| 253 | + {row.type === 'message' && ( |
| 254 | + <div id={`message-${row.message.id}`} className="scroll-mt-4"> |
| 255 | + <MessageItem message={row.message as MessageType} cli={cli} /> |
| 256 | + </div> |
| 257 | + )} |
| 258 | + {row.type === 'taskStatus' && renderTaskStatus()} |
| 259 | + {row.type === 'fileChanges' && ( |
| 260 | + <div className="flex flex-row px-3 py-2 border rounded-md items-center bg-muted/50 mt-2"> |
| 261 | + <div |
| 262 | + className="flex-1 text-xs cursor-pointer hover:text-primary transition-colors" |
| 263 | + onClick={() => setFileChangesDialogOpen(true)} |
| 264 | + > |
| 265 | + {fileChanges.length} 个文件被修改,是否提交保存 |
| 266 | + </div> |
| 267 | + <Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => setShowSubmitButton(false)}> |
| 268 | + 不急 |
| 269 | + </Button> |
| 270 | + <Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => sendUserInput("用 git 提交所有修改,并推送到远程仓库")}> |
| 271 | + 提交 |
| 272 | + </Button> |
| 273 | + </div> |
| 274 | + )} |
215 | 275 | </div> |
216 | | - |
217 | | - <Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => { |
218 | | - setShowSubmitButton(false) |
219 | | - }}> |
220 | | - 不急 |
221 | | - </Button> |
222 | | - <Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => { |
223 | | - sendUserInput("用 git 提交所有修改,并推送到远程仓库") |
224 | | - }}> |
225 | | - 提交 |
226 | | - </Button> |
227 | | - </div> |
228 | | - ) : null} |
| 276 | + ) |
| 277 | + })} |
229 | 278 | </div> |
230 | 279 | </div> |
231 | 280 | <FileChangesDialog |
|
0 commit comments