@@ -70,6 +70,8 @@ import {
7070 BROWSER_SESSION_RESOURCE_ID ,
7171 isAddressableResource ,
7272 isEphemeralResource ,
73+ type MothershipResourceUpdate ,
74+ mergeChatResource ,
7375 sanitizeChatResources ,
7476 TERMINAL_SESSION_RESOURCE_ID ,
7577} from '@/lib/copilot/resources/types'
@@ -211,7 +213,7 @@ export interface UseChatReturn {
211213 resources : MothershipResource [ ]
212214 activeResourceId : string | null
213215 setActiveResourceId : ( id : string | null ) => void
214- addResource : ( resource : MothershipResource ) => boolean
216+ addResource : ( resource : MothershipResourceUpdate ) => boolean
215217 removeResource : ( resourceType : MothershipResourceType , resourceId : string ) => void
216218 reorderResources : ( resources : MothershipResource [ ] ) => void
217219 messageQueue : QueuedMessage [ ]
@@ -1383,6 +1385,7 @@ export function useChat(
13831385 */
13841386 const undisplayableResourcesRef = useRef < MothershipResource [ ] > ( [ ] )
13851387 const pendingPersistResourceKeysRef = useRef < Set < string > > ( new Set ( ) )
1388+ const pendingClearViewIdKeysRef = useRef < Set < string > > ( new Set ( ) )
13861389 const inFlightResourceAddsRef = useRef < Map < string , Promise < unknown > > > ( new Map ( ) )
13871390 const reorderNeededAfterFlushRef = useRef ( false )
13881391
@@ -1642,6 +1645,7 @@ export function useChat(
16421645 useTableViewPinStore . getState ( ) . reset ( )
16431646 undisplayableResourcesRef . current = [ ]
16441647 pendingPersistResourceKeysRef . current . clear ( )
1648+ pendingClearViewIdKeysRef . current . clear ( )
16451649 inFlightResourceAddsRef . current . clear ( )
16461650 reorderNeededAfterFlushRef . current = false
16471651 resetEphemeralPreviewState ( )
@@ -1679,9 +1683,18 @@ export function useChat(
16791683 const key = `${ resource . type } :${ resource . id } `
16801684 if ( ! pendingKeys . has ( key ) ) continue
16811685 pendingKeys . delete ( key )
1686+ const shouldClearViewId = pendingClearViewIdKeysRef . current . has ( key )
16821687 const promise = requestJson ( addMothershipChatResourceContract , {
1683- body : { chatId, resource } ,
1688+ body : {
1689+ chatId,
1690+ resource,
1691+ ...( shouldClearViewId ? { clearViewId : true as const } : { } ) ,
1692+ } ,
16841693 } )
1694+ . then ( ( result ) => {
1695+ if ( shouldClearViewId ) pendingClearViewIdKeysRef . current . delete ( key )
1696+ return result
1697+ } )
16851698 . catch ( ( err ) => {
16861699 pendingPersistResourceKeysRef . current . add ( key )
16871700 logger . warn ( 'Failed to flush pending resource; will retry on next hydration' , err )
@@ -1798,20 +1811,30 @@ export function useChat(
17981811 const source = chatHistory ?. messages . map ( toDisplayMessage ) ?? pendingMessages
17991812 return source . map ( ( m ) => restoreRevealedSimKeysForMessage ( m , revealedSimKeysRef . current ) )
18001813 } , [ chatHistory , pendingMessages ] )
1801- const addResource = useCallback ( ( resource : MothershipResource ) : boolean => {
1814+ const addResource = useCallback ( ( resourceUpdate : MothershipResourceUpdate ) : boolean => {
18021815 // The single fan-in for tab creation, so the invariant lives here.
1803- if ( ! isAddressableResource ( resource ) ) {
1804- logger . warn ( 'Ignored a resource with no id' , { type : resource . type , title : resource . title } )
1816+ if ( ! isAddressableResource ( resourceUpdate ) ) {
1817+ logger . warn ( 'Ignored a resource with no id' , {
1818+ type : resourceUpdate . type ,
1819+ title : resourceUpdate . title ,
1820+ } )
18051821 return false
18061822 }
1807- if ( resourcesRef . current . some ( ( r ) => r . type === resource . type && r . id === resource . id ) ) {
1823+ const existing = resourcesRef . current . find (
1824+ ( r ) => r . type === resourceUpdate . type && r . id === resourceUpdate . id
1825+ )
1826+ const resource = mergeChatResource ( existing , resourceUpdate )
1827+ if ( existing && resource === existing ) {
18081828 return false
18091829 }
18101830
18111831 setResources ( ( prev ) => {
1812- const exists = prev . some ( ( r ) => r . type === resource . type && r . id === resource . id )
1813- if ( exists ) return prev
1814- return [ ...prev , resource ]
1832+ const current = prev . find ( ( r ) => r . type === resource . type && r . id === resource . id )
1833+ if ( ! current ) return [ ...prev , resource ]
1834+ const merged = mergeChatResource ( current , resourceUpdate )
1835+ return merged === current
1836+ ? prev
1837+ : prev . map ( ( r ) => ( r . type === resource . type && r . id === resource . id ? merged : r ) )
18151838 } )
18161839 // Synthetic result/preview panels are in-memory only. The browser tab
18171840 // metadata is persisted even though its live page remains desktop-owned.
@@ -1821,19 +1844,34 @@ export function useChat(
18211844
18221845 const persistChatId = chatIdRef . current ?? selectedChatIdRef . current
18231846 const key = `${ resource . type } :${ resource . id } `
1847+ const shouldClearViewId = resourceUpdate . clearViewId === true
1848+ if ( shouldClearViewId ) {
1849+ pendingClearViewIdKeysRef . current . add ( key )
1850+ } else if ( resourceUpdate . viewId !== undefined ) {
1851+ pendingClearViewIdKeysRef . current . delete ( key )
1852+ }
18241853 // `resourcesRef` is written during render, so adds of the same resource in
18251854 // one tick all read the pre-render list and all pass the check above. State
18261855 // converges (the updater is idempotent) but each fired its own POST — 5-6
18271856 // per resource in production.
18281857 const alreadyPersisting =
18291858 inFlightResourceAddsRef . current . has ( key ) || pendingPersistResourceKeysRef . current . has ( key )
18301859 if ( alreadyPersisting ) {
1831- return true
1860+ pendingPersistResourceKeysRef . current . add ( key )
1861+ return existing === undefined
18321862 }
18331863 if ( persistChatId ) {
18341864 const promise = requestJson ( addMothershipChatResourceContract , {
1835- body : { chatId : persistChatId , resource } ,
1865+ body : {
1866+ chatId : persistChatId ,
1867+ resource,
1868+ ...( shouldClearViewId ? { clearViewId : true as const } : { } ) ,
1869+ } ,
18361870 } )
1871+ . then ( ( result ) => {
1872+ if ( shouldClearViewId ) pendingClearViewIdKeysRef . current . delete ( key )
1873+ return result
1874+ } )
18371875 . catch ( ( err ) => {
18381876 pendingPersistResourceKeysRef . current . add ( key )
18391877 logger . warn ( 'Failed to persist resource; will retry on next hydration' , err )
@@ -1845,7 +1883,7 @@ export function useChat(
18451883 } else {
18461884 pendingPersistResourceKeysRef . current . add ( key )
18471885 }
1848- return true
1886+ return existing === undefined
18491887 } , [ ] )
18501888
18511889 const removeResource = useCallback ( ( resourceType : MothershipResourceType , resourceId : string ) => {
@@ -1856,6 +1894,7 @@ export function useChat(
18561894 if ( isEphemeralResource ( { type : resourceType , id : resourceId , title : '' } ) ) return
18571895
18581896 const key = `${ resourceType } :${ resourceId } `
1897+ pendingClearViewIdKeysRef . current . delete ( key )
18591898 const wasPending = pendingPersistResourceKeysRef . current . delete ( key )
18601899 const inFlightAdd = inFlightResourceAddsRef . current . get ( key )
18611900 if ( wasPending && ! inFlightAdd ) return
@@ -2344,6 +2383,7 @@ export function useChat(
23442383 setActiveResourceId ( null )
23452384 useTableViewPinStore . getState ( ) . reset ( )
23462385 pendingPersistResourceKeysRef . current . clear ( )
2386+ pendingClearViewIdKeysRef . current . clear ( )
23472387 inFlightResourceAddsRef . current . clear ( )
23482388 reorderNeededAfterFlushRef . current = false
23492389 resetEphemeralPreviewState ( )
0 commit comments