@@ -33,6 +33,7 @@ import * as Log from "@opencode-ai/core/util/log"
3333import { emptyConsoleState , type ConsoleState } from "@opencode-ai/core/v1/config/console-state"
3434import path from "path"
3535import { aggregateFailures } from "./aggregate-failures"
36+ import { mergeFetchedMessages , optimisticParts , type OptimisticPromptPart } from "./sync-optimistic"
3637
3738export const { use : useSync , provider : SyncProvider } = createSimpleContext ( {
3839 name : "Sync" ,
@@ -122,6 +123,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
122123 const touchPart = ( sessionID : string , partID : string ) => {
123124 hydratingSessions . get ( sessionID ) ?. parts . add ( partID )
124125 }
126+ const optimisticMessages = new Set < string > ( )
125127
126128 function sessionListQuery ( ) : { scope ?: "project" ; path ?: string } {
127129 if ( ! kv . get ( "session_directory_filter_enabled" , true ) ) return { scope : "project" }
@@ -235,6 +237,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
235237 break
236238
237239 case "session.deleted" : {
240+ for ( const message of store . message [ event . properties . info . id ] ?? [ ] ) optimisticMessages . delete ( message . id )
238241 const result = Binary . search ( store . session , event . properties . info . id , ( s ) => s . id )
239242 if ( result . found ) {
240243 setStore (
@@ -324,6 +327,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
324327 }
325328 case "message.removed" : {
326329 touchMessage ( event . properties . sessionID , event . properties . messageID )
330+ optimisticMessages . delete ( event . properties . messageID )
327331 const messages = store . message [ event . properties . sessionID ]
328332 const result = Binary . search ( messages , event . properties . messageID , ( m ) => m . id )
329333 if ( result . found ) {
@@ -339,6 +343,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
339343 }
340344 case "message.part.updated" : {
341345 touchPart ( event . properties . part . sessionID , event . properties . part . id )
346+ optimisticMessages . delete ( event . properties . part . messageID )
342347 const parts = store . part [ event . properties . part . messageID ]
343348 if ( ! parts ) {
344349 setStore ( "part" , event . properties . part . messageID , [ event . properties . part ] )
@@ -555,6 +560,66 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
555560 if ( last . role === "user" ) return "working"
556561 return last . time . completed ? "idle" : "working"
557562 } ,
563+ addOptimisticPrompt ( input : {
564+ sessionID : string
565+ messageID : string
566+ agent : string
567+ model : { providerID : string ; modelID : string }
568+ variant ?: string
569+ parts : OptimisticPromptPart [ ]
570+ } ) {
571+ optimisticMessages . add ( input . messageID )
572+ const messages = store . message [ input . sessionID ]
573+ const match = messages ? Binary . search ( messages , input . messageID , ( m ) => m . id ) : undefined
574+ const info : Message = {
575+ id : input . messageID ,
576+ sessionID : input . sessionID ,
577+ role : "user" ,
578+ time : { created : Date . now ( ) } ,
579+ agent : input . agent ,
580+ model : {
581+ providerID : input . model . providerID ,
582+ modelID : input . model . modelID ,
583+ ...( input . variant ? { variant : input . variant } : { } ) ,
584+ } ,
585+ }
586+ batch ( ( ) => {
587+ if ( ! messages ) {
588+ setStore ( "message" , input . sessionID , [ info ] )
589+ } else if ( ! match ?. found ) {
590+ setStore (
591+ "message" ,
592+ input . sessionID ,
593+ produce ( ( draft ) => {
594+ Binary . insert ( draft , info , ( message ) => message . id )
595+ } ) ,
596+ )
597+ }
598+ setStore ( "part" , input . messageID , reconcile ( optimisticParts ( input ) ) )
599+ } )
600+ } ,
601+ removeOptimisticPrompt ( sessionID : string , messageID : string ) {
602+ if ( ! optimisticMessages . delete ( messageID ) ) return
603+ const messages = store . message [ sessionID ]
604+ const match = messages ? Binary . search ( messages , messageID , ( m ) => m . id ) : undefined
605+ batch ( ( ) => {
606+ if ( match ?. found ) {
607+ setStore (
608+ "message" ,
609+ sessionID ,
610+ produce ( ( draft ) => {
611+ draft . splice ( match . index , 1 )
612+ } ) ,
613+ )
614+ }
615+ setStore (
616+ "part" ,
617+ produce ( ( draft ) => {
618+ delete draft [ messageID ]
619+ } ) ,
620+ )
621+ } )
622+ } ,
558623 async sync ( sessionID : string ) {
559624 if ( fullSyncedSessions . has ( sessionID ) ) return
560625 const syncing = syncingSessions . get ( sessionID )
@@ -571,13 +636,19 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
571636 setStore (
572637 produce ( ( draft ) => {
573638 const match = Binary . search ( draft . session , sessionID , ( s ) => s . id )
639+ const merged = mergeFetchedMessages ( {
640+ currentMessages : draft . message [ sessionID ] ?? [ ] ,
641+ currentParts : draft . part ,
642+ fetched : messages . data ?? [ ] ,
643+ optimisticMessages,
644+ } )
574645 if ( match . found ) draft . session [ match . index ] = session . data !
575646 if ( ! match . found ) draft . session . splice ( match . index , 0 , session . data ! )
576647 draft . todo [ sessionID ] = todo . data ?? [ ]
577648 const currentMessages = draft . message [ sessionID ] ?? [ ]
578- const infos = ( messages . data ?? [ ] ) . flatMap ( ( message ) => {
579- if ( ! tracker . messages . has ( message . info . id ) ) return [ message . info ]
580- const current = currentMessages . find ( ( item ) => item . id === message . info . id )
649+ const infos = merged . messages . flatMap ( ( message ) => {
650+ if ( ! tracker . messages . has ( message . id ) ) return [ message ]
651+ const current = currentMessages . find ( ( item ) => item . id === message . id )
581652 return current ? [ current ] : [ ]
582653 } )
583654 infos . push (
@@ -588,13 +659,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
588659 const removed = infos . slice ( 0 , - 100 )
589660 const visible = infos . slice ( - 100 )
590661 const visibleIDs = new Set ( visible . map ( ( message ) => message . id ) )
591- for ( const message of messages . data ?? [ ] ) {
592- if ( ! visibleIDs . has ( message . info . id ) ) {
593- delete draft . part [ message . info . id ]
594- continue
595- }
596- const currentParts = draft . part [ message . info . id ] ?? [ ]
597- const parts = message . parts . flatMap ( ( part ) => {
662+ for ( const messageID of merged . resolved ) {
663+ optimisticMessages . delete ( messageID )
664+ }
665+ for ( const message of visible ) {
666+ const fetchedParts = merged . parts . get ( message . id )
667+ const currentParts = draft . part [ message . id ] ?? [ ]
668+ const parts = ( fetchedParts ?? currentParts ) . flatMap ( ( part ) => {
598669 const current = currentParts . find ( ( item ) => item . id === part . id )
599670 if ( tracker . parts . has ( part . id ) ) return current ? [ current ] : [ ]
600671 if (
@@ -613,7 +684,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
613684 ( part ) => tracker . parts . has ( part . id ) && ! parts . some ( ( item ) => item . id === part . id ) ,
614685 ) ,
615686 )
616- draft . part [ message . info . id ] = parts
687+ draft . part [ message . id ] = parts
688+ }
689+ for ( const message of merged . messages ) {
690+ if ( visibleIDs . has ( message . id ) ) continue
691+ delete draft . part [ message . id ]
617692 }
618693 for ( const message of removed ) delete draft . part [ message . id ]
619694 draft . message [ sessionID ] = visible
0 commit comments