@@ -36,6 +36,7 @@ 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 { resolveResourceEventPresentation } from '@/app/workspace/[workspaceId]/home/resource-view-policy'
3940import { resourceParam , resourceUrlKeys } from '@/app/workspace/[workspaceId]/home/search-params'
4041import { useFolders } from '@/hooks/queries/folders'
4142import { useMarkMothershipChatRead } from '@/hooks/queries/mothership-chats'
@@ -202,23 +203,32 @@ export function Home({ chatId, userName, userId }: HomeProps) {
202203
203204 const { mutate : markRead } = useMarkMothershipChatRead ( workspaceId )
204205
205- const [ isResourceCollapsed , setIsResourceCollapsed ] = useState ( true )
206+ const [ isResourceCollapsed , setIsResourceCollapsedState ] = useState ( true )
206207 const [ skipResourceTransition , setSkipResourceTransition ] = useState ( false )
207208 const [ resourceActivityIds , setResourceActivityIds ] = useState < Set < string > > ( new Set ( ) )
208209 const isResourceCollapsedRef = useRef ( isResourceCollapsed )
209- isResourceCollapsedRef . current = isResourceCollapsed
210- const userOwnsResourceViewRef = useRef ( false )
210+ const setResourceCollapsed = useCallback ( ( collapsed : boolean ) => {
211+ isResourceCollapsedRef . current = collapsed
212+ setIsResourceCollapsedState ( collapsed )
213+ } , [ ] )
214+ const resourceCollapseOwnedByUserRef = useRef ( false )
215+ const resourceSelectionOwnedByUserRef = useRef ( false )
211216 const activeResourceParamRef = useRef ( activeResourceParam )
212217 activeResourceParamRef . current = activeResourceParam
213218
214219 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-
220220 const activeResourceId = activeResourceParamRef . current
221- if ( ! shouldActivateResourceEvent ( activeResourceId , resourceId , options ) ) {
221+ const presentation = resolveResourceEventPresentation ( {
222+ activeResourceId,
223+ activationRequested : shouldActivateResourceEvent ( activeResourceId , resourceId , options ) ,
224+ panelCollapseOwnedByUser : resourceCollapseOwnedByUserRef . current ,
225+ panelCollapsed : isResourceCollapsedRef . current ,
226+ resourceId,
227+ selectionOwnedByUser : resourceSelectionOwnedByUserRef . current ,
228+ } )
229+
230+ if ( presentation . revealPanel ) setResourceCollapsed ( false )
231+ if ( presentation . markActivity ) {
222232 setResourceActivityIds ( ( current ) => new Set ( current ) . add ( resourceId ) )
223233 return
224234 }
@@ -228,7 +238,7 @@ export function Home({ chatId, userName, userId }: HomeProps) {
228238 next . delete ( resourceId )
229239 return next
230240 } )
231- if ( activeResourceId !== resourceId ) {
241+ if ( presentation . activateResource && activeResourceId !== resourceId ) {
232242 activeResourceParamRef . current = resourceId
233243 setActiveResourceUrl ( resourceId )
234244 }
@@ -282,10 +292,11 @@ export function Home({ chatId, userName, userId }: HomeProps) {
282292 const resourceAttentionChatIdRef = useRef ( resolvedChatId )
283293
284294 const collapseResource = useCallback ( ( ) => {
285- userOwnsResourceViewRef . current = true
295+ resourceCollapseOwnedByUserRef . current = true
296+ resourceSelectionOwnedByUserRef . current = true
286297 clearWidth ( )
287- setIsResourceCollapsed ( true )
288- } , [ clearWidth ] )
298+ setResourceCollapsed ( true )
299+ } , [ clearWidth , setResourceCollapsed ] )
289300
290301 const clearResourceActivity = useCallback ( ( resourceId : string ) => {
291302 setResourceActivityIds ( ( current ) => {
@@ -297,15 +308,16 @@ export function Home({ chatId, userName, userId }: HomeProps) {
297308 } , [ ] )
298309
299310 const expandResource = ( ) => {
300- userOwnsResourceViewRef . current = true
311+ resourceCollapseOwnedByUserRef . current = false
312+ resourceSelectionOwnedByUserRef . current = true
301313 const activeResourceId = activeResourceParamRef . current
302314 if ( activeResourceId ) clearResourceActivity ( activeResourceId )
303- setIsResourceCollapsed ( false )
315+ setResourceCollapsed ( false )
304316 }
305317
306318 const selectResourceFromUser = useCallback (
307319 ( resourceId : string ) => {
308- userOwnsResourceViewRef . current = true
320+ resourceSelectionOwnedByUserRef . current = true
309321 clearResourceActivity ( resourceId )
310322 if ( effectiveActiveResourceIdRef . current === resourceId ) return
311323 effectiveActiveResourceIdRef . current = resourceId
@@ -317,24 +329,30 @@ export function Home({ chatId, userName, userId }: HomeProps) {
317329
318330 const addResourceFromUser = useCallback (
319331 ( resource : MothershipResource ) => {
320- userOwnsResourceViewRef . current = true
332+ resourceCollapseOwnedByUserRef . current = false
333+ resourceSelectionOwnedByUserRef . current = true
321334 addResource ( resource )
322335 selectResourceFromUser ( resource . id )
323- setIsResourceCollapsed ( false )
336+ setResourceCollapsed ( false )
324337 } ,
325- [ addResource , selectResourceFromUser ]
338+ [ addResource , selectResourceFromUser , setResourceCollapsed ]
326339 )
327340
328341 const handleResourceResizePointerDown = useCallback (
329342 ( event : PointerEvent < HTMLDivElement > ) => {
330- userOwnsResourceViewRef . current = true
343+ resourceSelectionOwnedByUserRef . current = true
331344 handleResizePointerDown ( event )
332345 } ,
333346 [ handleResizePointerDown ]
334347 )
335348
336349 const handleResourceInteraction = useCallback ( ( ) => {
337- userOwnsResourceViewRef . current = true
350+ resourceSelectionOwnedByUserRef . current = true
351+ } , [ ] )
352+
353+ const prepareResourceViewForAgentTurn = useCallback ( ( ) => {
354+ resourceSelectionOwnedByUserRef . current = false
355+ setResourceActivityIds ( new Set ( ) )
338356 } , [ ] )
339357
340358 useEffect ( ( ) => {
@@ -345,13 +363,14 @@ export function Home({ chatId, userName, userId }: HomeProps) {
345363 markRead ( resolvedChatId )
346364 } else {
347365 clearWidth ( )
348- setIsResourceCollapsed ( true )
366+ setResourceCollapsed ( true )
349367 }
350368 if ( ! resolvedChatId || ( previousChatId && previousChatId !== resolvedChatId ) ) {
351- userOwnsResourceViewRef . current = false
369+ resourceCollapseOwnedByUserRef . current = false
370+ resourceSelectionOwnedByUserRef . current = false
352371 setResourceActivityIds ( new Set ( ) )
353372 }
354- } , [ resolvedChatId , markRead , clearWidth ] )
373+ } , [ resolvedChatId , markRead , clearWidth , setResourceCollapsed ] )
355374
356375 useEffect ( ( ) => {
357376 if ( wasSendingRef . current && ! isSending && resolvedChatId ) {
@@ -363,22 +382,22 @@ export function Home({ chatId, userName, userId }: HomeProps) {
363382 useEffect ( ( ) => {
364383 if (
365384 ! ( resources . length > 0 && isResourceCollapsedRef . current ) ||
366- userOwnsResourceViewRef . current
385+ resourceCollapseOwnedByUserRef . current
367386 ) {
368387 return
369388 }
370- setIsResourceCollapsed ( false )
389+ setResourceCollapsed ( false )
371390 setSkipResourceTransition ( true )
372391 const id = requestAnimationFrame ( ( ) => setSkipResourceTransition ( false ) )
373392 return ( ) => cancelAnimationFrame ( id )
374- } , [ resources ] )
393+ } , [ resources , setResourceCollapsed ] )
375394
376395 useEffect ( ( ) => {
377396 if ( resources . length === 0 && ! isResourceCollapsedRef . current ) {
378397 clearWidth ( )
379- setIsResourceCollapsed ( true )
398+ setResourceCollapsed ( true )
380399 }
381- } , [ resources , clearWidth ] )
400+ } , [ resources , clearWidth , setResourceCollapsed ] )
382401
383402 useEffect ( ( ) => {
384403 const resourceIds = new Set ( resources . map ( ( resource ) => resource . id ) )
@@ -413,11 +432,10 @@ export function Home({ chatId, userName, userId }: HomeProps) {
413432 setIsInputEntering ( true )
414433 }
415434
416- userOwnsResourceViewRef . current = false
417- setResourceActivityIds ( new Set ( ) )
435+ prepareResourceViewForAgentTurn ( )
418436 sendMessage ( trimmed || 'Analyze the attached file(s).' , fileAttachments , contexts )
419437 } ,
420- [ workspaceId , chatId , sendMessage ]
438+ [ workspaceId , chatId , prepareResourceViewForAgentTurn , sendMessage ]
421439 )
422440
423441 /**
@@ -431,13 +449,14 @@ export function Home({ chatId, userName, userId }: HomeProps) {
431449 const detail = ( e as CustomEvent < MothershipSendMessageDetail > ) . detail
432450 if ( ! detail ?. message ) return
433451 e . preventDefault ( )
452+ prepareResourceViewForAgentTurn ( )
434453 sendMessage ( detail . message , detail . fileAttachments , detail . contexts , {
435454 ...( detail . resumeUserMessageId ? { resumeUserMessageId : detail . resumeUserMessageId } : { } ) ,
436455 } )
437456 }
438457 window . addEventListener ( MOTHERSHIP_SEND_MESSAGE_EVENT , handler )
439458 return ( ) => window . removeEventListener ( MOTHERSHIP_SEND_MESSAGE_EVENT , handler )
440- } , [ sendMessage ] )
459+ } , [ prepareResourceViewForAgentTurn , sendMessage ] )
441460
442461 /**
443462 * Consumes a one-shot handoff left by another surface and applies it to this
@@ -462,6 +481,7 @@ export function Home({ chatId, userName, userId }: HomeProps) {
462481 const handoff = MothershipHandoffStorage . consume ( workspaceId )
463482 if ( ! handoff ) return
464483 if ( handoff . message ) {
484+ prepareResourceViewForAgentTurn ( )
465485 sendMessage ( handoff . message , handoff . fileAttachments , handoff . contexts , {
466486 ...( handoff . resumeUserMessageId
467487 ? { resumeUserMessageId : handoff . resumeUserMessageId }
@@ -477,7 +497,7 @@ export function Home({ chatId, userName, userId }: HomeProps) {
477497 // keep it one-shot — and harmless either way, since `consume` clears the entry
478498 // atomically and any re-run would find nothing.
479499 // eslint-disable-next-line react-hooks/exhaustive-deps -- see above
480- } , [ chatId , workspaceId , sendMessage ] )
500+ } , [ chatId , workspaceId , prepareResourceViewForAgentTurn , sendMessage ] )
481501
482502 function resolveResourceFromContext (
483503 context : ChatContext
@@ -583,6 +603,12 @@ export function Home({ chatId, userName, userId }: HomeProps) {
583603 const hasMessages = messages . length > 0
584604 const showChatSkeleton = Boolean ( chatId ) && ! hasMessages && isChatHistoryPending
585605 const draftScopeKey = `${ workspaceId } :${ chatId ?? 'new' } `
606+ const resourceActivityCount = resourceActivityIds . size
607+ const resourceToggleLabel = isResourceCollapsed
608+ ? resourceActivityCount > 0
609+ ? `Expand resource view, ${ resourceActivityCount } resource${ resourceActivityCount === 1 ? '' : 's' } updated`
610+ : 'Expand resource view'
611+ : 'Collapse resource view'
586612
587613 // The empty state is the chat pane's content, not a layout of its own. It
588614 // used to return early, which meant the resource panel and its toggle did
@@ -720,13 +746,16 @@ export function Home({ chatId, userName, userId }: HomeProps) {
720746 size = { null }
721747 type = 'button'
722748 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' }
749+ 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)]"
750+ aria-label = { resourceToggleLabel }
725751 >
726752 < span className = 'relative' >
727753 < PanelLeft className = '-scale-x-100 size-[16px] text-[var(--text-icon)]' />
728754 { isResourceCollapsed && resourceActivityIds . size > 0 && (
729- < span className = '-top-0.5 -right-0.5 absolute size-1.5 rounded-full bg-[var(--brand-primary)]' />
755+ < span
756+ aria-hidden = 'true'
757+ className = '-top-0.5 -right-0.5 absolute size-1.5 rounded-full bg-[var(--brand-primary)]'
758+ />
730759 ) }
731760 </ span >
732761 </ Button >
0 commit comments