@wildwinter/app-shell 0.57.0 → 0.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -0
- package/dist/chunk-4JQWNC6Z.js +319 -0
- package/dist/chunk-4JQWNC6Z.js.map +1 -0
- package/dist/chunk-ICYPXUNA.js +8 -0
- package/dist/chunk-ICYPXUNA.js.map +1 -0
- package/dist/{chunk-XJM733E6.js → chunk-OO6O4LEL.js} +6 -2
- package/dist/chunk-OO6O4LEL.js.map +1 -0
- package/dist/{chunk-CPNTROWD.js → chunk-OZQ2BYXH.js} +13 -4
- package/dist/chunk-OZQ2BYXH.js.map +1 -0
- package/dist/{chunk-5BKGDTYD.js → chunk-RMQNBWLZ.js} +3 -3
- package/dist/{chunk-5BKGDTYD.js.map → chunk-RMQNBWLZ.js.map} +1 -1
- package/dist/index.cjs +334 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/property-list.cjs.map +1 -1
- package/dist/property-list.js +2 -2
- package/dist/tool-window.cjs +11 -2
- package/dist/tool-window.cjs.map +1 -1
- package/dist/tool-window.d.cts +10 -4
- package/dist/tool-window.d.ts +10 -4
- package/dist/tool-window.js +1 -1
- package/dist/updater.cjs.map +1 -1
- package/dist/updater.js +1 -1
- package/dist/value-control.cjs +4 -0
- package/dist/value-control.cjs.map +1 -1
- package/dist/value-control.d.cts +4 -4
- package/dist/value-control.d.ts +4 -4
- package/dist/value-control.js +1 -1
- package/dist/value-rows.cjs +8 -1
- package/dist/value-rows.cjs.map +1 -1
- package/dist/value-rows.js +5 -2
- package/dist/value-rows.js.map +1 -1
- package/dist/view-client.cjs +172 -0
- package/dist/view-client.cjs.map +1 -0
- package/dist/view-client.d.cts +46 -0
- package/dist/view-client.d.ts +46 -0
- package/dist/view-client.js +165 -0
- package/dist/view-client.js.map +1 -0
- package/dist/view-host-web.cjs +327 -0
- package/dist/view-host-web.cjs.map +1 -0
- package/dist/view-host-web.d.cts +61 -0
- package/dist/view-host-web.d.ts +61 -0
- package/dist/view-host-web.js +4 -0
- package/dist/view-host-web.js.map +1 -0
- package/dist/view-host.cjs +532 -0
- package/dist/view-host.cjs.map +1 -0
- package/dist/view-host.d.cts +178 -0
- package/dist/view-host.d.ts +178 -0
- package/dist/view-host.js +426 -0
- package/dist/view-host.js.map +1 -0
- package/dist/view-preload.cjs +16 -0
- package/dist/view-preload.cjs.map +1 -0
- package/dist/view-preload.js +8 -0
- package/dist/view-preload.js.map +1 -0
- package/dist/view-protocol.cjs +12 -0
- package/dist/view-protocol.cjs.map +1 -0
- package/dist/view-protocol.d.cts +257 -0
- package/dist/view-protocol.d.ts +257 -0
- package/dist/view-protocol.js +3 -0
- package/dist/view-protocol.js.map +1 -0
- package/package.json +25 -4
- package/dist/chunk-CPNTROWD.js.map +0 -1
- package/dist/chunk-XJM733E6.js.map +0 -1
package/dist/property-list.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { PROPERTY_TYPES, defaultControl, mountPropertyList } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
export { PROPERTY_TYPES, defaultControl, mountPropertyList } from './chunk-RMQNBWLZ.js';
|
|
2
|
+
import './chunk-OO6O4LEL.js';
|
|
3
3
|
import './chunk-W4WC4FO5.js';
|
|
4
4
|
//# sourceMappingURL=property-list.js.map
|
|
5
5
|
//# sourceMappingURL=property-list.js.map
|
package/dist/tool-window.cjs
CHANGED
|
@@ -126,6 +126,9 @@ function openToolWindow(existing, opts) {
|
|
|
126
126
|
existing.focus();
|
|
127
127
|
return existing;
|
|
128
128
|
}
|
|
129
|
+
if (opts.url === void 0 && (opts.page === void 0 || opts.rendererDir === void 0)) {
|
|
130
|
+
throw new Error("openToolWindow needs a url, or a page and its rendererDir");
|
|
131
|
+
}
|
|
129
132
|
const w = new electron.BrowserWindow({
|
|
130
133
|
...opts.rect,
|
|
131
134
|
...opts.min !== void 0 ? { minWidth: opts.min.width, minHeight: opts.min.height } : {},
|
|
@@ -136,14 +139,20 @@ function openToolWindow(existing, opts) {
|
|
|
136
139
|
contextIsolation: true,
|
|
137
140
|
nodeIntegration: false,
|
|
138
141
|
sandbox: true,
|
|
139
|
-
|
|
142
|
+
// Electron's defaults, said out loud: no webview tag, and a dropped file never navigates the window.
|
|
143
|
+
webviewTag: false,
|
|
144
|
+
navigateOnDragDrop: false,
|
|
145
|
+
preload: opts.preload,
|
|
146
|
+
...opts.partition !== void 0 ? { partition: opts.partition } : {},
|
|
147
|
+
...opts.devTools !== void 0 ? { devTools: opts.devTools } : {}
|
|
140
148
|
}
|
|
141
149
|
});
|
|
142
150
|
if (opts.pinTo !== void 0) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);
|
|
143
151
|
if (opts.remember) rememberBounds(w, opts.remember);
|
|
144
152
|
w.once("ready-to-show", () => w.show());
|
|
145
153
|
if (opts.onClosed) w.on("closed", opts.onClosed);
|
|
146
|
-
if (
|
|
154
|
+
if (opts.url !== void 0) void w.loadURL(opts.url);
|
|
155
|
+
else if (process.env["ELECTRON_RENDERER_URL"]) void w.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}/${opts.page}`);
|
|
147
156
|
else void w.loadFile(path.join(opts.rendererDir, opts.page));
|
|
148
157
|
return w;
|
|
149
158
|
}
|
package/dist/tool-window.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tool-window.ts"],"names":["screen","app","BrowserWindow","join"],"mappings":"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","file":"tool-window.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, app, screen } from \"electron\";\nimport { join } from \"node:path\";\nimport type { Satellite } from \"./session.js\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n // Raise within OUR stack. This used to setAlwaysOnTop(true), which on macOS\n // and Windows floats the window above every other APPLICATION - and stuck\n // that way, since nothing ever unset it.\n w.moveTop();\n}\n\n/** macOS pinned windows, for the activation toggle below. */\nconst macPinned = new Set<BrowserWindow>();\nlet macActivationWired = false;\nfunction wireMacActivation(): void {\n if (macActivationWired) return;\n macActivationWired = true;\n // Floating is a GLOBAL window level: left on while another app is front,\n // the pinned window would sit over that app too - the exact 2026-08-25\n // complaint. Scoping it to our app's active state keeps the pin meaning\n // \"above my editor\" and nothing more.\n // Not while a modal has the pins held (frontForModal): the editor is in\n // front for it, and coming back to the app must not cover it again.\n app.on(\"did-become-active\", () => { if (pinsHeld > 0) return; for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\"); });\n app.on(\"did-resign-active\", () => { for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false); });\n}\n\n/**\n * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the\n * world. Two findings shaped the mechanism, one per platform family:\n *\n * - Bare `alwaysOnTop` (the first implementation) floats the window over\n * every other application on macOS and Windows, so a pinned Board meant\n * nothing else on the machine could come to the front (a Storyletter user\n * report, 2026-08-25).\n * - A CHILD window (the second implementation) is position-coupled to its\n * parent on macOS: dragging the editor dragged the Board with it (a\n * Storyletter user report, 2026-08-28). Windows and Linux owned windows do\n * not move with their owner, so the child mechanism stays right for them.\n *\n * So: on macOS the pin is ACTIVATION-SCOPED alwaysOnTop - floating while\n * this app is active, dropped the moment it resigns - which is above the\n * editor without following its drags and without covering anyone else. On\n * Windows and Linux the pin is a child window, which stays over its parent\n * and stacks normally against everything else. Each branch clears the other\n * mechanism first, which also heals a window an older build pinned the old\n * way. `platform` is a seam for the tests; leave it defaulted.\n */\nexport function pinToolWindow(\n w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean,\n platform: NodeJS.Platform = process.platform,\n): void {\n if (!w || w.isDestroyed()) return;\n if (platform === \"darwin\") {\n w.setParentWindow(null);\n if (on) {\n if (!macPinned.has(w)) {\n macPinned.add(w);\n w.once(\"closed\", () => macPinned.delete(w));\n }\n wireMacActivation();\n // Raise only when the app is active now and no modal has the pins\n // held; otherwise the next did-become-active, or the release, raises it.\n w.setAlwaysOnTop(pinsHeld === 0 && BrowserWindow.getFocusedWindow() !== null, \"floating\");\n } else {\n macPinned.delete(w);\n w.setAlwaysOnTop(false);\n }\n return;\n }\n w.setAlwaysOnTop(false);\n const owner = on && parent && !parent.isDestroyed() ? parent : null;\n // While a modal has the pins held, remember the choice and apply it at the\n // release, so the editor stays in front until the modal closes.\n if (pinsHeld > 0) {\n if (owner) heldChildren.set(w, owner); else heldChildren.delete(w);\n w.setParentWindow(null);\n return;\n }\n w.setParentWindow(owner);\n}\n\n// ---------------------------------------------------------------------------\n// Fronting the main window for a modal. A pinned tool window rides above the\n// editor by design, so a modal the EDITOR shows (the updater's prompts) opened\n// underneath one: the editor blurred behind its scrim and the prompt was\n// hidden, with nothing on screen to say why (both apps, 2026-10-02, the Play\n// window over the editor). While such a modal is up the pins step aside and\n// the editor comes forward; when it closes, every pin goes back as it was.\n// ---------------------------------------------------------------------------\n\n/** How many modals are holding the pins down. The pins return at zero. */\nlet pinsHeld = 0;\n/** Windows and Linux: the tool windows unparented by the hold, and the window\n * each goes back to. */\nconst heldChildren = new Map<BrowserWindow, BrowserWindow>();\n\n/**\n * Bring `main` in front of every tool window, pinned ones included, for as\n * long as a modal it shows is open: un-minimise, show, focus, with the pins\n * set aside. Returns the release, which puts the pins back (idempotent; holds\n * nest, and the pins return when the last is released). Call it only while\n * the app is active: focusing a window of an inactive app is the focus steal\n * the platforms forbid. `platform` is a seam for the tests.\n */\nexport function frontForModal(main: BrowserWindow, platform: NodeJS.Platform = process.platform): () => void {\n if (main.isDestroyed()) return () => {};\n pinsHeld++;\n if (platform === \"darwin\") {\n for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false);\n } else {\n for (const w of BrowserWindow.getAllWindows()) {\n if (w === main || w.isDestroyed()) continue;\n const owner = w.getParentWindow();\n if (owner !== main) continue;\n heldChildren.set(w, main);\n w.setParentWindow(null);\n }\n }\n if (main.isMinimized()) main.restore();\n main.show();\n main.focus();\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (--pinsHeld > 0) return;\n if (platform === \"darwin\") {\n // Only float again if the app is active; otherwise did-become-active does.\n const active = BrowserWindow.getFocusedWindow() !== null;\n if (active) for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\");\n } else {\n for (const [w, owner] of heldChildren) {\n if (!w.isDestroyed() && !owner.isDestroyed()) w.setParentWindow(owner);\n }\n heldChildren.clear();\n }\n };\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). */\n page: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** The window to PIN ABOVE: the app's main window, resolved at open time (a\n * getter, because the main window can be recreated). With `pinned` true\n * the new tool window rides above it - on macOS via activation-scoped\n * alwaysOnTop (a child window there follows the editor's drags), on\n * Windows and Linux as a child window (bare alwaysOnTop there floats over\n * the whole machine). See pinToolWindow for both findings. */\n pinTo?: () => BrowserWindow | undefined;\n /** Start pinned (the host remembers); flip later with `pinToolWindow`. */\n pinned?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n preload: opts.preload,\n },\n });\n if (opts.pinTo !== undefined) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir, opts.page));\n return w;\n}\n\n// ---------------------------------------------------------------------------\n// The tool windows AS DATA: one row per window, and everything that walks the\n// windows walks the rows. Lifted from Storyletter's TOOL_WINDOWS table +\n// openToolWindowFor + rescueWindows (studio/src/main/index.ts), where it had\n// replaced four near-identical openX() functions whose differences were all\n// mistakes (only one forwarded its console; Reset View rescued two of four).\n// Patterpad's three hand copies (createPlayWindow / createSearchWindow /\n// createCoverageWindow and a four-branch rescueWindows) become three rows.\n//\n// Two things the table makes structurally impossible: forgetting to register\n// a window as a satellite for the project-changed nudge (every row is\n// registered when a session is given), and Reset View rescuing a subset (it\n// walks the rows).\n// ---------------------------------------------------------------------------\n\n\nexport interface ToolWindowSpec<N extends string = string> {\n /** The window's key: the store slice it remembers under, the dev-console\n * prefix, and what `open(name)` takes. */\n name: N;\n title: string;\n /** Renderer entry, relative to the env's rendererDir. */\n page: string;\n def: ToolWindowSize;\n min: ToolWindowSize;\n /** The remembered rect, read at open time (a store read-through). */\n bounds: () => ToolWindowBounds | undefined;\n /** Persist a moved / resized rect (debounced by the factory). */\n remember: (bounds: ToolWindowBounds) => void;\n /** Whether it starts pinned above the editor (both apps default true). */\n pinned: () => boolean;\n /** Frameless (the family's shape: the renderer draws `toolWindowHead`).\n * Default false = frameless; pass `frame: true` for an OS title bar. */\n frame?: boolean;\n /** The project-changed channel for THIS window when it differs from the\n * env's (Storyletter's Links window listens on \"links:focus\"). */\n channel?: string;\n /** Drop what main caches for the outgoing project on this window's behalf\n * (a coverage report, a search anchor). Runs whether the window is open\n * or not: see session.ts. */\n clear?: () => void;\n}\n\nexport interface ToolWindowEnv {\n /** Absolute path of the built renderer dir. */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n /** The window to pin above: the app's main window, resolved at open time. */\n pinTo: () => BrowserWindow | undefined;\n /** Register every row as a satellite of the project session, so a new\n * project cannot open underneath a tool window that still shows the old\n * one. `channel` is the nudge each window listens on (a row may override). */\n session?: { addSatellite: (satellite: Satellite) => () => void; channel: string };\n /** Reset View's store half, run first: forget every remembered rect and\n * set pinned true (`resetWindows(store)` from app-store, or the app's own). */\n resetStore?: () => void;\n /** The channel a rescued window's renderer hears its re-pin on, so its pin\n * button agrees with the store (default \"state:pinned\"). */\n pinChannel?: string;\n /** Runs once per window created: forward its console in dev, say. */\n onOpened?: (w: BrowserWindow, name: string) => void;\n}\n\nexport interface ToolWindows<N extends string> {\n /** Open (or focus) one window by name. */\n open(name: N): BrowserWindow;\n /** The live window, if open. */\n get(name: N): BrowserWindow | undefined;\n /** Reset View's window half, over the table: store reset, then every open\n * window restored, default size, centred, re-pinned, and told so. */\n rescue(): void;\n /** Every window that is open right now. */\n all(): BrowserWindow[];\n readonly names: readonly N[];\n}\n\n/** Build the table. The windows are owned here: a host reads them through\n * `get(name)` rather than holding module variables of its own. */\nexport function defineToolWindows<N extends string>(specs: ToolWindowSpec<N>[], env: ToolWindowEnv): ToolWindows<N> {\n const live = new Map<N, BrowserWindow>();\n const byName = (name: N): ToolWindowSpec<N> => {\n const found = specs.find((s) => s.name === name);\n if (!found) throw new Error(`no tool window \"${name}\"`);\n return found;\n };\n const get = (name: N): BrowserWindow | undefined => {\n const w = live.get(name);\n return w && !w.isDestroyed() ? w : undefined;\n };\n if (env.session) {\n for (const spec of specs) {\n env.session.addSatellite({\n window: () => get(spec.name),\n channel: spec.channel ?? env.session.channel,\n ...(spec.clear ? { clear: spec.clear } : {}),\n });\n }\n }\n const open = (name: N): BrowserWindow => {\n const spec = byName(name);\n const existing = get(name);\n const opened = openToolWindow(existing, {\n title: spec.title, page: spec.page, frame: spec.frame ?? false,\n rendererDir: env.rendererDir, preload: env.preload,\n rect: savedWindowRect(spec.bounds(), spec.def, spec.min),\n min: spec.min,\n pinTo: env.pinTo,\n pinned: spec.pinned(),\n remember: spec.remember,\n });\n if (opened !== existing) {\n live.set(name, opened);\n // Identity-guarded: a stale close never clears a newer window.\n opened.on(\"closed\", () => { if (live.get(name) === opened) live.delete(name); });\n env.onOpened?.(opened, name);\n }\n return opened;\n };\n const rescue = (): void => {\n env.resetStore?.();\n for (const spec of specs) rescueToolWindow(get(spec.name), spec.def);\n // ACTUALLY pin them, then tell them. `rescueToolWindow` restores, resizes,\n // centres and raises but never pins; the store now says pinned, so the\n // window and its button have to agree before the claim is made.\n for (const spec of specs) {\n const w = get(spec.name);\n if (!w) continue;\n pinToolWindow(w, env.pinTo(), true);\n w.webContents.send(env.pinChannel ?? \"state:pinned\", true);\n }\n };\n return {\n open, get, rescue,\n all: () => specs.map((s) => get(s.name)).filter((w): w is BrowserWindow => w !== undefined),\n names: specs.map((s) => s.name),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/tool-window.ts"],"names":["screen","app","BrowserWindow","join"],"mappings":"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","file":"tool-window.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, app, screen } from \"electron\";\nimport { join } from \"node:path\";\nimport type { Satellite } from \"./session.js\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n // Raise within OUR stack. This used to setAlwaysOnTop(true), which on macOS\n // and Windows floats the window above every other APPLICATION - and stuck\n // that way, since nothing ever unset it.\n w.moveTop();\n}\n\n/** macOS pinned windows, for the activation toggle below. */\nconst macPinned = new Set<BrowserWindow>();\nlet macActivationWired = false;\nfunction wireMacActivation(): void {\n if (macActivationWired) return;\n macActivationWired = true;\n // Floating is a GLOBAL window level: left on while another app is front,\n // the pinned window would sit over that app too - the exact 2026-08-25\n // complaint. Scoping it to our app's active state keeps the pin meaning\n // \"above my editor\" and nothing more.\n // Not while a modal has the pins held (frontForModal): the editor is in\n // front for it, and coming back to the app must not cover it again.\n app.on(\"did-become-active\", () => { if (pinsHeld > 0) return; for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\"); });\n app.on(\"did-resign-active\", () => { for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false); });\n}\n\n/**\n * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the\n * world. Two findings shaped the mechanism, one per platform family:\n *\n * - Bare `alwaysOnTop` (the first implementation) floats the window over\n * every other application on macOS and Windows, so a pinned Board meant\n * nothing else on the machine could come to the front (a Storyletter user\n * report, 2026-08-25).\n * - A CHILD window (the second implementation) is position-coupled to its\n * parent on macOS: dragging the editor dragged the Board with it (a\n * Storyletter user report, 2026-08-28). Windows and Linux owned windows do\n * not move with their owner, so the child mechanism stays right for them.\n *\n * So: on macOS the pin is ACTIVATION-SCOPED alwaysOnTop - floating while\n * this app is active, dropped the moment it resigns - which is above the\n * editor without following its drags and without covering anyone else. On\n * Windows and Linux the pin is a child window, which stays over its parent\n * and stacks normally against everything else. Each branch clears the other\n * mechanism first, which also heals a window an older build pinned the old\n * way. `platform` is a seam for the tests; leave it defaulted.\n */\nexport function pinToolWindow(\n w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean,\n platform: NodeJS.Platform = process.platform,\n): void {\n if (!w || w.isDestroyed()) return;\n if (platform === \"darwin\") {\n w.setParentWindow(null);\n if (on) {\n if (!macPinned.has(w)) {\n macPinned.add(w);\n w.once(\"closed\", () => macPinned.delete(w));\n }\n wireMacActivation();\n // Raise only when the app is active now and no modal has the pins\n // held; otherwise the next did-become-active, or the release, raises it.\n w.setAlwaysOnTop(pinsHeld === 0 && BrowserWindow.getFocusedWindow() !== null, \"floating\");\n } else {\n macPinned.delete(w);\n w.setAlwaysOnTop(false);\n }\n return;\n }\n w.setAlwaysOnTop(false);\n const owner = on && parent && !parent.isDestroyed() ? parent : null;\n // While a modal has the pins held, remember the choice and apply it at the\n // release, so the editor stays in front until the modal closes.\n if (pinsHeld > 0) {\n if (owner) heldChildren.set(w, owner); else heldChildren.delete(w);\n w.setParentWindow(null);\n return;\n }\n w.setParentWindow(owner);\n}\n\n// ---------------------------------------------------------------------------\n// Fronting the main window for a modal. A pinned tool window rides above the\n// editor by design, so a modal the EDITOR shows (the updater's prompts) opened\n// underneath one: the editor blurred behind its scrim and the prompt was\n// hidden, with nothing on screen to say why (both apps, 2026-10-02, the Play\n// window over the editor). While such a modal is up the pins step aside and\n// the editor comes forward; when it closes, every pin goes back as it was.\n// ---------------------------------------------------------------------------\n\n/** How many modals are holding the pins down. The pins return at zero. */\nlet pinsHeld = 0;\n/** Windows and Linux: the tool windows unparented by the hold, and the window\n * each goes back to. */\nconst heldChildren = new Map<BrowserWindow, BrowserWindow>();\n\n/**\n * Bring `main` in front of every tool window, pinned ones included, for as\n * long as a modal it shows is open: un-minimise, show, focus, with the pins\n * set aside. Returns the release, which puts the pins back (idempotent; holds\n * nest, and the pins return when the last is released). Call it only while\n * the app is active: focusing a window of an inactive app is the focus steal\n * the platforms forbid. `platform` is a seam for the tests.\n */\nexport function frontForModal(main: BrowserWindow, platform: NodeJS.Platform = process.platform): () => void {\n if (main.isDestroyed()) return () => {};\n pinsHeld++;\n if (platform === \"darwin\") {\n for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false);\n } else {\n for (const w of BrowserWindow.getAllWindows()) {\n if (w === main || w.isDestroyed()) continue;\n const owner = w.getParentWindow();\n if (owner !== main) continue;\n heldChildren.set(w, main);\n w.setParentWindow(null);\n }\n }\n if (main.isMinimized()) main.restore();\n main.show();\n main.focus();\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (--pinsHeld > 0) return;\n if (platform === \"darwin\") {\n // Only float again if the app is active; otherwise did-become-active does.\n const active = BrowserWindow.getFocusedWindow() !== null;\n if (active) for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\");\n } else {\n for (const [w, owner] of heldChildren) {\n if (!w.isDestroyed() && !owner.isDestroyed()) w.setParentWindow(owner);\n }\n heldChildren.clear();\n }\n };\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). Not used with `url`. */\n page?: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). Not used with `url`. */\n rendererDir?: string;\n /** Load this URL instead of a page from the renderer dir (a view window's `patter-view://...`). */\n url?: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n /** A session partition of the window's own (a view's in-memory `view-<id>`); the default session otherwise. */\n partition?: string;\n /** Whether the window may open devtools; Electron's default (yes) when absent. */\n devTools?: boolean;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** The window to PIN ABOVE: the app's main window, resolved at open time (a\n * getter, because the main window can be recreated). With `pinned` true\n * the new tool window rides above it - on macOS via activation-scoped\n * alwaysOnTop (a child window there follows the editor's drags), on\n * Windows and Linux as a child window (bare alwaysOnTop there floats over\n * the whole machine). See pinToolWindow for both findings. */\n pinTo?: () => BrowserWindow | undefined;\n /** Start pinned (the host remembers); flip later with `pinToolWindow`. */\n pinned?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n if (opts.url === undefined && (opts.page === undefined || opts.rendererDir === undefined)) {\n throw new Error(\"openToolWindow needs a url, or a page and its rendererDir\");\n }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n // Electron's defaults, said out loud: no webview tag, and a dropped file never navigates the window.\n webviewTag: false, navigateOnDragDrop: false,\n preload: opts.preload,\n ...(opts.partition !== undefined ? { partition: opts.partition } : {}),\n ...(opts.devTools !== undefined ? { devTools: opts.devTools } : {}),\n },\n });\n if (opts.pinTo !== undefined) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (opts.url !== undefined) void w.loadURL(opts.url);\n else if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir!, opts.page!));\n return w;\n}\n\n// ---------------------------------------------------------------------------\n// The tool windows AS DATA: one row per window, and everything that walks the\n// windows walks the rows. Lifted from Storyletter's TOOL_WINDOWS table +\n// openToolWindowFor + rescueWindows (studio/src/main/index.ts), where it had\n// replaced four near-identical openX() functions whose differences were all\n// mistakes (only one forwarded its console; Reset View rescued two of four).\n// Patterpad's three hand copies (createPlayWindow / createSearchWindow /\n// createCoverageWindow and a four-branch rescueWindows) become three rows.\n//\n// Two things the table makes structurally impossible: forgetting to register\n// a window as a satellite for the project-changed nudge (every row is\n// registered when a session is given), and Reset View rescuing a subset (it\n// walks the rows).\n// ---------------------------------------------------------------------------\n\n\nexport interface ToolWindowSpec<N extends string = string> {\n /** The window's key: the store slice it remembers under, the dev-console\n * prefix, and what `open(name)` takes. */\n name: N;\n title: string;\n /** Renderer entry, relative to the env's rendererDir. */\n page: string;\n def: ToolWindowSize;\n min: ToolWindowSize;\n /** The remembered rect, read at open time (a store read-through). */\n bounds: () => ToolWindowBounds | undefined;\n /** Persist a moved / resized rect (debounced by the factory). */\n remember: (bounds: ToolWindowBounds) => void;\n /** Whether it starts pinned above the editor (both apps default true). */\n pinned: () => boolean;\n /** Frameless (the family's shape: the renderer draws `toolWindowHead`).\n * Default false = frameless; pass `frame: true` for an OS title bar. */\n frame?: boolean;\n /** The project-changed channel for THIS window when it differs from the\n * env's (Storyletter's Links window listens on \"links:focus\"). */\n channel?: string;\n /** Drop what main caches for the outgoing project on this window's behalf\n * (a coverage report, a search anchor). Runs whether the window is open\n * or not: see session.ts. */\n clear?: () => void;\n}\n\nexport interface ToolWindowEnv {\n /** Absolute path of the built renderer dir. */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n /** The window to pin above: the app's main window, resolved at open time. */\n pinTo: () => BrowserWindow | undefined;\n /** Register every row as a satellite of the project session, so a new\n * project cannot open underneath a tool window that still shows the old\n * one. `channel` is the nudge each window listens on (a row may override). */\n session?: { addSatellite: (satellite: Satellite) => () => void; channel: string };\n /** Reset View's store half, run first: forget every remembered rect and\n * set pinned true (`resetWindows(store)` from app-store, or the app's own). */\n resetStore?: () => void;\n /** The channel a rescued window's renderer hears its re-pin on, so its pin\n * button agrees with the store (default \"state:pinned\"). */\n pinChannel?: string;\n /** Runs once per window created: forward its console in dev, say. */\n onOpened?: (w: BrowserWindow, name: string) => void;\n}\n\nexport interface ToolWindows<N extends string> {\n /** Open (or focus) one window by name. */\n open(name: N): BrowserWindow;\n /** The live window, if open. */\n get(name: N): BrowserWindow | undefined;\n /** Reset View's window half, over the table: store reset, then every open\n * window restored, default size, centred, re-pinned, and told so. */\n rescue(): void;\n /** Every window that is open right now. */\n all(): BrowserWindow[];\n readonly names: readonly N[];\n}\n\n/** Build the table. The windows are owned here: a host reads them through\n * `get(name)` rather than holding module variables of its own. */\nexport function defineToolWindows<N extends string>(specs: ToolWindowSpec<N>[], env: ToolWindowEnv): ToolWindows<N> {\n const live = new Map<N, BrowserWindow>();\n const byName = (name: N): ToolWindowSpec<N> => {\n const found = specs.find((s) => s.name === name);\n if (!found) throw new Error(`no tool window \"${name}\"`);\n return found;\n };\n const get = (name: N): BrowserWindow | undefined => {\n const w = live.get(name);\n return w && !w.isDestroyed() ? w : undefined;\n };\n if (env.session) {\n for (const spec of specs) {\n env.session.addSatellite({\n window: () => get(spec.name),\n channel: spec.channel ?? env.session.channel,\n ...(spec.clear ? { clear: spec.clear } : {}),\n });\n }\n }\n const open = (name: N): BrowserWindow => {\n const spec = byName(name);\n const existing = get(name);\n const opened = openToolWindow(existing, {\n title: spec.title, page: spec.page, frame: spec.frame ?? false,\n rendererDir: env.rendererDir, preload: env.preload,\n rect: savedWindowRect(spec.bounds(), spec.def, spec.min),\n min: spec.min,\n pinTo: env.pinTo,\n pinned: spec.pinned(),\n remember: spec.remember,\n });\n if (opened !== existing) {\n live.set(name, opened);\n // Identity-guarded: a stale close never clears a newer window.\n opened.on(\"closed\", () => { if (live.get(name) === opened) live.delete(name); });\n env.onOpened?.(opened, name);\n }\n return opened;\n };\n const rescue = (): void => {\n env.resetStore?.();\n for (const spec of specs) rescueToolWindow(get(spec.name), spec.def);\n // ACTUALLY pin them, then tell them. `rescueToolWindow` restores, resizes,\n // centres and raises but never pins; the store now says pinned, so the\n // window and its button have to agree before the claim is made.\n for (const spec of specs) {\n const w = get(spec.name);\n if (!w) continue;\n pinToolWindow(w, env.pinTo(), true);\n w.webContents.send(env.pinChannel ?? \"state:pinned\", true);\n }\n };\n return {\n open, get, rescue,\n all: () => specs.map((s) => get(s.name)).filter((w): w is BrowserWindow => w !== undefined),\n names: specs.map((s) => s.name),\n };\n}\n"]}
|
package/dist/tool-window.d.cts
CHANGED
|
@@ -70,12 +70,18 @@ declare function pinToolWindow(w: BrowserWindow | undefined | null, parent: Brow
|
|
|
70
70
|
declare function frontForModal(main: BrowserWindow, platform?: NodeJS.Platform): () => void;
|
|
71
71
|
interface ToolWindowOptions {
|
|
72
72
|
title: string;
|
|
73
|
-
/** Renderer entry, relative to rendererDir (e.g. "table.html", "search/index.html"). */
|
|
74
|
-
page
|
|
75
|
-
/** Absolute path of the built renderer dir (join(import.meta.dirname, "../renderer")). */
|
|
76
|
-
rendererDir
|
|
73
|
+
/** Renderer entry, relative to rendererDir (e.g. "table.html", "search/index.html"). Not used with `url`. */
|
|
74
|
+
page?: string;
|
|
75
|
+
/** Absolute path of the built renderer dir (join(import.meta.dirname, "../renderer")). Not used with `url`. */
|
|
76
|
+
rendererDir?: string;
|
|
77
|
+
/** Load this URL instead of a page from the renderer dir (a view window's `patter-view://...`). */
|
|
78
|
+
url?: string;
|
|
77
79
|
/** Absolute path of the preload bridge. */
|
|
78
80
|
preload: string;
|
|
81
|
+
/** A session partition of the window's own (a view's in-memory `view-<id>`); the default session otherwise. */
|
|
82
|
+
partition?: string;
|
|
83
|
+
/** Whether the window may open devtools; Electron's default (yes) when absent. */
|
|
84
|
+
devTools?: boolean;
|
|
79
85
|
rect: ToolWindowBounds;
|
|
80
86
|
min?: ToolWindowSize;
|
|
81
87
|
/** false = frameless: the renderer draws its own slim drag bar (.swin-head). */
|
package/dist/tool-window.d.ts
CHANGED
|
@@ -70,12 +70,18 @@ declare function pinToolWindow(w: BrowserWindow | undefined | null, parent: Brow
|
|
|
70
70
|
declare function frontForModal(main: BrowserWindow, platform?: NodeJS.Platform): () => void;
|
|
71
71
|
interface ToolWindowOptions {
|
|
72
72
|
title: string;
|
|
73
|
-
/** Renderer entry, relative to rendererDir (e.g. "table.html", "search/index.html"). */
|
|
74
|
-
page
|
|
75
|
-
/** Absolute path of the built renderer dir (join(import.meta.dirname, "../renderer")). */
|
|
76
|
-
rendererDir
|
|
73
|
+
/** Renderer entry, relative to rendererDir (e.g. "table.html", "search/index.html"). Not used with `url`. */
|
|
74
|
+
page?: string;
|
|
75
|
+
/** Absolute path of the built renderer dir (join(import.meta.dirname, "../renderer")). Not used with `url`. */
|
|
76
|
+
rendererDir?: string;
|
|
77
|
+
/** Load this URL instead of a page from the renderer dir (a view window's `patter-view://...`). */
|
|
78
|
+
url?: string;
|
|
77
79
|
/** Absolute path of the preload bridge. */
|
|
78
80
|
preload: string;
|
|
81
|
+
/** A session partition of the window's own (a view's in-memory `view-<id>`); the default session otherwise. */
|
|
82
|
+
partition?: string;
|
|
83
|
+
/** Whether the window may open devtools; Electron's default (yes) when absent. */
|
|
84
|
+
devTools?: boolean;
|
|
79
85
|
rect: ToolWindowBounds;
|
|
80
86
|
min?: ToolWindowSize;
|
|
81
87
|
/** false = frameless: the renderer draws its own slim drag bar (.swin-head). */
|
package/dist/tool-window.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { centeredOnPrimary, defineToolWindows, frontForModal, openToolWindow, pinToolWindow, rememberBounds, rescueToolWindow, savedWindowRect } from './chunk-
|
|
1
|
+
export { centeredOnPrimary, defineToolWindows, frontForModal, openToolWindow, pinToolWindow, rememberBounds, rescueToolWindow, savedWindowRect } from './chunk-OZQ2BYXH.js';
|
|
2
2
|
//# sourceMappingURL=tool-window.js.map
|
|
3
3
|
//# sourceMappingURL=tool-window.js.map
|
package/dist/updater.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tool-window.ts","../src/updater.ts"],"names":["BrowserWindow","electronUpdater","join","app","appendFileSync","ipcMain"],"mappings":"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","file":"updater.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, app, screen } from \"electron\";\nimport { join } from \"node:path\";\nimport type { Satellite } from \"./session.js\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n // Raise within OUR stack. This used to setAlwaysOnTop(true), which on macOS\n // and Windows floats the window above every other APPLICATION - and stuck\n // that way, since nothing ever unset it.\n w.moveTop();\n}\n\n/** macOS pinned windows, for the activation toggle below. */\nconst macPinned = new Set<BrowserWindow>();\nlet macActivationWired = false;\nfunction wireMacActivation(): void {\n if (macActivationWired) return;\n macActivationWired = true;\n // Floating is a GLOBAL window level: left on while another app is front,\n // the pinned window would sit over that app too - the exact 2026-08-25\n // complaint. Scoping it to our app's active state keeps the pin meaning\n // \"above my editor\" and nothing more.\n // Not while a modal has the pins held (frontForModal): the editor is in\n // front for it, and coming back to the app must not cover it again.\n app.on(\"did-become-active\", () => { if (pinsHeld > 0) return; for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\"); });\n app.on(\"did-resign-active\", () => { for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false); });\n}\n\n/**\n * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the\n * world. Two findings shaped the mechanism, one per platform family:\n *\n * - Bare `alwaysOnTop` (the first implementation) floats the window over\n * every other application on macOS and Windows, so a pinned Board meant\n * nothing else on the machine could come to the front (a Storyletter user\n * report, 2026-08-25).\n * - A CHILD window (the second implementation) is position-coupled to its\n * parent on macOS: dragging the editor dragged the Board with it (a\n * Storyletter user report, 2026-08-28). Windows and Linux owned windows do\n * not move with their owner, so the child mechanism stays right for them.\n *\n * So: on macOS the pin is ACTIVATION-SCOPED alwaysOnTop - floating while\n * this app is active, dropped the moment it resigns - which is above the\n * editor without following its drags and without covering anyone else. On\n * Windows and Linux the pin is a child window, which stays over its parent\n * and stacks normally against everything else. Each branch clears the other\n * mechanism first, which also heals a window an older build pinned the old\n * way. `platform` is a seam for the tests; leave it defaulted.\n */\nexport function pinToolWindow(\n w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean,\n platform: NodeJS.Platform = process.platform,\n): void {\n if (!w || w.isDestroyed()) return;\n if (platform === \"darwin\") {\n w.setParentWindow(null);\n if (on) {\n if (!macPinned.has(w)) {\n macPinned.add(w);\n w.once(\"closed\", () => macPinned.delete(w));\n }\n wireMacActivation();\n // Raise only when the app is active now and no modal has the pins\n // held; otherwise the next did-become-active, or the release, raises it.\n w.setAlwaysOnTop(pinsHeld === 0 && BrowserWindow.getFocusedWindow() !== null, \"floating\");\n } else {\n macPinned.delete(w);\n w.setAlwaysOnTop(false);\n }\n return;\n }\n w.setAlwaysOnTop(false);\n const owner = on && parent && !parent.isDestroyed() ? parent : null;\n // While a modal has the pins held, remember the choice and apply it at the\n // release, so the editor stays in front until the modal closes.\n if (pinsHeld > 0) {\n if (owner) heldChildren.set(w, owner); else heldChildren.delete(w);\n w.setParentWindow(null);\n return;\n }\n w.setParentWindow(owner);\n}\n\n// ---------------------------------------------------------------------------\n// Fronting the main window for a modal. A pinned tool window rides above the\n// editor by design, so a modal the EDITOR shows (the updater's prompts) opened\n// underneath one: the editor blurred behind its scrim and the prompt was\n// hidden, with nothing on screen to say why (both apps, 2026-10-02, the Play\n// window over the editor). While such a modal is up the pins step aside and\n// the editor comes forward; when it closes, every pin goes back as it was.\n// ---------------------------------------------------------------------------\n\n/** How many modals are holding the pins down. The pins return at zero. */\nlet pinsHeld = 0;\n/** Windows and Linux: the tool windows unparented by the hold, and the window\n * each goes back to. */\nconst heldChildren = new Map<BrowserWindow, BrowserWindow>();\n\n/**\n * Bring `main` in front of every tool window, pinned ones included, for as\n * long as a modal it shows is open: un-minimise, show, focus, with the pins\n * set aside. Returns the release, which puts the pins back (idempotent; holds\n * nest, and the pins return when the last is released). Call it only while\n * the app is active: focusing a window of an inactive app is the focus steal\n * the platforms forbid. `platform` is a seam for the tests.\n */\nexport function frontForModal(main: BrowserWindow, platform: NodeJS.Platform = process.platform): () => void {\n if (main.isDestroyed()) return () => {};\n pinsHeld++;\n if (platform === \"darwin\") {\n for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false);\n } else {\n for (const w of BrowserWindow.getAllWindows()) {\n if (w === main || w.isDestroyed()) continue;\n const owner = w.getParentWindow();\n if (owner !== main) continue;\n heldChildren.set(w, main);\n w.setParentWindow(null);\n }\n }\n if (main.isMinimized()) main.restore();\n main.show();\n main.focus();\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (--pinsHeld > 0) return;\n if (platform === \"darwin\") {\n // Only float again if the app is active; otherwise did-become-active does.\n const active = BrowserWindow.getFocusedWindow() !== null;\n if (active) for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\");\n } else {\n for (const [w, owner] of heldChildren) {\n if (!w.isDestroyed() && !owner.isDestroyed()) w.setParentWindow(owner);\n }\n heldChildren.clear();\n }\n };\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). */\n page: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** The window to PIN ABOVE: the app's main window, resolved at open time (a\n * getter, because the main window can be recreated). With `pinned` true\n * the new tool window rides above it - on macOS via activation-scoped\n * alwaysOnTop (a child window there follows the editor's drags), on\n * Windows and Linux as a child window (bare alwaysOnTop there floats over\n * the whole machine). See pinToolWindow for both findings. */\n pinTo?: () => BrowserWindow | undefined;\n /** Start pinned (the host remembers); flip later with `pinToolWindow`. */\n pinned?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n preload: opts.preload,\n },\n });\n if (opts.pinTo !== undefined) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir, opts.page));\n return w;\n}\n\n// ---------------------------------------------------------------------------\n// The tool windows AS DATA: one row per window, and everything that walks the\n// windows walks the rows. Lifted from Storyletter's TOOL_WINDOWS table +\n// openToolWindowFor + rescueWindows (studio/src/main/index.ts), where it had\n// replaced four near-identical openX() functions whose differences were all\n// mistakes (only one forwarded its console; Reset View rescued two of four).\n// Patterpad's three hand copies (createPlayWindow / createSearchWindow /\n// createCoverageWindow and a four-branch rescueWindows) become three rows.\n//\n// Two things the table makes structurally impossible: forgetting to register\n// a window as a satellite for the project-changed nudge (every row is\n// registered when a session is given), and Reset View rescuing a subset (it\n// walks the rows).\n// ---------------------------------------------------------------------------\n\n\nexport interface ToolWindowSpec<N extends string = string> {\n /** The window's key: the store slice it remembers under, the dev-console\n * prefix, and what `open(name)` takes. */\n name: N;\n title: string;\n /** Renderer entry, relative to the env's rendererDir. */\n page: string;\n def: ToolWindowSize;\n min: ToolWindowSize;\n /** The remembered rect, read at open time (a store read-through). */\n bounds: () => ToolWindowBounds | undefined;\n /** Persist a moved / resized rect (debounced by the factory). */\n remember: (bounds: ToolWindowBounds) => void;\n /** Whether it starts pinned above the editor (both apps default true). */\n pinned: () => boolean;\n /** Frameless (the family's shape: the renderer draws `toolWindowHead`).\n * Default false = frameless; pass `frame: true` for an OS title bar. */\n frame?: boolean;\n /** The project-changed channel for THIS window when it differs from the\n * env's (Storyletter's Links window listens on \"links:focus\"). */\n channel?: string;\n /** Drop what main caches for the outgoing project on this window's behalf\n * (a coverage report, a search anchor). Runs whether the window is open\n * or not: see session.ts. */\n clear?: () => void;\n}\n\nexport interface ToolWindowEnv {\n /** Absolute path of the built renderer dir. */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n /** The window to pin above: the app's main window, resolved at open time. */\n pinTo: () => BrowserWindow | undefined;\n /** Register every row as a satellite of the project session, so a new\n * project cannot open underneath a tool window that still shows the old\n * one. `channel` is the nudge each window listens on (a row may override). */\n session?: { addSatellite: (satellite: Satellite) => () => void; channel: string };\n /** Reset View's store half, run first: forget every remembered rect and\n * set pinned true (`resetWindows(store)` from app-store, or the app's own). */\n resetStore?: () => void;\n /** The channel a rescued window's renderer hears its re-pin on, so its pin\n * button agrees with the store (default \"state:pinned\"). */\n pinChannel?: string;\n /** Runs once per window created: forward its console in dev, say. */\n onOpened?: (w: BrowserWindow, name: string) => void;\n}\n\nexport interface ToolWindows<N extends string> {\n /** Open (or focus) one window by name. */\n open(name: N): BrowserWindow;\n /** The live window, if open. */\n get(name: N): BrowserWindow | undefined;\n /** Reset View's window half, over the table: store reset, then every open\n * window restored, default size, centred, re-pinned, and told so. */\n rescue(): void;\n /** Every window that is open right now. */\n all(): BrowserWindow[];\n readonly names: readonly N[];\n}\n\n/** Build the table. The windows are owned here: a host reads them through\n * `get(name)` rather than holding module variables of its own. */\nexport function defineToolWindows<N extends string>(specs: ToolWindowSpec<N>[], env: ToolWindowEnv): ToolWindows<N> {\n const live = new Map<N, BrowserWindow>();\n const byName = (name: N): ToolWindowSpec<N> => {\n const found = specs.find((s) => s.name === name);\n if (!found) throw new Error(`no tool window \"${name}\"`);\n return found;\n };\n const get = (name: N): BrowserWindow | undefined => {\n const w = live.get(name);\n return w && !w.isDestroyed() ? w : undefined;\n };\n if (env.session) {\n for (const spec of specs) {\n env.session.addSatellite({\n window: () => get(spec.name),\n channel: spec.channel ?? env.session.channel,\n ...(spec.clear ? { clear: spec.clear } : {}),\n });\n }\n }\n const open = (name: N): BrowserWindow => {\n const spec = byName(name);\n const existing = get(name);\n const opened = openToolWindow(existing, {\n title: spec.title, page: spec.page, frame: spec.frame ?? false,\n rendererDir: env.rendererDir, preload: env.preload,\n rect: savedWindowRect(spec.bounds(), spec.def, spec.min),\n min: spec.min,\n pinTo: env.pinTo,\n pinned: spec.pinned(),\n remember: spec.remember,\n });\n if (opened !== existing) {\n live.set(name, opened);\n // Identity-guarded: a stale close never clears a newer window.\n opened.on(\"closed\", () => { if (live.get(name) === opened) live.delete(name); });\n env.onOpened?.(opened, name);\n }\n return opened;\n };\n const rescue = (): void => {\n env.resetStore?.();\n for (const spec of specs) rescueToolWindow(get(spec.name), spec.def);\n // ACTUALLY pin them, then tell them. `rescueToolWindow` restores, resizes,\n // centres and raises but never pins; the store now says pinned, so the\n // window and its button have to agree before the claim is made.\n for (const spec of specs) {\n const w = get(spec.name);\n if (!w) continue;\n pinToolWindow(w, env.pinTo(), true);\n w.webContents.send(env.pinChannel ?? \"state:pinned\", true);\n }\n };\n return {\n open, get, rescue,\n all: () => specs.map((s) => get(s.name)).filter((w): w is BrowserWindow => w !== undefined),\n names: specs.map((s) => s.name),\n };\n}\n","// ---------------------------------------------------------------------------\n// In-app auto-update over electron-updater: the family's, lifted from Patterpad\n// where it was hardened in the field.\n//\n// This is not a wrapper anybody should write twice. Every rule in it is a bug\n// that reached a user:\n//\n// - Downloads are driven HERE rather than by electron-updater's autoDownload,\n// because holding the CancellationToken is what lets a stall watchdog kill\n// and retry a download that hangs. A hung stream emits neither progress nor\n// `error`, so anything built on events alone sits \"downloading\" for ever\n// (Patterpad #33: a Windows user's download did exactly that, all day).\n// - Differential download is off. On Windows it stalled silently across an\n// Electron major bump, where almost no blocks match anyway.\n// - There is a PERSISTENT LOG in userData, because electron-updater is silent\n// by default and a stalled download otherwise leaves nothing to diagnose.\n// - Installing asks the renderer whether there is unsaved work, and offers\n// Save / Discard / Cancel. \"Restart Now\" must never silently drop an\n// author's afternoon.\n// - Every prompt is THEMED, in the renderer, never a stock OS dialog.\n// - Every prompt brings its window to the FRONT, past pinned tool windows,\n// for as long as it is open. It used to open under the Play window, so\n// the editor blurred behind a prompt nobody could see. A prompt that\n// arrives while the app is in the background waits for the app to come\n// back before it takes the front, and one nobody asked for puts focus on\n// its safe button, so a keystroke meant for the window it covered cannot\n// answer it.\n//\n// Only runs in a packaged build; in development the manual check says so rather\n// than failing obscurely.\n//\n// THE SEAMS: the app supplies its NAME (it appears in every message), a way to\n// find the ACTIVE WINDOW, and a renderer that answers four channels - the\n// contract below. Everything else is the same in every app.\n// ---------------------------------------------------------------------------\n\nimport { app, BrowserWindow, ipcMain } from \"electron\";\nimport { appendFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ProgressInfo, UpdateDownloadedEvent, UpdateInfo } from \"electron-updater\";\nimport electronUpdater from \"electron-updater\";\nimport { frontForModal } from \"./tool-window.js\";\n\n/** Download progress, broadcast to every window so a live dialog can show it. */\nexport interface UpdaterDownloadProgress {\n percent: number;\n transferred: number;\n total: number;\n bytesPerSecond: number;\n /** Which version is coming, so a dialog can name it. */\n version: string;\n}\n\n/** A themed prompt, answered by the renderer with the chosen button index -\n * the same contract as `dialog.showMessageBox`'s `response`. */\nexport interface UpdaterPromptOptions {\n message: string;\n detail?: string;\n buttons: string[];\n defaultId?: number;\n cancelId?: number;\n /** Show live download progress in the dialog. */\n progress?: boolean;\n /** Nobody asked for this prompt: it arrived on its own (an update finished\n * downloading). Focus starts on the cancel button, so a keystroke meant for\n * whatever the prompt covered dismisses it rather than answering yes. */\n unasked?: boolean;\n}\n\n/** The channels the app's renderer must answer. Named here so both sides agree. */\nexport const UPDATER_CHANNELS = {\n /** main -> renderer: \"are there unsaved edits?\"; reply on `dirtyReply`. */\n checkDirty: \"updater:check-dirty\",\n dirtyReply: \"updater:dirty-reply\",\n /** main -> renderer: \"save now\"; reply `{ ok }` on `saveDone`. */\n saveBeforeInstall: \"updater:save-before-install\",\n saveDone: \"updater:save-done\",\n /** main -> renderer: show this prompt; reply with the button index. */\n prompt: \"updater:prompt\",\n promptReply: \"updater:prompt-reply\",\n /** main -> renderer: live download progress. */\n progress: \"updater:download-progress\",\n} as const;\n\nexport interface UpdaterOptions {\n /** The product name, as it should read in every message (\"Patterpad\"). */\n appName: string;\n /** Which window the prompts belong to. */\n activeWindow: () => BrowserWindow | null;\n /** The log file inside userData. */\n logFile?: string;\n}\n\nlet appName = \"This app\";\nlet activeWindowFn: () => BrowserWindow | null = () =>\n BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0] ?? null;\nconst activeWindow = (): BrowserWindow | null => activeWindowFn();\n\nconst { autoUpdater, CancellationToken } = electronUpdater;\n\nlet updateDownloaded: UpdateDownloadedEvent | null = null;\n// Remember the last background error so the manual \"Check for Updates\" dialog can\n// surface it - otherwise a broken feed (network policy, expired cert, GitHub outage)\n// stays invisible and the user has no idea why updates never arrive.\nlet lastBackgroundError: string | null = null;\n\n// Persistent updater log (userData/updater.log). electron-updater is silent by default; without a log a\n// Windows download that stalls without emitting `error` (found in 0.1.5) leaves nothing to diagnose.\nlet logPath = join(app.getPath(\"userData\"), \"updater.log\");\nconst writeLog = (level: string, args: unknown[]): void => {\n try { appendFileSync(logPath, `${new Date().toISOString()} [${level}] ${args.map((a) => (a instanceof Error ? a.stack || a.message : String(a))).join(\" \")}\\n`); } catch { /* logging must never throw */ }\n};\nautoUpdater.logger = {\n info: (...a: unknown[]) => writeLog(\"info\", a),\n warn: (...a: unknown[]) => writeLog(\"warn\", a),\n error: (...a: unknown[]) => writeLog(\"error\", a),\n debug: (...a: unknown[]) => writeLog(\"debug\", a),\n};\n\n// Downloads are driven by the download manager below, NOT electron-updater's autoDownload: holding\n// the CancellationToken ourselves is what lets the stall watchdog kill and retry a download that\n// hangs without ever erroring (#33 - a Windows user's download sat \"in the background\" all day; a\n// hung stream emits neither progress nor `error`, so nothing built on events alone can recover it).\nautoUpdater.autoDownload = false;\nautoUpdater.autoInstallOnAppQuit = true;\n// Force a plain full download instead of the block-by-block differential. On Windows the differential\n// path stalled silently (0.1.5): across an Electron-major bump almost no blocks match, so it downloads\n// nearly everything anyway, and it's fragile for unsigned installers. Full download is one reliable\n// stream. No-op on macOS (Squirrel.Mac always fetches the whole zip).\nautoUpdater.disableDifferentialDownload = true;\n\nautoUpdater.on(\"error\", (err) => {\n console.error(\"AutoUpdater error:\", err?.message || err);\n lastBackgroundError = err?.message || String(err);\n});\n\n// ---------------------------------------------------------------------------\n// Download manager: start, watch, retry. Platform-agnostic - it rides\n// electron-updater's shared events, identical for NSIS (win), zip (mac), and\n// AppImage (linux) feeds.\n// ---------------------------------------------------------------------------\n\nconst STALL_MS = 3 * 60 * 1000; // no progress event for this long = the download is hung\nconst WATCH_EVERY_MS = 30 * 1000; // how often the watchdog looks at the clock\nconst RETRY_DELAY_MS = 15 * 1000; // pause before re-attempting a killed/failed download\nconst MAX_ATTEMPTS = 3; // per check cycle; the 6-hourly background check starts a fresh cycle\n\ntype DownloadState = {\n info: UpdateInfo;\n token: InstanceType<typeof CancellationToken>;\n attempts: number; // attempts consumed this cycle, including the running one\n lastProgressAt: number;\n cancelledByWatchdog: boolean;\n watchdog: NodeJS.Timeout;\n};\nlet download: DownloadState | null = null;\n\nfunction broadcastProgress(p: UpdaterDownloadProgress): void {\n for (const w of BrowserWindow.getAllWindows()) {\n if (!w.isDestroyed() && !w.webContents.isDestroyed()) w.webContents.send(UPDATER_CHANNELS.progress, p);\n }\n}\n\nfunction clearDownload(): void {\n if (download) clearInterval(download.watchdog);\n download = null;\n}\n\n/** Begin (or re-attempt) downloading `info`. One state machine for every platform. */\nfunction beginDownload(info: UpdateInfo, attempts: number): void {\n clearDownload();\n const token = new CancellationToken();\n const state: DownloadState = {\n info, token, attempts: attempts + 1, lastProgressAt: Date.now(), cancelledByWatchdog: false,\n watchdog: setInterval(() => {\n if (download !== state) return;\n const quiet = Date.now() - state.lastProgressAt;\n if (quiet < STALL_MS) return;\n // Hung: no progress and no error for STALL_MS. Kill it; the download promise's catch retries.\n writeLog(\"warn\", [`watchdog: no download progress for ${Math.round(quiet / 1000)}s - cancelling (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n state.cancelledByWatchdog = true;\n state.token.cancel();\n }, WATCH_EVERY_MS),\n };\n download = state;\n writeLog(\"info\", [`download: starting ${info.version} (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n\n autoUpdater.downloadUpdate(token).then(() => {\n // Success is reported via the update-downloaded event; it clears the state.\n }).catch((err: unknown) => {\n if (download !== state) return; // superseded by a newer attempt/cycle\n const why = state.cancelledByWatchdog ? \"stalled (killed by the watchdog)\"\n : (err instanceof Error ? err.message : String(err));\n clearDownload();\n if (state.attempts < MAX_ATTEMPTS) {\n writeLog(\"warn\", [`download: attempt ${state.attempts} failed - ${why}; retrying in ${RETRY_DELAY_MS / 1000}s`]);\n setTimeout(() => { if (!download && !updateDownloaded) beginDownload(info, state.attempts); }, RETRY_DELAY_MS);\n } else {\n // Out of attempts for this cycle. Record it where the manual check surfaces it; the next\n // background check (6-hourly, or Help > Check for Updates) starts a fresh cycle.\n lastBackgroundError = `Downloading ${info.version} failed ${MAX_ATTEMPTS} times, most recently with \"${why}\". It will be tried again on the next check.`;\n writeLog(\"error\", [`download: giving up on ${info.version} this cycle - ${why}`]);\n }\n });\n}\n\nautoUpdater.on(\"update-available\", (info: UpdateInfo) => {\n if (download || updateDownloaded) return; // already downloading it, or already have it\n beginDownload(info, 0);\n});\n\nautoUpdater.on(\"download-progress\", (p: ProgressInfo) => {\n if (download) download.lastProgressAt = Date.now();\n broadcastProgress({\n percent: p.percent, transferred: p.transferred, total: p.total,\n bytesPerSecond: p.bytesPerSecond, version: download?.info.version ?? \"\",\n });\n});\n\n/** Ask the renderer (one-shot, timeout-guarded) whether the open scene has unsaved edits.\n * Resolves false on any error/timeout - the renderer's dirty flag is the only truth here. */\nfunction askRendererIsDirty(win: BrowserWindow | null): Promise<boolean> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(false); return; }\n const onReply = (_e: unknown, isDirty: boolean) => { clearTimeout(timeout); resolve(!!isDirty); };\n ipcMain.once(UPDATER_CHANNELS.dirtyReply, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.dirtyReply, onReply);\n console.warn(\"Updater dirty-check timed out; assuming clean.\");\n resolve(false);\n }, 2000);\n win.webContents.send(UPDATER_CHANNELS.checkDirty);\n });\n}\n\n/** Trigger a save in the renderer and wait for completion. Saves run the lock-aware VC write path,\n * so the timeout is generous; if it elapses we abort rather than install over a half-saved project. */\nfunction triggerRendererSave(win: BrowserWindow | null): Promise<{ ok: boolean }> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve({ ok: false }); return; }\n const onReply = (_e: unknown, result: { ok: boolean }) => { clearTimeout(timeout); resolve(result || { ok: false }); };\n ipcMain.once(UPDATER_CHANNELS.saveDone, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.saveDone, onReply);\n console.warn(\"Updater save timed out.\");\n resolve({ ok: false });\n }, 30000);\n win.webContents.send(UPDATER_CHANNELS.saveBeforeInstall);\n });\n}\n\n/** Show a THEMED prompt in the renderer (never a stock OS dialog; design-language \"coherent to the edges\")\n * and resolve the chosen button index - the same contract as dialog.showMessageBox's `response`. Falls\n * back to cancelId/defaultId if there's no live renderer to answer (so an install is never blocked on it). */\nfunction themedPrompt(win: BrowserWindow | null, opts: UpdaterPromptOptions): Promise<number> {\n return new Promise((resolve) => {\n const fallback = opts.cancelId ?? opts.defaultId ?? 0;\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(fallback); return; }\n const release = frontWhileOpen(win);\n const done = (idx: number): void => { release(); resolve(idx); };\n const onReply = (_e: unknown, idx: number) => { clearTimeout(timeout); done(typeof idx === \"number\" ? idx : fallback); };\n ipcMain.once(UPDATER_CHANNELS.promptReply, onReply);\n // Generous: these are user-facing prompts. If the renderer never answers (gone / hung), fall back safely.\n const timeout = setTimeout(() => { ipcMain.removeListener(UPDATER_CHANNELS.promptReply, onReply); done(fallback); }, 300000);\n win.webContents.send(UPDATER_CHANNELS.prompt, opts);\n });\n}\n\n/** Keep `win` in front of the app's other windows, pinned tool windows included, while a prompt is open\n * on it, and return the release. If the app is in the background the fronting waits until one of its\n * windows takes focus again: a prompt is no reason to pull an app over whatever the user went to. */\nfunction frontWhileOpen(win: BrowserWindow): () => void {\n let release: (() => void) | null = null;\n let finished = false;\n const front = (): void => { if (!finished && !win.isDestroyed()) release = frontForModal(win); };\n if (BrowserWindow.getFocusedWindow()) front();\n else app.once(\"browser-window-focus\", front);\n return () => {\n finished = true;\n app.removeListener(\"browser-window-focus\", front);\n release?.();\n };\n}\n\n/** quitAndInstall, guarded by the unsaved-edits check. autoInstallOnAppQuit routes through the normal\n * window-close save path already, so only this explicit \"Restart Now\" needs the guard. */\nasync function quitAndInstallSafely(): Promise<void> {\n const win = activeWindow();\n if (!(await askRendererIsDirty(win))) { autoUpdater.quitAndInstall(); return; }\n\n const response = await themedPrompt(win, {\n buttons: [\"Save and restart\", \"Discard and restart\", \"Cancel\"],\n defaultId: 0,\n cancelId: 2,\n message: \"You have unsaved changes.\",\n detail: `Save them before restarting to install ${appName} ${updateDownloaded?.version || \"\"}?`,\n });\n\n if (response === 2) return; // Cancel - abort the install\n if (response === 0) {\n const result = await triggerRendererSave(win);\n if (!result.ok) {\n await themedPrompt(win, {\n message: \"Save failed\",\n detail: \"Your changes could not be saved, so the update was not installed. Save manually, then try again.\",\n buttons: [\"OK\"],\n });\n return;\n }\n }\n // response === 1 (Discard) falls through.\n autoUpdater.quitAndInstall();\n}\n\nautoUpdater.on(\"update-downloaded\", (info: UpdateDownloadedEvent) => {\n clearDownload();\n updateDownloaded = info;\n lastBackgroundError = null; // it got here in the end; stale retry noise would only mislead\n writeLog(\"info\", [`download: ${info.version} downloaded and ready to install`]);\n const win = activeWindow();\n if (!win) return;\n void themedPrompt(win, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`,\n unasked: true,\n }).then((response) => { if (response === 0) void quitAndInstallSafely(); });\n});\n\n/** Name the app and say how to find its window. Call once, at startup. */\nexport function configureUpdater(opts: UpdaterOptions): void {\n appName = opts.appName;\n activeWindowFn = opts.activeWindow;\n if (opts.logFile !== undefined) logPath = join(app.getPath(\"userData\"), opts.logFile);\n}\n\n/** Fire-and-forget background check; periodic + on launch. No-op in dev. */\nexport function startBackgroundUpdateCheck(): void {\n if (!app.isPackaged) return;\n autoUpdater.checkForUpdates().catch((err) => {\n console.error(\"AutoUpdater background check failed:\", err?.message || err);\n });\n}\n\n/** The Help ▸ Check for Updates… handler: always gives the user feedback (downloading / up to date /\n * error), unlike the silent background check. */\nexport async function manualCheckForUpdates(win?: BrowserWindow | null): Promise<void> {\n const parent = win || activeWindow();\n\n if (!app.isPackaged) {\n await themedPrompt(parent, {\n message: \"Updates unavailable in a development build\",\n detail: `Auto-update only runs in a packaged build. This is version ${app.getVersion()}.`,\n buttons: [\"OK\"],\n });\n return;\n }\n\n if (updateDownloaded) {\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${updateDownloaded.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n return;\n }\n\n if (download) {\n // A download is already in flight: show it, live (the dialog subscribes to progress events).\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${download.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n return;\n }\n\n try {\n // A manual check starts a fresh retry cycle: if the last one gave up, this is the user asking again.\n await autoUpdater.checkForUpdates();\n // With autoDownload off, `update-available` (fired during the await) starts the managed download,\n // so by here `download` is set iff the feed had something newer. (Read through locals: TS narrows the\n // module-level `download` to null after the guard above and cannot see the event handler mutate it.)\n const started = download as DownloadState | null;\n const ready = updateDownloaded as UpdateDownloadedEvent | null;\n if (started) {\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${started.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n lastBackgroundError = null;\n } else if (ready) {\n // The check completed a download between our earlier guard and now (tiny window, but free to handle).\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"], defaultId: 0, cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${ready.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n } else if (lastBackgroundError) {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date. The last update check reported an error:\\n${lastBackgroundError}`,\n buttons: [\"OK\"],\n });\n lastBackgroundError = null;\n } else {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date.`,\n buttons: [\"OK\"],\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const detail = lastBackgroundError && lastBackgroundError !== message\n ? `${message}\\n\\nA previous background check also failed with:\\n${lastBackgroundError}`\n : message;\n await themedPrompt(parent, { message: \"Update check failed\", detail, buttons: [\"OK\"] });\n lastBackgroundError = null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/tool-window.ts","../src/updater.ts"],"names":["BrowserWindow","electronUpdater","join","app","appendFileSync","ipcMain"],"mappings":"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","file":"updater.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, app, screen } from \"electron\";\nimport { join } from \"node:path\";\nimport type { Satellite } from \"./session.js\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n // Raise within OUR stack. This used to setAlwaysOnTop(true), which on macOS\n // and Windows floats the window above every other APPLICATION - and stuck\n // that way, since nothing ever unset it.\n w.moveTop();\n}\n\n/** macOS pinned windows, for the activation toggle below. */\nconst macPinned = new Set<BrowserWindow>();\nlet macActivationWired = false;\nfunction wireMacActivation(): void {\n if (macActivationWired) return;\n macActivationWired = true;\n // Floating is a GLOBAL window level: left on while another app is front,\n // the pinned window would sit over that app too - the exact 2026-08-25\n // complaint. Scoping it to our app's active state keeps the pin meaning\n // \"above my editor\" and nothing more.\n // Not while a modal has the pins held (frontForModal): the editor is in\n // front for it, and coming back to the app must not cover it again.\n app.on(\"did-become-active\", () => { if (pinsHeld > 0) return; for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\"); });\n app.on(\"did-resign-active\", () => { for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false); });\n}\n\n/**\n * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the\n * world. Two findings shaped the mechanism, one per platform family:\n *\n * - Bare `alwaysOnTop` (the first implementation) floats the window over\n * every other application on macOS and Windows, so a pinned Board meant\n * nothing else on the machine could come to the front (a Storyletter user\n * report, 2026-08-25).\n * - A CHILD window (the second implementation) is position-coupled to its\n * parent on macOS: dragging the editor dragged the Board with it (a\n * Storyletter user report, 2026-08-28). Windows and Linux owned windows do\n * not move with their owner, so the child mechanism stays right for them.\n *\n * So: on macOS the pin is ACTIVATION-SCOPED alwaysOnTop - floating while\n * this app is active, dropped the moment it resigns - which is above the\n * editor without following its drags and without covering anyone else. On\n * Windows and Linux the pin is a child window, which stays over its parent\n * and stacks normally against everything else. Each branch clears the other\n * mechanism first, which also heals a window an older build pinned the old\n * way. `platform` is a seam for the tests; leave it defaulted.\n */\nexport function pinToolWindow(\n w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean,\n platform: NodeJS.Platform = process.platform,\n): void {\n if (!w || w.isDestroyed()) return;\n if (platform === \"darwin\") {\n w.setParentWindow(null);\n if (on) {\n if (!macPinned.has(w)) {\n macPinned.add(w);\n w.once(\"closed\", () => macPinned.delete(w));\n }\n wireMacActivation();\n // Raise only when the app is active now and no modal has the pins\n // held; otherwise the next did-become-active, or the release, raises it.\n w.setAlwaysOnTop(pinsHeld === 0 && BrowserWindow.getFocusedWindow() !== null, \"floating\");\n } else {\n macPinned.delete(w);\n w.setAlwaysOnTop(false);\n }\n return;\n }\n w.setAlwaysOnTop(false);\n const owner = on && parent && !parent.isDestroyed() ? parent : null;\n // While a modal has the pins held, remember the choice and apply it at the\n // release, so the editor stays in front until the modal closes.\n if (pinsHeld > 0) {\n if (owner) heldChildren.set(w, owner); else heldChildren.delete(w);\n w.setParentWindow(null);\n return;\n }\n w.setParentWindow(owner);\n}\n\n// ---------------------------------------------------------------------------\n// Fronting the main window for a modal. A pinned tool window rides above the\n// editor by design, so a modal the EDITOR shows (the updater's prompts) opened\n// underneath one: the editor blurred behind its scrim and the prompt was\n// hidden, with nothing on screen to say why (both apps, 2026-10-02, the Play\n// window over the editor). While such a modal is up the pins step aside and\n// the editor comes forward; when it closes, every pin goes back as it was.\n// ---------------------------------------------------------------------------\n\n/** How many modals are holding the pins down. The pins return at zero. */\nlet pinsHeld = 0;\n/** Windows and Linux: the tool windows unparented by the hold, and the window\n * each goes back to. */\nconst heldChildren = new Map<BrowserWindow, BrowserWindow>();\n\n/**\n * Bring `main` in front of every tool window, pinned ones included, for as\n * long as a modal it shows is open: un-minimise, show, focus, with the pins\n * set aside. Returns the release, which puts the pins back (idempotent; holds\n * nest, and the pins return when the last is released). Call it only while\n * the app is active: focusing a window of an inactive app is the focus steal\n * the platforms forbid. `platform` is a seam for the tests.\n */\nexport function frontForModal(main: BrowserWindow, platform: NodeJS.Platform = process.platform): () => void {\n if (main.isDestroyed()) return () => {};\n pinsHeld++;\n if (platform === \"darwin\") {\n for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false);\n } else {\n for (const w of BrowserWindow.getAllWindows()) {\n if (w === main || w.isDestroyed()) continue;\n const owner = w.getParentWindow();\n if (owner !== main) continue;\n heldChildren.set(w, main);\n w.setParentWindow(null);\n }\n }\n if (main.isMinimized()) main.restore();\n main.show();\n main.focus();\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (--pinsHeld > 0) return;\n if (platform === \"darwin\") {\n // Only float again if the app is active; otherwise did-become-active does.\n const active = BrowserWindow.getFocusedWindow() !== null;\n if (active) for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\");\n } else {\n for (const [w, owner] of heldChildren) {\n if (!w.isDestroyed() && !owner.isDestroyed()) w.setParentWindow(owner);\n }\n heldChildren.clear();\n }\n };\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). Not used with `url`. */\n page?: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). Not used with `url`. */\n rendererDir?: string;\n /** Load this URL instead of a page from the renderer dir (a view window's `patter-view://...`). */\n url?: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n /** A session partition of the window's own (a view's in-memory `view-<id>`); the default session otherwise. */\n partition?: string;\n /** Whether the window may open devtools; Electron's default (yes) when absent. */\n devTools?: boolean;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** The window to PIN ABOVE: the app's main window, resolved at open time (a\n * getter, because the main window can be recreated). With `pinned` true\n * the new tool window rides above it - on macOS via activation-scoped\n * alwaysOnTop (a child window there follows the editor's drags), on\n * Windows and Linux as a child window (bare alwaysOnTop there floats over\n * the whole machine). See pinToolWindow for both findings. */\n pinTo?: () => BrowserWindow | undefined;\n /** Start pinned (the host remembers); flip later with `pinToolWindow`. */\n pinned?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n if (opts.url === undefined && (opts.page === undefined || opts.rendererDir === undefined)) {\n throw new Error(\"openToolWindow needs a url, or a page and its rendererDir\");\n }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n // Electron's defaults, said out loud: no webview tag, and a dropped file never navigates the window.\n webviewTag: false, navigateOnDragDrop: false,\n preload: opts.preload,\n ...(opts.partition !== undefined ? { partition: opts.partition } : {}),\n ...(opts.devTools !== undefined ? { devTools: opts.devTools } : {}),\n },\n });\n if (opts.pinTo !== undefined) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (opts.url !== undefined) void w.loadURL(opts.url);\n else if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir!, opts.page!));\n return w;\n}\n\n// ---------------------------------------------------------------------------\n// The tool windows AS DATA: one row per window, and everything that walks the\n// windows walks the rows. Lifted from Storyletter's TOOL_WINDOWS table +\n// openToolWindowFor + rescueWindows (studio/src/main/index.ts), where it had\n// replaced four near-identical openX() functions whose differences were all\n// mistakes (only one forwarded its console; Reset View rescued two of four).\n// Patterpad's three hand copies (createPlayWindow / createSearchWindow /\n// createCoverageWindow and a four-branch rescueWindows) become three rows.\n//\n// Two things the table makes structurally impossible: forgetting to register\n// a window as a satellite for the project-changed nudge (every row is\n// registered when a session is given), and Reset View rescuing a subset (it\n// walks the rows).\n// ---------------------------------------------------------------------------\n\n\nexport interface ToolWindowSpec<N extends string = string> {\n /** The window's key: the store slice it remembers under, the dev-console\n * prefix, and what `open(name)` takes. */\n name: N;\n title: string;\n /** Renderer entry, relative to the env's rendererDir. */\n page: string;\n def: ToolWindowSize;\n min: ToolWindowSize;\n /** The remembered rect, read at open time (a store read-through). */\n bounds: () => ToolWindowBounds | undefined;\n /** Persist a moved / resized rect (debounced by the factory). */\n remember: (bounds: ToolWindowBounds) => void;\n /** Whether it starts pinned above the editor (both apps default true). */\n pinned: () => boolean;\n /** Frameless (the family's shape: the renderer draws `toolWindowHead`).\n * Default false = frameless; pass `frame: true` for an OS title bar. */\n frame?: boolean;\n /** The project-changed channel for THIS window when it differs from the\n * env's (Storyletter's Links window listens on \"links:focus\"). */\n channel?: string;\n /** Drop what main caches for the outgoing project on this window's behalf\n * (a coverage report, a search anchor). Runs whether the window is open\n * or not: see session.ts. */\n clear?: () => void;\n}\n\nexport interface ToolWindowEnv {\n /** Absolute path of the built renderer dir. */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n /** The window to pin above: the app's main window, resolved at open time. */\n pinTo: () => BrowserWindow | undefined;\n /** Register every row as a satellite of the project session, so a new\n * project cannot open underneath a tool window that still shows the old\n * one. `channel` is the nudge each window listens on (a row may override). */\n session?: { addSatellite: (satellite: Satellite) => () => void; channel: string };\n /** Reset View's store half, run first: forget every remembered rect and\n * set pinned true (`resetWindows(store)` from app-store, or the app's own). */\n resetStore?: () => void;\n /** The channel a rescued window's renderer hears its re-pin on, so its pin\n * button agrees with the store (default \"state:pinned\"). */\n pinChannel?: string;\n /** Runs once per window created: forward its console in dev, say. */\n onOpened?: (w: BrowserWindow, name: string) => void;\n}\n\nexport interface ToolWindows<N extends string> {\n /** Open (or focus) one window by name. */\n open(name: N): BrowserWindow;\n /** The live window, if open. */\n get(name: N): BrowserWindow | undefined;\n /** Reset View's window half, over the table: store reset, then every open\n * window restored, default size, centred, re-pinned, and told so. */\n rescue(): void;\n /** Every window that is open right now. */\n all(): BrowserWindow[];\n readonly names: readonly N[];\n}\n\n/** Build the table. The windows are owned here: a host reads them through\n * `get(name)` rather than holding module variables of its own. */\nexport function defineToolWindows<N extends string>(specs: ToolWindowSpec<N>[], env: ToolWindowEnv): ToolWindows<N> {\n const live = new Map<N, BrowserWindow>();\n const byName = (name: N): ToolWindowSpec<N> => {\n const found = specs.find((s) => s.name === name);\n if (!found) throw new Error(`no tool window \"${name}\"`);\n return found;\n };\n const get = (name: N): BrowserWindow | undefined => {\n const w = live.get(name);\n return w && !w.isDestroyed() ? w : undefined;\n };\n if (env.session) {\n for (const spec of specs) {\n env.session.addSatellite({\n window: () => get(spec.name),\n channel: spec.channel ?? env.session.channel,\n ...(spec.clear ? { clear: spec.clear } : {}),\n });\n }\n }\n const open = (name: N): BrowserWindow => {\n const spec = byName(name);\n const existing = get(name);\n const opened = openToolWindow(existing, {\n title: spec.title, page: spec.page, frame: spec.frame ?? false,\n rendererDir: env.rendererDir, preload: env.preload,\n rect: savedWindowRect(spec.bounds(), spec.def, spec.min),\n min: spec.min,\n pinTo: env.pinTo,\n pinned: spec.pinned(),\n remember: spec.remember,\n });\n if (opened !== existing) {\n live.set(name, opened);\n // Identity-guarded: a stale close never clears a newer window.\n opened.on(\"closed\", () => { if (live.get(name) === opened) live.delete(name); });\n env.onOpened?.(opened, name);\n }\n return opened;\n };\n const rescue = (): void => {\n env.resetStore?.();\n for (const spec of specs) rescueToolWindow(get(spec.name), spec.def);\n // ACTUALLY pin them, then tell them. `rescueToolWindow` restores, resizes,\n // centres and raises but never pins; the store now says pinned, so the\n // window and its button have to agree before the claim is made.\n for (const spec of specs) {\n const w = get(spec.name);\n if (!w) continue;\n pinToolWindow(w, env.pinTo(), true);\n w.webContents.send(env.pinChannel ?? \"state:pinned\", true);\n }\n };\n return {\n open, get, rescue,\n all: () => specs.map((s) => get(s.name)).filter((w): w is BrowserWindow => w !== undefined),\n names: specs.map((s) => s.name),\n };\n}\n","// ---------------------------------------------------------------------------\n// In-app auto-update over electron-updater: the family's, lifted from Patterpad\n// where it was hardened in the field.\n//\n// This is not a wrapper anybody should write twice. Every rule in it is a bug\n// that reached a user:\n//\n// - Downloads are driven HERE rather than by electron-updater's autoDownload,\n// because holding the CancellationToken is what lets a stall watchdog kill\n// and retry a download that hangs. A hung stream emits neither progress nor\n// `error`, so anything built on events alone sits \"downloading\" for ever\n// (Patterpad #33: a Windows user's download did exactly that, all day).\n// - Differential download is off. On Windows it stalled silently across an\n// Electron major bump, where almost no blocks match anyway.\n// - There is a PERSISTENT LOG in userData, because electron-updater is silent\n// by default and a stalled download otherwise leaves nothing to diagnose.\n// - Installing asks the renderer whether there is unsaved work, and offers\n// Save / Discard / Cancel. \"Restart Now\" must never silently drop an\n// author's afternoon.\n// - Every prompt is THEMED, in the renderer, never a stock OS dialog.\n// - Every prompt brings its window to the FRONT, past pinned tool windows,\n// for as long as it is open. It used to open under the Play window, so\n// the editor blurred behind a prompt nobody could see. A prompt that\n// arrives while the app is in the background waits for the app to come\n// back before it takes the front, and one nobody asked for puts focus on\n// its safe button, so a keystroke meant for the window it covered cannot\n// answer it.\n//\n// Only runs in a packaged build; in development the manual check says so rather\n// than failing obscurely.\n//\n// THE SEAMS: the app supplies its NAME (it appears in every message), a way to\n// find the ACTIVE WINDOW, and a renderer that answers four channels - the\n// contract below. Everything else is the same in every app.\n// ---------------------------------------------------------------------------\n\nimport { app, BrowserWindow, ipcMain } from \"electron\";\nimport { appendFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ProgressInfo, UpdateDownloadedEvent, UpdateInfo } from \"electron-updater\";\nimport electronUpdater from \"electron-updater\";\nimport { frontForModal } from \"./tool-window.js\";\n\n/** Download progress, broadcast to every window so a live dialog can show it. */\nexport interface UpdaterDownloadProgress {\n percent: number;\n transferred: number;\n total: number;\n bytesPerSecond: number;\n /** Which version is coming, so a dialog can name it. */\n version: string;\n}\n\n/** A themed prompt, answered by the renderer with the chosen button index -\n * the same contract as `dialog.showMessageBox`'s `response`. */\nexport interface UpdaterPromptOptions {\n message: string;\n detail?: string;\n buttons: string[];\n defaultId?: number;\n cancelId?: number;\n /** Show live download progress in the dialog. */\n progress?: boolean;\n /** Nobody asked for this prompt: it arrived on its own (an update finished\n * downloading). Focus starts on the cancel button, so a keystroke meant for\n * whatever the prompt covered dismisses it rather than answering yes. */\n unasked?: boolean;\n}\n\n/** The channels the app's renderer must answer. Named here so both sides agree. */\nexport const UPDATER_CHANNELS = {\n /** main -> renderer: \"are there unsaved edits?\"; reply on `dirtyReply`. */\n checkDirty: \"updater:check-dirty\",\n dirtyReply: \"updater:dirty-reply\",\n /** main -> renderer: \"save now\"; reply `{ ok }` on `saveDone`. */\n saveBeforeInstall: \"updater:save-before-install\",\n saveDone: \"updater:save-done\",\n /** main -> renderer: show this prompt; reply with the button index. */\n prompt: \"updater:prompt\",\n promptReply: \"updater:prompt-reply\",\n /** main -> renderer: live download progress. */\n progress: \"updater:download-progress\",\n} as const;\n\nexport interface UpdaterOptions {\n /** The product name, as it should read in every message (\"Patterpad\"). */\n appName: string;\n /** Which window the prompts belong to. */\n activeWindow: () => BrowserWindow | null;\n /** The log file inside userData. */\n logFile?: string;\n}\n\nlet appName = \"This app\";\nlet activeWindowFn: () => BrowserWindow | null = () =>\n BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0] ?? null;\nconst activeWindow = (): BrowserWindow | null => activeWindowFn();\n\nconst { autoUpdater, CancellationToken } = electronUpdater;\n\nlet updateDownloaded: UpdateDownloadedEvent | null = null;\n// Remember the last background error so the manual \"Check for Updates\" dialog can\n// surface it - otherwise a broken feed (network policy, expired cert, GitHub outage)\n// stays invisible and the user has no idea why updates never arrive.\nlet lastBackgroundError: string | null = null;\n\n// Persistent updater log (userData/updater.log). electron-updater is silent by default; without a log a\n// Windows download that stalls without emitting `error` (found in 0.1.5) leaves nothing to diagnose.\nlet logPath = join(app.getPath(\"userData\"), \"updater.log\");\nconst writeLog = (level: string, args: unknown[]): void => {\n try { appendFileSync(logPath, `${new Date().toISOString()} [${level}] ${args.map((a) => (a instanceof Error ? a.stack || a.message : String(a))).join(\" \")}\\n`); } catch { /* logging must never throw */ }\n};\nautoUpdater.logger = {\n info: (...a: unknown[]) => writeLog(\"info\", a),\n warn: (...a: unknown[]) => writeLog(\"warn\", a),\n error: (...a: unknown[]) => writeLog(\"error\", a),\n debug: (...a: unknown[]) => writeLog(\"debug\", a),\n};\n\n// Downloads are driven by the download manager below, NOT electron-updater's autoDownload: holding\n// the CancellationToken ourselves is what lets the stall watchdog kill and retry a download that\n// hangs without ever erroring (#33 - a Windows user's download sat \"in the background\" all day; a\n// hung stream emits neither progress nor `error`, so nothing built on events alone can recover it).\nautoUpdater.autoDownload = false;\nautoUpdater.autoInstallOnAppQuit = true;\n// Force a plain full download instead of the block-by-block differential. On Windows the differential\n// path stalled silently (0.1.5): across an Electron-major bump almost no blocks match, so it downloads\n// nearly everything anyway, and it's fragile for unsigned installers. Full download is one reliable\n// stream. No-op on macOS (Squirrel.Mac always fetches the whole zip).\nautoUpdater.disableDifferentialDownload = true;\n\nautoUpdater.on(\"error\", (err) => {\n console.error(\"AutoUpdater error:\", err?.message || err);\n lastBackgroundError = err?.message || String(err);\n});\n\n// ---------------------------------------------------------------------------\n// Download manager: start, watch, retry. Platform-agnostic - it rides\n// electron-updater's shared events, identical for NSIS (win), zip (mac), and\n// AppImage (linux) feeds.\n// ---------------------------------------------------------------------------\n\nconst STALL_MS = 3 * 60 * 1000; // no progress event for this long = the download is hung\nconst WATCH_EVERY_MS = 30 * 1000; // how often the watchdog looks at the clock\nconst RETRY_DELAY_MS = 15 * 1000; // pause before re-attempting a killed/failed download\nconst MAX_ATTEMPTS = 3; // per check cycle; the 6-hourly background check starts a fresh cycle\n\ntype DownloadState = {\n info: UpdateInfo;\n token: InstanceType<typeof CancellationToken>;\n attempts: number; // attempts consumed this cycle, including the running one\n lastProgressAt: number;\n cancelledByWatchdog: boolean;\n watchdog: NodeJS.Timeout;\n};\nlet download: DownloadState | null = null;\n\nfunction broadcastProgress(p: UpdaterDownloadProgress): void {\n for (const w of BrowserWindow.getAllWindows()) {\n if (!w.isDestroyed() && !w.webContents.isDestroyed()) w.webContents.send(UPDATER_CHANNELS.progress, p);\n }\n}\n\nfunction clearDownload(): void {\n if (download) clearInterval(download.watchdog);\n download = null;\n}\n\n/** Begin (or re-attempt) downloading `info`. One state machine for every platform. */\nfunction beginDownload(info: UpdateInfo, attempts: number): void {\n clearDownload();\n const token = new CancellationToken();\n const state: DownloadState = {\n info, token, attempts: attempts + 1, lastProgressAt: Date.now(), cancelledByWatchdog: false,\n watchdog: setInterval(() => {\n if (download !== state) return;\n const quiet = Date.now() - state.lastProgressAt;\n if (quiet < STALL_MS) return;\n // Hung: no progress and no error for STALL_MS. Kill it; the download promise's catch retries.\n writeLog(\"warn\", [`watchdog: no download progress for ${Math.round(quiet / 1000)}s - cancelling (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n state.cancelledByWatchdog = true;\n state.token.cancel();\n }, WATCH_EVERY_MS),\n };\n download = state;\n writeLog(\"info\", [`download: starting ${info.version} (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n\n autoUpdater.downloadUpdate(token).then(() => {\n // Success is reported via the update-downloaded event; it clears the state.\n }).catch((err: unknown) => {\n if (download !== state) return; // superseded by a newer attempt/cycle\n const why = state.cancelledByWatchdog ? \"stalled (killed by the watchdog)\"\n : (err instanceof Error ? err.message : String(err));\n clearDownload();\n if (state.attempts < MAX_ATTEMPTS) {\n writeLog(\"warn\", [`download: attempt ${state.attempts} failed - ${why}; retrying in ${RETRY_DELAY_MS / 1000}s`]);\n setTimeout(() => { if (!download && !updateDownloaded) beginDownload(info, state.attempts); }, RETRY_DELAY_MS);\n } else {\n // Out of attempts for this cycle. Record it where the manual check surfaces it; the next\n // background check (6-hourly, or Help > Check for Updates) starts a fresh cycle.\n lastBackgroundError = `Downloading ${info.version} failed ${MAX_ATTEMPTS} times, most recently with \"${why}\". It will be tried again on the next check.`;\n writeLog(\"error\", [`download: giving up on ${info.version} this cycle - ${why}`]);\n }\n });\n}\n\nautoUpdater.on(\"update-available\", (info: UpdateInfo) => {\n if (download || updateDownloaded) return; // already downloading it, or already have it\n beginDownload(info, 0);\n});\n\nautoUpdater.on(\"download-progress\", (p: ProgressInfo) => {\n if (download) download.lastProgressAt = Date.now();\n broadcastProgress({\n percent: p.percent, transferred: p.transferred, total: p.total,\n bytesPerSecond: p.bytesPerSecond, version: download?.info.version ?? \"\",\n });\n});\n\n/** Ask the renderer (one-shot, timeout-guarded) whether the open scene has unsaved edits.\n * Resolves false on any error/timeout - the renderer's dirty flag is the only truth here. */\nfunction askRendererIsDirty(win: BrowserWindow | null): Promise<boolean> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(false); return; }\n const onReply = (_e: unknown, isDirty: boolean) => { clearTimeout(timeout); resolve(!!isDirty); };\n ipcMain.once(UPDATER_CHANNELS.dirtyReply, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.dirtyReply, onReply);\n console.warn(\"Updater dirty-check timed out; assuming clean.\");\n resolve(false);\n }, 2000);\n win.webContents.send(UPDATER_CHANNELS.checkDirty);\n });\n}\n\n/** Trigger a save in the renderer and wait for completion. Saves run the lock-aware VC write path,\n * so the timeout is generous; if it elapses we abort rather than install over a half-saved project. */\nfunction triggerRendererSave(win: BrowserWindow | null): Promise<{ ok: boolean }> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve({ ok: false }); return; }\n const onReply = (_e: unknown, result: { ok: boolean }) => { clearTimeout(timeout); resolve(result || { ok: false }); };\n ipcMain.once(UPDATER_CHANNELS.saveDone, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.saveDone, onReply);\n console.warn(\"Updater save timed out.\");\n resolve({ ok: false });\n }, 30000);\n win.webContents.send(UPDATER_CHANNELS.saveBeforeInstall);\n });\n}\n\n/** Show a THEMED prompt in the renderer (never a stock OS dialog; design-language \"coherent to the edges\")\n * and resolve the chosen button index - the same contract as dialog.showMessageBox's `response`. Falls\n * back to cancelId/defaultId if there's no live renderer to answer (so an install is never blocked on it). */\nfunction themedPrompt(win: BrowserWindow | null, opts: UpdaterPromptOptions): Promise<number> {\n return new Promise((resolve) => {\n const fallback = opts.cancelId ?? opts.defaultId ?? 0;\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(fallback); return; }\n const release = frontWhileOpen(win);\n const done = (idx: number): void => { release(); resolve(idx); };\n const onReply = (_e: unknown, idx: number) => { clearTimeout(timeout); done(typeof idx === \"number\" ? idx : fallback); };\n ipcMain.once(UPDATER_CHANNELS.promptReply, onReply);\n // Generous: these are user-facing prompts. If the renderer never answers (gone / hung), fall back safely.\n const timeout = setTimeout(() => { ipcMain.removeListener(UPDATER_CHANNELS.promptReply, onReply); done(fallback); }, 300000);\n win.webContents.send(UPDATER_CHANNELS.prompt, opts);\n });\n}\n\n/** Keep `win` in front of the app's other windows, pinned tool windows included, while a prompt is open\n * on it, and return the release. If the app is in the background the fronting waits until one of its\n * windows takes focus again: a prompt is no reason to pull an app over whatever the user went to. */\nfunction frontWhileOpen(win: BrowserWindow): () => void {\n let release: (() => void) | null = null;\n let finished = false;\n const front = (): void => { if (!finished && !win.isDestroyed()) release = frontForModal(win); };\n if (BrowserWindow.getFocusedWindow()) front();\n else app.once(\"browser-window-focus\", front);\n return () => {\n finished = true;\n app.removeListener(\"browser-window-focus\", front);\n release?.();\n };\n}\n\n/** quitAndInstall, guarded by the unsaved-edits check. autoInstallOnAppQuit routes through the normal\n * window-close save path already, so only this explicit \"Restart Now\" needs the guard. */\nasync function quitAndInstallSafely(): Promise<void> {\n const win = activeWindow();\n if (!(await askRendererIsDirty(win))) { autoUpdater.quitAndInstall(); return; }\n\n const response = await themedPrompt(win, {\n buttons: [\"Save and restart\", \"Discard and restart\", \"Cancel\"],\n defaultId: 0,\n cancelId: 2,\n message: \"You have unsaved changes.\",\n detail: `Save them before restarting to install ${appName} ${updateDownloaded?.version || \"\"}?`,\n });\n\n if (response === 2) return; // Cancel - abort the install\n if (response === 0) {\n const result = await triggerRendererSave(win);\n if (!result.ok) {\n await themedPrompt(win, {\n message: \"Save failed\",\n detail: \"Your changes could not be saved, so the update was not installed. Save manually, then try again.\",\n buttons: [\"OK\"],\n });\n return;\n }\n }\n // response === 1 (Discard) falls through.\n autoUpdater.quitAndInstall();\n}\n\nautoUpdater.on(\"update-downloaded\", (info: UpdateDownloadedEvent) => {\n clearDownload();\n updateDownloaded = info;\n lastBackgroundError = null; // it got here in the end; stale retry noise would only mislead\n writeLog(\"info\", [`download: ${info.version} downloaded and ready to install`]);\n const win = activeWindow();\n if (!win) return;\n void themedPrompt(win, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`,\n unasked: true,\n }).then((response) => { if (response === 0) void quitAndInstallSafely(); });\n});\n\n/** Name the app and say how to find its window. Call once, at startup. */\nexport function configureUpdater(opts: UpdaterOptions): void {\n appName = opts.appName;\n activeWindowFn = opts.activeWindow;\n if (opts.logFile !== undefined) logPath = join(app.getPath(\"userData\"), opts.logFile);\n}\n\n/** Fire-and-forget background check; periodic + on launch. No-op in dev. */\nexport function startBackgroundUpdateCheck(): void {\n if (!app.isPackaged) return;\n autoUpdater.checkForUpdates().catch((err) => {\n console.error(\"AutoUpdater background check failed:\", err?.message || err);\n });\n}\n\n/** The Help ▸ Check for Updates… handler: always gives the user feedback (downloading / up to date /\n * error), unlike the silent background check. */\nexport async function manualCheckForUpdates(win?: BrowserWindow | null): Promise<void> {\n const parent = win || activeWindow();\n\n if (!app.isPackaged) {\n await themedPrompt(parent, {\n message: \"Updates unavailable in a development build\",\n detail: `Auto-update only runs in a packaged build. This is version ${app.getVersion()}.`,\n buttons: [\"OK\"],\n });\n return;\n }\n\n if (updateDownloaded) {\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${updateDownloaded.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n return;\n }\n\n if (download) {\n // A download is already in flight: show it, live (the dialog subscribes to progress events).\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${download.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n return;\n }\n\n try {\n // A manual check starts a fresh retry cycle: if the last one gave up, this is the user asking again.\n await autoUpdater.checkForUpdates();\n // With autoDownload off, `update-available` (fired during the await) starts the managed download,\n // so by here `download` is set iff the feed had something newer. (Read through locals: TS narrows the\n // module-level `download` to null after the guard above and cannot see the event handler mutate it.)\n const started = download as DownloadState | null;\n const ready = updateDownloaded as UpdateDownloadedEvent | null;\n if (started) {\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${started.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n lastBackgroundError = null;\n } else if (ready) {\n // The check completed a download between our earlier guard and now (tiny window, but free to handle).\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"], defaultId: 0, cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${ready.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n } else if (lastBackgroundError) {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date. The last update check reported an error:\\n${lastBackgroundError}`,\n buttons: [\"OK\"],\n });\n lastBackgroundError = null;\n } else {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date.`,\n buttons: [\"OK\"],\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const detail = lastBackgroundError && lastBackgroundError !== message\n ? `${message}\\n\\nA previous background check also failed with:\\n${lastBackgroundError}`\n : message;\n await themedPrompt(parent, { message: \"Update check failed\", detail, buttons: [\"OK\"] });\n lastBackgroundError = null;\n }\n}\n"]}
|
package/dist/updater.js
CHANGED
package/dist/value-control.cjs
CHANGED
|
@@ -467,6 +467,10 @@ function valueHelpText(decl, value) {
|
|
|
467
467
|
return h === void 0 ? [] : [`${v}: ${h}`];
|
|
468
468
|
}).join("\n");
|
|
469
469
|
}
|
|
470
|
+
if (decl.type === "pick") {
|
|
471
|
+
const held = typeof value === "string" && value !== "" ? value : typeof decl.default === "string" ? decl.default : "";
|
|
472
|
+
return held === "" ? "" : said(held) ?? "";
|
|
473
|
+
}
|
|
470
474
|
return "";
|
|
471
475
|
}
|
|
472
476
|
var fallbackFlags = (decl) => Array.isArray(decl.default) ? decl.default.filter((v) => typeof v === "string") : [];
|