Repository: cline/cline
Pull request #3545: Optimizing memory management for task timeline via virtuoso
Author: login: pashpashpash
Created: 2025-05-15T02:44:04Z | Merged: 2025-05-15T02:54:24Z

### Description

Optimizing memory management for task timeline via virtuoso - HOPEFULLY FIXES GRAY SCREEN ISSUES

### Test Procedure

Scroll around (quickly) on a long task and notice how they render in

### Type of Change

<!-- Put an 'x' in all boxes that apply -->

-   [x] 🐛 Bug fix (non-breaking change which fixes an issue)
-   [ ] ✨ New feature (non-breaking change which adds functionality)
-   [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
-   [ ] ♻️ Refactor Changes
-   [ ] 💅 Cosmetic Changes
-   [ ] 📚 Documentation update
-   [ ] 🏃 Workflow Changes

### Pre-flight Checklist

<!-- Put an 'x' in all boxes that apply -->

-   [x] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
-   [x] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
-   [x] I have created a changeset using `npm run changeset` (required for user-facing changes)
-   [x] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)

### Screenshots

<!-- For UI changes, add screenshots here -->

### Additional Notes

<!-- Add any additional notes for reviewers -->

<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> Optimizes memory management in `TaskTimeline.tsx` using `react-virtuoso` for efficient rendering and improved performance.
> 
>   - **Behavior**:
>     - Optimizes memory management in `TaskTimeline.tsx` using `react-virtuoso` for rendering timeline blocks.
>     - Ensures timeline scrolls to the end when messages change using `virtuosoRef`.
>     - Handles mouse enter/leave events with `useCallback` for performance.
>   - **Components**:
>     - Replaces manual rendering of timeline blocks with `Virtuoso` component.
>     - Adds `TimelineBlock` function to render individual blocks.
>   - **Misc**:
>     - Removes unused `scrollableRef` and related logic.
>     - Adds CSS to hide scrollbars in `.timeline-virtuoso`.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=cline%2Fcline&utm_source=github&utm_medium=referral)<sup> for 3a04e346b6502a7c23a89f4340284568b46e5e58. You can [customize](https://app.ellipsis.dev/cline/settings/summaries) this summary. It will automatically update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->


Diff:
diff --git a/.changeset/afraid-jobs-sing.md b/.changeset/afraid-jobs-sing.md
new file mode 100644
index 00000000000..dff434edf9c
--- /dev/null
+++ b/.changeset/afraid-jobs-sing.md
@@ -0,0 +1,5 @@
+---
+"claude-dev": patch
+---
+
+Optimized memory management for task timeline via virtuoso
diff --git a/webview-ui/src/components/chat/TaskTimeline.tsx b/webview-ui/src/components/chat/TaskTimeline.tsx
index 17b41652be2..bdec6ef5a14 100644
--- a/webview-ui/src/components/chat/TaskTimeline.tsx
+++ b/webview-ui/src/components/chat/TaskTimeline.tsx
@@ -1,4 +1,5 @@
-import React, { useMemo, useState, useRef, useEffect } from "react"
+import React, { useMemo, useState, useRef, useEffect, useCallback } from "react"
+import { Virtuoso } from "react-virtuoso"
 import { ClineMessage } from "@shared/ExtensionMessage"
 import { combineApiRequests } from "@shared/combineApiRequests"
 import { combineCommandSequences } from "@shared/combineCommandSequences"
@@ -136,11 +137,7 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
 		}
 	}, [taskTimelinePropsMessages])
 
-	if (taskTimelinePropsMessages.length === 0) {
-		return null
-	}
-
-	const handleMouseEnter = (message: ClineMessage, event: React.MouseEvent<HTMLDivElement>) => {
+	const handleMouseEnter = useCallback((message: ClineMessage, event: React.MouseEvent<HTMLDivElement>) => {
 		setHoveredMessage(message)
 
 		const viewportWidth = window.innerWidth
@@ -150,11 +147,56 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
 		const x = TOOLTIP_MARGIN
 
 		setTooltipPosition({ x, y: event.clientY })
-	}
+	}, [])
 
