Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions src/boot/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
findDockEntryForWindowId,
} from './geometry';
import type { WindowManager } from '../window-manager';
import type { NativeWindowRestoreState } from '../native-windows';
import { workAreaRectOf } from '../work-area';
import type { Window } from '../window';
import type { DesktopConfig, Session, SessionWindow, WindowConfig } from '../types';
Expand Down Expand Up @@ -82,16 +83,20 @@ export function hasRestorableSession(
}

/**
* Reopen a native window by id. Supplied by `desktop.ts`, which owns
* the dispatch: shell built-ins (OS Settings, Bug Report) have their
* own openers, everything else routes to
* `nativeWindows.openById( id )`.
* Reopen a native window from its saved instance identity. Supplied by
* `desktop.ts`, which owns the dispatch: shell built-ins (OS Settings,
* Bug Report) have their own openers, everything else routes through
* the native-window registry using the stable base id.
*
* Returns `false` when nothing answers to that id — a plugin
* deactivated since the session was saved. The restore skips those
* silently; a missing plugin isn't an error worth surfacing at boot.
*/
export type OpenNativeWindow = ( id: string ) => boolean;
export type OpenNativeWindow = (
instanceId: string,
baseId?: string,
state?: NativeWindowRestoreState,
) => boolean;

/**
* Wait for a window to appear in the manager, for openers that don't
Expand Down Expand Up @@ -149,10 +154,10 @@ function waitForWindow(
* windows are reconstructed from their saved URL. Native windows
* (`native: true` — OS Settings, Bug Report, anything registered via
* `openstation_register_window()`) have no URL to iframe: they're
* reopened by asking their owner through `openNative`, with the saved
* geometry / desktop / state staged via
* `manager.seedWindowRestoreState()` so the opener's own config
* doesn't flatten them back to defaults.
* reopened by asking their owner through `openNative`. The stable
* `baseId` selects the registered definition while the saved instance
* id and restore-time state are passed separately, so duplicate native
* instances return under the same identities they were saved with.
*/
export async function restoreSession(
manager: WindowManager,
Expand Down Expand Up @@ -180,12 +185,11 @@ export async function restoreSession(
);
}

