@@ -2,7 +2,7 @@ import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@v
22import { useExtensionState } from "../../context/ExtensionStateContext"
33import { vscode } from "../../utils/vscode"
44import { Virtuoso } from "react-virtuoso"
5- import { memo , useMemo , useState , useEffect } from "react"
5+ import React , { memo , useMemo , useState , useEffect } from "react"
66import Fuse , { FuseResult } from "fuse.js"
77import { formatLargeNumber } from "../../utils/format"
88
@@ -82,30 +82,28 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
8282 const taskHistorySearchResults = useMemo ( ( ) => {
8383 let results = searchQuery ? highlight ( fuse . search ( searchQuery ) ) : presentableTasks
8484
85- results . sort ( ( a , b ) => {
85+ // First apply search if needed
86+ const searchResults = searchQuery ? results : presentableTasks ;
87+
88+ // Then sort the results
89+ return [ ...searchResults ] . sort ( ( a , b ) => {
8690 switch ( sortOption ) {
8791 case "oldest" :
88- return a . ts - b . ts
92+ return ( a . ts || 0 ) - ( b . ts || 0 ) ;
8993 case "mostExpensive" :
90- return ( b . totalCost || 0 ) - ( a . totalCost || 0 )
94+ return ( b . totalCost || 0 ) - ( a . totalCost || 0 ) ;
9195 case "mostTokens" :
92- return (
93- ( b . tokensIn || 0 ) +
94- ( b . tokensOut || 0 ) +
95- ( b . cacheWrites || 0 ) +
96- ( b . cacheReads || 0 ) -
97- ( ( a . tokensIn || 0 ) + ( a . tokensOut || 0 ) + ( a . cacheWrites || 0 ) + ( a . cacheReads || 0 ) )
98- )
96+ const aTokens = ( a . tokensIn || 0 ) + ( a . tokensOut || 0 ) + ( a . cacheWrites || 0 ) + ( a . cacheReads || 0 ) ;
97+ const bTokens = ( b . tokensIn || 0 ) + ( b . tokensOut || 0 ) + ( b . cacheWrites || 0 ) + ( b . cacheReads || 0 ) ;
98+ return bTokens - aTokens ;
9999 case "mostRelevant" :
100- // NOTE: you must never sort directly on object since it will cause members to be reordered
101- return searchQuery ? 0 : b . ts - a . ts // Keep fuse order if searching, otherwise sort by newest
100+ // Keep fuse order if searching, otherwise sort by newest
101+ return searchQuery ? 0 : ( b . ts || 0 ) - ( a . ts || 0 ) ;
102102 case "newest" :
103103 default :
104- return b . ts - a . ts
104+ return ( b . ts || 0 ) - ( a . ts || 0 ) ;
105105 }
106- } )
107-
108- return results
106+ } ) ;
109107 } , [ presentableTasks , searchQuery , fuse , sortOption ] )
110108
111109 return (
@@ -227,9 +225,16 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
227225 overflowY : "scroll" ,
228226 } }
229227 data = { taskHistorySearchResults }
228+ data-testid = "virtuoso-container"
229+ components = { {
230+ List : React . forwardRef ( ( props , ref ) => (
231+ < div { ...props } ref = { ref } data-testid = "virtuoso-item-list" />
232+ ) )
233+ } }
230234 itemContent = { ( index , item ) => (
231235 < div
232236 key = { item . id }
237+ data-testid = { `task-item-${ item . id } ` }
233238 className = "history-item"
234239 style = { {
235240 cursor : "pointer" ,
@@ -263,23 +268,23 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
263268 { formatDate ( item . ts ) }
264269 </ span >
265270 < div style = { { display : "flex" , gap : "4px" } } >
266- < VSCodeButton
267- appearance = "icon "
268- title = "Copy Prompt "
269- className = "copy-button "
270- onClick = { ( e ) => handleCopyTask ( e , item . task ) } >
271- < span className = "codicon codicon-copy" > </ span >
272- </ VSCodeButton >
273- < VSCodeButton
274- appearance = "icon "
275- title = "Delete Task "
276- onClick = { ( e ) => {
277- e . stopPropagation ( )
278- handleDeleteHistoryItem ( item . id )
279- } }
280- className = "delete-button" >
281- < span className = "codicon codicon-trash" > </ span >
282- </ VSCodeButton >
271+ < button
272+ title = "Copy Prompt "
273+ className = "copy-button "
274+ data-appearance = "icon "
275+ onClick = { ( e ) => handleCopyTask ( e , item . task ) } >
276+ < span className = "codicon codicon-copy" > </ span >
277+ </ button >
278+ < button
279+ title = "Delete Task "
280+ className = "delete-button "
281+ data-appearance = "icon"
282+ onClick = { ( e ) => {
283+ e . stopPropagation ( )
284+ handleDeleteHistoryItem ( item . id )
285+ } } >
286+ < span className = "codicon codicon-trash" > </ span >
287+ </ button >
283288 </ div >
284289 </ div >
285290 < div
@@ -298,6 +303,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
298303 />
299304 < div style = { { display : "flex" , flexDirection : "column" , gap : "4px" } } >
300305 < div
306+ data-testid = "tokens-container"
301307 style = { {
302308 display : "flex" ,
303309 justifyContent : "space-between" ,
@@ -318,6 +324,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
318324 Tokens:
319325 </ span >
320326 < span
327+ data-testid = "tokens-in"
321328 style = { {
322329 display : "flex" ,
323330 alignItems : "center" ,
@@ -335,6 +342,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
335342 { formatLargeNumber ( item . tokensIn || 0 ) }
336343 </ span >
337344 < span
345+ data-testid = "tokens-out"
338346 style = { {
339347 display : "flex" ,
340348 alignItems : "center" ,
@@ -357,6 +365,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
357365
358366 { ! ! item . cacheWrites && (
359367 < div
368+ data-testid = "cache-container"
360369 style = { {
361370 display : "flex" ,
362371 alignItems : "center" ,
@@ -371,6 +380,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
371380 Cache:
372381 </ span >
373382 < span
383+ data-testid = "cache-writes"
374384 style = { {
375385 display : "flex" ,
376386 alignItems : "center" ,
@@ -388,6 +398,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
388398 +{ formatLargeNumber ( item . cacheWrites || 0 ) }
389399 </ span >
390400 < span
401+ data-testid = "cache-reads"
391402 style = { {
392403 display : "flex" ,
393404 alignItems : "center" ,
@@ -499,31 +510,48 @@ export const highlight = (
499510 if ( regions . length === 0 ) {
500511 return inputText
501512 }
502-
513+
503514 // Sort and merge overlapping regions
504515 const mergedRegions = mergeRegions ( regions )
505-
506- let content = ""
507- let nextUnhighlightedRegionStartingIndex = 0
508-
509- mergedRegions . forEach ( ( region ) => {
510- const start = region [ 0 ]
511- const end = region [ 1 ]
512- const lastRegionNextIndex = end + 1
513-
514- content += [
515- inputText . substring ( nextUnhighlightedRegionStartingIndex , start ) ,
516- `<span class="${ highlightClassName } ">` ,
517- inputText . substring ( start , lastRegionNextIndex ) ,
518- "</span>" ,
519- ] . join ( "" )
520-
521- nextUnhighlightedRegionStartingIndex = lastRegionNextIndex
516+
517+ // Convert regions to a list of parts with their highlight status
518+ const parts : { text : string ; highlight : boolean } [ ] = [ ]
519+ let lastIndex = 0
520+
521+ mergedRegions . forEach ( ( [ start , end ] ) => {
522+ // Add non-highlighted text before this region
523+ if ( start > lastIndex ) {
524+ parts . push ( {
525+ text : inputText . substring ( lastIndex , start ) ,
526+ highlight : false
527+ } )
528+ }
529+
530+ // Add highlighted text
531+ parts . push ( {
532+ text : inputText . substring ( start , end + 1 ) ,
533+ highlight : true
534+ } )
535+
536+ lastIndex = end + 1
522537 } )
523-
524- content += inputText . substring ( nextUnhighlightedRegionStartingIndex )
525-
526- return content
538+
539+ // Add any remaining text
540+ if ( lastIndex < inputText . length ) {
541+ parts . push ( {
542+ text : inputText . substring ( lastIndex ) ,
543+ highlight : false
544+ } )
545+ }
546+
547+ // Build final string
548+ return parts
549+ . map ( part =>
550+ part . highlight
551+ ? `<span class="${ highlightClassName } ">${ part . text } </span>`
552+ : part . text
553+ )
554+ . join ( '' )
527555 }
528556
529557 return fuseSearchResult
0 commit comments