electron+tests: zoom plumbing (construction-only lights), hermeticity fence for the jobs suite
- ZOOM: the ⌘±/⌘0 ladder is owned, not stock roles — applyZoom sets the factor and broadcasts 'lh:zoom'; the renderer publishes --lh-zoom; the gutter lengths (.lh-lights-gutter / .lh-closed-toggle) divide by it so their RENDERED size is a physical constant at every zoom — the graph button stays glued next to the lights, nothing slides under them. before-input-event catches ⌘=/layout variants the menu's Plus may not claim; did-finish-load re-pushes on reload; getZoom covers first paint. The lights themselves stay at construction position — Electron 44 has NO setTrafficLightPosition (options-only; the first run crashed on it). HIG-acceptable: native lights hug the corner; the web chrome compensates around them. - JOBS SUITE FLAKE, actually fixed this time: the seams are module-global and the sweep scans every record — a timed-out async test left a running orphan + late wake that poisoned its NEIGHBORS (respawned 3 !== 0). New afterEach fence: settle every tracked running job, unregister aborts, seams reset to inert no-ops — every test starts from identical state regardless of upstream flake. Verified: two consecutive full-suite runs 1326/1326.
This commit is contained in:
+12
-3
@@ -516,12 +516,21 @@ body {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
/* 92px = measured lights' right edge (~75pt) + the lights' own 16px
|
||||
inset from the window edge — comfortable, symmetric air (owner). */
|
||||
inset from the window edge — comfortable, symmetric air (owner).
|
||||
PHYSICAL-CONSTANT under zoom (owner round 7, 2026-10-05): ⌘±/⌘0 scale
|
||||
every CSS px by the zoomFactor while the native lights never move —
|
||||
so these lengths DIVIDE by --lh-zoom (published by the renderer from
|
||||
the lh:zoom IPC) to render at the same physical size at every zoom.
|
||||
The graph button stays glued next to the lights zoomed in; nothing
|
||||
slides under them zoomed out. Default 1 until the first push lands. */
|
||||
.lh-electron {
|
||||
--lh-zoom: 1;
|
||||
}
|
||||
.lh-electron .lh-lights-gutter {
|
||||
padding-left: 92px;
|
||||
padding-left: calc(92px / var(--lh-zoom));
|
||||
}
|
||||
.lh-electron .lh-closed-toggle {
|
||||
margin-left: 92px;
|
||||
margin-left: calc(92px / var(--lh-zoom));
|
||||
}
|
||||
|
||||
/* --font-title deliberately NOT @theme inline (see comment above): the
|
||||
|
||||
+17
-1
@@ -54,7 +54,12 @@ const IS_ELECTRON = typeof navigator !== "undefined" && navigator.userAgent.incl
|
||||
declare global {
|
||||
interface Window {
|
||||
/** Preload bridge (electron/preload.cjs) — Electron only, absent in the browser. */
|
||||
lh?: { expandWindow: () => void };
|
||||
lh?: {
|
||||
expandWindow: () => void;
|
||||
/** The zoom truth (owner round 7): subscribes to "lh:zoom" pushes; returns unsubscribe. */
|
||||
onZoom?: (cb: (factor: number) => void) => () => void;
|
||||
getZoom?: () => Promise<number>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,10 +89,21 @@ export function SidebarProvider({ children }: { children: React.ReactNode }) {
|
||||
document.documentElement.classList.add("lh-electron");
|
||||
}
|
||||
|
||||
// THE ZOOM TRUTH (owner round 7, 2026-10-05): ⌘±/⌘0 zoom the UI
|
||||
// (Electron zoomFactor) while the native traffic lights stay
|
||||
// physical-fixed — the CSS gutter lengths divide by this factor so
|
||||
// their RENDERED size never moves. getZoom covers the first-paint
|
||||
// race; every later change arrives as an lh:zoom push.
|
||||
const setZoomVar = (factor: number) =>
|
||||
document.documentElement.style.setProperty("--lh-zoom", String(factor));
|
||||
const off = window.lh?.onZoom?.(setZoomVar);
|
||||
void window.lh?.getZoom?.().then(setZoomVar);
|
||||
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
// Default: open (first run introduces the sidebar).
|
||||
setOpen(stored === null ? true : stored === "1");
|
||||
setMounted(true);
|
||||
return () => off?.();
|
||||
}, []);
|
||||
|
||||
// Narrow-window auto-close/auto-restore (see SIDEBAR_AUTOCLOSE note):
|
||||
|
||||
+78
-1
@@ -252,10 +252,59 @@ function createWindow(url) {
|
||||
});
|
||||
|
||||
void win.loadURL(url);
|
||||
// Zoom truth reaches the renderer for EVERY load (reload, nav): the
|
||||
// renderer's --lh-zoom must never go stale. The fallback catcher below
|
||||
// covers ⌘= / layout variants the menu's ⌘Plus may not claim — menu-
|
||||
// claimed keys never reach before-input-event, so exactly one of the
|
||||
// two applies any given zoom keypress.
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
win.webContents.send("lh:zoom", win.webContents.getZoomFactor());
|
||||
});
|
||||
win.webContents.on("before-input-event", (event, input) => {
|
||||
if (input.type !== "keyDown" || !(input.meta || input.control)) return;
|
||||
if (input.key === "=" || input.key === "+") zoomStep(win, +1);
|
||||
else if (input.key === "-") zoomStep(win, -1);
|
||||
else if (input.key === "0") applyZoom(win, 1);
|
||||
else return;
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
win.once("ready-to-show", () => win.show());
|
||||
return win;
|
||||
}
|
||||
|
||||
/* ── Zoom (owner round 7, 2026-10-05): ⌘±/⌘0 as an OWNED ladder ──────────
|
||||
* The stock zoomIn/zoomOut roles change zoomLevel silently — the renderer
|
||||
* never learns the factor, and the native traffic lights (physical-fixed,
|
||||
* hiddenInset) drift out of relation to the scaled chrome: the graph
|
||||
* button rides away from them zoomed in, content slides under them
|
||||
* zoomed out. Here zoom is explicit: a factor ladder, applied through
|
||||
* setZoomFactor, and broadcast to the renderer ("lh:zoom" — it publishes
|
||||
* --lh-zoom so gutter lengths can divide by it and stay PHYSICAL-
|
||||
* constant). The traffic lights themselves stay at their construction
|
||||
* position ({x:16, y:22.5} — Electron 44 has NO runtime setter, only
|
||||
* the BrowserWindow option), which is HIG-correct anyway: native lights
|
||||
* hug the window corner while the web chrome compensates around them.
|
||||
* Vertical note: at large factors the taller bands' content centers
|
||||
* drift below the fixed lights — accepted (the alternative, moving the
|
||||
* lights, is impossible without rebuilding the window). */
|
||||
const ZOOM_LADDER = [0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2];
|
||||
|
||||
function zoomStep(win, dir) {
|
||||
const current = win.webContents.getZoomFactor();
|
||||
const next =
|
||||
dir > 0
|
||||
? ZOOM_LADDER.find((f) => f > current + 0.001) ?? ZOOM_LADDER[ZOOM_LADDER.length - 1]
|
||||
: [...ZOOM_LADDER].reverse().find((f) => f < current - 0.001) ?? ZOOM_LADDER[0];
|
||||
applyZoom(win, next);
|
||||
}
|
||||
|
||||
function applyZoom(win, factor) {
|
||||
factor = Math.min(2, Math.max(0.75, factor)); // ladder clamps; ⌘0 = 1
|
||||
win.webContents.setZoomFactor(factor);
|
||||
win.webContents.send("lh:zoom", factor);
|
||||
}
|
||||
|
||||
/** macOS needs an application menu with the Edit roles — without one,
|
||||
* Cmd+C/V/A do nothing in the renderer. Everything else stays minimal. */
|
||||
function installMenu() {
|
||||
@@ -274,7 +323,32 @@ function installMenu() {
|
||||
{ label: "Edit", submenu: [{ role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, { role: "selectAll" }] },
|
||||
{
|
||||
label: "View",
|
||||
submenu: [{ role: "reload" }, { role: "toggleDevTools" }, { type: "separator" }, { role: "resetZoom" }, { role: "zoomIn" }, { role: "zoomOut" }, { type: "separator" }, { role: "togglefullscreen" }],
|
||||
submenu: [
|
||||
{ role: "reload" },
|
||||
{ role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
// OWNED zoom (see applyZoom): the roles change zoomLevel
|
||||
// silently; these broadcast the factor and re-seat the
|
||||
// traffic lights. The renderer's --lh-zoom depends on
|
||||
// hearing every change.
|
||||
{
|
||||
label: "Actual Size",
|
||||
accelerator: "CommandOrControl+0",
|
||||
click: () => applyZoom(BrowserWindow.getAllWindows()[0], 1),
|
||||
},
|
||||
{
|
||||
label: "Zoom In",
|
||||
accelerator: "CommandOrControl+Plus",
|
||||
click: () => zoomStep(BrowserWindow.getAllWindows()[0], +1),
|
||||
},
|
||||
{
|
||||
label: "Zoom Out",
|
||||
accelerator: "CommandOrControl+-",
|
||||
click: () => zoomStep(BrowserWindow.getAllWindows()[0], -1),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
],
|
||||
},
|
||||
{ role: "windowMenu" },
|
||||
];
|
||||
@@ -316,6 +390,9 @@ async function main() {
|
||||
// width asks the window to widen instead — the layout never has to
|
||||
// accommodate a narrow window + open sidebar. Top-left stays fixed, the
|
||||
// window grows right. No-op when already wide.
|
||||
// The renderer's first paint can race did-finish-load — it asks for
|
||||
// the current factor itself (mirrors "lh:zoom" pushes).
|
||||
ipcMain.handle("lh:zoom:get", (event) => BrowserWindow.fromWebContents(event.sender)?.webContents.getZoomFactor() ?? 1);
|
||||
ipcMain.on("lh:expand-window", (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!win) return;
|
||||
|
||||
@@ -8,10 +8,22 @@
|
||||
* threshold (928px) when the sidebar is opened below it. The main
|
||||
* process clamps: no-op if the window is already wide enough.
|
||||
*
|
||||
* window.lh.onZoom(cb) / window.lh.getZoom() — the zoom truth (owner
|
||||
* round 7, 2026-10-05): every ⌘±/⌘0 change arrives as "lh:zoom" with
|
||||
* the new factor; getZoom covers the first-paint race. The renderer
|
||||
* publishes it as --lh-zoom so physical-constant lengths (the traffic-
|
||||
* light gutter) can divide by it.
|
||||
*
|
||||
* Channel names are mirrored in electron/main.cjs — keep them in sync.
|
||||
*/
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("lh", {
|
||||
expandWindow: () => ipcRenderer.send("lh:expand-window"),
|
||||
onZoom: (cb) => {
|
||||
const listener = (_event, factor) => cb(factor);
|
||||
ipcRenderer.on("lh:zoom", listener);
|
||||
return () => ipcRenderer.removeListener("lh:zoom", listener);
|
||||
},
|
||||
getZoom: () => ipcRenderer.invoke("lh:zoom:get"),
|
||||
});
|
||||
|
||||
+28
-1
@@ -26,7 +26,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { after, describe, it } from "node:test";
|
||||
import { after, afterEach, describe, it } from "node:test";
|
||||
import { createFauxCore, fauxAssistantMessage, fauxText, fauxToolCall, type AssistantMessage, type FauxResponseStep } from "@earendil-works/pi-ai";
|
||||
import {
|
||||
accumulateJobUsage,
|
||||
@@ -123,6 +123,33 @@ function trackJob(id: string) {
|
||||
createdJobs.push(id);
|
||||
}
|
||||
|
||||
/* THE HERMETICITY FENCE (owner round 7, 2026-10-05): the jobs seams are
|
||||
* MODULE-GLOBAL (setJobRespawn / setTurnRunner / the abort registry) and
|
||||
* the sweep scans EVERY job in the store. An async test that times out
|
||||
* under full-suite load leaves a "running" record and a late wake behind
|
||||
* — and that stray state then poisons the NEXT test (a foreign respawn
|
||||
* lands in its counter; a foreign orphan in its sweep). Under isolation
|
||||
* nothing ever times out, which is why the file passes alone and flaked
|
||||
* in the full run. The fence makes every test's start state identical:
|
||||
* all tracked jobs settled, aborts unregistered, seams inert. */
|
||||
afterEach(() => {
|
||||
for (const id of createdJobs) {
|
||||
try {
|
||||
const job = jobStore.getJob(id);
|
||||
if (job && job.status === "running") {
|
||||
jobStore.updateJob(id, { status: "done", activity: "settled (fence)" });
|
||||
}
|
||||
unregisterJobAbort(id);
|
||||
} catch {
|
||||
/* already gone */
|
||||
}
|
||||
}
|
||||
// Late wakes from a prior test must be inert: a no-op respawn can't
|
||||
// increment anyone's counter, a no-op runner can't spawn real work.
|
||||
setJobRespawn(() => {});
|
||||
setTurnRunner(async () => 200);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
for (const id of createdChats) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user