@@ -33,6 +33,7 @@ import * as Log from "@opencode-ai/core/util/log"
3333import { emptyConsoleState , type ConsoleState } from "@/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" ,
@@ -114,6 +115,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
114115 const [ autoaccept ] = kv . signal < "none" | "edit" > ( "permission_auto_accept" , "edit" )
115116
116117 const fullSyncedSessions = new Set < string > ( )
118+ const optimisticMessages = new Set < string > ( )
117119
118120 function sessionListQuery ( ) : { scope ?: "project" ; path ?: string } {
119121 if ( ! kv . get ( "session_directory_filter_enabled" , true ) ) return { scope : "project" }
@@ -227,6 +229,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
227229 break
228230
229231 case "session.deleted" : {
232+ for ( const message of store . message [ event . properties . info . id ] ?? [ ] ) optimisticMessages . delete ( message . id )
230233 const result = Binary . search ( store . session , event . properties . info . id , ( s ) => s . id )
231234 if ( result . found ) {
232235 setStore (
@@ -298,6 +301,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
298301 break
299302 }
300303 case "message.removed" : {
304+ optimisticMessages . delete ( event . properties . messageID )
301305 const messages = store . message [ event . properties . sessionID ]
302306 const result = Binary . search ( messages , event . properties . messageID , ( m ) => m . id )
303307 if ( result . found ) {
@@ -312,6 +316,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
312316 break
313317 }
314318 case "message.part.updated" : {
319+ optimisticMessages . delete ( event . properties . part . messageID )
315320 const parts = store . part [ event . properties . part . messageID ]
316321 if ( ! parts ) {
317322 setStore ( "part" , event . properties . part . messageID , [ event . properties . part ] )
@@ -526,6 +531,66 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
526531 if ( last . role === "user" ) return "working"
527532 return last . time . completed ? "idle" : "working"
528533 } ,
534+ addOptimisticPrompt ( input : {
535+ sessionID : string
536+ messageID : string
537+ agent : string
538+ model : { providerID : string ; modelID : string }
539+ variant ?: string
540+ parts : OptimisticPromptPart [ ]
541+ } ) {
542+ optimisticMessages . add ( input . messageID )
543+ const messages = store . message [ input . sessionID ]
544+ const match = messages ? Binary . search ( messages , input . messageID , ( m ) => m . id ) : undefined
545+ const info : Message = {
546+ id : input . messageID ,
547+ sessionID : input . sessionID ,
548+ role : "user" ,
549+ time : { created : Date . now ( ) } ,
550+ agent : input . agent ,
551+ model : {
552+ providerID : input . model . providerID ,
553+ modelID : input . model . modelID ,
554+ ...( input . variant ? { variant : input . variant } : { } ) ,
555+ } ,
556+ }
557+ batch ( ( ) => {
558+ if ( ! messages ) {
559+ setStore ( "message" , input . sessionID , [ info ] )
560+ } else if ( ! match ?. found ) {
561+ setStore (
562+ "message" ,
563+ input . sessionID ,
564+ produce ( ( draft ) => {
565+ Binary . insert ( draft , info , ( message ) => message . id )
566+ } ) ,
567+ )
568+ }
569+ setStore ( "part" , input . messageID , reconcile ( optimisticParts ( input ) ) )
570+ } )
571+ } ,
572+ removeOptimisticPrompt ( sessionID : string , messageID : string ) {
573+ if ( ! optimisticMessages . delete ( messageID ) ) return
574+ const messages = store . message [ sessionID ]
575+ const match = messages ? Binary . search ( messages , messageID , ( m ) => m . id ) : undefined
576+ batch ( ( ) => {
577+ if ( match ?. found ) {
578+ setStore (
579+ "message" ,
580+ sessionID ,
581+ produce ( ( draft ) => {
582+ draft . splice ( match . index , 1 )
583+ } ) ,
584+ )
585+ }
586+ setStore (
587+ "part" ,
588+ produce ( ( draft ) => {
589+ delete draft [ messageID ]
590+ } ) ,
591+ )
592+ } )
593+ } ,
529594 async sync ( sessionID : string ) {
530595 if ( fullSyncedSessions . has ( sessionID ) ) return
531596 const [ session , messages , todo , diff ] = await Promise . all ( [
@@ -537,15 +602,22 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
537602 setStore (
538603 produce ( ( draft ) => {
539604 const match = Binary . search ( draft . session , sessionID , ( s ) => s . id )
605+ const merged = mergeFetchedMessages ( {
606+ currentMessages : draft . message [ sessionID ] ?? [ ] ,
607+ currentParts : draft . part ,
608+ fetched : messages . data ?? [ ] ,
609+ optimisticMessages,
610+ } )
540611 if ( match . found ) draft . session [ match . index ] = session . data !
541612 if ( ! match . found ) draft . session . splice ( match . index , 0 , session . data ! )
542613 draft . todo [ sessionID ] = todo . data ?? [ ]
543- const infos : ( typeof draft . message ) [ string ] = [ ]
544- for ( const message of messages . data ?? [ ] ) {
545- infos . push ( message . info )
546- draft . part [ message . info . id ] = message . parts
614+ draft . message [ sessionID ] = merged . messages
615+ for ( const messageID of merged . resolved ) {
616+ optimisticMessages . delete ( messageID )
617+ }
618+ for ( const [ messageID , parts ] of merged . parts ) {
619+ draft . part [ messageID ] = parts
547620 }
548- draft . message [ sessionID ] = infos
549621 draft . session_diff [ sessionID ] = diff . data ?? [ ]
550622 } ) ,
551623 )
0 commit comments