@@ -17,12 +17,14 @@ import { mockOpenCodeServer } from "../utils/mock-server"
1717import { installSseTransport } from "../utils/sse-transport"
1818import { expectSessionTitle } from "../utils/waits"
1919
20- const assistants = Array . from ( { length : 14 } , ( _ , index ) =>
20+ const initialPageSize = 20
21+ const historyPageSize = 200
22+ const assistants = Array . from ( { length : initialPageSize + 1 } , ( _ , index ) =>
2123 assistantMessage ( [ textPart ( `prt_history_root_${ index } ` , `Assistant response ${ index } ` ) ] , {
2224 id : `msg_${ String ( index + 1001 ) . padStart ( 4 , "0" ) } _history_root_assistant` ,
2325 parentID : userID ,
2426 created : 1700000001000 + index * 1_000 ,
25- completed : index < 13 ,
27+ completed : index < initialPageSize ,
2628 } ) ,
2729)
2830const messages = [ userMessage ( ) , ...assistants ]
@@ -46,7 +48,7 @@ const scenarios = [
4648test . use ( { viewport : { width : 646 , height : 1385 } } )
4749
4850for ( const scenario of scenarios ) {
49- test ( `keeps the latest user turn visible through ${ scenario . name } ` , async ( { page } ) => {
51+ test ( `keeps visible timeline content visible through ${ scenario . name } ` , async ( { page } ) => {
5052 const requests : { before ?: string ; phase : "start" | "end" } [ ] = [ ]
5153 const pages : { before ?: string ; limit : number } [ ] = [ ]
5254 const roots : { sessionID : string ; messageID : string } [ ] = [ ]
@@ -101,36 +103,60 @@ for (const scenario of scenarios) {
101103 }
102104 } ,
103105 } )
104- await page . addInitScript (
105- ( { userPartID, lastPartID } ) => {
106- const state = { armed : false , hidden : false , samples : 0 , stop : false }
107- ; ( window as Window & { __historyRootProbe ?: typeof state } ) . __historyRootProbe = state
108- const sample = ( ) => {
109- if ( state . armed ) {
110- const virtual = document . querySelector < HTMLElement > ( "[data-timeline-virtual-content]" )
111- const viewport = virtual ?. closest < HTMLElement > ( ".scroll-view__viewport" )
112- const view = viewport ?. getBoundingClientRect ( )
113- const visible = ( partID : string ) => {
114- const part = viewport ?. querySelector < HTMLElement > ( `[data-timeline-part-id="${ partID } "]` )
115- const rect = part ?. getBoundingClientRect ( )
116- return (
117- ! ! rect &&
118- ! ! view &&
119- rect . width > 0 &&
120- rect . height > 0 &&
121- rect . bottom > view . top &&
122- rect . top < view . bottom
123- )
124- }
125- if ( ! virtual || ! visible ( userPartID ) || ! visible ( lastPartID ) ) state . hidden = true
126- state . samples ++
106+ await page . addInitScript ( ( ) => {
107+ const visibleParts = ( ) => {
108+ const virtual = document . querySelector < HTMLElement > ( "[data-timeline-virtual-content]" )
109+ const viewport = virtual ?. closest < HTMLElement > ( ".scroll-view__viewport" )
110+ const view = viewport ?. getBoundingClientRect ( )
111+ if ( ! viewport || ! view ) return [ ]
112+ return [ ...viewport . querySelectorAll < HTMLElement > ( "[data-timeline-part-id]" ) ]
113+ . filter ( ( part ) => {
114+ const rect = part . getBoundingClientRect ( )
115+ return rect . width > 0 && rect . height > 0 && rect . bottom > view . top && rect . top < view . bottom
116+ } )
117+ . flatMap ( ( part ) => ( part . dataset . timelinePartId ? [ part . dataset . timelinePartId ] : [ ] ) )
118+ }
119+ const state = {
120+ armed : false ,
121+ hidden : false ,
122+ visibleParts : [ ] as string [ ] ,
123+ samples : 0 ,
124+ stop : false ,
125+ arm ( ) {
126+ state . visibleParts = visibleParts ( )
127+ state . armed = true
128+ } ,
129+ }
130+ ; ( window as Window & { __historyRootProbe ?: typeof state } ) . __historyRootProbe = state
131+ const sample = ( ) => {
132+ if ( state . armed ) {
133+ const virtual = document . querySelector < HTMLElement > ( "[data-timeline-virtual-content]" )
134+ const viewport = virtual ?. closest < HTMLElement > ( ".scroll-view__viewport" )
135+ const view = viewport ?. getBoundingClientRect ( )
136+ const visible = ( partID : string ) => {
137+ const part = viewport ?. querySelector < HTMLElement > ( `[data-timeline-part-id="${ CSS . escape ( partID ) } "]` )
138+ const rect = part ?. getBoundingClientRect ( )
139+ return (
140+ ! ! rect &&
141+ ! ! view &&
142+ rect . width > 0 &&
143+ rect . height > 0 &&
144+ rect . bottom > view . top &&
145+ rect . top < view . bottom
146+ )
127147 }
128- if ( ! state . stop ) requestAnimationFrame ( ( ) => setTimeout ( sample , 0 ) )
148+ if (
149+ ! virtual ||
150+ state . visibleParts . length === 0 ||
151+ state . visibleParts . some ( ( partID ) => ! visible ( partID ) )
152+ )
153+ state . hidden = true
154+ state . samples ++
129155 }
130- requestAnimationFrame ( ( ) => setTimeout ( sample , 0 ) )
131- } ,
132- { userPartID , lastPartID } ,
133- )
156+ if ( ! state . stop ) requestAnimationFrame ( ( ) => setTimeout ( sample , 0 ) )
157+ }
158+ requestAnimationFrame ( ( ) => setTimeout ( sample , 0 ) )
159+ } )
134160
135161 await page . goto ( `/${ base64Encode ( directory ) } /session/${ sessionID } ` )
136162 await transport . waitForConnection ( )
@@ -143,23 +169,28 @@ for (const scenario of scenarios) {
143169 "messages:start:latest" ,
144170 "messages:end:latest" ,
145171 `message:${ userID } ` ,
146- `messages:start:${ messages . at ( - 2 ) ! . info . id } ` ,
172+ `messages:start:${ messages . at ( - initialPageSize ) ! . info . id } ` ,
147173 ] )
174+ await expect ( page . locator ( '[data-timeline-part-id^="prt_history_root_"]' ) ) . toHaveCount ( initialPageSize )
148175 await page . evaluate ( ( ) => {
149176 ; (
150177 window as Window & {
151- __historyRootProbe ?: { armed : boolean }
178+ __historyRootProbe ?: { arm ( ) : void }
152179 }
153- ) . __historyRootProbe ! . armed = true
180+ ) . __historyRootProbe ! . arm ( )
154181 } )
155182 await waitForProbeSamples ( page , 0 )
156- expect ( await historyRootHidden ( page ) ) . toBe ( false )
183+ expect ( await visibleContentHidden ( page ) ) . toBe ( false )
157184 const beforeHistory = await probeSamples ( page )
158185 history . resolve ( )
159- await expect ( page . locator ( '[data-timeline-part-id^="prt_history_root_"]' ) ) . toHaveCount ( 14 )
186+ await expect ( page . locator ( '[data-timeline-part-id^="prt_history_root_"]' ) ) . toHaveCount ( assistants . length )
187+ await expect . poll ( ( ) => requests . filter ( ( request ) => request . phase === "end" ) . length ) . toBe ( 2 )
160188 await expect ( page . getByRole ( "button" , { name : "Stop" } ) ) . toBeVisible ( )
161189 await waitForProbeSamples ( page , beforeHistory )
162- expect ( pages [ 0 ] ) . toEqual ( { before : undefined , limit : 2 } )
190+ expect ( pages ) . toEqual ( [
191+ { before : undefined , limit : initialPageSize } ,
192+ { before : messages . at ( - initialPageSize ) ! . info . id , limit : historyPageSize } ,
193+ ] )
163194 expect ( roots ) . toEqual ( [ { sessionID, messageID : userID } ] )
164195
165196 const message = messageUpdated ( scenario . info )
@@ -213,7 +244,7 @@ async function waitForProbeSamples(page: Page, after: number) {
213244 )
214245}
215246
216- function historyRootHidden ( page : Page ) {
247+ function visibleContentHidden ( page : Page ) {
217248 return page . evaluate (
218249 ( ) => ( window as Window & { __historyRootProbe ?: { hidden : boolean } } ) . __historyRootProbe ! . hidden ,
219250 )
0 commit comments