@@ -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" }
@@ -252,6 +260,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
252260 }
253261
254262 case "message.updated" : {
263+ touchMessage ( event . properties . info . sessionID , event . properties . info . id )
255264 const messages = store . message [ event . properties . info . sessionID ]
256265 if ( ! messages ) {
257266 setStore ( "message" , event . properties . info . sessionID , [ event . properties . info ] )
@@ -291,6 +300,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
291300 break
292301 }
293302 case "message.removed" : {
303+ touchMessage ( event . properties . sessionID , event . properties . messageID )
294304 const messages = store . message [ event . properties . sessionID ]
295305 const result = Binary . search ( messages , event . properties . messageID , ( m ) => m . id )
296306 if ( result . found ) {
@@ -305,6 +315,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
305315 break
306316 }
307317 case "message.part.updated" : {
318+ touchPart ( event . properties . part . sessionID , event . properties . part . id )
308319 const parts = store . part [ event . properties . part . messageID ]
309320 if ( ! parts ) {
310321 setStore ( "part" , event . properties . part . messageID , [ event . properties . part ] )
@@ -330,6 +341,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
330341 if ( ! parts ) break
331342 const result = Binary . search ( parts , event . properties . partID , ( p ) => p . id )
332343 if ( ! result . found ) break
344+ touchPart ( event . properties . sessionID , event . properties . partID )
333345 setStore (
334346 "part" ,
335347 event . properties . messageID ,
@@ -344,6 +356,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
344356 }
345357
346358 case "message.part.removed" : {
359+ touchPart ( event . properties . sessionID , event . properties . partID )
347360 const parts = store . part [ event . properties . messageID ]
348361 const result = Binary . search ( parts , event . properties . partID , ( p ) => p . id )
349362 if ( result . found ) {
@@ -521,28 +534,76 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
521534 } ,
522535 async sync ( sessionID : string ) {
523536 if ( fullSyncedSessions . has ( sessionID ) ) return
524- const [ session , messages , todo , diff ] = await Promise . all ( [
525- sdk . client . session . get ( { sessionID } , { throwOnError : true } ) ,
526- sdk . client . session . messages ( { sessionID, limit : 100 } ) ,
527- sdk . client . session . todo ( { sessionID } ) ,
528- sdk . client . session . diff ( { sessionID } ) ,
529- ] )
530- setStore (
531- produce ( ( draft ) => {
532- const match = Binary . search ( draft . session , sessionID , ( s ) => s . id )
533- if ( match . found ) draft . session [ match . index ] = session . data !
534- if ( ! match . found ) draft . session . splice ( match . index , 0 , session . data ! )
535- draft . todo [ sessionID ] = todo . data ?? [ ]
536- const infos : ( typeof draft . message ) [ string ] = [ ]
537- for ( const message of messages . data ?? [ ] ) {
538- infos . push ( message . info )
539- draft . part [ message . info . id ] = message . parts
540- }
541- draft . message [ sessionID ] = infos
542- draft . session_diff [ sessionID ] = diff . data ?? [ ]
543- } ) ,
544- )
545- fullSyncedSessions . add ( sessionID )
537+ const syncing = syncingSessions . get ( sessionID )
538+ if ( syncing ) return syncing
539+ const tracker = { messages : new Set < string > ( ) , parts : new Set < string > ( ) }
540+ hydratingSessions . set ( sessionID , tracker )
541+ const task = ( async ( ) => {
542+ const [ session , messages , todo , diff ] = await Promise . all ( [
543+ sdk . client . session . get ( { sessionID } , { throwOnError : true } ) ,
544+ sdk . client . session . messages ( { sessionID, limit : 100 } ) ,
545+ sdk . client . session . todo ( { sessionID } ) ,
546+ sdk . client . session . diff ( { sessionID } ) ,
547+ ] )
548+ setStore (
549+ produce ( ( draft ) => {
550+ const match = Binary . search ( draft . session , sessionID , ( s ) => s . id )
551+ if ( match . found ) draft . session [ match . index ] = session . data !
552+ if ( ! match . found ) draft . session . splice ( match . index , 0 , session . data ! )
553+ draft . todo [ sessionID ] = todo . data ?? [ ]
554+ const currentMessages = draft . message [ sessionID ] ?? [ ]
555+ const infos = ( messages . data ?? [ ] ) . flatMap ( ( message ) => {
556+ if ( ! tracker . messages . has ( message . info . id ) ) return [ message . info ]
557+ const current = currentMessages . find ( ( item ) => item . id === message . info . id )
558+ return current ? [ current ] : [ ]
559+ } )
560+ infos . push (
561+ ...currentMessages . filter (
562+ ( message ) => tracker . messages . has ( message . id ) && ! infos . some ( ( item ) => item . id === message . id ) ,
563+ ) ,
564+ )
565+ const removed = infos . slice ( 0 , - 100 )
566+ const visible = infos . slice ( - 100 )
567+ const visibleIDs = new Set ( visible . map ( ( message ) => message . id ) )
568+ for ( const message of messages . data ?? [ ] ) {
569+ if ( ! visibleIDs . has ( message . info . id ) ) {
570+ delete draft . part [ message . info . id ]
571+ continue
572+ }
573+ const currentParts = draft . part [ message . info . id ] ?? [ ]
574+ const parts = message . parts . flatMap ( ( part ) => {
575+ const current = currentParts . find ( ( item ) => item . id === part . id )
576+ if ( tracker . parts . has ( part . id ) ) return current ? [ current ] : [ ]
577+ if (
578+ current &&
579+ ( part . type === "text" || part . type === "reasoning" ) &&
580+ ( current . type === "text" || current . type === "reasoning" ) &&
581+ part . text . length === 0 &&
582+ current . text . length > 0
583+ ) {
584+ return [ current ]
585+ }
586+ return [ part ]
587+ } )
588+ parts . push (
589+ ...currentParts . filter (
590+ ( part ) => tracker . parts . has ( part . id ) && ! parts . some ( ( item ) => item . id === part . id ) ,
591+ ) ,
592+ )
593+ draft . part [ message . info . id ] = parts
594+ }
595+ for ( const message of removed ) delete draft . part [ message . id ]
596+ draft . message [ sessionID ] = visible
597+ draft . session_diff [ sessionID ] = diff . data ?? [ ]
598+ } ) ,
599+ )
600+ fullSyncedSessions . add ( sessionID )
601+ } ) ( ) . finally ( ( ) => {
602+ syncingSessions . delete ( sessionID )
603+ hydratingSessions . delete ( sessionID )
604+ } )
605+ syncingSessions . set ( sessionID , task )
606+ return task
546607 } ,
547608 } ,
548609 bootstrap,
0 commit comments