@@ -41,216 +41,12 @@ import { createScrollSpy } from "@/pages/session/scroll-spy"
4141import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
4242import { SessionSidePanel } from "@/pages/session/session-side-panel"
4343import { TerminalPanel } from "@/pages/session/terminal-panel"
44+ import { createSessionHistoryWindow , emptyUserMessages } from "@/pages/session/history-window"
4445import { useSessionCommands } from "@/pages/session/use-session-commands"
4546import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
4647import { same } from "@/utils/same"
4748import { formatServerError } from "@/utils/server-errors"
4849
49- const emptyUserMessages : UserMessage [ ] = [ ]
50-
51- type SessionHistoryWindowInput = {
52- sessionID : ( ) => string | undefined
53- messagesReady : ( ) => boolean
54- visibleUserMessages : ( ) => UserMessage [ ]
55- historyMore : ( ) => boolean
56- historyLoading : ( ) => boolean
57- loadMore : ( sessionID : string ) => Promise < void >
58- userScrolled : ( ) => boolean
59- scroller : ( ) => HTMLDivElement | undefined
60- }
61-
62- /**
63- * Maintains the rendered history window for a session timeline.
64- *
65- * It keeps initial paint bounded to recent turns, reveals cached turns in
66- * small batches while scrolling upward, and prefetches older history near top.
67- */
68- function createSessionHistoryWindow ( input : SessionHistoryWindowInput ) {
69- const turnInit = 10
70- const turnBatch = 8
71- const turnScrollThreshold = 200
72- const turnPrefetchBuffer = 16
73- const prefetchCooldownMs = 400
74- const prefetchNoGrowthLimit = 2
75-
76- const [ state , setState ] = createStore ( {
77- turnID : undefined as string | undefined ,
78- turnStart : 0 ,
79- prefetchUntil : 0 ,
80- prefetchNoGrowth : 0 ,
81- } )
82-
83- const initialTurnStart = ( len : number ) => ( len > turnInit ? len - turnInit : 0 )
84-
85- const turnStart = createMemo ( ( ) => {
86- const id = input . sessionID ( )
87- const len = input . visibleUserMessages ( ) . length
88- if ( ! id || len <= 0 ) return 0
89- if ( state . turnID !== id ) return initialTurnStart ( len )
90- if ( state . turnStart <= 0 ) return 0
91- if ( state . turnStart >= len ) return initialTurnStart ( len )
92- return state . turnStart
93- } )
94-
95- const setTurnStart = ( start : number ) => {
96- const id = input . sessionID ( )
97- const next = start > 0 ? start : 0
98- if ( ! id ) {
99- setState ( { turnID : undefined , turnStart : next } )
100- return
101- }
102- setState ( { turnID : id , turnStart : next } )
103- }
104-
105- const renderedUserMessages = createMemo (
106- ( ) => {
107- const msgs = input . visibleUserMessages ( )
108- const start = turnStart ( )
109- if ( start <= 0 ) return msgs
110- return msgs . slice ( start )
111- } ,
112- emptyUserMessages ,
113- {
114- equals : same ,
115- } ,
116- )
117-
118- const preserveScroll = ( fn : ( ) => void ) => {
119- const el = input . scroller ( )
120- if ( ! el ) {
121- fn ( )
122- return
123- }
124- const beforeTop = el . scrollTop
125- fn ( )
126- void el . scrollHeight
127- el . scrollTop = beforeTop
128- }
129-
130- const backfillTurns = ( ) => {
131- const start = turnStart ( )
132- if ( start <= 0 ) return
133-
134- const next = start - turnBatch
135- const nextStart = next > 0 ? next : 0
136-
137- preserveScroll ( ( ) => setTurnStart ( nextStart ) )
138- }
139-
140- /** Button path: reveal all cached turns, fetch older history, reveal one batch. */
141- const loadAndReveal = async ( ) => {
142- const id = input . sessionID ( )
143- if ( ! id ) return
144-
145- const start = turnStart ( )
146- const beforeVisible = input . visibleUserMessages ( ) . length
147-
148- if ( start > 0 ) setTurnStart ( 0 )
149-
150- if ( ! input . historyMore ( ) || input . historyLoading ( ) ) return
151-
152- await input . loadMore ( id )
153- if ( input . sessionID ( ) !== id ) return
154-
155- const afterVisible = input . visibleUserMessages ( ) . length
156- const growth = afterVisible - beforeVisible
157- if ( state . prefetchNoGrowth ) setState ( "prefetchNoGrowth" , 0 )
158- if ( growth <= 0 ) return
159- if ( turnStart ( ) !== 0 ) return
160-
161- const target = Math . min ( afterVisible , Math . max ( beforeVisible , renderedUserMessages ( ) . length ) + turnBatch )
162- const nextStart = Math . max ( 0 , afterVisible - target )
163- preserveScroll ( ( ) => setTurnStart ( nextStart ) )
164- }
165-
166- /** Scroll/prefetch path: fetch older history from server. */
167- const fetchOlderMessages = async ( opts ?: { prefetch ?: boolean } ) => {
168- const id = input . sessionID ( )
169- if ( ! id ) return
170- if ( ! input . historyMore ( ) || input . historyLoading ( ) ) return
171-
172- if ( opts ?. prefetch ) {
173- const now = Date . now ( )
174- if ( state . prefetchUntil > now ) return
175- if ( state . prefetchNoGrowth >= prefetchNoGrowthLimit ) return
176- setState ( "prefetchUntil" , now + prefetchCooldownMs )
177- }
178-
179- const start = turnStart ( )
180- const beforeVisible = input . visibleUserMessages ( ) . length
181- const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages ( ) . length
182-
183- await input . loadMore ( id )
184- if ( input . sessionID ( ) !== id ) return
185-
186- const afterVisible = input . visibleUserMessages ( ) . length
187- const growth = afterVisible - beforeVisible
188-
189- if ( opts ?. prefetch ) {
190- setState ( "prefetchNoGrowth" , growth > 0 ? 0 : state . prefetchNoGrowth + 1 )
191- } else if ( growth > 0 && state . prefetchNoGrowth ) {
192- setState ( "prefetchNoGrowth" , 0 )
193- }
194-
195- if ( growth <= 0 ) return
196- if ( turnStart ( ) !== start ) return
197-
198- const reveal = ! opts ?. prefetch
199- const currentRendered = renderedUserMessages ( ) . length
200- const base = Math . max ( beforeRendered , currentRendered )
201- const target = reveal ? Math . min ( afterVisible , base + turnBatch ) : base
202- const nextStart = Math . max ( 0 , afterVisible - target )
203- preserveScroll ( ( ) => setTurnStart ( nextStart ) )
204- }
205-
206- const onScrollerScroll = ( ) => {
207- if ( ! input . userScrolled ( ) ) return
208- const el = input . scroller ( )
209- if ( ! el ) return
210- if ( el . scrollHeight - el . clientHeight + el . scrollTop >= turnScrollThreshold ) return
211-
212- const start = turnStart ( )
213- if ( start > 0 ) {
214- if ( start <= turnPrefetchBuffer ) {
215- void fetchOlderMessages ( { prefetch : true } )
216- }
217- backfillTurns ( )
218- return
219- }
220-
221- void fetchOlderMessages ( )
222- }
223-
224- createEffect (
225- on (
226- input . sessionID ,
227- ( ) => {
228- setState ( { prefetchUntil : 0 , prefetchNoGrowth : 0 } )
229- } ,
230- { defer : true } ,
231- ) ,
232- )
233-
234- createEffect (
235- on (
236- ( ) => [ input . sessionID ( ) , input . messagesReady ( ) ] as const ,
237- ( [ id , ready ] ) => {
238- if ( ! id || ! ready ) return
239- setTurnStart ( initialTurnStart ( input . visibleUserMessages ( ) . length ) )
240- } ,
241- { defer : true } ,
242- ) ,
243- )
244-
245- return {
246- turnStart,
247- setTurnStart,
248- renderedUserMessages,
249- loadAndReveal,
250- onScrollerScroll,
251- }
252- }
253-
25450export default function Page ( ) {
25551 const globalSync = useGlobalSync ( )
25652 const layout = useLayout ( )
@@ -1090,6 +886,7 @@ export default function Page() {
1090886
1091887 let scrollStateFrame : number | undefined
1092888 let scrollStateTarget : HTMLDivElement | undefined
889+ let historyFillFrame : number | undefined
1093890 const scrollSpy = createScrollSpy ( {
1094891 onActive : ( id ) => {
1095892 if ( id === store . messageId ) return
@@ -1159,7 +956,9 @@ export default function Page() {
1159956 scroller = el
1160957 autoScroll . scrollRef ( el )
1161958 scrollSpy . setContainer ( el )
1162- if ( el ) scheduleScrollState ( el )
959+ if ( ! el ) return
960+ scheduleScrollState ( el )
961+ scheduleHistoryFill ( )
1163962 }
1164963
1165964 createResizeObserver (
@@ -1168,6 +967,7 @@ export default function Page() {
1168967 const el = scroller
1169968 if ( el ) scheduleScrollState ( el )
1170969 scrollSpy . markDirty ( )
970+ scheduleHistoryFill ( )
1171971 } ,
1172972 )
1173973
@@ -1182,6 +982,45 @@ export default function Page() {
1182982 scroller : ( ) => scroller ,
1183983 } )
1184984
985+ const scheduleHistoryFill = ( ) => {
986+ if ( historyFillFrame !== undefined ) return
987+
988+ historyFillFrame = requestAnimationFrame ( ( ) => {
989+ historyFillFrame = undefined
990+
991+ if ( ! params . id || ! messagesReady ( ) ) return
992+ if ( autoScroll . userScrolled ( ) || historyLoading ( ) ) return
993+
994+ const el = scroller
995+ if ( ! el ) return
996+ if ( el . scrollHeight > el . clientHeight + 1 ) return
997+ if ( historyWindow . turnStart ( ) <= 0 && ! historyMore ( ) ) return
998+
999+ void historyWindow . loadAndReveal ( )
1000+ } )
1001+ }
1002+
1003+ createEffect (
1004+ on (
1005+ ( ) =>
1006+ [
1007+ params . id ,
1008+ messagesReady ( ) ,
1009+ historyWindow . turnStart ( ) ,
1010+ historyMore ( ) ,
1011+ historyLoading ( ) ,
1012+ autoScroll . userScrolled ( ) ,
1013+ visibleUserMessages ( ) . length ,
1014+ ] as const ,
1015+ ( [ id , ready , start , more , loading , scrolled ] ) => {
1016+ if ( ! id || ! ready || loading || scrolled ) return
1017+ if ( start <= 0 && ! more ) return
1018+ scheduleHistoryFill ( )
1019+ } ,
1020+ { defer : true } ,
1021+ ) ,
1022+ )
1023+
11851024 createResizeObserver (
11861025 ( ) => promptDock ,
11871026 ( { height } ) => {
@@ -1199,6 +1038,7 @@ export default function Page() {
11991038
12001039 if ( el ) scheduleScrollState ( el )
12011040 scrollSpy . markDirty ( )
1041+ scheduleHistoryFill ( )
12021042 } ,
12031043 )
12041044
@@ -1228,6 +1068,7 @@ export default function Page() {
12281068 document . removeEventListener ( "keydown" , handleKeyDown )
12291069 scrollSpy . destroy ( )
12301070 if ( scrollStateFrame !== undefined ) cancelAnimationFrame ( scrollStateFrame )
1071+ if ( historyFillFrame !== undefined ) cancelAnimationFrame ( historyFillFrame )
12311072 } )
12321073
12331074 return (
0 commit comments