diff --git a/apps/desktop/main/electron-window-backend.ts b/apps/desktop/main/electron-window-backend.ts index 54a88a72..05f1ce44 100644 --- a/apps/desktop/main/electron-window-backend.ts +++ b/apps/desktop/main/electron-window-backend.ts @@ -892,6 +892,11 @@ export class ElectronWindowBackend implements WindowBackend, BackendMigrationShi kernelWindowHidden(id, opSeq); } + // Realizes the kernel's hold above other apps / release pair on the existing + // window: only its level changes, it is never re-created, shown or focused. + // macOS restacks an ordinary-level window of a non-active app behind the + // active app's frontmost window shortly after it is shown; the floating + // level is exempt from that restack and takes neither activation nor key. pinAlwaysOnTop(id: WindowId, on: boolean): void { const win = this.windows.get(id)?.win; if (!win) return; diff --git a/apps/desktop/main/window-backend.ts b/apps/desktop/main/window-backend.ts index 88b22add..ce5d0e63 100644 --- a/apps/desktop/main/window-backend.ts +++ b/apps/desktop/main/window-backend.ts @@ -87,6 +87,10 @@ export interface WindowBackend { // (window-kernel.ts isStaleWindowEcho). hideWindow(id: WindowId, opSeq?: number): void; + // Hold a window above other applications' windows (`on`), or return it to + // ordinary stacking (`!on`). Never activates the app, never focuses the + // window, and acts on the existing window. A backend whose platform keeps an + // inactive app's windows where they were shown may answer with a no-op. pinAlwaysOnTop(id: WindowId, on: boolean): void; // Bounded per guest and over the whole batch, so it can never turn a quit diff --git a/apps/desktop/main/window-kernel-executor.test.ts b/apps/desktop/main/window-kernel-executor.test.ts index e27e3ab7..084ee888 100644 --- a/apps/desktop/main/window-kernel-executor.test.ts +++ b/apps/desktop/main/window-kernel-executor.test.ts @@ -979,6 +979,30 @@ describe('effect performance', () => { assert.deepStrictEqual(backend.of('setDesktopMembership'), []); }); + it('holds a page above other apps on an inactive raise request, and releases it on activation', () => { + const { exec, backend } = makeExecutor(); + exec.dispatch({ type: 'AppResigned' }); + exec.dispatch(register(wid(1), { kind: 'page-host', role: 'content' })); + exec.dispatch({ type: 'WindowShown', id: wid(1) }); + backend.calls.length = 0; + exec.dispatch({ type: 'FocusRaiseRequested', id: wid(1) }); + assert.deepStrictEqual(backend.of('pinAlwaysOnTop').map((c) => c.args), [[wid(1), true]]); + + backend.calls.length = 0; + exec.dispatch({ type: 'AppActivated' }); + assert.deepStrictEqual(backend.of('pinAlwaysOnTop').map((c) => c.args), [[wid(1), false]]); + }); + + it('skips the hold and its release under headless — a transparent window must not sit over other apps', () => { + const { exec, backend } = makeExecutor({ headless: true }); + exec.dispatch({ type: 'AppResigned' }); + exec.dispatch(register(wid(1), { kind: 'page-host', role: 'content' })); + exec.dispatch({ type: 'WindowShown', id: wid(1) }); + exec.dispatch({ type: 'FocusRaiseRequested', id: wid(1) }); + exec.dispatch({ type: 'AppActivated' }); + assert.deepStrictEqual(backend.of('pinAlwaysOnTop'), []); + }); + it('drives an overlay attach through the port with both arguments, and a detach when the target goes null — no headless gate', () => { const { exec, backend } = makeExecutor({ headless: true }); const hostA = wid(1); diff --git a/apps/desktop/main/window-kernel-executor.ts b/apps/desktop/main/window-kernel-executor.ts index c90adf10..4e8f7f7a 100644 --- a/apps/desktop/main/window-kernel-executor.ts +++ b/apps/desktop/main/window-kernel-executor.ts @@ -525,6 +525,19 @@ export class WindowKernelExecutor { this.backend.hideApp(); return; } + case 'HoldAboveOtherApps': { + // Headless-gated: a headless window is fully transparent but still takes clicks, so + // holding one above other applications would put an invisible click target over them. + if (this.headless) return; + this.backend.pinAlwaysOnTop(effect.id, true); + return; + } + case 'ReleaseFromAboveOtherApps': { + // Gated with the hold it undoes — under headless there is nothing to release. + if (this.headless) return; + this.backend.pinAlwaysOnTop(effect.id, false); + return; + } case 'AttachOverlayTo': { // No headless gate — this only asks the backend to frame `hostId`; the visibility that // follows is gated by its own `ShowWindow`/`RaiseWindow` effects. diff --git a/apps/desktop/main/window-kernel.test.ts b/apps/desktop/main/window-kernel.test.ts index c634d9ad..c9ed255b 100644 --- a/apps/desktop/main/window-kernel.test.ts +++ b/apps/desktop/main/window-kernel.test.ts @@ -19,6 +19,7 @@ import { isStaleSelfEcho, selectTransientAutoclose, isAppResignSweepEligible, + holdsAppOnScreen, isNonParentRole, TRANSIENT_ROLES, BASE_LAYER, @@ -7310,3 +7311,143 @@ describe('window-kernel: handing the user back when nothing is left on screen', assert.deepStrictEqual(after.effects.filter((e) => e.type === 'HideApp'), [{ type: 'HideApp' }]); }); }); + +describe('window-kernel: a page asked forward while the app is inactive is held above other apps', () => { + const page = (id: WindowId, extra: Partial> = {}): Event => ({ + type: 'WindowRegistered', + id, + kind: 'page-host', + layer: BASE_LAYER, + canBecomeKey: true, + acceptsInput: true, + class: 'primary', + role: 'content', + ...extra, + }); + const record = (state: State, id: WindowId): WindowRecord => state.order.find((w) => w.id === id)!; + const holds = (effects: Effect[]): Effect[] => effects.filter((e) => e.type === 'HoldAboveOtherApps'); + const releases = (effects: Effect[]): Effect[] => effects.filter((e) => e.type === 'ReleaseFromAboveOtherApps'); + + // An open that asks to come forward is a show followed by a raise request; + // session restore and any `show: false` open is the show alone. + const openForward = (id: WindowId, extra?: Partial>): Event[] => [ + page(id, extra), + { type: 'WindowShown', id }, + { type: 'FocusRaiseRequested', id }, + ]; + + it('holds the page when the raise request arrives with the app inactive', () => { + const after = foldWithEffects([{ type: 'AppResigned' }, ...openForward(wid(1))]); + assert.strictEqual(record(after.state, wid(1)).heldAboveOtherApps, true); + assert.deepStrictEqual(holds(after.effects), [{ type: 'HoldAboveOtherApps', id: wid(1) }]); + assert.deepStrictEqual(releases(after.effects), []); + // The hold takes nothing: the app stays inactive and no window becomes key. + assert.strictEqual(after.state.appActive, false); + assert.strictEqual(after.state.key, null); + assert.strictEqual(record(after.state, wid(1)).layer, BASE_LAYER, 'the declared layer is untouched'); + }); + + it('a show with no raise request holds nothing — session restore and show:false opens', () => { + const after = foldWithEffects([{ type: 'AppResigned' }, page(wid(1)), { type: 'WindowShown', id: wid(1) }]); + assert.notStrictEqual(record(after.state, wid(1)).heldAboveOtherApps, true); + assert.deepStrictEqual(holds(after.effects), []); + }); + + it('holds nothing while the app is active', () => { + const after = foldWithEffects(openForward(wid(1))); + assert.notStrictEqual(record(after.state, wid(1)).heldAboveOtherApps, true); + assert.deepStrictEqual(holds(after.effects), []); + }); + + it('a raise carrying user intent activates the app instead, so nothing is held', () => { + const after = foldWithEffects([ + { type: 'AppResigned' }, + page(wid(1)), + { type: 'WindowShown', id: wid(1) }, + { type: 'FocusRaiseRequested', id: wid(1), intent: 'user' }, + ]); + assert.notStrictEqual(record(after.state, wid(1)).heldAboveOtherApps, true); + assert.deepStrictEqual(holds(after.effects), []); + }); + + it('only a visible base-layer content page is held', () => { + const cases: Array<[string, Event[]]> = [ + ['a palette', openForward(wid(1), { role: 'palette', class: 'palette' })], + ['a quick-view', openForward(wid(1), { role: 'quick-view', class: 'quick-view' })], + ['a browser-kind window', openForward(wid(1), { kind: 'browser' })], + ['a window already above the base layer', openForward(wid(1), { layer: BASE_LAYER + 1 })], + ['a window that cannot become key', openForward(wid(1), { canBecomeKey: false })], + ['a page that is not on screen', [page(wid(1)), { type: 'FocusRaiseRequested', id: wid(1) }]], + ]; + for (const [label, events] of cases) { + const after = foldWithEffects([{ type: 'AppResigned' }, ...events]); + assert.notStrictEqual(record(after.state, wid(1)).heldAboveOtherApps, true, label); + assert.deepStrictEqual(holds(after.effects), [], label); + } + }); + + it('a second raise request on a held page issues no second hold', () => { + const held = fold([{ type: 'AppResigned' }, ...openForward(wid(1))]); + const again = foldWithEffects([{ type: 'FocusRaiseRequested', id: wid(1) }], held); + assert.strictEqual(record(again.state, wid(1)).heldAboveOtherApps, true); + assert.deepStrictEqual(holds(again.effects), []); + }); + + it('app activation releases every held page, and only those', () => { + const held = fold([ + page(wid(9)), + { type: 'WindowShown', id: wid(9) }, + { type: 'AppResigned' }, + ...openForward(wid(1)), + ...openForward(wid(2)), + ]); + const after = foldWithEffects([{ type: 'AppActivated' }], held); + for (const w of after.state.order) { + assert.notStrictEqual(w.heldAboveOtherApps, true, String(w.id)); + assert.strictEqual(w.visible, true, 'the same window stays on screen'); + } + assert.deepStrictEqual( + releases(after.effects).map((e) => ('id' in e ? e.id : null)).sort(), + [wid(1), wid(2)], + ); + // Released once: a later activation has nothing left to release. + const later = foldWithEffects([{ type: 'AppResigned' }, { type: 'AppActivated' }], after.state); + assert.deepStrictEqual(releases(later.effects), []); + }); + + it('the app-resign sweep neither hides nor dismisses a held page, and it still holds the app on screen', () => { + // The resign half of the dance, the open, then the settle. + const after = foldWithEffects([{ type: 'AppResigned' }, ...openForward(wid(1)), { type: 'AppResignSettled' }]); + const held = record(after.state, wid(1)); + assert.strictEqual(held.visible, true); + assert.notStrictEqual(held.pendingHidden, true); + assert.notStrictEqual(held.hiddenByAppBlur, true); + assert.strictEqual(held.heldAboveOtherApps, true); + assert.strictEqual(isAppResignSweepEligible(held), false); + assert.strictEqual(holdsAppOnScreen(held), true); + assert.deepStrictEqual(selectTransientAutoclose(after.state, null), []); + assert.ok(!after.effects.some((e) => e.type === 'HideWindow' || e.type === 'DismissWindow' || e.type === 'HideApp')); + }); + + it('a held page going off screen is released, so a later show does not float', () => { + const held = fold([{ type: 'AppResigned' }, ...openForward(wid(1))]); + const hidden = foldWithEffects([{ type: 'WindowHidden', id: wid(1) }], held); + assert.notStrictEqual(record(hidden.state, wid(1)).heldAboveOtherApps, true); + assert.deepStrictEqual(releases(hidden.effects), [{ type: 'ReleaseFromAboveOtherApps', id: wid(1) }]); + const reshown = foldWithEffects([{ type: 'WindowShown', id: wid(1) }], hidden.state); + assert.deepStrictEqual(holds(reshown.effects), []); + }); + + it('a held page closing issues no release — there is no window left to restack', () => { + const held = fold([{ type: 'AppResigned' }, ...openForward(wid(1))]); + const closed = foldWithEffects([{ type: 'WindowClosed', id: wid(1) }], held); + assert.deepStrictEqual(releases(closed.effects), []); + }); + + it('a re-registration keeps the hold — it is a lived fact, not a declared one', () => { + const held = fold([{ type: 'AppResigned' }, ...openForward(wid(1))]); + const again = foldWithEffects([page(wid(1))], held); + assert.strictEqual(record(again.state, wid(1)).heldAboveOtherApps, true); + assert.deepStrictEqual([...holds(again.effects), ...releases(again.effects)], []); + }); +}); diff --git a/apps/desktop/main/window-kernel.ts b/apps/desktop/main/window-kernel.ts index 3033e234..3620539a 100644 --- a/apps/desktop/main/window-kernel.ts +++ b/apps/desktop/main/window-kernel.ts @@ -85,6 +85,13 @@ export interface WindowRecord { appLifetime?: boolean; contentFocused?: boolean; + + // A lived fact, orthogonal to the declared `layer`: this window was asked + // forward while the app was inactive, so it sits above other applications' + // windows — without activation and without content focus — until the app + // becomes active. Written by `withHeldAboveOtherApps` / cleared by + // `withoutHeldAboveOtherApps` only; see `admitsHoldAboveOtherApps`. + heldAboveOtherApps?: boolean; } export type FocusCause = { kind: 'machine'; opSeq: number } | { kind: 'world' }; @@ -282,6 +289,12 @@ export type Effect = // Destroy every registered page-host guest WebContents, process-wide. The one effect that names no window. | { type: 'DestroyAllGuests' } | { type: 'HideApp' } + // Keep this window above other applications' windows without activating the + // app or focusing the window — emitted when `heldAboveOtherApps` goes true. + | { type: 'HoldAboveOtherApps'; id: WindowId } + // Return a held window to ordinary stacking — the same window, only its + // stacking changes. Emitted when `heldAboveOtherApps` goes from true to not-true. + | { type: 'ReleaseFromAboveOtherApps'; id: WindowId } | { type: 'FocusContent'; id: WindowId } | { type: 'AttachOverlayTo'; overlayId: WindowId; hostId: WindowId | null }; @@ -446,6 +459,8 @@ export function evolve(state: State, ev: Event): State { ...(existing.pendingHidden !== undefined ? { pendingHidden: existing.pendingHidden } : {}), // Dropping this would silently turn an observed `false` back into "nothing reported yet", a different answer. ...(existing.contentFocused !== undefined ? { contentFocused: existing.contentFocused } : {}), + // A lived fact too: dropping it would strand the window above other apps with no release ever issued. + ...(existing.heldAboveOtherApps !== undefined ? { heldAboveOtherApps: existing.heldAboveOtherApps } : {}), // Intrinsic, declared-at-registration properties — an event that // names one overwrites; an event that omits one keeps the existing // value rather than erasing it. @@ -614,8 +629,18 @@ export function evolve(state: State, ev: Event): State { const target = state.order.find((w) => w.id === ev.id); const hidesGenuinely = target !== undefined && target.visible === true; // The confirming echo clears a doomed mark, genuine or not — this is the report a teardown was waiting for. + // A hold ends with the window leaving the screen — `heldAboveOtherApps` + // can never be true on a hidden window, so a later show (a keepLive + // reuse, a restore-style show) starts from ordinary stacking. const order = state.order.map((w) => - w.id === ev.id ? { ...w, visible: false, ...(w.pendingHidden === true ? { pendingHidden: false } : {}) } : w, + w.id === ev.id + ? { + ...w, + visible: false, + ...(w.pendingHidden === true ? { pendingHidden: false } : {}), + ...(w.heldAboveOtherApps === true ? { heldAboveOtherApps: false } : {}), + } + : w, ); const successor = hidesGenuinely && state.key === ev.id && state.appActive === true @@ -881,11 +906,15 @@ export function evolve(state: State, ev: Event): State { // that single number once. const stamped = (reshow && !w.visible) || (assertsFocus && w.id === latched); const unverifiedMode = markFsModeUnverified(w.fsMode); - if (!reshow && !stamped && unverifiedMode === w.fsMode) { + // Every window held above other apps returns to ordinary stacking: + // the app is the active one now, so its windows stack as its own. + const released = w.heldAboveOtherApps === true; + if (!reshow && !stamped && !released && unverifiedMode === w.fsMode) { return w; } return { ...w, + ...(released ? { heldAboveOtherApps: false } : {}), ...(reshow ? { visible: true, hiddenByAppBlur: false } : {}), ...(stamped ? { lastOpSeq: opSeq } : {}), fsMode: unverifiedMode, @@ -1298,9 +1327,19 @@ export function evolve(state: State, ev: Event): State { const issuesRaiseRequest = state.key !== ev.id && targetRecord !== undefined && admitsFocusAssert(targetRecord, targetRecord.visible); const requestOpSeq = issuesRaiseRequest ? state.opSeqCounter + 1 : state.opSeqCounter; + // An inactive app cannot honour a raise by coming forward unless the + // raise carries user intent, and an ordinary-level window of an inactive + // app is restacked behind the active app's windows by the OS. So a raise + // request that will not activate the app holds its target above other + // apps instead. A show with no raise request — session restore, any + // `show: false` open — never reaches this arm, so it never holds. + const holds = + !state.appActive && ev.intent !== 'user' && targetRecord !== undefined && admitsHoldAboveOtherApps(targetRecord); return { ...state, - order: state.order.map((w) => (w.id === ev.id ? { ...w, lastOpSeq: requestOpSeq } : w)), + order: state.order.map((w) => + w.id === ev.id ? { ...w, lastOpSeq: requestOpSeq, ...(holds ? { heldAboveOtherApps: true } : {}) } : w, + ), opSeqCounter: requestOpSeq, lastRaiseRequest: { id: ev.id, seq: nextRaiseRequestSeq(state), ...(ev.intent === 'user' ? { intent: 'user' as const } : {}) }, version: state.version + 1, @@ -1519,6 +1558,19 @@ export function react(prev: State, ev: Event, next: State): Effect[] { effects.push({ type: 'DismissWindow', id: nextRecord.id }); } + // Hold above other apps: a per-record structural diff of + // `heldAboveOtherApps`, written only by `evolve` (`FocusRaiseRequested` + // sets it; `AppActivated` and `WindowHidden` clear it). A record that left + // `next.order` yields no release — there is no window left to restack. + // Ahead of `SetStacking` on purpose: a released window re-enters ordinary + // stacking first, and the replay below then orders it among its peers. + for (const nextRecord of next.order) { + const wasHeld = prev.order.find((w) => w.id === nextRecord.id)?.heldAboveOtherApps === true; + const isHeld = nextRecord.heldAboveOtherApps === true; + if (isHeld && !wasHeld) effects.push({ type: 'HoldAboveOtherApps', id: nextRecord.id }); + else if (!isHeld && wasHeld) effects.push({ type: 'ReleaseFromAboveOtherApps', id: nextRecord.id }); + } + // SetStacking: a minimal replay, not a re-assert of everything eligible. The // native primitive is move-to-top, so a window whose index didn't change can // still end up wrong once a different window moves past it — the unit that @@ -2080,16 +2132,29 @@ function appBlurRevealAdmits(state: State, record: WindowRecord): boolean { } // Disjoint from `selectTransientAutoclose` by construction; asserted by a property test. +// +// A window held above other apps (`heldAboveOtherApps`) is never eligible: it +// keeps its declared `layer`, and it is on screen over another app on purpose — +// the clause states that outright rather than leaning on `isContentWindow`. export function isAppResignSweepEligible(record: WindowRecord): boolean { return ( record.visible && record.layer > BASE_LAYER && record.acceptsInput && !record.selfClosesOnBlur && + record.heldAboveOtherApps !== true && !isContentWindow(record) ); } +// Which window a non-activating raise request holds above other apps: a page +// the user works in, on screen, at the base layer. A window declared above the +// base layer already sits over other apps and needs no hold; a palette or +// quick-view owns its own on-blur policy. +export function admitsHoldAboveOtherApps(record: WindowRecord): boolean { + return record.kind === 'page-host' && record.layer === BASE_LAYER && isContentWindow(record); +} + export function admitsFocusAssert(record: WindowRecord, visible: boolean): boolean { return visible && !record.pendingHidden; } diff --git a/apps/desktop/tests/desktop/cmd-url-unfocused.spec.ts b/apps/desktop/tests/desktop/cmd-url-unfocused.spec.ts index aa83e336..00438d5a 100644 --- a/apps/desktop/tests/desktop/cmd-url-unfocused.spec.ts +++ b/apps/desktop/tests/desktop/cmd-url-unfocused.spec.ts @@ -122,7 +122,10 @@ test.describe('cmd URL open while the app is in the background @desktop', () => const state = (await app.evaluateMain!((_e: unknown) => { return (globalThis as any).__peek_test.kernelStateForTest(); - })) as { appActive: boolean; order: Array<{ id: string; visible: boolean; pendingHidden?: boolean }> }; + })) as { + appActive: boolean; + order: Array<{ id: string; visible: boolean; pendingHidden?: boolean; heldAboveOtherApps?: boolean }>; + }; const timeline = ((await app.evaluateMain!((_e: unknown) => { return (globalThis as any).__peek_test.kernelTimelineForTest(); })) as string[]).slice(baselineLen); @@ -148,5 +151,34 @@ test.describe('cmd URL open while the app is in the background @desktop', () => return win ? win.isVisible() : null; }) as any, opened.id); expect(nativeVisible).toBe(true); + + // With the app inactive the page is held above other apps' windows: the + // kernel marks the record and issues exactly one hold for it, and no + // release yet. The native level is not asserted — the executor does not + // realize the hold under headless, so nothing floats over the real display. + const effectsFor = (lines: string[], effect: string) => + lines.filter((l) => l.includes(`${effect}(${pageWmId})`)); + expect(page!.heldAboveOtherApps, timeline.join('\n')).toBe(true); + expect(effectsFor(timeline, 'HoldAboveOtherApps').length, timeline.join('\n')).toBe(1); + expect(effectsFor(timeline, 'ReleaseFromAboveOtherApps'), timeline.join('\n')).toEqual([]); + + // The app becoming active returns the same window to ordinary stacking. + await app.evaluateMain!((_e: unknown) => { + (globalThis as any).__peek_test.setAppFocusedForTest(true); + }); + const stateAfter = (await app.evaluateMain!((_e: unknown) => { + return (globalThis as any).__peek_test.kernelStateForTest(); + })) as { appActive: boolean; order: Array<{ id: string; visible: boolean; heldAboveOtherApps?: boolean }> }; + const timelineAfter = ((await app.evaluateMain!((_e: unknown) => { + return (globalThis as any).__peek_test.kernelTimelineForTest(); + })) as string[]).slice(baselineLen); + + expect(stateAfter.appActive).toBe(true); + const pageAfter = stateAfter.order.find((w) => w.id === pageWmId); + expect(pageAfter, 'the page window left the kernel on activation').toBeTruthy(); + expect(pageAfter!.visible).toBe(true); + expect(pageAfter!.heldAboveOtherApps === true, timelineAfter.join('\n')).toBe(false); + expect(stateAfter.order.some((w) => w.heldAboveOtherApps === true)).toBe(false); + expect(effectsFor(timelineAfter, 'ReleaseFromAboveOtherApps').length, timelineAfter.join('\n')).toBe(1); }); }); diff --git a/docs/design/window-rules.md b/docs/design/window-rules.md index 1651efdc..0e6bb22c 100644 --- a/docs/design/window-rules.md +++ b/docs/design/window-rules.md @@ -29,6 +29,7 @@ Each term is followed by the name the code uses for it. - **Application window** — a window the user works in (today one of the roles in `CONTENT_ROLES`). A window may be neither transient nor an application window. - **Panel** — a window declared to never activate the app: it shows, and takes keystrokes, while the app stays in the background (`type: 'panel'` at open, `WindowRecord.nonActivating`). - **Pinned** — kept above the normal stack of windows (`layer` above `BASE_LAYER`, from `alwaysOnTop`). +- **Held above other apps** — an unpinned window kept, for now, above other apps' windows without the app activating and without the window taking key; its declared layer is unchanged and it is the same window throughout (`heldAboveOtherApps`, `admitsHoldAboveOtherApps`). - **Prior window** — the window that had focus when this one took it (`priorKey`). - **Opener** — the window that opened this one (`parentId`). - **Attached overlay** — a window bound to a host and shown, hidden and moved with it (`overlayFor`). Its **host** is the application window it is bound to. @@ -47,7 +48,7 @@ Each term is followed by the name the code uses for it. ## Gestures — what the user does and must see -- **G1 panel-from-anywhere.** A panel focused with user intent while another app is frontmost appears and takes typing while the app stays in the background; the app comes forward only when an application window opens or is focused. With the app already active the same focus is an ordinary focus. +- **G1 panel-from-anywhere.** A panel focused with user intent while another app is frontmost appears and takes typing while the app stays in the background; the app comes forward only when an application window is focused with user intent or the user focuses one. An unpinned page-hosting application window focused without user intent while the app is inactive — a page opened from the panel — does not bring the app forward: it is held above other apps, the other app keeps key, and the app activating returns every window held that way to ordinary stacking. A window shown without a focus (a restored window, a window opened not to show) is never held. With the app already active the same focus is an ordinary focus. - **G2 escape-is-one-change.** Escape does exactly one thing per press: content that consumes it changes only that content; otherwise the one key window hides or closes; with nothing to hide, the Escape fallback window opens. - **G3 hide-returns.** When a transient hides, key returns to its prior window: a window of the app if the app was active when it opened, the other app if not. No window that was not in use comes forward. - **G4 transient-opens-every-time.** A transient opened over an application window shows above that window and key, on every open, and stays until dismissed or the app deactivates. @@ -67,7 +68,7 @@ Each term is followed by the name the code uses for it. - **F3 confirmation-drives-nothing.** The OS report confirming an asked-for focus causes no further focus change and no dismissal. - **F4 raise-means-key.** A window the manager focuses ends up key and accepting input; if it was already key it is only brought to the front. - **F5 raise-needs-a-surface.** A focus command lands only on a window that is visible and not pending hidden; on any other record it does nothing. -- **F6 intent-beats-inactive.** A focus carrying user intent reaches its target regardless of the frontmost app: a panel takes key without activating the app, any other window brings the app forward. The app-inactive mask applies only to focus commands without user intent. +- **F6 intent-beats-inactive.** A focus carrying user intent reaches its target regardless of the frontmost app: a panel takes key without activating the app, any other window brings the app forward. The app-inactive mask applies only to focus commands without user intent: such a focus never activates the app and never moves key, and on an unpinned page-hosting application window it holds that window above other apps instead (G1) until the app activates or the window leaves the screen. - **F7 hide-returns-key.** When the key window hides or closes, key goes to its prior window if that window is still visible; if that window is gone, to the topmost visible application window; if nothing in the app was key before it, nothing is focused. - **F8 dismissal-keeps-the-app.** A dismissal never deactivates the app: if the app was frontmost it stays frontmost, with no key window when nothing visible remains. Focus is never handed to another app by a dismissal.