11import type { DevframeClientCommand , DevframeDockEntry , DevframeDockUserEntry , DevframeRpcClientFunctions , DevframeViewIframe } from '@devframes/hub'
2- import type { CommandsContext , DevframeClientContext , DevframeRpcClient , DockClientScriptContext , DockEntryState , DockPanelStorage , DockRegistration , DockRendererManifest , DocksContext } from '@devframes/hub/client'
2+ import type { CommandsContext , DevframeClientContext , DevframeRpcClient , DockClientScriptContext , DockEntryState , DockPanelStorage , DockRegistration , DockRendererManifest , DocksContext , DockSessionStorage } from '@devframes/hub/client'
33import type { SharedState } from 'devframe/utils/shared-state'
44import type { WhenContext } from 'devframe/utils/when'
55import type { Ref } from 'vue'
@@ -11,7 +11,7 @@ import { BUILTIN_ENTRIES, BUILTIN_ENTRY_SETTINGS, DEFAULT_CATEGORIES_ORDER, HUB_
1111import { useBranding } from './branding'
1212import { createCommandsContext } from './commands'
1313import { docksGroupByCategories , getCategoryLabel , getGroupMembers , getGroupMembersGrouped , getRegisteredGroupIds , resolveCommandIcon , resolveGroupDefaultChild } from './dock-settings'
14- import { createDockEntryState , DEFAULT_DOCK_PANEL_STORE , sharedStateToRef , useDocksEntries } from './docks'
14+ import { createDockEntryState , DEFAULT_DOCK_PANEL_STORE , DEFAULT_DOCK_SESSION_STORE , sharedStateToRef , useDocksEntries } from './docks'
1515import { createClientMessagesClient } from './messages-client'
1616import { registerMainFrameDockActionHandler , triggerMainFrameDockAction , useIsDockPopupOpen } from './popup'
1717import { executeSetupScript } from './setup-script'
@@ -21,6 +21,7 @@ export async function createDocksContext(
2121 clientType : 'embedded' | 'standalone' ,
2222 rpc : DevframeRpcClient ,
2323 panelStore ?: Ref < DockPanelStorage > ,
24+ sessionStore ?: Ref < DockSessionStorage > ,
2425) : Promise < DocksContext > {
2526 if ( docksContextByRpc . has ( rpc ) ) {
2627 return docksContextByRpc . get ( rpc ) !
@@ -72,7 +73,29 @@ export async function createDocksContext(
7273 return [ ...base , BUILTIN_ENTRY_SETTINGS ]
7374 } )
7475
75- const selectedId = ref < string | null > ( null )
76+ // Per-tab session UI state (open/selectedId/route). A caller (the embedded and
77+ // standalone bootstraps) passes a `sessionStorage`-backed ref so it survives a
78+ // reload; stories and the default path fall back to an in-memory ref.
79+ sessionStore ||= ref ( DEFAULT_DOCK_SESSION_STORE ( ) )
80+
81+ // Snapshot the persisted intent up front, before the pre-handshake untrusted
82+ // window (Dock.vue's `open`-gate, a revocation `switchEntry(null)`) can clear
83+ // the live session state. Re-applied once the RPC becomes trusted so a reload
84+ // lands back on the same dock — see the restore effect near the end.
85+ const restoreIntent = {
86+ open : sessionStore . value . open ,
87+ selectedId : sessionStore . value . selectedId ,
88+ route : sessionStore . value . route ,
89+ }
90+
91+ // `selectedId` is backed by the session store so the current selection both
92+ // drives the UI and persists across reloads through one source of truth.
93+ const selectedId = computed < string | null > ( {
94+ get : ( ) => sessionStore . value . selectedId ,
95+ set : ( id ) => {
96+ sessionStore . value . selectedId = id
97+ } ,
98+ } )
7699 const selected = computed (
77100 ( ) => entries . value . find ( entry => entry . id === selectedId . value )
78101 ?? BUILTIN_ENTRIES . find ( entry => entry . id === selectedId . value )
@@ -154,7 +177,7 @@ export async function createDocksContext(
154177 const isDockPopupOpen = useIsDockPopupOpen ( )
155178 const getWhenContext = ( ) : WhenContext => ( {
156179 clientType,
157- dockOpen : panelStore . value . open ,
180+ dockOpen : sessionStore . value . open ,
158181 paletteOpen : commandsContext ?. paletteOpen ?? false ,
159182 dockSelectedId : selectedId . value ?? '' ,
160183 popupOpen : isDockPopupOpen . value ,
@@ -173,12 +196,13 @@ export async function createDocksContext(
173196 const switchEntry = async ( id : string | null = null ) => {
174197 if ( id == null ) {
175198 selectedId . value = null
176- panelStore . value . open = false
199+ sessionStore . value . open = false
200+ sessionStore . value . route = null
177201 return true
178202 }
179203 if ( id === '~client-auth-notice' ) {
180204 selectedId . value = id
181- panelStore . value . open = true
205+ sessionStore . value . open = true
182206 return true
183207 }
184208 const entry = entries . value . find ( e => e . id === id )
@@ -245,7 +269,12 @@ export async function createDocksContext(
245269 frameNavCurrentMember . set ( entry . frameId , entry . id )
246270
247271 selectedId . value = entry . id
248- panelStore . value . open = true
272+ sessionStore . value . open = true
273+ // Only an iframe dock owns an address-bar route; ViewIframe keeps
274+ // `session.route` current for it. Clear it for anything else so a stale
275+ // route from a previous iframe isn't persisted against a non-iframe dock.
276+ if ( entry . type !== 'iframe' )
277+ sessionStore . value . route = null
249278 return true
250279 }
251280
@@ -385,7 +414,7 @@ export async function createDocksContext(
385414 when : 'dockOpen && !paletteOpen' ,
386415 keybindings : [ { key : 'Escape' } ] ,
387416 action : ( ) => {
388- panelStore . value . open = false
417+ sessionStore . value . open = false
389418 selectedId . value = null
390419 } ,
391420 } ,
@@ -501,12 +530,27 @@ export async function createDocksContext(
501530 }
502531 } )
503532
533+ // One-shot boot route (see `DocksPanelContext.consumeBootRoute`): the persisted
534+ // address-bar URL is handed back to exactly the iframe dock that was selected
535+ // before the reload, once.
536+ let bootRoute : string | null = restoreIntent . selectedId != null ? restoreIntent . route : null
537+ const consumeBootRoute = ( id : string ) : string | null => {
538+ if ( bootRoute != null && id === restoreIntent . selectedId ) {
539+ const route = bootRoute
540+ bootRoute = null
541+ return route
542+ }
543+ return null
544+ }
545+
504546 docksContext = reactive ( {
505547 panel : {
506548 store : panelStore ,
549+ session : sessionStore ,
507550 isDragging : false ,
508551 isResizing : false ,
509552 isVertical : computed ( ( ) => panelStore . value . position === 'left' || panelStore . value . position === 'right' ) ,
553+ consumeBootRoute,
510554 } ,
511555 docks : {
512556 selectedId,
@@ -552,6 +596,29 @@ export async function createDocksContext(
552596 return switchEntry ( entry . id )
553597 } )
554598
599+ // Restore the persisted selection once the RPC is trusted. A reload starts
600+ // untrusted, and Dock.vue force-closes the panel during that window (and a
601+ // revocation clears the selection), so the durable intent captured in
602+ // `restoreIntent` is re-applied here after the handshake — re-running the
603+ // dock's setup script and re-opening the panel on the dock the developer left
604+ // open. `switchEntry` reads `session.route` back through `consumeBootRoute`
605+ // when the restored iframe boots.
606+ const applyRestore = ( ) : void => {
607+ if ( restoreIntent . open && restoreIntent . selectedId != null )
608+ void switchEntry ( restoreIntent . selectedId )
609+ }
610+ if ( rpc . isTrusted ) {
611+ applyRestore ( )
612+ }
613+ else {
614+ const off = rpc . events . on ( 'rpc:is-trusted:updated' , ( isTrusted ) => {
615+ if ( ! isTrusted )
616+ return
617+ off ( )
618+ applyRestore ( )
619+ } )
620+ }
621+
555622 docksContextByRpc . set ( rpc , docksContext )
556623 return docksContext
557624}
0 commit comments