fix(desktop): keep onboarding's temporary layout out of mode memory

This commit is contained in:
brooklyn!
2026-09-22 16:21:47 -05:00
parent ff7642c268
commit c21df8e7eb
3 changed files with 64 additions and 11 deletions

View File

@@ -13,8 +13,11 @@ import { applyLayoutPreset } from '@/components/pane-shell/tree/presets'
import {
$activePresetId,
$layoutTree,
$userPlacedPanes,
adoptContributedPanes,
dismissTreePane,
markActivePreset,
persistTree,
resetEnforcedDocks,
undismissTreePanes
} from '@/components/pane-shell/tree/store'
@@ -23,10 +26,12 @@ import { DOCKED_SIDEBAR_MIN_PX } from '@/hooks/use-mobile'
import { TRANSLATIONS } from '@/i18n/catalog'
import { getRuntimeI18nLocale } from '@/i18n/runtime'
import { isOnboardingEnabled } from '@/lib/onboarding-enabled'
import { $interfaceMode, type InterfaceMode, setInterfaceMode } from '@/store/interface-mode'
import { setSidebarOpen } from '@/store/layout'
import { loadMachineProfile, machineUserName } from '@/store/machine'
import { skipGuide } from '@/store/onboarding-gate'
import { setOnboardingSurfaceActive } from '@/store/onboarding-presence'
import { $paneStates, type PaneStateSnapshot } from '@/store/panes'
import { $activeSessionId, $selectedStoredSessionId } from '@/store/session'
/** True from guide kickoff until assembly places the picked layout. Skip and a failed kickoff also clear it. */
@@ -62,7 +67,12 @@ export function pickOnboardingGreeting(): string {
/** Applying a layout remounts the card, so its selection must outlive the component. */
export const $chatLayoutPicked = atom(false)
let previousLayout: { id: string; tree: LayoutNode | null } | null = null
let previousLayout: {
id: string
tree: LayoutNode | null
panes: Record<string, PaneStateSnapshot>
placed: ReadonlySet<string>
} | null = null
/** The guide's shape, all at once: the solo layout and the small centred
* window. Called on the tick the guide is owed (film ended, or a boot that
@@ -85,7 +95,12 @@ export function startChatOnboardingSolo(): void {
return
}
previousLayout = { id: $activePresetId.get(), tree: $layoutTree.get() }
previousLayout = {
id: $activePresetId.get(),
tree: $layoutTree.get(),
panes: $paneStates.get(),
placed: $userPlacedPanes.get()
}
$chatOnboardingSolo.set(true)
$chatLayoutPicked.set(false)
// The local machine probe finishes before the backend boots, letting the
@@ -104,7 +119,10 @@ export function startChatOnboardingSolo(): void {
export function endChatOnboardingSolo(): void {
$chatOnboardingSolo.set(false)
$onboardingGreeting.set('')
restorePreviousLayout()
}
function restorePreviousLayout() {
const previous = previousLayout
previousLayout = null
@@ -112,8 +130,11 @@ export function endChatOnboardingSolo(): void {
const tree = previous.tree ?? registry.getArea('layouts').find(preset => preset.id === 'default')?.data
if (tree) {
// SAFETY: layout contributions declare LayoutNode data, like the saved tree.
applyLayoutPreset(previous.tree ? previous.id : 'default', tree as LayoutNode)
$layoutTree.set(tree as LayoutNode)
$paneStates.set(previous.panes)
$userPlacedPanes.set(previous.placed)
markActivePreset(previous.tree ? previous.id : 'default')
persistTree()
}
}
}
@@ -171,9 +192,17 @@ function reconcileLayout(id: string, tree: LayoutNode): void {
/** Grow only when leaving solo mode: repeating the delta would make the window larger on every re-pick.
* Reconcile panes on every pick. */
export function assembleChatOnboarding(id: string, tree: LayoutNode): void {
export function assembleChatOnboarding(id: string, tree: LayoutNode, mode?: InterfaceMode): void {
const firstPick = $chatOnboardingSolo.get()
if (mode && mode !== $interfaceMode.get()) {
// The guide's temporary solo tree is not an Advanced workspace to remember.
restorePreviousLayout()
setInterfaceMode(mode)
}
previousLayout = null
if (firstPick) {
const growth = LAYOUT_GROWTH.get(id) ?? { left: 220 }

View File

@@ -25,7 +25,6 @@ import { SearchField } from '@/components/ui/search-field'
import { registry } from '@/contrib/registry'
import { connectorIconUrl, connectorTitle } from '@/lib/connector-tools'
import { useConnectorCatalog } from '@/store/connector-catalog'
import { setInterfaceMode } from '@/store/interface-mode'
import { $onboardingAnswers, setOnboardingAnswers } from '@/store/onboarding-answers'
import { useTheme } from '@/themes'
import { setAccentOverride } from '@/themes/accent-override'
@@ -209,10 +208,6 @@ export function LayoutCard({ locked }: CardProps) {
// Skip leaves the mode alone, so only an actual choice sets it.
const layout = LAYOUTS.find(candidate => candidate.id === id)
if (layout) {
setInterfaceMode(layout.mode)
}
const preset = registry.getArea('layouts').find(contribution => contribution.id === id)
if (!preset?.data) {
@@ -224,7 +219,7 @@ export function LayoutCard({ locked }: CardProps) {
// Swapping only the preset tree on a re-pick kept the previous layout's dismissals and dock records, and the two
// layouts came up mixed together.
// SAFETY: Layout presets declare data: LayoutNode (pane-shell/tree/presets.ts).
assembleChatOnboarding(preset.id, preset.data as LayoutNode)
assembleChatOnboarding(preset.id, preset.data as LayoutNode, layout?.mode)
}
return (

View File

@@ -0,0 +1,29 @@
import { afterEach, expect, it, vi } from 'vitest'
afterEach(() => vi.unstubAllGlobals())
it('does not save the temporary onboarding layout as the Advanced workspace', async () => {
window.localStorage.clear()
vi.resetModules()
const grow = vi.fn()
vi.stubGlobal('hermesDesktop', { guestOnboardingEnabled: true, chatOnboarding: { grow } })
const { DEFAULT_TREE, BASIC_TREE, registerLayoutPresets } = await import('@/app/contrib/layout-presets')
const tree = await import('@/components/pane-shell/tree/store')
const mode = await import('@/store/interface-mode')
const panes = await import('@/store/panes')
const guide = await import('./assembly')
registerLayoutPresets()
tree.declareDefaultTree(DEFAULT_TREE, BASIC_TREE)
panes.setPaneWidthOverride('chat-sidebar', 310)
const sizes = structuredClone(panes.$paneStates.get())
const before = structuredClone(tree.$layoutTree.get())
guide.startChatOnboardingSolo()
expect(tree.$activePresetId.get()).toBe('chat-solo')
guide.assembleChatOnboarding('sidebar-left', BASIC_TREE, 'simple')
expect(mode.$interfaceMode.get()).toBe('simple')
expect(grow).toHaveBeenCalledOnce()
mode.setInterfaceMode('advanced')
expect(tree.$layoutTree.get()).toEqual(before)
expect(panes.$paneStates.get()).toEqual(sizes)
expect(tree.$activePresetId.get()).toBe('default')
})