-	const handleMouseLeave = () => {
+	const handleMouseLeave = useCallback(() => {
 		setHoveredMessage(null)
 		setTooltipPosition(null)
+	}, [])
+
+	// Calculate the item size (width of block + gap)
+	const itemWidth = parseInt(BLOCK_WIDTH.replace("px", "")) + parseInt(BLOCK_GAP.replace("px", ""))
+
+	// Size function for Virtuoso
+	const itemSize = useCallback(() => itemWidth, [itemWidth])
+
+	// Virtuoso requires a reference to scroll to the end
+	const virtuosoRef = useRef<any>(null)
+
+	// Render a timeline block
+	const TimelineBlock = useCallback(
+		(index: number) => {
+			const message = taskTimelinePropsMessages[index]
+			return (
+				<div
+					style={{
+						width: BLOCK_WIDTH,
+						height: "100%",
+						backgroundColor: getBlockColor(message),
+						flexShrink: 0,
+						cursor: "pointer",
+						marginRight: BLOCK_GAP,
+					}}
+					onMouseEnter={(e) => handleMouseEnter(message, e)}
+					onMouseLeave={handleMouseLeave}
+				/>
+			)
+		},
+		[taskTimelinePropsMessages, handleMouseEnter, handleMouseLeave],
+	)
+
+	// Scroll to the end when messages change
+	useEffect(() => {
+		if (virtuosoRef.current && taskTimelinePropsMessages.length > 0) {
+			virtuosoRef.current.scrollToIndex({
+				index: taskTimelinePropsMessages.length - 1,
+				align: "end",
+			})
+		}
+	}, [taskTimelinePropsMessages])
+
+	if (taskTimelinePropsMessages.length === 0) {
+		return null
 	}
 
 	return (
@@ -167,41 +209,32 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
 				marginBottom: "4px",
 				overflow: "hidden",
 			}}>
-			<div
-				ref={scrollableRef}
+			<style>
+				{`
+					/* Hide scrollbar for Chrome, Safari and Opera */
+					.timeline-virtuoso::-webkit-scrollbar {
+						display: none;
+					}
+					.timeline-virtuoso {
+						scrollbar-width: none;
+						-ms-overflow-style: none;
+					}
+				`}
+			</style>
+
+			<Virtuoso
+				ref={virtuosoRef}
+				className="timeline-virtuoso"
 				style={{
-					display: "flex",
 					height: TIMELINE_HEIGHT,
-					overflowX: "auto",
-					scrollbarWidth: "none",
-					msOverflowStyle: "none",
 					width: "100%",
-					WebkitOverflowScrolling: "touch",
-					gap: BLOCK_GAP, // Using flexbox gap instead of marginRight
-				}}>
-				<style>
-					{`
-            /* Hide scrollbar for Chrome, Safari and Opera */
-            div::-webkit-scrollbar {
-              display: none;
-            }
-          `}
-				</style>
-				{taskTimelinePropsMessages.map((message, index) => (
-					<div
-						key={index}
-						style={{
-							width: BLOCK_WIDTH,
-							height: "100%",
-							backgroundColor: getBlockColor(message),
-							flexShrink: 0,
-							cursor: "pointer",
-						}}
-						onMouseEnter={(e) => handleMouseEnter(message, e)}
-						onMouseLeave={handleMouseLeave}
-					/>
-				))}
-			</div>
+				}}
+				totalCount={taskTimelinePropsMessages.length}
+				itemContent={TimelineBlock}
+				horizontalDirection={true}
+				increaseViewportBy={12}
+				fixedItemHeight={itemWidth}
+			/>
 
 			{hoveredMessage && containerRef.current && tooltipPosition && (
 				<div


Reviews:
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T02:48:17Z
user: login: pashpashpash
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T02:49:31Z
user: login: canvrno
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T02:53:17Z
user: login: dcbartlett
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T02:53:50Z

Review comments:
diff_hunk: @@ -167,41 +210,32 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
 				marginBottom: "4px",
 				overflow: "hidden",
 			}}>
-			<div
-				ref={scrollableRef}
+			<style>
+				{`
+					/* Hide scrollbar for Chrome, Safari and Opera */
+					.timeline-virtuoso::-webkit-scrollbar {
+						display: none;
+					}
+					.timeline-virtuoso {
+						scrollbar-width: none;
+						-ms-overflow-style: none;
+					}
+				`}
+			</style>
+
+			<Virtuoso
+				ref={virtuosoRef}
+				className="timeline-virtuoso"
 				style={{
-					display: "flex",
 					height: TIMELINE_HEIGHT,
-					overflowX: "auto",
-					scrollbarWidth: "none",
-					msOverflowStyle: "none",
 					width: "100%",
-					WebkitOverflowScrolling: "touch",
-					gap: BLOCK_GAP, // Using flexbox gap instead of marginRight
-				}}>
-				<style>
-					{`
-            /* Hide scrollbar for Chrome, Safari and Opera */
-            div::-webkit-scrollbar {
-              display: none;
-            }
-          `}
-				</style>
-				{taskTimelinePropsMessages.map((message, index) => (
-					<div
-						key={index}
-						style={{
-							width: BLOCK_WIDTH,
-							height: "100%",
-							backgroundColor: getBlockColor(message),
-							flexShrink: 0,
-							cursor: "pointer",
-						}}
-						onMouseEnter={(e) => handleMouseEnter(message, e)}
-						onMouseLeave={handleMouseLeave}
-					/>
-				))}
-			</div>
+				}}
+				totalCount={taskTimelinePropsMessages.length}
+				itemContent={TimelineBlock}
+				horizontalDirection={true}
+				increaseViewportBy={12}
+				fixedItemHeight={itemWidth}
path: webview-ui/src/components/chat/TaskTimeline.tsx
user: login: ellipsis-dev[bot]
body: When using a horizontal `Virtuoso`, use `fixedItemWidth` instead of `fixedItemHeight`. The current prop is likely causing layout issues.
```suggestion
				fixedItemWidth={itemWidth}
```

