1- import React , { useMemo , useState , useRef , useEffect } from "react"
1+ import React , { useMemo , useState , useRef , useEffect , useCallback } from "react"
2+ import { Virtuoso } from "react-virtuoso"
23import { ClineMessage } from "@shared/ExtensionMessage"
34import { combineApiRequests } from "@shared/combineApiRequests"
45import { combineCommandSequences } from "@shared/combineCommandSequences"
@@ -136,11 +137,7 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
136137 }
137138 } , [ taskTimelinePropsMessages ] )
138139
139- if ( taskTimelinePropsMessages . length === 0 ) {
140- return null
141- }
142-
143- const handleMouseEnter = ( message : ClineMessage , event : React . MouseEvent < HTMLDivElement > ) => {
140+ const handleMouseEnter = useCallback ( ( message : ClineMessage , event : React . MouseEvent < HTMLDivElement > ) => {
144141 setHoveredMessage ( message )
145142
146143 const viewportWidth = window . innerWidth
@@ -150,11 +147,56 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
150147 const x = TOOLTIP_MARGIN
151148
152149 setTooltipPosition ( { x, y : event . clientY } )
153- }
150+ } , [ ] )
154151
155- const handleMouseLeave = ( ) => {
152+ const handleMouseLeave = useCallback ( ( ) => {
156153 setHoveredMessage ( null )
157154 setTooltipPosition ( null )
155+ } , [ ] )
156+
157+ // Calculate the item size (width of block + gap)
158+ const itemWidth = parseInt ( BLOCK_WIDTH . replace ( "px" , "" ) ) + parseInt ( BLOCK_GAP . replace ( "px" , "" ) )
159+
160+ // Size function for Virtuoso
161+ const itemSize = useCallback ( ( ) => itemWidth , [ itemWidth ] )
162+
163+ // Virtuoso requires a reference to scroll to the end
164+ const virtuosoRef = useRef < any > ( null )
165+
166+ // Render a timeline block
167+ const TimelineBlock = useCallback (
168+ ( index : number ) => {
169+ const message = taskTimelinePropsMessages [ index ]
170+ return (
171+ < div
172+ style = { {
173+ width : BLOCK_WIDTH ,
174+ height : "100%" ,
175+ backgroundColor : getBlockColor ( message ) ,
176+ flexShrink : 0 ,
177+ cursor : "pointer" ,
178+ marginRight : BLOCK_GAP ,
179+ } }
180+ onMouseEnter = { ( e ) => handleMouseEnter ( message , e ) }
181+ onMouseLeave = { handleMouseLeave }
182+ />
183+ )
184+ } ,
185+ [ taskTimelinePropsMessages , handleMouseEnter , handleMouseLeave ] ,
186+ )
187+
188+ // Scroll to the end when messages change
189+ useEffect ( ( ) => {
190+ if ( virtuosoRef . current && taskTimelinePropsMessages . length > 0 ) {
191+ virtuosoRef . current . scrollToIndex ( {
192+ index : taskTimelinePropsMessages . length - 1 ,
193+ align : "end" ,
194+ } )
195+ }
196+ } , [ taskTimelinePropsMessages ] )
197+
198+ if ( taskTimelinePropsMessages . length === 0 ) {
199+ return null
158200 }
159201
160202 return (
@@ -167,41 +209,32 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
167209 marginBottom : "4px" ,
168210 overflow : "hidden" ,
169211 } } >
170- < div
171- ref = { scrollableRef }
212+ < style >
213+ { `
214+ /* Hide scrollbar for Chrome, Safari and Opera */
215+ .timeline-virtuoso::-webkit-scrollbar {
216+ display: none;
217+ }
218+ .timeline-virtuoso {
219+ scrollbar-width: none;
220+ -ms-overflow-style: none;
221+ }
222+ ` }
223+ </ style >
224+
225+ < Virtuoso
226+ ref = { virtuosoRef }
227+ className = "timeline-virtuoso"
172228 style = { {
173- display : "flex" ,
174229 height : TIMELINE_HEIGHT ,
175- overflowX : "auto" ,
176- scrollbarWidth : "none" ,
177- msOverflowStyle : "none" ,
178230 width : "100%" ,
179- WebkitOverflowScrolling : "touch" ,
180- gap : BLOCK_GAP , // Using flexbox gap instead of marginRight
181- } } >
182- < style >
183- { `
184- /* Hide scrollbar for Chrome, Safari and Opera */
185- div::-webkit-scrollbar {
186- display: none;
187- }
188- ` }
189- </ style >
190- { taskTimelinePropsMessages . map ( ( message , index ) => (
191- < div
192- key = { index }
193- style = { {
194- width : BLOCK_WIDTH ,
195- height : "100%" ,
196- backgroundColor : getBlockColor ( message ) ,
197- flexShrink : 0 ,
198- cursor : "pointer" ,
199- } }
200- onMouseEnter = { ( e ) => handleMouseEnter ( message , e ) }
201- onMouseLeave = { handleMouseLeave }
202- />
203- ) ) }
204- </ div >
231+ } }
232+ totalCount = { taskTimelinePropsMessages . length }
233+ itemContent = { TimelineBlock }
234+ horizontalDirection = { true }
235+ increaseViewportBy = { 12 }
236+ fixedItemHeight = { itemWidth }
237+ />
205238
206239 { hoveredMessage && containerRef . current && tooltipPosition && (
207240 < div
0 commit comments