@@ -75,7 +75,6 @@ type VcsMode = "git" | "branch"
7575
7676type SessionHistoryWindowInput = {
7777 sessionID : ( ) => string | undefined
78- messagesReady : ( ) => boolean
7978 loaded : ( ) => number
8079 visibleUserMessages : ( ) => UserMessage [ ]
8180 historyMore : ( ) => boolean
@@ -85,205 +84,78 @@ type SessionHistoryWindowInput = {
8584 scroller : ( ) => HTMLDivElement | undefined
8685}
8786
88- /**
89- * Maintains the rendered history window for a session timeline.
90- *
91- * It keeps initial paint bounded to recent turns, reveals cached turns in
92- * small batches while scrolling upward, and prefetches older history near top.
93- */
94- function createSessionHistoryWindow ( input : SessionHistoryWindowInput ) {
95- const turnInit = 10
96- const turnBatch = 8
97- const turnScrollThreshold = 200
98- const turnPrefetchBuffer = 16
99- const prefetchCooldownMs = 400
100- const prefetchNoGrowthLimit = 2
87+ function createSessionHistoryLoader ( input : SessionHistoryWindowInput ) {
88+ const historyScrollThreshold = 200
89+ let shiftFrame : number | undefined
10190
10291 const [ state , setState ] = createStore ( {
103- turnID : undefined as string | undefined ,
104- turnStart : 0 ,
105- prefetchUntil : 0 ,
106- prefetchNoGrowth : 0 ,
92+ shift : false ,
10793 } )
10894
109- const initialTurnStart = ( len : number ) => ( len > turnInit ? len - turnInit : 0 )
110-
111- const turnStart = createMemo ( ( ) => {
112- const id = input . sessionID ( )
113- const len = input . visibleUserMessages ( ) . length
114- if ( ! id || len <= 0 ) return 0
115- if ( state . turnID !== id ) return initialTurnStart ( len )
116- if ( state . turnStart <= 0 ) return 0
117- if ( state . turnStart >= len ) return initialTurnStart ( len )
118- return state . turnStart
119- } )
120-
121- const setTurnStart = ( start : number ) => {
122- const id = input . sessionID ( )
123- const next = start > 0 ? start : 0
124- if ( ! id ) {
125- setState ( { turnID : undefined , turnStart : next } )
126- return
127- }
128- setState ( { turnID : id , turnStart : next } )
129- }
130-
131- const renderedUserMessages = createMemo (
132- ( ) => {
133- const msgs = input . visibleUserMessages ( )
134- const start = turnStart ( )
135- if ( start <= 0 ) return msgs
136- return msgs . slice ( start )
137- } ,
95+ const userMessages = createMemo (
96+ ( ) => input . visibleUserMessages ( ) ,
13897 emptyUserMessages ,
13998 {
14099 equals : same ,
141100 } ,
142101 )
143102
144- const preserveScroll = ( fn : ( ) => void ) => {
145- const el = input . scroller ( )
146- if ( ! el ) {
147- fn ( )
148- return
149- }
150- const beforeTop = el . scrollTop
151- const beforeHeight = el . scrollHeight
152- fn ( )
153- requestAnimationFrame ( ( ) => {
154- const delta = el . scrollHeight - beforeHeight
155- if ( ! delta ) return
156- el . scrollTop = beforeTop + delta
157- } )
158- }
159-
160- const backfillTurns = ( ) => {
161- const start = turnStart ( )
162- if ( start <= 0 ) return
163-
164- const next = start - turnBatch
165- const nextStart = next > 0 ? next : 0
166-
167- preserveScroll ( ( ) => setTurnStart ( nextStart ) )
103+ const cancelShiftReset = ( ) => {
104+ if ( shiftFrame === undefined ) return
105+ cancelAnimationFrame ( shiftFrame )
106+ shiftFrame = undefined
168107 }
169108
170- /** Button path: reveal all cached turns, fetch older history, reveal one batch. */
171- const loadAndReveal = async ( ) => {
172- const id = input . sessionID ( )
173- if ( ! id ) return
174-
175- const start = turnStart ( )
176- const beforeVisible = input . visibleUserMessages ( ) . length
177- let loaded = input . loaded ( )
178-
179- if ( start > 0 ) setTurnStart ( 0 )
180-
181- if ( ! input . historyMore ( ) || input . historyLoading ( ) ) return
182-
183- let afterVisible = beforeVisible
184- let added = 0
185-
186- while ( true ) {
187- await input . loadMore ( id )
188- if ( input . sessionID ( ) !== id ) return
189-
190- afterVisible = input . visibleUserMessages ( ) . length
191- const nextLoaded = input . loaded ( )
192- const raw = nextLoaded - loaded
193- added += raw
194- loaded = nextLoaded
195-
196- if ( afterVisible > beforeVisible ) break
197- if ( raw <= 0 ) break
198- if ( ! input . historyMore ( ) ) break
199- }
200-
201- if ( added <= 0 ) return
202- if ( state . prefetchNoGrowth ) setState ( "prefetchNoGrowth" , 0 )
203-
204- const growth = afterVisible - beforeVisible
205- if ( growth <= 0 ) return
206- if ( turnStart ( ) !== 0 ) return
207-
208- const target = Math . min ( afterVisible , beforeVisible + turnBatch )
209- setTurnStart ( Math . max ( 0 , afterVisible - target ) )
109+ const scheduleShiftReset = ( ) => {
110+ cancelShiftReset ( )
111+ shiftFrame = requestAnimationFrame ( ( ) => {
112+ shiftFrame = undefined
113+ setState ( "shift" , false )
114+ } )
210115 }
211116
212- /** Scroll/prefetch path: fetch older history from server. */
213- const fetchOlderMessages = async ( opts ?: { prefetch ?: boolean } ) => {
117+ const fetchOlderMessages = async ( ) => {
214118 const id = input . sessionID ( )
215119 if ( ! id ) return
216120 if ( ! input . historyMore ( ) || input . historyLoading ( ) ) return
217121
218- if ( opts ?. prefetch ) {
219- const now = Date . now ( )
220- if ( state . prefetchUntil > now ) return
221- if ( state . prefetchNoGrowth >= prefetchNoGrowthLimit ) return
222- setState ( "prefetchUntil" , now + prefetchCooldownMs )
223- }
224-
225- const start = turnStart ( )
122+ // TODO(session-timeline): switch this to core cursor-based part pagination when that API lands.
226123 const beforeVisible = input . visibleUserMessages ( ) . length
227- const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages ( ) . length
228124 let loaded = input . loaded ( )
229- let added = 0
230125 let growth = 0
231126
127+ cancelShiftReset ( )
128+ setState ( "shift" , true )
129+
232130 while ( true ) {
233131 await input . loadMore ( id )
234132 if ( input . sessionID ( ) !== id ) return
235133
236134 const nextLoaded = input . loaded ( )
237135 const raw = nextLoaded - loaded
238- added += raw
239136 loaded = nextLoaded
240137 growth = input . visibleUserMessages ( ) . length - beforeVisible
241138
242139 if ( growth > 0 ) break
243140 if ( raw <= 0 ) break
244- if ( opts ?. prefetch ) break
245141 if ( ! input . historyMore ( ) ) break
246142 }
247143
248- const afterVisible = input . visibleUserMessages ( ) . length
249-
250- if ( opts ?. prefetch ) {
251- setState ( "prefetchNoGrowth" , added > 0 ? 0 : state . prefetchNoGrowth + 1 )
252- } else if ( added > 0 && state . prefetchNoGrowth ) {
253- setState ( "prefetchNoGrowth" , 0 )
254- }
255-
256- if ( added <= 0 ) return
257- if ( growth <= 0 ) return
258-
259- if ( opts ?. prefetch ) {
260- const current = turnStart ( )
261- preserveScroll ( ( ) => setTurnStart ( current + growth ) )
144+ if ( growth > 0 ) {
145+ scheduleShiftReset ( )
262146 return
263147 }
264148
265- if ( turnStart ( ) !== start ) return
266-
267- const currentRendered = renderedUserMessages ( ) . length
268- const base = Math . max ( beforeRendered , currentRendered )
269- const target = Math . min ( afterVisible , base + turnBatch )
270- preserveScroll ( ( ) => setTurnStart ( Math . max ( 0 , afterVisible - target ) ) )
149+ setState ( "shift" , false )
271150 }
272151
152+ const loadAndReveal = ( ) => fetchOlderMessages ( )
153+
273154 const onScrollerScroll = ( ) => {
274155 if ( ! input . userScrolled ( ) ) return
275156 const el = input . scroller ( )
276157 if ( ! el ) return
277- if ( el . scrollTop >= turnScrollThreshold ) return
278-
279- const start = turnStart ( )
280- if ( start > 0 ) {
281- if ( start <= turnPrefetchBuffer ) {
282- void fetchOlderMessages ( { prefetch : true } )
283- }
284- backfillTurns ( )
285- return
286- }
158+ if ( el . scrollTop >= historyScrollThreshold ) return
287159
288160 void fetchOlderMessages ( )
289161 }
@@ -292,27 +164,18 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
292164 on (
293165 input . sessionID ,
294166 ( ) => {
295- setState ( { prefetchUntil : 0 , prefetchNoGrowth : 0 } )
167+ cancelShiftReset ( )
168+ setState ( { shift : false } )
296169 } ,
297170 { defer : true } ,
298171 ) ,
299172 )
300173
301- createEffect (
302- on (
303- ( ) => [ input . sessionID ( ) , input . messagesReady ( ) ] as const ,
304- ( [ id , ready ] ) => {
305- if ( ! id || ! ready ) return
306- setTurnStart ( initialTurnStart ( input . visibleUserMessages ( ) . length ) )
307- } ,
308- { defer : true } ,
309- ) ,
310- )
174+ onCleanup ( cancelShiftReset )
311175
312176 return {
313- turnStart,
314- setTurnStart,
315- renderedUserMessages,
177+ userMessages,
178+ shift : ( ) => state . shift ,
316179 loadAndReveal,
317180 onScrollerScroll,
318181 }
@@ -737,6 +600,7 @@ export default function Page() {
737600 let dockHeight = 0
738601 let scroller : HTMLDivElement | undefined
739602 let content : HTMLDivElement | undefined
603+ let revealMessage = ( _id : string ) => { }
740604 let scrollMark = 0
741605 let messageMark = 0
742606
@@ -1403,9 +1267,8 @@ export default function Page() {
14031267 } ,
14041268 )
14051269
1406- const historyWindow = createSessionHistoryWindow ( {
1270+ const historyLoader = createSessionHistoryLoader ( {
14071271 sessionID : ( ) => params . id ,
1408- messagesReady,
14091272 loaded : ( ) => messages ( ) . length ,
14101273 visibleUserMessages,
14111274 historyMore,
@@ -1427,9 +1290,9 @@ export default function Page() {
14271290 const el = scroller
14281291 if ( ! el ) return
14291292 if ( el . scrollHeight > el . clientHeight + 1 ) return
1430- if ( historyWindow . turnStart ( ) <= 0 && ! historyMore ( ) ) return
1293+ if ( ! historyMore ( ) ) return
14311294
1432- void historyWindow . loadAndReveal ( )
1295+ void historyLoader . loadAndReveal ( )
14331296 } )
14341297 }
14351298
@@ -1439,15 +1302,14 @@ export default function Page() {
14391302 [
14401303 params . id ,
14411304 messagesReady ( ) ,
1442- historyWindow . turnStart ( ) ,
14431305 historyMore ( ) ,
14441306 historyLoading ( ) ,
14451307 autoScroll . userScrolled ( ) ,
14461308 visibleUserMessages ( ) . length ,
14471309 ] as const ,
1448- ( [ id , ready , start , more , loading , scrolled ] ) => {
1310+ ( [ id , ready , more , loading , scrolled ] ) => {
14491311 if ( ! id || ! ready || loading || scrolled ) return
1450- if ( start <= 0 && ! more ) return
1312+ if ( ! more ) return
14511313 fill ( )
14521314 } ,
14531315 { defer : true } ,
@@ -1754,15 +1616,14 @@ export default function Page() {
17541616 historyMore,
17551617 historyLoading,
17561618 loadMore : ( sessionID ) => sync . session . history . loadMore ( sessionID ) ,
1757- turnStart : historyWindow . turnStart ,
17581619 currentMessageId : ( ) => store . messageId ,
17591620 pendingMessage : ( ) => ui . pendingMessage ,
17601621 setPendingMessage : ( value ) => setUi ( "pendingMessage" , value ) ,
17611622 setActiveMessage,
1762- setTurnStart : historyWindow . setTurnStart ,
17631623 autoScroll,
17641624 scroller : ( ) => scroller ,
17651625 anchor,
1626+ revealMessage : ( id ) => revealMessage ( id ) ,
17661627 scheduleScrollState,
17671628 consumePendingMessage : layout . pendingMessage . consume ,
17681629 } )
@@ -1858,7 +1719,7 @@ export default function Page() {
18581719 onMarkScrollGesture = { markScrollGesture }
18591720 hasScrollGesture = { hasScrollGesture }
18601721 onUserScroll = { markUserScroll }
1861- onTurnBackfillScroll = { historyWindow . onScrollerScroll }
1722+ onHistoryScroll = { historyLoader . onScrollerScroll }
18621723 onAutoScrollInteraction = { autoScroll . handleInteraction }
18631724 centered = { centered ( ) }
18641725 setContentRef = { ( el ) => {
@@ -1868,14 +1729,12 @@ export default function Page() {
18681729 const root = scroller
18691730 if ( root ) scheduleScrollState ( root )
18701731 } }
1871- turnStart = { historyWindow . turnStart ( ) }
1872- historyMore = { historyMore ( ) }
1873- historyLoading = { historyLoading ( ) }
1874- onLoadEarlier = { ( ) => {
1875- void historyWindow . loadAndReveal ( )
1876- } }
1877- renderedUserMessages = { historyWindow . renderedUserMessages ( ) }
1732+ historyShift = { historyLoader . shift ( ) }
1733+ userMessages = { historyLoader . userMessages ( ) }
18781734 anchor = { anchor }
1735+ setRevealMessage = { ( fn ) => {
1736+ revealMessage = fn
1737+ } }
18791738 />
18801739 </ Show >
18811740 </ Match >
0 commit comments