Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
4a193cd
WIP: test(feel): frame sampler and measure-first feel tests (FL-01…FL…
siyabendoezdemir Oct 10, 2026
1244e05
docs(feel): baseline measured-vs-target table before any fix
siyabendoezdemir Oct 10, 2026
8c5849f
test(feel): read each frame after every animation-frame callback
siyabendoezdemir Oct 10, 2026
f5aad20
feat(feel): one motion token set and a reduced-motion setting (FL-01,…
siyabendoezdemir Oct 10, 2026
4621635
fix(sidebar): the hover card turns round from where it is (FL-03, FL-01)
siyabendoezdemir Oct 10, 2026
85244c0
fix(rows): a press selects in its frame; hover fades turn round; a cl…
siyabendoezdemir Oct 10, 2026
4b3f596
fix(windows): send the new selection before waking a sleeping tab (FL…
siyabendoezdemir Oct 10, 2026
0f154bd
fix(drag): the dragged row is drawn under the pointer (FL-10)
siyabendoezdemir Oct 10, 2026
aad3a6e
perf(sidebar): draw only the rows near the visible band (FL-21)
siyabendoezdemir Oct 10, 2026
3130d63
feat(motion): the sidebar's slide starts a frame along, so the frame …
siyabendoezdemir Oct 11, 2026
4e3b1b4
test: budget tests measure again in rounds when a round misses (D46, …
siyabendoezdemir Oct 11, 2026
538534c
fix(harness): the perf and capture scripts run main-process code in a…
siyabendoezdemir Oct 11, 2026
1920500
fix(e2e): functionSource throws Playwright's own error for a source t…
siyabendoezdemir Oct 11, 2026
8fe6b82
test(feel): notes keep three decimals for × ratios too
siyabendoezdemir Oct 11, 2026
4b11c52
feat(feel): FL-07 a row pinned or moved by key travels with the other…
siyabendoezdemir Oct 11, 2026
617d14e
feat(feel): FL-08 row buttons fade their circle in and spring the gly…
siyabendoezdemir Oct 11, 2026
d45a6b2
test(feel): FL-11 ⌘⇧K sweeps the rows out one after another within 45…
siyabendoezdemir Oct 11, 2026
85885eb
feat(feel): FL-12 a folder or the pinned section the user opens fades…
siyabendoezdemir Oct 11, 2026
5742dcd
fix(feel): FL-20 a removed Favorite shrinks to about 0.85 as it fades…
siyabendoezdemir Oct 11, 2026
2fe1f17
feat(feel): FL-22 toasts rise in with the pop spring, push the older …
siyabendoezdemir Oct 11, 2026
80bf591
feat(feel): FL-25 popovers show in the first frame and close with the…
siyabendoezdemir Oct 11, 2026
d62183f
feat(feel): FL-15 icon buttons spring back from 0.94 on release
siyabendoezdemir Oct 11, 2026
0ed8221
fix(feel): FL-18 a drop zone already shown moves to its new half at once
siyabendoezdemir Oct 11, 2026
5935a10
test(feel): FL-23 loading pill and FL-27 window audits
siyabendoezdemir Oct 11, 2026
2fa3c7c
test(feel): FL-27 resize asks for a handful of widths in between, not 11
siyabendoezdemir Oct 11, 2026
8443086
fix(feel): FL-19 a Peek's close and expand move in their first frame;…
siyabendoezdemir Oct 11, 2026
5416e5c
test(feel): FL-18 the zone may take a fourth frame to show after the …
siyabendoezdemir Oct 11, 2026
5afc625
test(feel): FL-03 polls the hover sidebar's leave every 16 ms
siyabendoezdemir Oct 11, 2026
b5939f6
docs(feel): measured-vs-target table before and after, notes on each …
siyabendoezdemir Oct 11, 2026
7b6179c
Merge origin/main (PR #22, import) into lane/feel
siyabendoezdemir Oct 11, 2026
86fca4d
test(feel): FL-26 each ⌃Tab moves the highlight in its frame and lett…
siyabendoezdemir Oct 11, 2026
1ee0f6b
docs(feel): FL-26 rows, measured on the lane merged with main's switcher
siyabendoezdemir Oct 11, 2026
80b1d00
Merge origin/main (PRs #23 and #24, keys and the test temp folder) in…
siyabendoezdemir Oct 11, 2026
0f9fbfb
test(feel): FL-26 presses Tab again only after the shell drew the las…
siyabendoezdemir Oct 11, 2026
1af4432
fix(feel): toasts follow reduced motion; a guard keeps every motion u…
siyabendoezdemir Oct 11, 2026
f2bddfd
fix(feel): every sidebar row tells its place in its list, drawn or no…
siyabendoezdemir Oct 11, 2026
a4adf9b
test(feel): FL-01's interruption check compares speeds from the frame…
siyabendoezdemir Oct 11, 2026
d74dd06
fix(feel): a selected row out of sight scrolls in on Orbit's own 225 …
siyabendoezdemir Oct 11, 2026
b2410f7
test(feel): FL-01's bar check also asks the exit to fade over frames,…
siyabendoezdemir Oct 11, 2026
e60e252
fix(feel): a toast under reduced motion fades in and out where it res…
siyabendoezdemir Oct 11, 2026
dd89018
test(feel): FL-09 asserts ⌘W selects the next tab in the first frame,…
siyabendoezdemir Oct 11, 2026
567c6bd
refactor(feel): a Peek close settles when its curve ends; the early s…
siyabendoezdemir Oct 11, 2026
689221d
test(peek): PK-06's swipe reads the card's last frame once the spring…
siyabendoezdemir Oct 11, 2026
4b69a76
test(commandbar): the comment says FL-17 is one round timed frame by …
siyabendoezdemir Oct 11, 2026
c917900
perf: commit F5's cold-start "B" build as a patch beside scripts/perf…
siyabendoezdemir Oct 11, 2026
28ba347
docs(feel): fix round 1's measures in FEEL.md, D70's notes and an AP-…
siyabendoezdemir Oct 11, 2026
3a5f9ed
test(feel): FL-17's keystroke echo judged on the best of three rounds…
siyabendoezdemir Oct 11, 2026
2a7391e
test(commandbar): the comment says FL-17 is now measured in rounds too
siyabendoezdemir Oct 11, 2026
ef0eaff
fix(feel): reduced motion turned on while Orbit runs reaches what is …
siyabendoezdemir Oct 11, 2026
cda6ec6
test(feel): FL-18's drop zone judged on the best of three rounds (D46…
siyabendoezdemir Oct 11, 2026
a9b0ad2
test(feel): FL-09's ⌘W test caps every close at 2 frames, beside the …
siyabendoezdemir Oct 11, 2026
9fc8bff
test(feel): the rubber band's log line names FL-13's own spring, not …
siyabendoezdemir Oct 11, 2026
9bef710
test(feel): FL-11's sweep judged on the best of three rounds (D46)
siyabendoezdemir Oct 11, 2026
1fdee1e
test(tabs): FL-07's pin test reads the pinned row once the room it le…
siyabendoezdemir Oct 11, 2026
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
58 changes: 29 additions & 29 deletions FEATURES.md

Large diffs are not rendered by default.

292 changes: 292 additions & 0 deletions docs/FEEL.md

Large diffs are not rendered by default.

32 changes: 26 additions & 6 deletions docs/feature-status.json

Large diffs are not rendered by default.

24 changes: 21 additions & 3 deletions scripts/capture/record.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,22 @@ const sealOffline = (app) =>
return Promise.all([...all].map((s) => s.resolveProxy('https://example.com/')))
}, `http://${OFFLINE_PROXY}`)

/**
* Quits Orbit as the e2e harness does (askQuit in tests/e2e/helpers.ts, PR #16): asked from a task of
* its own, not by the inspector's interrupt; Playwright's own close then only cleans up.
*/
async function quitOrbit(app) {
const exited = new Promise((resolve) => app.once('close', resolve))
await app
.evaluate(async ({ app: electronApp }) => {
await new Promise((r) => setImmediate(r))
electronApp.quit()
})
.catch(() => {})
await Promise.race([exited, sleep(15_000)])
await app.close()
}

const fx = await startFixtures()
const userData = mkdtempSync(join(tmpdir(), 'orbit-capture-'))
const main = resolve('out/main/index.js')
Expand All @@ -270,9 +286,11 @@ try {
if (!shell) await sleep(50)
}
await shell.getByTestId('sidebar').waitFor()
// Fill the display so the capture shows the whole window.
// Fill the display so the capture shows the whole window. From a task of its own: the inspector runs
// an evaluate by interrupt, and Orbit's resize handler would run part-way through other main-process code.
await app.evaluate(
({ BrowserWindow }, [w, h]) => {
async ({ BrowserWindow }, [w, h]) => {
await new Promise((r) => setImmediate(r))
for (const win of BrowserWindow.getAllWindows()) win.setBounds({ x: 0, y: 0, width: w, height: h })
},
[W, H],
Expand Down Expand Up @@ -313,7 +331,7 @@ try {
saved = true
} finally {
if (rec) await rec.stop()
await app?.close().catch(() => {})
if (app) await quitOrbit(app).catch(() => {})
fx.close()
rmSync(userData, { recursive: true, force: true })
if (!saved) rmSync(partial, { force: true })
Expand Down
47 changes: 47 additions & 0 deletions scripts/perf/f5-loading-view-after-first-frame.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
F5's cold-start A/B: PERF.md, "2026-10-10 23:20 UTC: cold start after chrome's loading view".

A: the loading indicator's view is made at `ready-to-show`, as Orbit does.
B: it is made once the shell has drawn its first frame (the `shell:first-frame` mark).

One build runs both: ORBIT_LOADING_AT=ready gives A, anything else gives B. The patch also marks
`loading:view` when the view is made. From the repository's root:

git apply scripts/perf/f5-loading-view-after-first-frame.patch && npm run build
ORBIT_LOADING_AT=ready xvfb-run -a -s "-screen 0 1440x900x24" node scripts/perf/run.mjs --cold-only --runs 7 --out perf-results/f5-a1.json
xvfb-run -a -s "-screen 0 1440x900x24" node scripts/perf/run.mjs --cold-only --runs 7 --out perf-results/f5-b1.json

Three rounds of each, alternating A and B; then `git apply -R` the patch and build again.

--- a/src/main/nav/loading.ts
+++ b/src/main/nav/loading.ts
@@ -104,6 +104,7 @@
if (live(s.view)) return s.view
s.ready = false
const view = windows.createUiView(windowId, 'loading')
+ ctx.perf.mark('loading:view')
s.view = view
viewContents(view)?.once('did-finish-load', () => {
if (s.view !== view) return
@@ -203,12 +204,19 @@

// Each window's view is made as the window shows, out of the window, so a load in its first second
// finds it ready and shows the indicator after the usual 150 ms (fix check A, F1 and round 2, F3).
- windows.onWindowCreated((windowId, win) => {
- win.once('ready-to-show', () => {
+ if (process.env.ORBIT_LOADING_AT === 'ready')
+ windows.onWindowCreated((windowId, win) => {
+ win.once('ready-to-show', () => {
+ const s = stateOf(windowId)
+ if (s) viewFor(windowId, s)
+ })
+ })
+ else
+ ctx.events.on('perf.mark', ({ name }, { windowId }) => {
+ if (name !== 'shell:first-frame' || !windowId) return
const s = stateOf(windowId)
if (s) viewFor(windowId, s)
})
- })

// Another tab shown (tab switch, Space switch, split focus), or the page card moved.
store.subscribe((next, prev) => {
28 changes: 23 additions & 5 deletions scripts/perf/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*
* Linux: xvfb-run -a -s "-screen 0 1440x900x24" node scripts/perf/run.mjs [--runs 5] [--out perf-results/latest.json]
* [--port 3350] (the fixture server's first port; it takes the next free one up to 3399)
* [--cold-only] (Orbit's cold start alone: no interactive scenarios, no Chromium baseline)
*/
import { execFileSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
Expand All @@ -22,7 +23,8 @@ const opt = (name, fallback) => {
}
const RUNS = Number(opt('runs', '5'))
const OUT = resolve(opt('out', 'perf-results/latest.json'))
const SKIP_CHROME = args.includes('--no-chrome')
const COLD_ONLY = args.includes('--cold-only')
const SKIP_CHROME = COLD_ONLY || args.includes('--no-chrome')
const FIRST_PORT = Number(opt('port', '3350'))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const median = (xs) => {
Expand Down Expand Up @@ -102,6 +104,22 @@ async function launchOrbit(userData) {
if (!shell) throw new Error('shell window not found')
return { app, shell }
}
/**
* Quits Orbit as the e2e harness does (askQuit in tests/e2e/helpers.ts, PR #16): asked from a task of
* its own, since Playwright's `app.close()` asks through the inspector, which can run the quit handlers
* part-way through other main-process code. Playwright's own close then only cleans up.
*/
async function quitOrbit(app) {
const exited = new Promise((resolve) => app.once('close', resolve))
await app
.evaluate(async ({ app: electronApp }) => {
await new Promise((r) => setImmediate(r))
electronApp.quit()
})
.catch(() => {})
await Promise.race([exited, sleep(15_000)])
await app.close()
}
const marks = (app) => app.evaluate(() => globalThis.__orbit.marks())
const orbitRun = (app, name, a) =>
app.evaluate((_e, [n, x]) => globalThis.__orbit.run(n, x, globalThis.__orbit.windowIds()[0]), [name, a])
Expand Down Expand Up @@ -141,13 +159,13 @@ async function orbitColdStart(fx) {
const tabId = await orbitRun(app, 'tabs.create', { url: fx.url(i), activate: true })
await waitTabsLoaded(app, [tabId])
await app.evaluate(() => globalThis.__orbit.flush())
await app.close()
await quitOrbit(app)
log('relaunch with restored tab')
const w0 = Date.now()
;({ app } = await launchOrbit(dir))
restoredPaint.push(await waitMark(app, `tab:${tabId}:loaded`))
restoredWall.push(Date.now() - w0)
await app.close()
await quitOrbit(app)
rmSync(dir, { recursive: true, force: true })
}
return {
Expand Down Expand Up @@ -423,7 +441,7 @@ async function orbitInteractive(fx) {
samples: bigTimes,
}

await app.close()
await quitOrbit(app)
rmSync(dir, { recursive: true, force: true })
return result
}
Expand Down Expand Up @@ -471,7 +489,7 @@ async function chromeBaseline(fx) {
const fx = await startFixtures()
const report = { date: new Date().toISOString(), platform: process.platform, runs: RUNS }
try {
report.orbit = { coldStart: await orbitColdStart(fx), ...(await orbitInteractive(fx)) }
report.orbit = { coldStart: await orbitColdStart(fx), ...(COLD_ONLY ? {} : await orbitInteractive(fx)) }
if (!SKIP_CHROME) {
try {
report.chromium = await chromeBaseline(fx)
Expand Down
2 changes: 2 additions & 0 deletions src/main/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { registerImport } from './import'
import { registerKeys } from './keys'
import { registerMedia } from './media'
import { registerMiniWindow } from './miniwindow'
import { registerMotion } from './motion'
import { registerNav } from './nav'
import { registerPeek } from './peek'
import { registerSettings } from './settings'
Expand All @@ -25,6 +26,7 @@ export function registerFeatures(ctx: MainContext): void {
// First: the user's key bindings, so whatever reads a shortcut as it registers reads theirs (KS-01).
registerKeys(ctx)
registerCore(ctx)
registerMotion(ctx)
const { ops } = registerTabs(ctx)
registerNav(ctx, ops)
// Before Peek and Mini Windows: a DevTools key held down is ignored before they act on it.
Expand Down
30 changes: 12 additions & 18 deletions src/main/media/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
Menu,
type MenuItemConstructorOptions,
screen,
systemPreferences,
type WebContents,
WebContentsView,
} from 'electron'
Expand Down Expand Up @@ -47,6 +46,7 @@ import {
import type { Rect } from '../../shared/rpc'
import type { TabId, WindowId } from '../../shared/state'
import type { MainContext } from '../context'
import { reducedMotion } from '../motion'
import { displayedTabs } from '../tabs/model'
import { loadRenderer, rendererEntry, uiWebPreferences } from '../windows'

Expand Down Expand Up @@ -281,7 +281,7 @@ export class PipPlayer {
}
}
}
if (from && (from.width < 8 || from.height < 8 || !richMotion())) from = undefined
if (from && (from.width < 8 || from.height < 8 || !!reducedMotion(this.ctx))) from = undefined
const start = from ?? target
const ms = ENTER_MS

Expand Down Expand Up @@ -385,7 +385,7 @@ export class PipPlayer {
release()
void this.showStill(null)
}
const fade = isMac && richMotion() && (why === 'close' || why === 'off' || why === 'ended')
const fade = isMac && !reducedMotion(this.ctx) && (why === 'close' || why === 'off' || why === 'ended')
const back = this.backing?.tabId === s.tabId ? this.backing : undefined
this.backing = undefined
if (win && !win.isDestroyed() && win.isVisible() && why === 'returned' && back?.to && this.still) {
Expand Down Expand Up @@ -447,7 +447,14 @@ export class PipPlayer {
/** The video's place in the page on screen, if the motion back there can be seen (MD-16). */
private homeOnScreen(s: Session, windowId: WindowId): Rect | undefined {
const source = this.ctx.windows.get(windowId)
if (!s.home || !richMotion() || !source || source.isDestroyed() || !source.isVisible() || source.isMinimized())
if (
!s.home ||
reducedMotion(this.ctx) ||
!source ||
source.isDestroyed() ||
!source.isVisible() ||
source.isMinimized()
)
return undefined
const content = source.getContentBounds()
const to = { ...s.home, x: content.x + s.home.x, y: content.y + s.home.y }
Expand Down Expand Up @@ -642,7 +649,7 @@ export class PipPlayer {
if (p?.corner === corner && p.size === size) return
d.settings.pipPlacement = { corner, size }
})
const rich = richMotion()
const rich = !reducedMotion(this.ctx)
this.lastSettle = { from: now, to: target, ms: rich ? null : RELEASE_MS }
if (rich) this.animateSpring(now, target, velocity)
else this.animateTimed(now, target, RELEASE_MS, undefined, EASE_OUT_CURVE)
Expand Down Expand Up @@ -894,16 +901,3 @@ function velocityOf(samples: { x: number; y: number; t: number }[]): { x: number
const dt = (last.t - first.t) / 1000
return { x: (last.x - first.x) / dt, y: (last.y - first.y) / dt }
}

/**
* No reduced motion asked for (FL-28): macOS Reduce Motion, Windows animation effects, the GTK
* animation setting, or Chromium's force switches. The same signal as the pages'
* prefers-reduced-motion (`shouldRenderRichAnimation` ignores the switches).
*/
function richMotion(): boolean {
try {
return !systemPreferences.getAnimationSettings().prefersReducedMotion
} catch {
return true
}
}
24 changes: 4 additions & 20 deletions src/main/miniwindow/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import {
app,
BrowserWindow,
globalShortcut,
Menu,
nativeTheme,
screen,
systemPreferences,
type WebContents,
} from 'electron'
import { app, BrowserWindow, globalShortcut, Menu, nativeTheme, screen, type WebContents } from 'electron'
import { toElectronAccelerator } from '../../shared/actions'
import { createWindowState } from '../../shared/defaults'
import { newId } from '../../shared/ids'
Expand All @@ -19,6 +10,7 @@ import { NEUTRAL_THEME, paletteFor } from '../../shared/theme'
import { insertItem, removeItem, sectionOf } from '../../shared/tree'
import { engineForWindow, parseInput, siteOf } from '../../shared/url'
import type { MainContext } from '../context'
import { reducedMotion } from '../motion'
import { archiveFloating, floatingTab, keepFloatingPagesOutOfTabClose } from '../peek/floating'
import { runPageAction } from '../peek/page-actions'
import type { CallContext } from '../registry'
Expand Down Expand Up @@ -127,14 +119,6 @@ function raise(win: BrowserWindow | undefined): void {
win.focus()
}

function reducedMotion(): boolean {
try {
return systemPreferences.getAnimationSettings().prefersReducedMotion
} catch {
return false
}
}

export function registerMiniWindow(ctx: MainContext): void {
const { store, commands, actions, windows, tabs } = ctx
const minis = new Map<WindowId, Mini>()
Expand Down Expand Up @@ -274,7 +258,7 @@ export function registerMiniWindow(ctx: MainContext): void {
}

const fadeIn = (win: BrowserWindow) => {
if (!canFade || reducedMotion()) {
if (!canFade || reducedMotion(ctx)) {
win.show()
return
}
Expand All @@ -297,7 +281,7 @@ export function registerMiniWindow(ctx: MainContext): void {
const closeWindow = (mini: Mini, opts: { shrink?: boolean } = {}) => {
const win = mini.win
if (win.isDestroyed()) return
if (!canFade || reducedMotion() || !win.isVisible()) {
if (!canFade || reducedMotion(ctx) || !win.isVisible()) {
win.close()
return
}
Expand Down
28 changes: 28 additions & 0 deletions src/main/motion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { systemPreferences } from 'electron'
import type { MainContext } from './context'

/**
* Reduced motion (FL-28, AP-17) as main sees it: on when the OS asks for it or Orbit's own setting
* forces it. Electron reads the OS setting per platform: Reduce Motion on macOS, animation effects
* off on Windows, the GTK animation setting on Linux (and Chromium's force switches). Main's own
* motion (Mini Window fades, the floating player's flights) asks this each time it would animate;
* renderers follow the same two signals themselves (src/renderer/src/motion/reduced.ts).
*/
export function reducedMotion(ctx: Pick<MainContext, 'store'>): boolean {
if (ctx.store.state.settings.reduceMotion) return true
try {
return systemPreferences.getAnimationSettings().prefersReducedMotion
} catch {
return false
}
}

/** The command bar's "Toggle Reduce Motion" (FL-28): Orbit's setting, on or off. */
export function registerMotion(ctx: MainContext): void {
const { actions, store } = ctx
actions.register('app.toggleReduceMotion', () =>
store.update((d) => {
d.settings.reduceMotion = !d.settings.reduceMotion
}),
)
}
2 changes: 2 additions & 0 deletions src/main/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ export function normalizeState(input: Partial<OrbitState>): OrbitState {
.map((s) => ({ id: String(s.id || s.keyword), name: String(s.name || s.keyword), keyword: s.keyword, url: s.url }))
if (typeof state.settings.searchSuggestions !== 'boolean') state.settings.searchSuggestions = false
if (typeof state.settings.searchTopResult !== 'boolean') state.settings.searchTopResult = false
// Reduced motion forced on (FL-28): off unless set.
if (typeof state.settings.reduceMotion !== 'boolean') state.settings.reduceMotion = false
// The Library section used last (AR-02): a section id, else the Archive.
const section = state.settings.librarySection
if (!(typeof section === 'string' && /^[\w.-]{1,64}$/.test(section))) state.settings.librarySection = 'archive'
Expand Down
1 change: 1 addition & 0 deletions src/main/settings/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const CHECKS: { [K in keyof Required<SettingsPatch>]: Check } = {
hardwareAcceleration: bool,
secureDns: (v) => (SECURE_DNS_CHOICES as readonly unknown[]).includes(v),
updateChecks: bool,
reduceMotion: bool,
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/main/test-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export function exposeTestHooks(ctx: MainContext, persistence: Persistence): voi
run: (name: CommandName, args: unknown, windowId?: string) =>
later(() => ctx.commands.run(name, args as never, { windowId })),
action: (id: string, windowId?: string) => later(() => ctx.actions.run(id, { windowId })),
flush: () => persistence.flush(),
/** Writes the state file in a task of its own: the write clears its timer, never to be done by interrupt. */
flush: () => later(() => persistence.flush()),
/** Writes it in the calling task, for a caller already in a task of its own (the e2e evaluate) that checks the state first. */
flushNow: () => persistence.flush(),
marks: () => ctx.perf.marks,
windowIds: () => ctx.windows.ids(),
liveTabs: () => ctx.tabs.liveTabIds(),
Expand Down
13 changes: 13 additions & 0 deletions src/main/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,19 @@ export class WindowManager {
const panes = tabId && frame ? fastSwitchPanes(state, prev, tabId, before, frame) : null
if (panes) {
rec.minPaneSeq = this.ctx.store.seq + 1
if (panes.some((p) => !this.liveContents(p.tabId))) {
// A sleeping tab's page takes longer than a frame to wake: it wakes just after this change
// has gone out, so the sidebar draws the new selection first (FL-29: ⌘W → next tab selected
// in a frame; the card shows the page's background meanwhile).
const shown = rec.panes
setImmediate(() => {
if (rec.win.isDestroyed() || rec.panes !== shown || activeTabOf(this.ctx.store.state, rec.id) !== tabId)
return
this.applyPanes(rec.id, panes, rec.radius)
if (tabId && !rec.overlayShown) this.liveContents(tabId)?.focus()
})
return
}
this.applyPanes(rec.id, panes, rec.radius)
} else if (!tabId) {
rec.minPaneSeq = this.ctx.store.seq + 1
Expand Down
Loading