From 43f119853d810025f1ebfe29e804490fce9985dd Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Mon, 17 Aug 2026 04:34:16 +0000 Subject: [PATCH 1/3] feat(hub): restore dock open/selection/route across reloads via session store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a per-tab `DockSessionStorage` (open, selectedId, route) distinct from the browser-shared `DockPanelStorage` geometry. `open` moves out of the panel store into this session store, joined by the selected dock id and the selected iframe dock's live address-bar route. The embedded and standalone bootstraps persist it to `sessionStorage`, and the docks context re-applies the persisted selection once the RPC handshake makes the client trusted — surviving the pre-handshake untrusted window that force-closes the panel. A restored iframe boots deep-linked to its saved route via a one-shot `consumeBootRoute`. Created with the help of an agent. --- .../src/client/components/dock/Dock.vue | 8 +- .../components/dock/DockEdge.stories.ts | 8 +- .../src/client/components/dock/DockEdge.vue | 2 +- .../components/dock/DockEmbedded.stories.ts | 2 +- .../client/components/dock/DockEmbedded.vue | 2 +- .../client/components/views/ViewIframe.vue | 20 ++++- packages/hub-ui/src/client/embedded/index.ts | 18 +++- packages/hub-ui/src/client/standalone/main.ts | 13 ++- packages/hub-ui/src/client/state/context.ts | 83 +++++++++++++++++-- packages/hub-ui/src/client/state/docks.ts | 18 +++- .../hub-ui/src/client/stories/mock-context.ts | 14 ++-- .../hub/src/client/__tests__/host.test.ts | 2 +- packages/hub/src/client/docks.ts | 40 ++++++++- packages/hub/src/client/host.ts | 16 +++- .../@devframes/hub/client.snapshot.d.ts | 8 +- 15 files changed, 217 insertions(+), 37 deletions(-) diff --git a/packages/hub-ui/src/client/components/dock/Dock.vue b/packages/hub-ui/src/client/components/dock/Dock.vue index bca2578e..9545a95e 100644 --- a/packages/hub-ui/src/client/components/dock/Dock.vue +++ b/packages/hub-ui/src/client/components/dock/Dock.vue @@ -86,7 +86,7 @@ const isRpcTrusted = useIsRpcTrusted(context, (isTrusted) => { else if (!isTrusted) { // On revocation: close current tab and panel context.docks.switchEntry(null) - context.panel.store.open = false + context.panel.session.open = false } }) @@ -183,7 +183,7 @@ const isMinimized = computed(() => { // @ts-expect-error compatibility const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 return !context.panel.isDragging - && !context.panel.store.open + && !context.panel.session.open && !isHovering.value && !isTouchDevice && context.panel.store.inactiveTimeout @@ -218,8 +218,8 @@ whenever(isMinimized, () => { }) onMounted(() => { - if (context.panel.store.open && !isRpcTrusted.value) - context.panel.store.open = false + if (context.panel.session.open && !isRpcTrusted.value) + context.panel.session.open = false if (isRpcTrusted.value) bringUp() recalculateCounter.value++ diff --git a/packages/hub-ui/src/client/components/dock/DockEdge.stories.ts b/packages/hub-ui/src/client/components/dock/DockEdge.stories.ts index 1dd68a1e..9fd837ab 100644 --- a/packages/hub-ui/src/client/components/dock/DockEdge.stories.ts +++ b/packages/hub-ui/src/client/components/dock/DockEdge.stories.ts @@ -38,7 +38,8 @@ function edgeStory(position: 'top' | 'right' | 'bottom' | 'left', open = true) { { entries: categorizedEntries, selectedId: open ? 'overview' : null, - panel: { mode: 'edge', position, open, height: 40, width: 30 }, + panel: { mode: 'edge', position, height: 40, width: 30 }, + session: { open }, }, ctx => [ h(DockEdge, { context: ctx }, { view: ({ entry }: any) => body(entry) }), @@ -75,7 +76,8 @@ export const CollapsedIdle: Story = { { entries: categorizedEntries, selectedId: null, - panel: { mode: 'edge', position: 'bottom', open: false, inactiveTimeout: 0 }, + panel: { mode: 'edge', position: 'bottom', inactiveTimeout: 0 }, + session: { open: false }, settings: { autoCollapseEdgeToolbar: true }, }, ctx => [ @@ -93,7 +95,7 @@ export const WithGroup: Story = { { entries: groupedEntries, selectedId: 'nuxt:overview', - panel: { mode: 'edge', position: 'bottom', open: true, height: 45 }, + panel: { mode: 'edge', position: 'bottom', height: 45 }, }, ctx => [ h(DockEdge, { context: ctx }, { view: ({ entry }: any) => body(entry) }), diff --git a/packages/hub-ui/src/client/components/dock/DockEdge.vue b/packages/hub-ui/src/client/components/dock/DockEdge.vue index 64b13995..0c3c81ef 100644 --- a/packages/hub-ui/src/client/components/dock/DockEdge.vue +++ b/packages/hub-ui/src/client/components/dock/DockEdge.vue @@ -37,7 +37,7 @@ const selectedEntry = computed(() => context.docks.selected) const activeGroup = computed(() => getEntryGroup(context.docks.entries, selectedEntry.value)) const hasPanelContent = computed(() => { const entry = selectedEntry.value - return context.panel.store.open + return context.panel.session.open && !!entry && entry.type !== 'action' }) diff --git a/packages/hub-ui/src/client/components/dock/DockEmbedded.stories.ts b/packages/hub-ui/src/client/components/dock/DockEmbedded.stories.ts index a3c110ef..c1278997 100644 --- a/packages/hub-ui/src/client/components/dock/DockEmbedded.stories.ts +++ b/packages/hub-ui/src/client/components/dock/DockEmbedded.stories.ts @@ -69,7 +69,7 @@ export const FloatReducedOverlap: Story = { export const Edge: Story = { render: () => ({ setup: () => mountWithContext( - { entries: blankEntries, selectedId: 'overview', panel: { mode: 'edge', position: 'bottom', open: true, height: 45 } }, + { entries: blankEntries, selectedId: 'overview', panel: { mode: 'edge', position: 'bottom', height: 45 } }, ctx => h(DockEmbedded, { context: ctx }), ), }), diff --git a/packages/hub-ui/src/client/components/dock/DockEmbedded.vue b/packages/hub-ui/src/client/components/dock/DockEmbedded.vue index b3d94b9e..ddc46058 100644 --- a/packages/hub-ui/src/client/components/dock/DockEmbedded.vue +++ b/packages/hub-ui/src/client/components/dock/DockEmbedded.vue @@ -33,7 +33,7 @@ useEventListener(window, 'mousedown', (e: MouseEvent) => { return if (isDockPopupOpen.value) return - if (!props.context.panel.store.open || props.context.panel.isDragging || props.context.panel.isResizing) + if (!props.context.panel.session.open || props.context.panel.isDragging || props.context.panel.isResizing) return const matched = e.composedPath().find((_el) => { diff --git a/packages/hub-ui/src/client/components/views/ViewIframe.vue b/packages/hub-ui/src/client/components/views/ViewIframe.vue index 23c13ae1..1c386305 100644 --- a/packages/hub-ui/src/client/components/views/ViewIframe.vue +++ b/packages/hub-ui/src/client/components/views/ViewIframe.vue @@ -224,12 +224,21 @@ let onIframeLoad: (() => void) | undefined onMounted(() => { const existed = props.panes.has(paneKey.value) + // Restore the address-bar route persisted before the last reload: the dock + // that was selected then boots deep-linked to where the developer left it, + // instead of the entry's default url. `consumeBootRoute` hands the saved URL + // back only for that dock, and only once, so a later switch can't reuse it. + const bootUrl = props.context.panel.consumeBootRoute?.(props.entry.id) ?? props.entry.url + if (!existed && bootUrl !== currentUrl.value) { + currentUrl.value = bootUrl + editingUrl.value = bootUrl + } // `src` is only assigned when the pane is first created, so re-mounting an // existing iframe (tab switch) preserves its navigation/scroll/JS state. For // a shared frame this is also the boot deep-link: the first member (or the // anchor) to become visible seeds the src, and every later switch soft-navs. const pane = props.panes.ensure(paneKey.value, { - src: props.entry.url, + src: bootUrl, style: { boxShadow: 'none', outline: 'none' }, }) const iframe = pane.iframe @@ -237,6 +246,15 @@ onMounted(() => { if (existed) updateCurrentUrl() + // Persist this dock's live route while it is the selected one, so the next + // reload can restore it. Only the selected dock writes, so switching docks + // never overwrites another's saved route. + const panelSession = props.context.panel.session + watchEffect(() => { + if (props.context.docks.selectedId === props.entry.id) + panelSession.route = currentUrl.value + }) + // Listen for iframe load events onIframeLoad = () => { isIframeLoading.value = false diff --git a/packages/hub-ui/src/client/embedded/index.ts b/packages/hub-ui/src/client/embedded/index.ts index e907cb19..28c300a8 100644 --- a/packages/hub-ui/src/client/embedded/index.ts +++ b/packages/hub-ui/src/client/embedded/index.ts @@ -1,8 +1,8 @@ -import type { DockPanelStorage } from '@devframes/hub/client' +import type { DockPanelStorage, DockSessionStorage } from '@devframes/hub/client' import { getDevframeRpcClient, setDevframeClientContext } from '@devframes/hub/client' -import { useLocalStorage } from '@vueuse/core' +import { useLocalStorage, useSessionStorage } from '@vueuse/core' import { applyPrimaryColor, setBranding } from '../state/branding' -import { DEFAULT_DOCK_PANEL_STORE } from '../state/docks' +import { DEFAULT_DOCK_PANEL_STORE, DEFAULT_DOCK_SESSION_STORE } from '../state/docks' import { setupEmbeddedVisibility } from './visibility' /** @@ -59,13 +59,23 @@ async function mountDock(): Promise { { mergeDefaults: true }, ) + // Per-tab session UI state (open dock + its route). `sessionStorage`, not + // `localStorage`: selection is per-tab navigation state, so two tabs against + // the same server keep their own rather than fighting over a shared one. It + // survives a reload and is restored after the auth handshake. + const session = useSessionStorage( + 'devframes-dock-session', + DEFAULT_DOCK_SESSION_STORE(), + { mergeDefaults: true }, + ) + // Resolve branding before the dock exists so the primary color and logo are // in place on the first paint. Read from `ConnectionMeta.configs.ui.branding`, // carried by the connection we just established above. const branding = setBranding(rpc.connectionMeta.configs?.ui?.branding || {}) const { createDocksContext } = await import('../state/context') - const context = await createDocksContext('embedded', rpc, state) + const context = await createDocksContext('embedded', rpc, state, session) setDevframeClientContext(context) const { DockEmbedded } = await import('../components/DockEmbedded') diff --git a/packages/hub-ui/src/client/standalone/main.ts b/packages/hub-ui/src/client/standalone/main.ts index 39dabd12..9ed17874 100644 --- a/packages/hub-ui/src/client/standalone/main.ts +++ b/packages/hub-ui/src/client/standalone/main.ts @@ -1,7 +1,10 @@ +import type { DockSessionStorage } from '@devframes/hub/client' import { getDevframeRpcClient, setDevframeClientContext } from '@devframes/hub/client' +import { useSessionStorage } from '@vueuse/core' import { watchEffect } from 'vue' import { applyDocumentHead, applyPrimaryColor, setBranding } from '../state/branding' import { isDark } from '../state/color-mode' +import { DEFAULT_DOCK_SESSION_STORE } from '../state/docks' // The standalone viewer — a vanilla shell served at the hub base itself // (`DevframeHubUi.viewer`): resolve the shared connection, build the docks @@ -37,8 +40,16 @@ async function main(): Promise { const branding = setBranding(rpc.connectionMeta.configs?.ui?.branding || {}) applyDocumentHead(document, branding) + // Per-tab session UI state (which dock is open + its route). `sessionStorage` + // so a reload restores the selection after the auth handshake, per-tab. + const session = useSessionStorage( + 'devframes-dock-session', + DEFAULT_DOCK_SESSION_STORE(), + { mergeDefaults: true }, + ) + const { createDocksContext } = await import('../state/context') - const context = await createDocksContext('standalone', rpc) + const context = await createDocksContext('standalone', rpc, undefined, session) setDevframeClientContext(context) const { DockStandalone } = await import('../components/DockStandalone') diff --git a/packages/hub-ui/src/client/state/context.ts b/packages/hub-ui/src/client/state/context.ts index 437180d7..5ae8546d 100644 --- a/packages/hub-ui/src/client/state/context.ts +++ b/packages/hub-ui/src/client/state/context.ts @@ -1,5 +1,5 @@ import type { DevframeClientCommand, DevframeDockEntry, DevframeDockUserEntry, DevframeRpcClientFunctions, DevframeViewIframe } from '@devframes/hub' -import type { CommandsContext, DevframeClientContext, DevframeRpcClient, DockClientScriptContext, DockEntryState, DockPanelStorage, DockRegistration, DockRendererManifest, DocksContext } from '@devframes/hub/client' +import type { CommandsContext, DevframeClientContext, DevframeRpcClient, DockClientScriptContext, DockEntryState, DockPanelStorage, DockRegistration, DockRendererManifest, DocksContext, DockSessionStorage } from '@devframes/hub/client' import type { SharedState } from 'devframe/utils/shared-state' import type { WhenContext } from 'devframe/utils/when' import type { Ref } from 'vue' @@ -11,7 +11,7 @@ import { BUILTIN_ENTRIES, BUILTIN_ENTRY_SETTINGS, DEFAULT_CATEGORIES_ORDER, HUB_ import { useBranding } from './branding' import { createCommandsContext } from './commands' import { docksGroupByCategories, getCategoryLabel, getGroupMembers, getGroupMembersGrouped, getRegisteredGroupIds, resolveCommandIcon, resolveGroupDefaultChild } from './dock-settings' -import { createDockEntryState, DEFAULT_DOCK_PANEL_STORE, sharedStateToRef, useDocksEntries } from './docks' +import { createDockEntryState, DEFAULT_DOCK_PANEL_STORE, DEFAULT_DOCK_SESSION_STORE, sharedStateToRef, useDocksEntries } from './docks' import { createClientMessagesClient } from './messages-client' import { registerMainFrameDockActionHandler, triggerMainFrameDockAction, useIsDockPopupOpen } from './popup' import { executeSetupScript } from './setup-script' @@ -21,6 +21,7 @@ export async function createDocksContext( clientType: 'embedded' | 'standalone', rpc: DevframeRpcClient, panelStore?: Ref, + sessionStore?: Ref, ): Promise { if (docksContextByRpc.has(rpc)) { return docksContextByRpc.get(rpc)! @@ -72,7 +73,29 @@ export async function createDocksContext( return [...base, BUILTIN_ENTRY_SETTINGS] }) - const selectedId = ref(null) + // Per-tab session UI state (open/selectedId/route). A caller (the embedded and + // standalone bootstraps) passes a `sessionStorage`-backed ref so it survives a + // reload; stories and the default path fall back to an in-memory ref. + sessionStore ||= ref(DEFAULT_DOCK_SESSION_STORE()) + + // Snapshot the persisted intent up front, before the pre-handshake untrusted + // window (Dock.vue's `open`-gate, a revocation `switchEntry(null)`) can clear + // the live session state. Re-applied once the RPC becomes trusted so a reload + // lands back on the same dock — see the restore effect near the end. + const restoreIntent = { + open: sessionStore.value.open, + selectedId: sessionStore.value.selectedId, + route: sessionStore.value.route, + } + + // `selectedId` is backed by the session store so the current selection both + // drives the UI and persists across reloads through one source of truth. + const selectedId = computed({ + get: () => sessionStore.value.selectedId, + set: (id) => { + sessionStore.value.selectedId = id + }, + }) const selected = computed( () => entries.value.find(entry => entry.id === selectedId.value) ?? BUILTIN_ENTRIES.find(entry => entry.id === selectedId.value) @@ -154,7 +177,7 @@ export async function createDocksContext( const isDockPopupOpen = useIsDockPopupOpen() const getWhenContext = (): WhenContext => ({ clientType, - dockOpen: panelStore.value.open, + dockOpen: sessionStore.value.open, paletteOpen: commandsContext?.paletteOpen ?? false, dockSelectedId: selectedId.value ?? '', popupOpen: isDockPopupOpen.value, @@ -173,12 +196,13 @@ export async function createDocksContext( const switchEntry = async (id: string | null = null) => { if (id == null) { selectedId.value = null - panelStore.value.open = false + sessionStore.value.open = false + sessionStore.value.route = null return true } if (id === '~client-auth-notice') { selectedId.value = id - panelStore.value.open = true + sessionStore.value.open = true return true } const entry = entries.value.find(e => e.id === id) @@ -245,7 +269,12 @@ export async function createDocksContext( frameNavCurrentMember.set(entry.frameId, entry.id) selectedId.value = entry.id - panelStore.value.open = true + sessionStore.value.open = true + // Only an iframe dock owns an address-bar route; ViewIframe keeps + // `session.route` current for it. Clear it for anything else so a stale + // route from a previous iframe isn't persisted against a non-iframe dock. + if (entry.type !== 'iframe') + sessionStore.value.route = null return true } @@ -385,7 +414,7 @@ export async function createDocksContext( when: 'dockOpen && !paletteOpen', keybindings: [{ key: 'Escape' }], action: () => { - panelStore.value.open = false + sessionStore.value.open = false selectedId.value = null }, }, @@ -501,12 +530,27 @@ export async function createDocksContext( } }) + // One-shot boot route (see `DocksPanelContext.consumeBootRoute`): the persisted + // address-bar URL is handed back to exactly the iframe dock that was selected + // before the reload, once. + let bootRoute: string | null = restoreIntent.selectedId != null ? restoreIntent.route : null + const consumeBootRoute = (id: string): string | null => { + if (bootRoute != null && id === restoreIntent.selectedId) { + const route = bootRoute + bootRoute = null + return route + } + return null + } + docksContext = reactive({ panel: { store: panelStore, + session: sessionStore, isDragging: false, isResizing: false, isVertical: computed(() => panelStore.value.position === 'left' || panelStore.value.position === 'right'), + consumeBootRoute, }, docks: { selectedId, @@ -552,6 +596,29 @@ export async function createDocksContext( return switchEntry(entry.id) }) + // Restore the persisted selection once the RPC is trusted. A reload starts + // untrusted, and Dock.vue force-closes the panel during that window (and a + // revocation clears the selection), so the durable intent captured in + // `restoreIntent` is re-applied here after the handshake — re-running the + // dock's setup script and re-opening the panel on the dock the developer left + // open. `switchEntry` reads `session.route` back through `consumeBootRoute` + // when the restored iframe boots. + const applyRestore = (): void => { + if (restoreIntent.open && restoreIntent.selectedId != null) + void switchEntry(restoreIntent.selectedId) + } + if (rpc.isTrusted) { + applyRestore() + } + else { + const off = rpc.events.on('rpc:is-trusted:updated', (isTrusted) => { + if (!isTrusted) + return + off() + applyRestore() + }) + } + docksContextByRpc.set(rpc, docksContext) return docksContext } diff --git a/packages/hub-ui/src/client/state/docks.ts b/packages/hub-ui/src/client/state/docks.ts index 88f70b6a..208ab6fc 100644 --- a/packages/hub-ui/src/client/state/docks.ts +++ b/packages/hub-ui/src/client/state/docks.ts @@ -1,5 +1,5 @@ import type { DevframeDockEntry } from '@devframes/hub' -import type { DevframeRpcClient, DockEntryState, DockEntryStateEvents, DockPanelStorage } from '@devframes/hub/client' +import type { DevframeRpcClient, DockEntryState, DockEntryStateEvents, DockPanelStorage, DockSessionStorage } from '@devframes/hub/client' import type { SharedState } from 'devframe/utils/shared-state' import type { Ref, ShallowRef } from 'vue' import { createEventEmitter } from 'devframe/utils/events' @@ -13,11 +13,25 @@ export function DEFAULT_DOCK_PANEL_STORE(): DockPanelStorage { top: 0, left: 10, position: 'bottom', - open: false, inactiveTimeout: 3_000, } } +/** + * The per-tab session UI state seed — `open`/`selectedId`/`route`. Persisted to + * `sessionStorage` by the embedded and standalone bootstraps so a reload (and + * the RPC auth handshake that follows one) restores the panel to the dock and + * route the developer left open. Distinct from {@link DEFAULT_DOCK_PANEL_STORE} + * (browser-shared `localStorage` geometry), because selection is per-tab. + */ +export function DEFAULT_DOCK_SESSION_STORE(): DockSessionStorage { + return { + open: false, + selectedId: null, + route: null, + } +} + export function createDockEntryState( entry: DevframeDockEntry, selected: Ref, diff --git a/packages/hub-ui/src/client/stories/mock-context.ts b/packages/hub-ui/src/client/stories/mock-context.ts index 8678bb79..5a02a455 100644 --- a/packages/hub-ui/src/client/stories/mock-context.ts +++ b/packages/hub-ui/src/client/stories/mock-context.ts @@ -1,11 +1,11 @@ import type { DevframeDockEntry, DevframeDocksUserSettings } from '@devframes/hub' -import type { DevframeRpcClient, DockPanelStorage, DocksContext, RpcClientEvents } from '@devframes/hub/client' +import type { DevframeRpcClient, DockPanelStorage, DocksContext, DockSessionStorage, RpcClientEvents } from '@devframes/hub/client' import { DEFAULT_STATE_USER_SETTINGS } from '@devframes/hub/constants' import { createEventEmitter } from 'devframe/utils/events' import { createSharedState } from 'devframe/utils/shared-state' import { ref } from 'vue' import { createDocksContext } from '../state/context' -import { DEFAULT_DOCK_PANEL_STORE } from '../state/docks' +import { DEFAULT_DOCK_PANEL_STORE, DEFAULT_DOCK_SESSION_STORE } from '../state/docks' /** * A story-only RPC client. Backs the three shared-state keys the dock context @@ -23,8 +23,10 @@ export interface CreateMockContextOptions { entries?: DevframeDockEntry[] /** Which client shell the context represents. */ clientType?: 'embedded' | 'standalone' - /** Overrides merged over the default panel store (mode, position, open, ...). */ + /** Overrides merged over the default panel store (mode, position, geometry, ...). */ panel?: Partial + /** Overrides merged over the default per-tab session store (open, selectedId, route). */ + session?: Partial /** Overrides merged over the default user settings (hidden, pinned, order, ...). */ settings?: Partial /** Entry id to pre-select (also opens the panel). */ @@ -110,6 +112,7 @@ export async function createMockDocksContext( entries = [], clientType = 'embedded', panel, + session, settings = {}, selectedId = null, isTrusted = true, @@ -117,12 +120,13 @@ export async function createMockDocksContext( const rpc = createMockRpc(entries, settings, isTrusted) const panelStore = ref({ ...DEFAULT_DOCK_PANEL_STORE(), ...panel }) + const sessionStore = ref({ ...DEFAULT_DOCK_SESSION_STORE(), ...session }) - const context = await createDocksContext(clientType, rpc, panelStore) + const context = await createDocksContext(clientType, rpc, panelStore, sessionStore) if (selectedId != null) { context.docks.selectedId = selectedId - context.panel.store.open = true + context.panel.session.open = true } return context as DocksContext & { rpc: MockRpcClient } diff --git a/packages/hub/src/client/__tests__/host.test.ts b/packages/hub/src/client/__tests__/host.test.ts index 8b358fc9..38d9939c 100644 --- a/packages/hub/src/client/__tests__/host.test.ts +++ b/packages/hub/src/client/__tests__/host.test.ts @@ -74,7 +74,7 @@ describe('createDevframeClientHost', () => { expect(getDevframeClientContext()).toBe(host.context) expect(host.context.clientType).toBe('standalone') expect(host.context.rpc).toBe(rpc) - expect(host.context.panel.store.open).toBe(true) + expect(host.context.panel.session.open).toBe(true) expect(host.context.when.context).toMatchObject({ clientType: 'standalone', dockOpen: true, diff --git a/packages/hub/src/client/docks.ts b/packages/hub/src/client/docks.ts index 4eba597f..78f452b7 100644 --- a/packages/hub/src/client/docks.ts +++ b/packages/hub/src/client/docks.ts @@ -16,10 +16,32 @@ export interface DockPanelStorage { top: number left: number position: 'left' | 'right' | 'bottom' | 'top' - open: boolean inactiveTimeout: number } +/** + * Per-tab UI state of the dock panel, distinct from the browser-shared geometry + * in {@link DockPanelStorage}. A viewer persists this to `sessionStorage` so a + * reload (or the RPC auth handshake that follows one) restores the panel to + * exactly where the developer left it — which dock was open, and the + * address-bar route of that dock's iframe. Kept `sessionStorage`-scoped (not + * `localStorage`) because it is per-tab navigation state: two tabs open against + * the same dev server each keep their own selection rather than fighting over a + * shared one. Every field is serializable. + */ +export interface DockSessionStorage { + /** Whether the dock panel is open. */ + open: boolean + /** The currently selected dock entry id, or `null` when nothing is open. */ + selectedId: string | null + /** + * The live address-bar URL of the selected iframe dock, restored as that + * iframe's boot `src` on the next load so the deep-linked route survives a + * reload. `null` when the selected dock has no iframe route to remember. + */ + route: string | null +} + export type DockClientType = 'embedded' | 'standalone' export interface DocksContext extends DevframeRpcContext { @@ -84,9 +106,25 @@ export type DevframeClientContext = DocksContext export interface DocksPanelContext { store: DockPanelStorage + /** + * Per-tab session UI state — whether the panel is open, which dock is + * selected, and that dock's iframe route. Restored across reloads (and the + * auth handshake that follows one) by a viewer that persists it to + * `sessionStorage`. + */ + session: DockSessionStorage isDragging: boolean isResizing: boolean readonly isVertical: boolean + /** + * Claim the persisted {@link DockSessionStorage.route boot route} for a dock + * id, once. Returns the saved address-bar URL only for the dock that was + * selected before the last reload — and only on the first call for it — so a + * restored iframe boots deep-linked while a later switch to a different dock + * (whose live route isn't reflected in {@link DockSessionStorage.route} yet) + * doesn't reuse the stale value. Returns `null` otherwise. + */ + consumeBootRoute?: (id: string) => string | null } export interface DocksEntriesContext { diff --git a/packages/hub/src/client/host.ts b/packages/hub/src/client/host.ts index 55c073e4..047bbb6a 100644 --- a/packages/hub/src/client/host.ts +++ b/packages/hub/src/client/host.ts @@ -151,7 +151,7 @@ export async function createDevframeClientHost( get context(): WhenContext { return { clientType, - dockOpen: panel.store.open, + dockOpen: panel.session.open, paletteOpen: commands.paletteOpen, dockSelectedId: selectedId ?? '', } @@ -330,8 +330,10 @@ export async function createDevframeClientHost( docks.entries = entries docks.groupedEntries = groupByCategory(entries, categoryOrder) - if (selectedId && !entryToStateMap.has(selectedId)) + if (selectedId && !entryToStateMap.has(selectedId)) { selectedId = null + panel.session.selectedId = null + } } function createDocksContext(): DocksEntriesContext { @@ -393,6 +395,9 @@ export async function createDevframeClientHost( const previous = selectedId selectedId = next + // Mirror onto the session context so a persisting host and the when-clause + // context see the current selection. + panel.session.selectedId = next if (previous) entryToStateMap.get(previous)?.events.emit('entry:deactivated') if (next) @@ -516,12 +521,17 @@ function createPanelContext(clientType: DockClientType): DocksPanelContext { top: 0, left: 0, position: 'right', + inactiveTimeout: 0, + } + const session: DocksPanelContext['session'] = { // A standalone runtime owns the page, so its "panel" is always open. open: clientType === 'standalone', - inactiveTimeout: 0, + selectedId: null, + route: null, } return { store, + session, isDragging: false, isResizing: false, get isVertical() { diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts index a3940459..bf3a423a 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts @@ -56,7 +56,6 @@ export interface DockPanelStorage { top: number; left: number; position: 'left' | 'right' | 'bottom' | 'top'; - open: boolean; inactiveTimeout: number; } export interface DockRegistration { @@ -105,11 +104,18 @@ export interface DocksEntriesContext { register: (_: T, _?: boolean) => DockRegistration; update: (_: DevframeDockUserEntry) => void; } +export interface DockSessionStorage { + open: boolean; + selectedId: string | null; + route: string | null; +} export interface DocksPanelContext { store: DockPanelStorage; + session: DockSessionStorage; isDragging: boolean; isResizing: boolean; readonly isVertical: boolean; + consumeBootRoute?: (_: string) => string | null; } export interface FrameNavClient { readonly ready: boolean; From c94c198d8c7b0d2d172977fe904c43d29bf89692 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 17 Aug 2026 14:01:07 +0900 Subject: [PATCH 2/3] chore: update fields --- .../client/components/views/ViewIframe.vue | 2 +- packages/hub-ui/src/client/state/context.ts | 54 ++++++++++--------- packages/hub-ui/src/client/state/docks.ts | 2 +- packages/hub/src/client/docks.ts | 9 ++-- packages/hub/src/client/host.ts | 8 +-- .../tsnapi/devframe/constants.snapshot.d.ts | 1 + .../tsnapi/devframe/constants.snapshot.js | 1 + .../tsnapi/devframe/index.snapshot.d.ts | 8 ++- .../tsnapi/devframe/types.snapshot.d.ts | 1 + 9 files changed, 51 insertions(+), 35 deletions(-) diff --git a/packages/hub-ui/src/client/components/views/ViewIframe.vue b/packages/hub-ui/src/client/components/views/ViewIframe.vue index 1c386305..ea7cb8e0 100644 --- a/packages/hub-ui/src/client/components/views/ViewIframe.vue +++ b/packages/hub-ui/src/client/components/views/ViewIframe.vue @@ -252,7 +252,7 @@ onMounted(() => { const panelSession = props.context.panel.session watchEffect(() => { if (props.context.docks.selectedId === props.entry.id) - panelSession.route = currentUrl.value + panelSession.selectedDockRoute = currentUrl.value }) // Listen for iframe load events diff --git a/packages/hub-ui/src/client/state/context.ts b/packages/hub-ui/src/client/state/context.ts index 5ae8546d..ecbc4a45 100644 --- a/packages/hub-ui/src/client/state/context.ts +++ b/packages/hub-ui/src/client/state/context.ts @@ -83,22 +83,27 @@ export async function createDocksContext( // the live session state. Re-applied once the RPC becomes trusted so a reload // lands back on the same dock — see the restore effect near the end. const restoreIntent = { - open: sessionStore.value.open, - selectedId: sessionStore.value.selectedId, - route: sessionStore.value.route, + ...sessionStore.value, } - // `selectedId` is backed by the session store so the current selection both + // `selectedDockId` is backed by the session store so the current selection both // drives the UI and persists across reloads through one source of truth. - const selectedId = computed({ - get: () => sessionStore.value.selectedId, + const selectedDockId = computed({ + get: () => sessionStore.value.selectedDockId, set: (id) => { - sessionStore.value.selectedId = id + sessionStore.value.selectedDockId = id }, }) + const selectedDockRoute = computed({ + get: () => sessionStore.value.selectedDockRoute, + set: (route) => { + sessionStore.value.selectedDockRoute = route + }, + }) + const selected = computed( - () => entries.value.find(entry => entry.id === selectedId.value) - ?? BUILTIN_ENTRIES.find(entry => entry.id === selectedId.value) + () => entries.value.find(entry => entry.id === selectedDockId.value) + ?? BUILTIN_ENTRIES.find(entry => entry.id === selectedDockId.value) ?? null, ) @@ -142,8 +147,8 @@ export async function createDocksContext( // Clearing the selection when the active member vanishes matches the // hub reconcile behavior for a removed selected entry (shared-iframe // soft-nav §7.5). - if (selectedId.value === entry.id) - selectedId.value = null + if (selectedDockId.value === entry.id) + selectedDockId.value = null }, } } @@ -179,7 +184,7 @@ export async function createDocksContext( clientType, dockOpen: sessionStore.value.open, paletteOpen: commandsContext?.paletteOpen ?? false, - dockSelectedId: selectedId.value ?? '', + dockSelectedId: selectedDockId.value ?? '', popupOpen: isDockPopupOpen.value, }) @@ -195,13 +200,13 @@ export async function createDocksContext( const switchEntry = async (id: string | null = null) => { if (id == null) { - selectedId.value = null + selectedDockId.value = null sessionStore.value.open = false - sessionStore.value.route = null + sessionStore.value.selectedDockRoute = null return true } if (id === '~client-auth-notice') { - selectedId.value = id + selectedDockId.value = id sessionStore.value.open = true return true } @@ -268,18 +273,18 @@ export async function createDocksContext( if (entry.type === 'iframe' && entry.frameId && !entry.subTabs) frameNavCurrentMember.set(entry.frameId, entry.id) - selectedId.value = entry.id + selectedDockId.value = entry.id sessionStore.value.open = true // Only an iframe dock owns an address-bar route; ViewIframe keeps // `session.route` current for it. Clear it for anything else so a stale // route from a previous iframe isn't persisted against a non-iframe dock. if (entry.type !== 'iframe') - sessionStore.value.route = null + sessionStore.value.selectedDockRoute = null return true } const toggleEntry = async (id: string) => { - if (selectedId.value === id) + if (selectedDockId.value === id) return switchEntry(null) return switchEntry(id) } @@ -415,7 +420,7 @@ export async function createDocksContext( keybindings: [{ key: 'Escape' }], action: () => { sessionStore.value.open = false - selectedId.value = null + selectedDockId.value = null }, }, { @@ -533,9 +538,9 @@ export async function createDocksContext( // One-shot boot route (see `DocksPanelContext.consumeBootRoute`): the persisted // address-bar URL is handed back to exactly the iframe dock that was selected // before the reload, once. - let bootRoute: string | null = restoreIntent.selectedId != null ? restoreIntent.route : null + let bootRoute: string | null = restoreIntent.selectedDockId != null ? restoreIntent.selectedDockRoute : null const consumeBootRoute = (id: string): string | null => { - if (bootRoute != null && id === restoreIntent.selectedId) { + if (bootRoute != null && id === restoreIntent.selectedDockId) { const route = bootRoute bootRoute = null return route @@ -553,7 +558,8 @@ export async function createDocksContext( consumeBootRoute, }, docks: { - selectedId, + selectedId: selectedDockId, + selectedRoute: selectedDockRoute, selected, entries, entryToStateMap: markRaw(dockEntryStateMap), @@ -604,8 +610,8 @@ export async function createDocksContext( // open. `switchEntry` reads `session.route` back through `consumeBootRoute` // when the restored iframe boots. const applyRestore = (): void => { - if (restoreIntent.open && restoreIntent.selectedId != null) - void switchEntry(restoreIntent.selectedId) + if (restoreIntent.open && restoreIntent.selectedDockId != null) + void switchEntry(restoreIntent.selectedDockId) } if (rpc.isTrusted) { applyRestore() diff --git a/packages/hub-ui/src/client/state/docks.ts b/packages/hub-ui/src/client/state/docks.ts index 208ab6fc..fa7ab018 100644 --- a/packages/hub-ui/src/client/state/docks.ts +++ b/packages/hub-ui/src/client/state/docks.ts @@ -28,7 +28,7 @@ export function DEFAULT_DOCK_SESSION_STORE(): DockSessionStorage { return { open: false, selectedId: null, - route: null, + selectedDockRoute: null, } } diff --git a/packages/hub/src/client/docks.ts b/packages/hub/src/client/docks.ts index 78f452b7..1ece56a4 100644 --- a/packages/hub/src/client/docks.ts +++ b/packages/hub/src/client/docks.ts @@ -33,13 +33,13 @@ export interface DockSessionStorage { /** Whether the dock panel is open. */ open: boolean /** The currently selected dock entry id, or `null` when nothing is open. */ - selectedId: string | null + selectedDockId: string | null /** * The live address-bar URL of the selected iframe dock, restored as that * iframe's boot `src` on the next load so the deep-linked route survives a * reload. `null` when the selected dock has no iframe route to remember. */ - route: string | null + selectedDockRoute: string | null } export type DockClientType = 'embedded' | 'standalone' @@ -117,11 +117,11 @@ export interface DocksPanelContext { isResizing: boolean readonly isVertical: boolean /** - * Claim the persisted {@link DockSessionStorage.route boot route} for a dock + * Claim the persisted {@link DockSessionStorage.selectedDockRoute boot route} for a dock * id, once. Returns the saved address-bar URL only for the dock that was * selected before the last reload — and only on the first call for it — so a * restored iframe boots deep-linked while a later switch to a different dock - * (whose live route isn't reflected in {@link DockSessionStorage.route} yet) + * (whose live route isn't reflected in {@link DockSessionStorage.selectedDockRoute} yet) * doesn't reuse the stale value. Returns `null` otherwise. */ consumeBootRoute?: (id: string) => string | null @@ -129,6 +129,7 @@ export interface DocksPanelContext { export interface DocksEntriesContext { selectedId: string | null + selectedRoute: string | null readonly selected: DevframeDockEntry | null entries: DevframeDockEntry[] entryToStateMap: Map diff --git a/packages/hub/src/client/host.ts b/packages/hub/src/client/host.ts index 047bbb6a..6b87888e 100644 --- a/packages/hub/src/client/host.ts +++ b/packages/hub/src/client/host.ts @@ -332,7 +332,7 @@ export async function createDevframeClientHost( docks.groupedEntries = groupByCategory(entries, categoryOrder) if (selectedId && !entryToStateMap.has(selectedId)) { selectedId = null - panel.session.selectedId = null + panel.session.selectedDockId = null } } @@ -397,7 +397,7 @@ export async function createDevframeClientHost( selectedId = next // Mirror onto the session context so a persisting host and the when-clause // context see the current selection. - panel.session.selectedId = next + panel.session.selectedDockId = next if (previous) entryToStateMap.get(previous)?.events.emit('entry:deactivated') if (next) @@ -526,8 +526,8 @@ function createPanelContext(clientType: DockClientType): DocksPanelContext { const session: DocksPanelContext['session'] = { // A standalone runtime owns the page, so its "panel" is always open. open: clientType === 'standalone', - selectedId: null, - route: null, + selectedDockId: null, + selectedDockRoute: null, } return { store, diff --git a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts index 8a69f9ca..9938da9f 100644 --- a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts @@ -13,6 +13,7 @@ export declare const DEVFRAME_CONNECTION_META_FILENAME: string; export declare const DEVFRAME_DOCK_IMPORTS_FILENAME: string; export declare const DEVFRAME_MCP_ROUTE: string; export declare const DEVFRAME_OTP_URL_PARAM: string; +export declare const DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE: string; export declare const DEVFRAME_RPC_DUMP_DIRNAME: string; export declare const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME: string; export declare const DEVFRAME_SSE_ROUTE: string; diff --git a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js index 76076a12..db97b2c1 100644 --- a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js @@ -13,6 +13,7 @@ export var DEVFRAME_CONNECTION_META_FILENAME /* const */ export var DEVFRAME_DOCK_IMPORTS_FILENAME /* const */ export var DEVFRAME_MCP_ROUTE /* const */ export var DEVFRAME_OTP_URL_PARAM /* const */ +export var DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE /* const */ export var DEVFRAME_RPC_DUMP_DIRNAME /* const */ export var DEVFRAME_RPC_DUMP_MANIFEST_FILENAME /* const */ export var DEVFRAME_SSE_ROUTE /* const */ diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts index a5a5e5bb..cfae43f1 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts @@ -356,10 +356,16 @@ export interface RemoteAssets { version: string; path?: string; provider?: RemoteAssetsProvider; - resolveFrom?: string; + resolveFrom?: string | null; fetch?: typeof globalThis.fetch; offline?: boolean; } +export interface RemoteAssetsErrorMessage { + type: 'devframe:remote-assets-error'; + package: string; + version: string; + reason: string; +} export interface RemoteAssetsProviderCustom { fileUrl: (_: string, _: string, _: string) => string; listFiles?: (_: string, _: string, _: typeof globalThis.fetch) => Promise; diff --git a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts index 3621b5e4..b87ce07b 100644 --- a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts @@ -56,6 +56,7 @@ export { EventsMap } export { EventUnsubscribe } export { McpRouteOptions } export { RemoteAssets } +export { RemoteAssetsErrorMessage } export { RemoteAssetsProvider } export { RemoteAssetsProviderCustom } export { RemoteAssetsStore } From 338b3b6edb33edae7e92e2cea17d8231266cee62 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 17 Aug 2026 14:09:29 +0900 Subject: [PATCH 3/3] chore: update --- packages/devframe/src/utils/remote-assets.test.ts | 7 +++++-- packages/hub-ui/src/client/state/context.ts | 4 ++-- packages/hub-ui/src/client/state/docks.ts | 4 ++-- packages/hub/src/client/host.ts | 13 +++++++++++++ .../tsnapi/@devframes/hub/client.snapshot.d.ts | 5 +++-- .../tsnapi/devframe/utils/remote-assets.snapshot.js | 4 ++-- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/devframe/src/utils/remote-assets.test.ts b/packages/devframe/src/utils/remote-assets.test.ts index 3b1dbcfc..b359ebc8 100644 --- a/packages/devframe/src/utils/remote-assets.test.ts +++ b/packages/devframe/src/utils/remote-assets.test.ts @@ -1,7 +1,7 @@ import type { AddressInfo } from 'node:net' import type { MockInstance } from 'vitest' import type { RemoteAssets, RemoteAssetsErrorMessage, RemoteAssetsStore } from '../types/remote-assets' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, writeFileSync } from 'node:fs' import { createServer } from 'node:http' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -12,8 +12,11 @@ import { DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE } from '../constants' import { resolveStaticAssetsSource } from './remote-assets' import { serveStaticHandler } from './serve-static' +// `realpathSync` because module resolution reports realpaths, while the system +// temp dir is a symlink on macOS (`/var` → `/private/var`) — a path built from +// the raw `mkdtempSync` result would never match a resolved one. function makeTmp(): string { - return mkdtempSync(join(tmpdir(), 'devframe-remote-assets-')) + return realpathSync(mkdtempSync(join(tmpdir(), 'devframe-remote-assets-'))) } /** A fake CDN over a flat `filePath -> contents` map, answering the jsDelivr listing API + per-file URLs. */ diff --git a/packages/hub-ui/src/client/state/context.ts b/packages/hub-ui/src/client/state/context.ts index ecbc4a45..cf1574bb 100644 --- a/packages/hub-ui/src/client/state/context.ts +++ b/packages/hub-ui/src/client/state/context.ts @@ -276,7 +276,7 @@ export async function createDocksContext( selectedDockId.value = entry.id sessionStore.value.open = true // Only an iframe dock owns an address-bar route; ViewIframe keeps - // `session.route` current for it. Clear it for anything else so a stale + // `session.selectedDockRoute` current for it. Clear it for anything else so a stale // route from a previous iframe isn't persisted against a non-iframe dock. if (entry.type !== 'iframe') sessionStore.value.selectedDockRoute = null @@ -607,7 +607,7 @@ export async function createDocksContext( // revocation clears the selection), so the durable intent captured in // `restoreIntent` is re-applied here after the handshake — re-running the // dock's setup script and re-opening the panel on the dock the developer left - // open. `switchEntry` reads `session.route` back through `consumeBootRoute` + // open. `switchEntry` reads `session.selectedDockRoute` back through `consumeBootRoute` // when the restored iframe boots. const applyRestore = (): void => { if (restoreIntent.open && restoreIntent.selectedDockId != null) diff --git a/packages/hub-ui/src/client/state/docks.ts b/packages/hub-ui/src/client/state/docks.ts index fa7ab018..56577424 100644 --- a/packages/hub-ui/src/client/state/docks.ts +++ b/packages/hub-ui/src/client/state/docks.ts @@ -18,7 +18,7 @@ export function DEFAULT_DOCK_PANEL_STORE(): DockPanelStorage { } /** - * The per-tab session UI state seed — `open`/`selectedId`/`route`. Persisted to + * The per-tab session UI state seed — `open`/`selectedDockId`/`selectedDockRoute`. Persisted to * `sessionStorage` by the embedded and standalone bootstraps so a reload (and * the RPC auth handshake that follows one) restores the panel to the dock and * route the developer left open. Distinct from {@link DEFAULT_DOCK_PANEL_STORE} @@ -27,7 +27,7 @@ export function DEFAULT_DOCK_PANEL_STORE(): DockPanelStorage { export function DEFAULT_DOCK_SESSION_STORE(): DockSessionStorage { return { open: false, - selectedId: null, + selectedDockId: null, selectedDockRoute: null, } } diff --git a/packages/hub/src/client/host.ts b/packages/hub/src/client/host.ts index 6b87888e..c16d3884 100644 --- a/packages/hub/src/client/host.ts +++ b/packages/hub/src/client/host.ts @@ -333,6 +333,7 @@ export async function createDevframeClientHost( if (selectedId && !entryToStateMap.has(selectedId)) { selectedId = null panel.session.selectedDockId = null + panel.session.selectedDockRoute = null } } @@ -344,6 +345,14 @@ export async function createDevframeClientHost( set selectedId(id: string | null) { void switchEntry(id) }, + // A mirror of the session field, so a persisting host reads and writes the + // selected iframe's route through the same context every viewer uses. + get selectedRoute() { + return panel.session.selectedDockRoute + }, + set selectedRoute(route: string | null) { + panel.session.selectedDockRoute = route + }, get selected() { return (selectedId && entryToStateMap.get(selectedId)?.entryMeta) || null }, @@ -398,6 +407,10 @@ export async function createDevframeClientHost( // Mirror onto the session context so a persisting host and the when-clause // context see the current selection. panel.session.selectedDockId = next + // Only an iframe dock owns an address-bar route, so drop the remembered one + // for anything else rather than leaving it attached to the new selection. + if (next === null || entryToStateMap.get(next)?.entryMeta.type !== 'iframe') + panel.session.selectedDockRoute = null if (previous) entryToStateMap.get(previous)?.events.emit('entry:deactivated') if (next) diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts index bf3a423a..0d10114a 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts @@ -92,6 +92,7 @@ export interface DocksContext extends DevframeRpcContext { } export interface DocksEntriesContext { selectedId: string | null; + selectedRoute: string | null; readonly selected: DevframeDockEntry | null; entries: DevframeDockEntry[]; entryToStateMap: Map; @@ -106,8 +107,8 @@ export interface DocksEntriesContext { } export interface DockSessionStorage { open: boolean; - selectedId: string | null; - route: string | null; + selectedDockId: string | null; + selectedDockRoute: string | null; } export interface DocksPanelContext { store: DockPanelStorage; diff --git a/tests/__snapshots__/tsnapi/devframe/utils/remote-assets.snapshot.js b/tests/__snapshots__/tsnapi/devframe/utils/remote-assets.snapshot.js index b083d861..97eb0ba9 100644 --- a/tests/__snapshots__/tsnapi/devframe/utils/remote-assets.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/utils/remote-assets.snapshot.js @@ -1,6 +1,6 @@ /** * Generated by tsnapi — public API snapshot of `devframe/utils/remote-assets` */ -// #region Functions -export function resolveStaticAssetsSource(_, _) {} +// #region Other +export { resolveStaticAssetsSource } // #endregion \ No newline at end of file