created_at: 2025-05-15T02:48:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,11 +147,57 @@
 		const x = TOOLTIP_MARGIN
 
 		setTooltipPosition({ x, y: event.clientY })
-	}
+	}, [])
 
-	const handleMouseLeave = () => {
+	const handleMouseLeave = useCallback(() => {
 		setHoveredMessage(null)
 		setTooltipPosition(null)
+	}, [])
+
+	// Calculate the item size (width of block + gap)
+	const itemWidth = parseInt(BLOCK_WIDTH.replace("px", "")) + parseInt(BLOCK_GAP.replace("px", ""))
+
+	// Size function for Virtuoso
+	const itemSize = useCallback(() => itemWidth, [itemWidth])
+
+	// Virtuoso requires a reference to scroll to the end
+	const virtuosoRef = useRef<any>(null)
+
+	// Render a timeline block
+	const TimelineBlock = useCallback(
+		(index: number) => {
+			console.log(`Rendering timeline block at index ${index}`)
path: webview-ui/src/components/chat/TaskTimeline.tsx
user: login: ellipsis-dev[bot]
body: Remove or disable the debug `console.log` in `TimelineBlock` to avoid cluttering production logs.
```suggestion

```

created_at: 2025-05-15T02:48:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,41 +210,32 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
 				marginBottom: "4px",
 				overflow: "hidden",
 			}}>
-			<div
-				ref={scrollableRef}
+			<style>
+				{`
+					/* Hide scrollbar for Chrome, Safari and Opera */
+					.timeline-virtuoso::-webkit-scrollbar {
+						display: none;
+					}
+					.timeline-virtuoso {
+						scrollbar-width: none;
+						-ms-overflow-style: none;
+					}
+				`}
+			</style>
+
+			<Virtuoso
+				ref={virtuosoRef}
+				className="timeline-virtuoso"
 				style={{
-					display: "flex",
 					height: TIMELINE_HEIGHT,
-					overflowX: "auto",
-					scrollbarWidth: "none",
-					msOverflowStyle: "none",
 					width: "100%",
-					WebkitOverflowScrolling: "touch",
-					gap: BLOCK_GAP, // Using flexbox gap instead of marginRight
-				}}>
-				<style>
-					{`
-            /* Hide scrollbar for Chrome, Safari and Opera */
-            div::-webkit-scrollbar {
-              display: none;
-            }
-          `}
-				</style>
-				{taskTimelinePropsMessages.map((message, index) => (
-					<div
-						key={index}
-						style={{
-							width: BLOCK_WIDTH,
-							height: "100%",
-							backgroundColor: getBlockColor(message),
-							flexShrink: 0,
-							cursor: "pointer",
-						}}
-						onMouseEnter={(e) => handleMouseEnter(message, e)}
-						onMouseLeave={handleMouseLeave}
-					/>
-				))}
-			</div>
+				}}
+				totalCount={taskTimelinePropsMessages.length}
+				itemContent={TimelineBlock}
+				horizontalDirection={true}
+				increaseViewportBy={12}
+				fixedItemHeight={itemWidth}
path: webview-ui/src/components/chat/TaskTimeline.tsx
user: login: pashpashpash
body: FAKE news - our version of virtuoso doesn't have that 
created_at: 2025-05-15T02:49:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-15T02:44:08Z
body: ###  🦋  Changeset detected

Latest commit: 3a04e346b6502a7c23a89f4340284568b46e5e58

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name       | Type  |
| ---------- | ----- |
| claude-dev | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/cline/cline/new/pashpashpash/virtuoso-timeline?filename=.changeset/chilly-apples-allow.md&value=---%0A%22claude-dev%22%3A%20patch%0A---%0A%0AOptimizing%20memory%20management%20for%20task%20timeline%20via%20virtuoso%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T02:47:45Z
body: <!-- COVERAGE_REPORT -->
## Coverage Report

### Extension Coverage

Base branch: 45%

PR branch: 46%

✅ Coverage increased or remained the same

### Webview Coverage

Base branch: 12%

PR branch: 12%

⚠️ **Warning: Coverage decreased by 0.01%**

Consider adding tests to cover your changes.

### Overall Assessment

⚠️ **Test coverage has decreased in this PR**

Please consider adding tests to maintain or improve coverage.



<sub>Last updated: 2025-05-15T02:54:07.106301</sub>
author_association: CONTRIBUTOR