// Stage every native window's saved geometry / desktop / state
// before triggering any open. The openers build their own
// `manager.open()` config from the registry and have no argument
// to carry restore-time values, so the manager merges these in by
// id as each window is constructed.
const nativeSeeds: Record< string, Partial< WindowConfig > > = {};
// Reserve every saved native instance id before restoring in session
// order. Native windows can wait on lazy framework and app bundles;
// the reservations prevent a user opening another copy during that
// wait from claiming an id the session is about to use.
const nativeSeeds: Record< string, NativeWindowRestoreState > = {};
for ( const win of config.session.windows ) {
if ( ! win.native ) {
continue;
Expand All @@ -209,7 +213,7 @@ export async function restoreSession(
};
}
if ( Object.keys( nativeSeeds ).length > 0 ) {
manager.seedWindowRestoreState( nativeSeeds );
manager.seedWindowRestoreState( nativeSeeds, { reserveIds: true } );
}

for ( const win of config.session.windows ) {
Expand All @@ -218,7 +222,17 @@ export async function restoreSession(
// id — a plugin deactivated since the session was saved — in
// which case there's simply no window to restore.
if ( win.native ) {
if ( ! openNative?.( win.id ) ) {
// Release this id immediately before its synchronous opener
// claims it as in-flight. The state itself is passed directly;
// nothing remains that a later fresh open could consume.
manager.discardWindowRestoreState( win.id );
if (
! openNative?.(
win.id,
win.baseId || win.id,
nativeSeeds[ win.id ],
)
) {
continue;
}
// Barrier: the openers are fire-and-forget, so without this
Expand Down Expand Up @@ -304,6 +318,11 @@ export async function restoreSession(
}
}

// Every saved native id has either been claimed or skipped. Clear
// anything left by a missing opener before a later user action can
// reuse that generated id.
manager.discardWindowRestoreState();

// Restore focus to whichever window the user left focused. If
// that id is no longer around (e.g., the saved focus pointed at
// a window we failed to reconstruct), `getById` returns
Expand Down
44 changes: 35 additions & 9 deletions src/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import {
createNativeWindowSync,
createRegisterWindow,
hydrateServerEntries,
type NativeWindowRestoreState,
type WindowLifecycleHandlers,
} from './native-windows';
import { iconsApi, renderDesktopIcons, type IconsApi } from './desktop-icons';
Expand Down Expand Up @@ -327,6 +328,7 @@ import type {
DesktopConfig,
DesktopWallpaperServerEntry,
NativeWindowDef,
WindowConfig,
} from './types';
import type { Window as DesktopWindow } from './window';

Expand Down Expand Up @@ -2282,7 +2284,8 @@ function init(): void {
mode: modeController.api,
// Bound late: `openNativeWindowById` is declared further down
// the boot, and a recent is only ever opened from a tap.
openNative: ( id ) => openNativeWindowById( id ),
openNative: ( id, baseId, state ) =>
openNativeWindowById( id, baseId, state ),
} );

// Mio — the desk companion. A first-class shell layer (sibling
Expand Down Expand Up @@ -3520,12 +3523,19 @@ function init(): void {
* `manager.open` so the admin-bar button, the dock system tile,
* and any future widget all reach the same window instance.
*/
function openBugReport(): void {
function openBugReport(
instanceId = BUG_REPORT_WINDOW_ID,
state?: NativeWindowRestoreState,
): void {
// The window's stylesheet is a `deferredStyles` entry, not a
// boot enqueue — inject on first open.
ensureDeferredStyle( 'desktop-mode-bug-report' );
void manager.open( {
id: BUG_REPORT_WINDOW_ID,
const bugReportConfig: Partial< WindowConfig > & {
id: string;
url: string;
title: string;
} = {
id: instanceId,
baseId: BUG_REPORT_WINDOW_ID,
url: `#${ BUG_REPORT_WINDOW_ID }`,
title: 'Report a bug',
Expand All @@ -3536,7 +3546,11 @@ function init(): void {
height: 620,
minWidth: 420,
minHeight: 480,
} );
...state,
};
void ( state
? manager.openNew( bugReportConfig )
: manager.open( bugReportConfig ) );
}

// Admin-bar "Report a bug" button. Inline JS in
Expand Down Expand Up @@ -3700,22 +3714,34 @@ function init(): void {
* belonged to a plugin that has since been deactivated. Callers
* treat that as "nothing to open", not as an error.
*/
function openNativeWindowById( nativeId: string ): boolean {
function openNativeWindowById(
nativeId: string,
baseId?: string,
state?: NativeWindowRestoreState,
): boolean {
const registeredId = baseId || nativeId;
// Station Home is opt-in (OS Settings → Features). Refusing the
// id here — not just in the URL remap above — is what keeps a
// saved session from resurrecting the window for a user who
// never opted in: every 1.1.2 session has it open, and restore
// reopens native windows by id without consulting the remap.
if (
nativeId === 'desktop-mode-dashboard' &&
registeredId === 'desktop-mode-dashboard' &&
osSettings.getOsSettingsSnapshot().stationHomeEnabled !== true
) {
return false;
}
if ( nativeId === BUG_REPORT_WINDOW_ID ) {
openBugReport();
if ( registeredId === BUG_REPORT_WINDOW_ID ) {
openBugReport( nativeId, state );
return true;
}
if ( state ) {
return nativeWindows.restoreById(
nativeId,
registeredId,
state,
);
}
return nativeWindows.openById( nativeId );
}

Expand Down
14 changes: 7 additions & 7 deletions src/mobile/constraints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type {
WindowManager,
} from '../window-manager';
import { workAreaRectOf } from '../work-area';
import type { OpenNativeWindow } from '../boot/session';
import type { MobileRecents } from './types';

const NS = 'openstation/mobile';
Expand All @@ -65,8 +66,8 @@ interface DisplacedGeometry {
export interface MobileConstraintsDeps {
manager: WindowManager;
mode: OsModeApi;
/** Opens a native window by id; `false` when nothing answers. */
openNative: ( id: string ) => boolean;
/** Opens or restores a native window; `false` when nothing answers. */
openNative: OpenNativeWindow;
}

export interface MobileConstraints {
Expand Down Expand Up @@ -365,13 +366,12 @@ export function installMobileConstraints( deps: MobileConstraintsDeps ): MobileC
open( win ) {
recentsApi.forget( win.id );
if ( win.native ) {
manager.seedWindowRestoreState( {
[ win.id ]: {
if (
! deps.openNative( win.id, win.baseId || win.id, {
desktopId: win.desktopId,
...( win.params ? { params: win.params } : {} ),
},
} );
if ( ! deps.openNative( win.id ) ) {
} )
) {
return;
}
return;
Expand Down
87 changes: 86 additions & 1 deletion src/native-windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type {
NativeWindowServerEntry,
NativeWindowTabEntry,
NativeWindowWireEntry,
WindowConfig,
} from './types';
import type { WindowManager } from './window-manager';
import type { Window as DesktopWindow } from './window';
Expand Down Expand Up @@ -956,6 +957,23 @@ export interface NativeWindowSync {
},
) => boolean;

/**
* Recreate one saved native-window instance through its registered
* owner. Unlike {@link openNewById}, the registry lookup uses the
* stable base id while the manager receives the exact saved instance
* id and restore-time state.
*
* Framework boot helper; callers opening a new window should use
* {@link openById} or {@link openNewById}.
*
* @internal
*/
restoreById: (
instanceId: string,
baseId: string,
state: NativeWindowRestoreState,
) => boolean;

/**
* Load a registered native window's bundle (companions first,
* then the window's own script) WITHOUT opening the window.
Expand Down Expand Up @@ -984,6 +1002,21 @@ export interface NativeWindowSync {
prewarmById: ( id: string ) => Promise< boolean >;
}

/** State owned by session restore rather than a native-window definition. */
export type NativeWindowRestoreState = Partial<
Pick<
WindowConfig,
| 'desktopId'
| 'x'
| 'y'
| 'width'
| 'height'
| 'initialState'
| 'params'
| 'gridSpan'
>
>;

/**
* Declare a server-registered native window's tabs in the window
* chrome.
Expand Down Expand Up @@ -1591,6 +1624,38 @@ export function createNativeWindowSync(
} );
};

/**
* Rebuild a saved instance without asking the registry to recognise
* its generated id. The base id finds the owning definition; the
* saved id and state go straight to `openNew()` so no restore data
* can linger and retarget a later user-initiated open.
*/
const restoreFromEntry = (
entry: NativeWindowServerEntry,
instanceId: string,
state: NativeWindowRestoreState,
): void => {
const finalRender = buildRender( entry );
const size = resolveSizeForEntry( entry );

void manager.openNew( {
id: instanceId,
baseId: entry.id,
native: true,
url: `#${ entry.id }`,
title: entry.title,
icon: entry.icon,
width: size.width,
height: size.height,
minWidth: entry.minWidth,
minHeight: entry.minHeight,
render: finalRender,
autofocus: entry.autofocus,
ownerHandle: entry.ownerHandle || entry.scriptHandle,
...state,
} );
};

const registerTile = async (
entry: NativeWindowServerEntry,
): Promise< void > => {
Expand Down Expand Up @@ -1722,6 +1787,19 @@ export function createNativeWindowSync(
return true;
};

const restoreById = (
instanceId: string,
baseId: string,
state: NativeWindowRestoreState,
): boolean => {
const entry = entriesById.get( baseId );
if ( ! entry ) {
return false;
}
restoreFromEntry( entry, instanceId, state );
return true;
};

// Persist the user's manually-resized size for EVERY window
// (native and classic iframe-backed alike) so the next fresh
// open lands at the same dimensions instead of the defaults.
Expand Down Expand Up @@ -1901,7 +1979,14 @@ export function createNativeWindowSync(
return apps?.prewarm?.( id ) === true;
};

return { sync, openById, openNewById, loadScriptById, prewarmById };
return {
sync,
openById,
openNewById,
restoreById,
loadScriptById,
prewarmById,
};
}

export function cloneTemplate(
Expand Down
Loading