@@ -36,6 +36,10 @@ import { captureEvent } from '@/lib/posthog/client'
3636import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export'
3737import { RESOURCE_HEADER_CLASSES } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
3838import { resolveWorkspaceResourceRef } from '@/app/workspace/[workspaceId]/home/resolve-resource-ref'
39+ import {
40+ resolveResourceEventPresentation ,
41+ resolveResourceSelectionUpdate ,
42+ } from '@/app/workspace/[workspaceId]/home/resource-view-policy'
3943import { resourceParam , resourceUrlKeys } from '@/app/workspace/[workspaceId]/home/search-params'
4044import { useFolders } from '@/hooks/queries/folders'
4145import { useMarkMothershipChatRead } from '@/hooks/queries/mothership-chats'
@@ -102,6 +106,8 @@ export function Home({ chatId, userName, userId }: HomeProps) {
102106 ...resourceParam . parser ,
103107 ...resourceUrlKeys ,
104108 } )
109+ const activeResourceParamRef = useRef ( activeResourceParam )
110+ activeResourceParamRef . current = activeResourceParam
105111 /**
106112 * Strips any leftover URL fragment on selection change, preserving the old
107113 * effect's `url.hash = ''` (the only hash usage on this surface) without a
@@ -114,11 +120,13 @@ export function Home({ chatId, userName, userId }: HomeProps) {
114120 */
115121 const setActiveResourceUrl = useCallback < Dispatch < SetStateAction < string | null > > > (
116122 ( action ) => {
123+ const nextResourceId = resolveResourceSelectionUpdate ( activeResourceParamRef . current , action )
124+ activeResourceParamRef . current = nextResourceId
117125 if ( typeof window !== 'undefined' && window . location . hash ) {
118126 const { pathname, search } = window . location
119127 window . history . replaceState ( window . history . state , '' , `${ pathname } ${ search } ` )
120128 }
121- void setResourceParam ( action )
129+ void setResourceParam ( nextResourceId )
122130 } ,
123131 [ setResourceParam ]
124132 )
@@ -202,23 +210,30 @@ export function Home({ chatId, userName, userId }: HomeProps) {
202210
203211 const { mutate : markRead } = useMarkMothershipChatRead ( workspaceId )
204212
205- const [ isResourceCollapsed , setIsResourceCollapsed ] = useState ( true )
213+ const [ isResourceCollapsed , setIsResourceCollapsedState ] = useState ( true )
206214 const [ skipResourceTransition , setSkipResourceTransition ] = useState ( false )
207215 const [ resourceActivityIds , setResourceActivityIds ] = useState < Set < string > > ( new Set ( ) )
208216 const isResourceCollapsedRef = useRef ( isResourceCollapsed )
209- isResourceCollapsedRef . current = isResourceCollapsed
210- const userOwnsResourceViewRef = useRef ( false )
211- const activeResourceParamRef = useRef ( activeResourceParam )
212- activeResourceParamRef . current = activeResourceParam
217+ const setResourceCollapsed = useCallback ( ( collapsed : boolean ) => {
218+ isResourceCollapsedRef . current = collapsed
219+ setIsResourceCollapsedState ( collapsed )
220+ } , [ ] )
221+ const resourceCollapseOwnedByUserRef = useRef ( false )
222+ const resourceSelectionOwnedByUserRef = useRef ( false )
213223
214224 function handleResourceEvent ( resourceId : string , options ?: ResourceEventOptions ) {
215- // Agent work surfaces the resource and switches to it as it is created or
216- // edited; only the browser session stays in the background behind an
217- // existing selection (see shouldActivateResourceEvent).
218- if ( isResourceCollapsedRef . current ) setIsResourceCollapsed ( false )
219-
220225 const activeResourceId = activeResourceParamRef . current
221- if ( ! shouldActivateResourceEvent ( activeResourceId , resourceId , options ) ) {
226+ const presentation = resolveResourceEventPresentation ( {
227+ activeResourceId,
228+ activationRequested : shouldActivateResourceEvent ( activeResourceId , resourceId , options ) ,
229+ panelCollapseOwnedByUser : resourceCollapseOwnedByUserRef . current ,
230+ panelCollapsed : isResourceCollapsedRef . current ,
231+ resourceId,
232+ selectionOwnedByUser : resourceSelectionOwnedByUserRef . current ,
233+ } )
234+
235+ if ( presentation . revealPanel ) setResourceCollapsed ( false )
236+ if ( presentation . markActivity ) {
222237 setResourceActivityIds ( ( current ) => new Set ( current ) . add ( resourceId ) )
223238 return
224239 }
@@ -228,7 +243,7 @@ export function Home({ chatId, userName, userId }: HomeProps) {
228243 next . delete ( resourceId )
229244 return next
230245 } )
231- if ( activeResourceId !== resourceId ) {
246+ if ( presentation . activateResource && activeResourceId !== resourceId ) {
232247 activeResourceParamRef . current = resourceId
233248 setActiveResourceUrl ( resourceId )
234249 }
@@ -282,10 +297,11 @@ export function Home({ chatId, userName, userId }: HomeProps) {
282297 const resourceAttentionChatIdRef = useRef ( resolvedChatId )
283298
284299 const collapseResource = useCallback ( ( ) => {
285- userOwnsResourceViewRef . current = true
300+ resourceCollapseOwnedByUserRef . current = true
301+ resourceSelectionOwnedByUserRef . current = true
286302 clearWidth ( )
287- setIsResourceCollapsed ( true )
288- } , [ clearWidth ] )
303+ setResourceCollapsed ( true )
304+ } , [ clearWidth , setResourceCollapsed ] )
289305
290306 const clearResourceActivity = useCallback ( ( resourceId : string ) => {
291307 setResourceActivityIds ( ( current ) => {
@@ -297,15 +313,16 @@ export function Home({ chatId, userName, userId }: HomeProps) {
297313 } , [ ] )
298314
299315 const expandResource = ( ) => {
300- userOwnsResourceViewRef . current = true
316+ resourceCollapseOwnedByUserRef . current = false
317+ resourceSelectionOwnedByUserRef . current = true
301318 const activeResourceId = activeResourceParamRef . current
302319 if ( activeResourceId ) clearResourceActivity ( activeResourceId )
303- setIsResourceCollapsed ( false )
320+ setResourceCollapsed ( false )
304321 }
305322
306323 const selectResourceFromUser = useCallback (
307324 ( resourceId : string ) => {
308- userOwnsResourceViewRef . current = true
325+ resourceSelectionOwnedByUserRef . current = true
309326 clearResourceActivity ( resourceId )
310327 if ( effectiveActiveResourceIdRef . current === resourceId ) return
311328 effectiveActiveResourceIdRef . current = resourceId
@@ -317,24 +334,30 @@ export function Home({ chatId, userName, userId }: HomeProps) {
317334
318335 const addResourceFromUser = useCallback (
319336 ( resource : MothershipResource ) => {
320- userOwnsResourceViewRef . current = true
337+ resourceCollapseOwnedByUserRef . current = false
338+ resourceSelectionOwnedByUserRef . current = true
321339 addResource ( resource )
322340 selectResourceFromUser ( resource . id )
323- setIsResourceCollapsed ( false )
341+ setResourceCollapsed ( false )
324342 } ,
325- [ addResource , selectResourceFromUser ]
343+ [ addResource , selectResourceFromUser , setResourceCollapsed ]
326344 )
327345
328346 const handleResourceResizePointerDown = useCallback (
329347 ( event : PointerEvent < HTMLDivElement > ) => {
330- userOwnsResourceViewRef . current = true
348+ resourceSelectionOwnedByUserRef . current = true
331349 handleResizePointerDown ( event )
332350 } ,
333351 [ handleResizePointerDown ]
334352 )
335353
336354 const handleResourceInteraction = useCallback ( ( ) => {
337- userOwnsResourceViewRef . current = true
355+ resourceSelectionOwnedByUserRef . current = true
356+ } , [ ] )
357+
358+ const prepareResourceViewForAgentTurn = useCallback ( ( ) => {
359+ resourceSelectionOwnedByUserRef . current = false
360+ setResourceActivityIds ( new Set ( ) )
338361 } , [ ] )
339362
340363 useEffect ( ( ) => {
@@ -345,13 +368,14 @@ export function Home({ chatId, userName, userId }: HomeProps) {
345368 markRead ( resolvedChatId )
346369 } else {
347370 clearWidth ( )
348- setIsResourceCollapsed ( true )
371+ setResourceCollapsed ( true )
349372 }
350373 if ( ! resolvedChatId || ( previousChatId && previousChatId !== resolvedChatId ) ) {
351- userOwnsResourceViewRef . current = false
374+ resourceCollapseOwnedByUserRef . current = false
375+ resourceSelectionOwnedByUserRef . current = false
352376 setResourceActivityIds ( new Set ( ) )
353377 }
354- } , [ resolvedChatId , markRead , clearWidth ] )
378+ } , [ resolvedChatId , markRead , clearWidth , setResourceCollapsed ] )
355379
356380 useEffect ( ( ) => {
357381 if ( wasSendingRef . current && ! isSending && resolvedChatId ) {
@@ -363,22 +387,22 @@ export function Home({ chatId, userName, userId }: HomeProps) {
363387 useEffect ( ( ) => {
364388 if (
365389 ! ( resources . length > 0 && isResourceCollapsedRef . current ) ||
366- userOwnsResourceViewRef . current
390+ resourceCollapseOwnedByUserRef . current
367391 ) {
368392 return
369393 }
370- setIsResourceCollapsed ( false )
394+ setResourceCollapsed ( false )
371395 setSkipResourceTransition ( true )
372396 const id = requestAnimationFrame ( ( ) => setSkipResourceTransition ( false ) )
373397 return ( ) => cancelAnimationFrame ( id )
374- } , [ resources ] )
398+ } , [ resources , setResourceCollapsed ] )
375399
376400 useEffect ( ( ) => {
377401 if ( resources . length === 0 && ! isResourceCollapsedRef . current ) {
378402 clearWidth ( )
379- setIsResourceCollapsed ( true )
403+ setResourceCollapsed ( true )
380404 }
381- } , [ resources , clearWidth ] )
405+ } , [ resources , clearWidth , setResourceCollapsed ] )
382406
383407 useEffect ( ( ) => {
384408 const resourceIds = new Set ( resources . map ( ( resource ) => resource . id ) )
@@ -413,11 +437,10 @@ export function Home({ chatId, userName, userId }: HomeProps) {
413437 setIsInputEntering ( true )
414438 }
415439
416- userOwnsResourceViewRef . current = false
417- setResourceActivityIds ( new Set ( ) )
440+ prepareResourceViewForAgentTurn ( )
418441 sendMessage ( trimmed || 'Analyze the attached file(s).' , fileAttachments , contexts )
419442 } ,
420- [ workspaceId , chatId , sendMessage ]
443+ [ workspaceId , chatId , prepareResourceViewForAgentTurn , sendMessage ]
421444 )
422445
423446 /**
@@ -431,13 +454,14 @@ export function Home({ chatId, userName, userId }: HomeProps) {
431454 const detail = ( e as CustomEvent < MothershipSendMessageDetail > ) . detail
432455 if ( ! detail ?. message ) return
433456 e . preventDefault ( )
457+ prepareResourceViewForAgentTurn ( )
434458 sendMessage ( detail . message , detail . fileAttachments , detail . contexts , {
435459 ...( detail . resumeUserMessageId ? { resumeUserMessageId : detail . resumeUserMessageId } : { } ) ,
436460 } )
437461 }
438462 window . addEventListener ( MOTHERSHIP_SEND_MESSAGE_EVENT , handler )
439463 return ( ) => window . removeEventListener ( MOTHERSHIP_SEND_MESSAGE_EVENT , handler )
440- } , [ sendMessage ] )
464+ } , [ prepareResourceViewForAgentTurn , sendMessage ] )
441465
442466 /**
443467 * Consumes a one-shot handoff left by another surface and applies it to this
@@ -462,6 +486,7 @@ export function Home({ chatId, userName, userId }: HomeProps) {
462486 const handoff = MothershipHandoffStorage . consume ( workspaceId )
463487 if ( ! handoff ) return
464488 if ( handoff . message ) {
489+ prepareResourceViewForAgentTurn ( )
465490 sendMessage ( handoff . message , handoff . fileAttachments , handoff . contexts , {
466491 ...( handoff . resumeUserMessageId
467492 ? { resumeUserMessageId : handoff . resumeUserMessageId }
@@ -477,7 +502,7 @@ export function Home({ chatId, userName, userId }: HomeProps) {
477502 // keep it one-shot — and harmless either way, since `consume` clears the entry
478503 // atomically and any re-run would find nothing.
479504 // eslint-disable-next-line react-hooks/exhaustive-deps -- see above
480- } , [ chatId , workspaceId , sendMessage ] )
505+ } , [ chatId , workspaceId , prepareResourceViewForAgentTurn , sendMessage ] )
481506
482507 function resolveResourceFromContext (
483508 context : ChatContext
@@ -583,6 +608,12 @@ export function Home({ chatId, userName, userId }: HomeProps) {
583608 const hasMessages = messages . length > 0
584609 const showChatSkeleton = Boolean ( chatId ) && ! hasMessages && isChatHistoryPending
585610 const draftScopeKey = `${ workspaceId } :${ chatId ?? 'new' } `
611+ const resourceActivityCount = resourceActivityIds . size
612+ const resourceToggleLabel = isResourceCollapsed
613+ ? resourceActivityCount > 0
614+ ? `Expand resource view, ${ resourceActivityCount } resource${ resourceActivityCount === 1 ? '' : 's' } updated`
615+ : 'Expand resource view'
616+ : 'Collapse resource view'
586617
587618 // The empty state is the chat pane's content, not a layout of its own. It
588619 // used to return early, which meant the resource panel and its toggle did
@@ -720,13 +751,16 @@ export function Home({ chatId, userName, userId }: HomeProps) {
720751 size = { null }
721752 type = 'button'
722753 onClick = { isResourceCollapsed ? expandResource : collapseResource }
723- className = ' size-[var(--resource-header-toggle-size)] rounded-[8px] hover-hover:bg-[var(--surface-active)]'
724- aria-label = { isResourceCollapsed ? 'Expand resource view' : 'Collapse resource view' }
754+ className = "after:-translate-x-1/2 after:-translate-y-1/2 relative size-[var(--resource-header-toggle-size)] rounded-[8px] after:absolute after:top-1/2 after:left-1/2 after:size-[var(--resource-header-toggle-hit-size)] after:content-[''] hover-hover:bg-[var(--surface-active)]"
755+ aria-label = { resourceToggleLabel }
725756 >
726757 < span className = 'relative' >
727758 < PanelLeft className = '-scale-x-100 size-[16px] text-[var(--text-icon)]' />
728759 { isResourceCollapsed && resourceActivityIds . size > 0 && (
729- < span className = '-top-0.5 -right-0.5 absolute size-1.5 rounded-full bg-[var(--brand-primary)]' />
760+ < span
761+ aria-hidden = 'true'
762+ className = '-top-0.5 -right-0.5 absolute size-1.5 rounded-full bg-[var(--brand-primary)]'
763+ />
730764 ) }
731765 </ span >
732766 </ Button >
0 commit comments