Browse Source

Revert "Rollback virtualized list changes"

This reverts commit e6f6d754b2d3610f954326e707402cd16a455848.
Saoud Rizwan 1 year ago
parent
commit
e5d86ffb8d
3 changed files with 41 additions and 20 deletions
  1. 13 0
      webview-ui/package-lock.json
  2. 1 0
      webview-ui/package.json
  3. 27 20
      webview-ui/src/components/ChatView.tsx

+ 13 - 0
webview-ui/package-lock.json

@@ -24,6 +24,7 @@
         "react-text-truncate": "^0.19.0",
         "react-textarea-autosize": "^8.5.3",
         "react-use": "^17.5.1",
+        "react-virtuoso": "^4.7.13",
         "rewire": "^7.0.0",
         "typescript": "^4.9.5",
         "web-vitals": "^2.1.4"
@@ -17516,6 +17517,18 @@
         "react-dom": "*"
       }
     },
+    "node_modules/react-virtuoso": {
+      "version": "4.7.13",
+      "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.7.13.tgz",
+      "integrity": "sha512-rabPhipwJ8rdA6TDk1vdVqVoU6eOkWukqoC1pNQVBCsvjBvIeJMi9nO079s0L7EsRzAxFFQNahX+8vuuY4F1Qg==",
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "react": ">=16 || >=17 || >= 18",
+        "react-dom": ">=16 || >=17 || >= 18"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

+ 1 - 0
webview-ui/package.json

@@ -19,6 +19,7 @@
     "react-text-truncate": "^0.19.0",
     "react-textarea-autosize": "^8.5.3",
     "react-use": "^17.5.1",
+    "react-virtuoso": "^4.7.13",
     "rewire": "^7.0.0",
     "typescript": "^4.9.5",
     "web-vitals": "^2.1.4"

+ 27 - 20
webview-ui/src/components/ChatView.tsx

@@ -11,6 +11,7 @@ import { getSyntaxHighlighterStyleFromTheme } from "../utilities/getSyntaxHighli
 import { vscode } from "../utilities/vscode"
 import ChatRow from "./ChatRow"
 import TaskHeader from "./TaskHeader"
+import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
 import Announcement from "./Announcement"
 
 interface ChatViewProps {
@@ -42,7 +43,8 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
 
 	const [syntaxHighlighterStyle, setSyntaxHighlighterStyle] = useState(vsDarkPlus)
 
-	const chatContainerRef = useRef<HTMLDivElement>(null)
+	const virtuosoRef = useRef<VirtuosoHandle>(null)
+
 	const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
 
 	const toggleRowExpansion = (ts: number) => {
@@ -312,23 +314,16 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
 	}, [modifiedMessages])
 
 	useEffect(() => {
+		// We use a setTimeout to ensure new content is rendered before scrolling to the bottom. virtuoso's followOutput would scroll to the bottom before the new content could render.
 		const timer = setTimeout(() => {
-			scrollToBottom()
-		}, 0)
+			// TODO: we can use virtuoso's isAtBottom to prevent scrolling if user is scrolled up, and show a 'scroll to bottom' button for better UX
+			// NOTE: scroll to bottom may not work if you use margin, see virtuoso's troubleshooting
+			virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "smooth" })
+		}, 50)
 
 		return () => clearTimeout(timer)
 	}, [visibleMessages])
 
-	const scrollToBottom = (instant: boolean = false) => {
-		if (chatContainerRef.current) {
-			const scrollOptions: ScrollToOptions = {
-				top: chatContainerRef.current.scrollHeight,
-				behavior: instant ? "auto" : "smooth",
-			}
-			chatContainerRef.current.scrollTo(scrollOptions)
-		}
-	}
-
 	const placeholderText = useMemo(() => {
 		if (messages.at(-1)?.ask === "command_output") {
 			return "Type input to command stdin..."
@@ -376,14 +371,23 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
 					</div>
 				</>
 			)}
-			<div
-				ref={chatContainerRef}
+			<Virtuoso
+				ref={virtuosoRef}
 				className="scrollable"
 				style={{
 					flexGrow: 1,
 					overflowY: "scroll", // always show scrollbar
-				}}>
-				{visibleMessages.map((message, index) => (
+				}}
+				// followOutput={(isAtBottom) => {
+				// 	const lastMessage = modifiedMessages.at(-1)
+				// 	if (lastMessage && shouldShowChatRow(lastMessage)) {
+				// 		return "smooth"
+				// 	}
+				// 	return false
+				// }}
+				increaseViewportBy={{ top: 0, bottom: Number.MAX_SAFE_INTEGER }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
+				data={visibleMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
+				itemContent={(index, message) => (
 					<ChatRow
 						key={message.ts}
 						message={message}
@@ -393,8 +397,8 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
 						lastModifiedMessage={modifiedMessages.at(-1)}
 						isLast={index === visibleMessages.length - 1}
 					/>
-				))}
-			</div>
+				)}
+			/>
 			<div
 				style={{
 					opacity: primaryButtonText || secondaryButtonText ? (enableButtons ? 1 : 0.5) : 0,
@@ -430,7 +434,10 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
 					disabled={textAreaDisabled}
 					onChange={(e) => setInputValue(e.target.value)}
 					onKeyDown={handleKeyDown}
-					onHeightChange={() => scrollToBottom(true)}
+					onHeightChange={() =>
+						//virtuosoRef.current?.scrollToIndex({ index: "LAST", align: "end", behavior: "auto" })
+						virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "auto" })
+					}
 					placeholder={placeholderText}
 					maxRows={10}
 					autoFocus={true}