@@ -8,24 +8,8 @@ export interface DesktopTab {
88 title : string
99}
1010
11- export interface DesktopTabResourceCallbacks {
12- /** Adds a tab without activating it; activation goes through {@link onResourceEvent}. */
13- addResource : ( resource : MothershipResource ) => void
14- removeResource : ( resourceType : MothershipResourceType , resourceId : string ) => void
15- /** Explicit user selection, which claims the strip's selection for the user. */
16- selectResource : ( resourceId : string ) => void
17- /**
18- * Adopts the desktop app's remembered tab as the shown resource without
19- * claiming the selection for the user, so agent activity can still take the
20- * view the way it does on any chat open.
21- */
22- restoreResource : ( resourceId : string ) => void
23- /** Agent activity on a tab, subject to the panel's user-ownership policy. */
24- onResourceEvent : ResourceEventHandler
25- }
26-
2711/** What the strip shares with every kind of desktop-backed resource tab. */
28- export interface DesktopTabStripOptions extends DesktopTabResourceCallbacks {
12+ export interface DesktopTabStripOptions {
2913 /** Desktop scope whose live tabs back this chat's resource tabs. */
3014 scopeId : string
3115 resources : readonly MothershipResource [ ]
@@ -35,15 +19,24 @@ export interface DesktopTabStripOptions extends DesktopTabResourceCallbacks {
3519 selectedResourceId : string | null
3620 /**
3721 * Whether the chat's stored resources have been applied to the strip.
38- *
39- * Adopting a tab writes it to `activeResourceId`, which is the one place the
40- * rest of the surface reads as the shown resource, so adopting on top of a
22+ * Adopting a tab writes it to `activeResourceId`, so adopting on top of a
4123 * provisional fallback would let the arrival order of the tab list and the
42- * chat history decide what the chat opens on. Waiting makes the outcome the
43- * same either way: the history pins a stored resource, or it pins nothing
44- * and the desktop app's remembered tab stands.
24+ * chat history decide what the chat opens on.
4525 */
4626 hydrated : boolean
27+ /** Adds a tab without activating it; activation goes through {@link onResourceEvent}. */
28+ addResource : ( resource : MothershipResource ) => void
29+ removeResource : ( resourceType : MothershipResourceType , resourceId : string ) => void
30+ /** Explicit user selection, which claims the strip's selection for the user. */
31+ selectResource : ( resourceId : string ) => void
32+ /**
33+ * Adopts the desktop app's remembered tab as the shown resource without
34+ * claiming the selection for the user, so agent activity can still take the
35+ * view the way it does on any chat open.
36+ */
37+ restoreResource : ( resourceId : string ) => void
38+ /** Agent activity on a tab, subject to the panel's user-ownership policy. */
39+ onResourceEvent : ResourceEventHandler
4740}
4841
4942interface UseDesktopTabResourcesOptions extends DesktopTabStripOptions {
@@ -65,21 +58,40 @@ interface UseDesktopTabResourcesOptions extends DesktopTabStripOptions {
6558}
6659
6760/**
68- * The desktop app's active tab to adopt in place of the strip's fallback: one
69- * the strip does not show yet, of the same kind as the fallback, and still in
70- * the strip — a tab just closed there stays the desktop app's active tab until
71- * the close lands.
61+ * The desktop app's active tab when it is not the tab the strip shows, and the
62+ * strip is on one of this kind. Null when the two already agree or the strip
63+ * is showing something else entirely.
7264 */
73- function nativeTabToAdopt (
65+ function nativeTabOffStrip (
7466 resources : readonly MothershipResource [ ] ,
7567 activeResourceId : string | null ,
7668 activeTabId : string | null ,
7769 type : MothershipResourceType
7870) : string | null {
7971 if ( ! activeTabId || activeTabId === activeResourceId ) return null
80- if ( resources . find ( ( resource ) => resource . id === activeResourceId ) ?. type !== type ) return null
81- const live = resources . some ( ( resource ) => resource . type === type && resource . id === activeTabId )
82- return live ? activeTabId : null
72+ return resources . find ( ( resource ) => resource . id === activeResourceId ) ?. type === type
73+ ? activeTabId
74+ : null
75+ }
76+
77+ /**
78+ * The same tab, narrowed to one the strip still holds as a resource: a tab
79+ * just closed there stays the desktop app's active tab until the close lands,
80+ * and adopting it would show a tab that is gone. Following a switch the user
81+ * made needs no such check — a brand-new tab is followed before the strip has
82+ * projected it.
83+ */
84+ function nativeTabToAdopt (
85+ resources : readonly MothershipResource [ ] ,
86+ activeResourceId : string | null ,
87+ activeTabId : string | null ,
88+ type : MothershipResourceType
89+ ) : string | null {
90+ const tabId = nativeTabOffStrip ( resources , activeResourceId , activeTabId , type )
91+ if ( ! tabId ) return null
92+ return resources . some ( ( resource ) => resource . type === type && resource . id === tabId )
93+ ? tabId
94+ : null
8395}
8496
8597/**
@@ -131,8 +143,6 @@ export function useDesktopTabResources({
131143 const requestedTabIdRef = useRef < string | null > ( null )
132144 const scopeIdRef = useRef ( scopeId )
133145 scopeIdRef . current = scopeId
134- const tabsRef = useRef ( tabs )
135- tabsRef . current = tabs
136146 const activeTabIdRef = useRef ( activeTabId )
137147 activeTabIdRef . current = activeTabId
138148 const resourcesRef = useRef ( resources )
@@ -141,6 +151,8 @@ export function useDesktopTabResources({
141151 activeResourceIdRef . current = activeResourceId
142152 /** Whether the strip shows an explicit selection rather than its fallback. */
143153 const explicitSelection = selectedResourceId !== null && selectedResourceId === activeResourceId
154+ const explicitSelectionRef = useRef ( explicitSelection )
155+ explicitSelectionRef . current = explicitSelection
144156 const hydratedRef = useRef ( hydrated )
145157 hydratedRef . current = hydrated
146158 /**
@@ -187,7 +199,6 @@ export function useDesktopTabResources({
187199 }
188200 } , [ addResource , hasSession , removeResource , resources , scopeId , tabs , type ] )
189201
190- /** Whether the selected resource is one of this kind's live tabs. */
191202 const selectedTabIsLive =
192203 selectedResourceId !== null && tabs . some ( ( tab ) => tab . id === selectedResourceId )
193204
@@ -232,15 +243,13 @@ export function useDesktopTabResources({
232243 }
233244 const activeResourceId = activeResourceIdRef . current
234245 if ( previousActiveTabId !== null ) {
235- const activeResource = resourcesRef . current . find (
236- ( resource ) => resource . id === activeResourceId
237- )
238- if ( activeTabId && activeResource ?. type === type && activeResource . id !== activeTabId ) {
239- selectResourceRef . current ( activeTabId )
240- }
246+ const tabId = nativeTabOffStrip ( resourcesRef . current , activeResourceId , activeTabId , type )
247+ if ( tabId ) selectResourceRef . current ( tabId )
241248 return
242249 }
243- if ( ! hydratedRef . current ) return
250+ // Same guards as the adopt effect above: a first report must not override
251+ // a selection the user made before the tab list arrived.
252+ if ( ! hydratedRef . current || explicitSelectionRef . current ) return
244253 const tabId = nativeTabToAdopt ( resourcesRef . current , activeResourceId , activeTabId , type )
245254 if ( tabId ) restoreResourceRef . current ( tabId )
246255 } , [ activeTabId , type ] )
0 commit comments