@@ -113,6 +113,14 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
113113 const kv = useKV ( )
114114
115115 const fullSyncedSessions = new Set < string > ( )
116+ const syncingSessions = new Map < string , Promise < void > > ( )
117+ const hydratingSessions = new Map < string , { messages : Set < string > ; parts : Set < string > } > ( )
118+ const touchMessage = ( sessionID : string , messageID : string ) => {
119+ hydratingSessions . get ( sessionID ) ?. messages . add ( messageID )
120+ }
121+ const touchPart = ( sessionID : string , partID : string ) => {
122+ hydratingSessions . get ( sessionID ) ?. parts . add ( partID )
123+ }
116124
117125 function sessionListQuery ( ) : { scope ?: "project" ; path ?: string } {
118126 if ( ! kv . get ( "session_directory_filter_enabled" , true ) ) return { scope : "project" }
@@ -251,6 +259,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
251259 }
252260
253261 case "message.updated" : {
262+ touchMessage ( event . properties . info . sessionID , event . properties . info . id )
254263 const messages = store . message [ event . properties . info . sessionID ]
255264 if ( ! messages ) {
256265 setStore ( "message" , event . properties . info . sessionID , [ event . properties . info ] )
@@ -290,6 +299,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
290299 break
291300 }
292301 case "message.removed" : {
302+ touchMessage ( event . properties . sessionID , event . properties . messageID )
293303 const messages = store . message [ event . properties . sessionID ]
294304 const result = Binary . search ( messages , event . properties . messageID , ( m ) => m . id )
295305 if ( result . found ) {
@@ -304,6 +314,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
304314 break
305315 }
306316 case "message.part.updated" : {
317+ touchPart ( event . properties . part . sessionID , event . properties . part . id )
307318 const parts = store . part [ event . properties . part . messageID ]
308319 if ( ! parts ) {
309320 setStore ( "part" , event . properties . part . messageID , [ event . properties . part ] )
@@ -329,6 +340,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
329340 if ( ! parts ) break
330341 const result = Binary . search ( parts , event . properties . partID , ( p ) => p . id )
331342 if ( ! result . found ) break
343+ touchPart ( event . properties . sessionID , event . properties . partID )
332344 setStore (
333345 "part" ,
334346 event . properties . messageID ,
@@ -343,6 +355,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
343355 }
344356
345357 case "message.part.removed" : {
358+ touchPart ( event . properties . sessionID , event . properties . partID )
346359 const parts = store . part [ event . properties . messageID ]
347360 const result = Binary . search ( parts , event . properties . partID , ( p ) => p . id )
348361 if ( result . found ) {
@@ -520,28 +533,76 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
520533 } ,
521534 async sync ( sessionID : string ) {
522535 if ( fullSyncedSessions . has ( sessionID ) ) return
523- const [ session , messages , todo , diff ] = await Promise . all ( [
524- sdk . client . session . get ( { sessionID } , { throwOnError : true } ) ,
525- sdk . client . session . messages ( { sessionID, limit : 100 } ) ,
526- sdk . client . session . todo ( { sessionID } ) ,
527- sdk . client . session . diff ( { sessionID } ) ,
528- ] )
529- setStore (
530- produce ( ( draft ) => {
531- const match = Binary . search ( draft . session , sessionID , ( s ) => s . id )
532- if ( match . found ) draft . session [ match . index ] = session . data !
533- if ( ! match . found ) draft . session . splice ( match . index , 0 , session . data ! )
534- draft . todo [ sessionID ] = todo . data ?? [ ]
535- const infos : ( typeof draft . message ) [ string ] = [ ]
536- for ( const message of messages . data ?? [ ] ) {
537- infos . push ( message . info )
538- draft . part [ message . info . id ] = message . parts
539- }
540- draft . message [ sessionID ] = infos
541- draft . session_diff [ sessionID ] = diff . data ?? [ ]
542- } ) ,
543- )
544- fullSyncedSessions . add ( sessionID )
536+ const syncing = syncingSessions . get ( sessionID )
537+ if ( syncing ) return syncing
538+ const tracker = { messages : new Set < string > ( ) , parts : new Set < string > ( ) }
539+ hydratingSessions . set ( sessionID , tracker )
540+ const task = ( async ( ) => {
541+ const [ session , messages , todo , diff ] = await Promise . all ( [
542+ sdk . client . session . get ( { sessionID } , { throwOnError : true } ) ,
543+ sdk . client . session . messages ( { sessionID, limit : 100 } ) ,
544+ sdk . client . session . todo ( { sessionID } ) ,
545+ sdk . client . session . diff ( { sessionID } ) ,
546+ ] )
547+ setStore (
548+ produce ( ( draft ) => {
549+ const match = Binary . search ( draft . session , sessionID , ( s ) => s . id )
550+ if ( match . found ) draft . session [ match . index ] = session . data !
551+ if ( ! match . found ) draft . session . splice ( match . index , 0 , session . data ! )
552+ draft . todo [ sessionID ] = todo . data ?? [ ]
553+ const currentMessages = draft . message [ sessionID ] ?? [ ]
554+ const infos = ( messages . data ?? [ ] ) . flatMap ( ( message ) => {
555+ if ( ! tracker . messages . has ( message . info . id ) ) return [ message . info ]
556+ const current = currentMessages . find ( ( item ) => item . id === message . info . id )
557+ return current ? [ current ] : [ ]
558+ } )
559+ infos . push (
560+ ...currentMessages . filter (
561+ ( message ) => tracker . messages . has ( message . id ) && ! infos . some ( ( item ) => item . id === message . id ) ,
562+ ) ,
563+ )
564+ const removed = infos . slice ( 0 , - 100 )
565+ const visible = infos . slice ( - 100 )
566+ const visibleIDs = new Set ( visible . map ( ( message ) => message . id ) )
567+ for ( const message of messages . data ?? [ ] ) {
568+ if ( ! visibleIDs . has ( message . info . id ) ) {
569+ delete draft . part [ message . info . id ]
570+ continue
571+ }
572+ const currentParts = draft . part [ message . info . id ] ?? [ ]
573+ const parts = message . parts . flatMap ( ( part ) => {
574+ const current = currentParts . find ( ( item ) => item . id === part . id )
575+ if ( tracker . parts . has ( part . id ) ) return current ? [ current ] : [ ]
576+ if (
577+ current &&
578+ ( part . type === "text" || part . type === "reasoning" ) &&
579+ ( current . type === "text" || current . type === "reasoning" ) &&
580+ part . text . length === 0 &&
581+ current . text . length > 0
582+ ) {
583+ return [ current ]
584+ }
585+ return [ part ]
586+ } )
587+ parts . push (
588+ ...currentParts . filter (
589+ ( part ) => tracker . parts . has ( part . id ) && ! parts . some ( ( item ) => item . id === part . id ) ,
590+ ) ,
591+ )
592+ draft . part [ message . info . id ] = parts
593+ }
594+ for ( const message of removed ) delete draft . part [ message . id ]
595+ draft . message [ sessionID ] = visible
596+ draft . session_diff [ sessionID ] = diff . data ?? [ ]
597+ } ) ,
598+ )
599+ fullSyncedSessions . add ( sessionID )
600+ } ) ( ) . finally ( ( ) => {
601+ syncingSessions . delete ( sessionID )
602+ hydratingSessions . delete ( sessionID )
603+ } )
604+ syncingSessions . set ( sessionID , task )
605+ return task
545606 } ,
546607 } ,
547608 bootstrap,
0 commit comments