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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions electron/recorder-window-sizing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
fitRecorderHeight,
type RecorderWindowSizingTarget,
} from "./recorder-window-sizing";

class WindowsFramedWindow implements RecorderWindowSizingTarget {
destroyed = false;
resizable = false;
size: [number, number] = [400, 500];
readonly resizeChanges: boolean[] = [];
readonly sizeChanges: Array<[number, number]> = [];

isDestroyed(): boolean {
return this.destroyed;
}

getContentSize(): [number, number] {
const frame: [number, number] = this.resizable ? [16, 39] : [2, 31];
return [this.size[0] - frame[0], this.size[1] - frame[1]];
}

getSize(): [number, number] {
return [...this.size];
}

isResizable(): boolean {
return this.resizable;
}

setResizable(resizable: boolean): void {
this.resizable = resizable;
this.resizeChanges.push(resizable);
}

setSize(width: number, height: number): void {
this.size = [width, height];
this.sizeChanges.push([width, height]);
}
}

test("recorder fitting preserves outer width across Windows frame changes", () => {
const win = new WindowsFramedWindow();

fitRecorderHeight(win, 600);
assert.deepEqual(win.size, [400, 631]);
assert.deepEqual(win.getContentSize(), [398, 600]);
assert.deepEqual(win.resizeChanges, [true, false]);
assert.deepEqual(win.sizeChanges, [[400, 631]]);

for (let i = 0; i < 5; i++) fitRecorderHeight(win, 600);
assert.deepEqual(win.size, [400, 631]);
assert.deepEqual(win.sizeChanges, [[400, 631]]);
});

test("recorder fitting clamps content height and ignores invalid requests", () => {
const win = new WindowsFramedWindow();

fitRecorderHeight(win, 10);
assert.equal(win.getContentSize()[1], 320);

fitRecorderHeight(win, 10_000);
assert.equal(win.getContentSize()[1], 720);

const size = [...win.size];
fitRecorderHeight(win, Number.NaN);
assert.deepEqual(win.size, size);

win.destroyed = true;
fitRecorderHeight(win, 500);
assert.deepEqual(win.size, size);
});
40 changes: 40 additions & 0 deletions electron/recorder-window-sizing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const RECORDER_MIN_HEIGHT = 320;
const RECORDER_MAX_HEIGHT = 720;

export interface RecorderWindowSizingTarget {
isDestroyed(): boolean;
getContentSize(): number[];
getSize(): number[];
isResizable(): boolean;
setResizable(resizable: boolean): void;
setSize(width: number, height: number, animate?: boolean): void;
}

/**
* Fit the recorder to its rendered content while preserving its fixed outer width.
* Windows changes the non-client frame thickness when resizability is toggled, so
* feeding a content width measured before that toggle into `setContentSize` grows
* the outer window on every ResizeObserver callback.
*/
export function fitRecorderHeight(
win: RecorderWindowSizingTarget,
contentHeight: number,
): void {
if (win.isDestroyed() || !Number.isFinite(contentHeight)) return;

const targetContentHeight = Math.round(
Math.max(RECORDER_MIN_HEIGHT, Math.min(RECORDER_MAX_HEIGHT, contentHeight)),
);
const [, currentContentHeight] = win.getContentSize();
if (Math.abs(currentContentHeight - targetContentHeight) < 1) return;

const [outerWidth, outerHeight] = win.getSize();
const targetOuterHeight = outerHeight + targetContentHeight - currentContentHeight;
const wasResizable = win.isResizable();
if (!wasResizable) win.setResizable(true);
try {
win.setSize(outerWidth, targetOuterHeight);
} finally {
if (!wasResizable && !win.isDestroyed()) win.setResizable(false);
}
}
25 changes: 1 addition & 24 deletions electron/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@ import {
resizeRecordingControlsBounds,
} from "./recording-controls-bounds";
import { windowIcon } from "./icons";
export { fitRecorderHeight } from "./recorder-window-sizing";

const dirname = path.dirname(fileURLToPath(import.meta.url));

/** Compact recording HUD — fixed width; height auto-fits its content (see
* `fitRecorderHeight`). The initial height is a sensible first paint before the
* renderer reports its true content height. */
const RECORDER = { width: 400, height: 500 };
/** Guard rails for the content-driven height so a bad measurement can't produce a
* degenerate window. */
const RECORDER_MIN_HEIGHT = 320;
const RECORDER_MAX_HEIGHT = 720;
/** Library sizing bounds; actual width adapts to the space beside the recorder. */
const LIBRARY = { desiredWidth: 1140, minWidth: 720, floorWidth: 520, maxHeight: 820 };
const MARGIN = 12;
Expand Down Expand Up @@ -70,26 +67,6 @@ export function createRecorderWindow(): BrowserWindow {
return win;
}

/**
* Fit the recorder window to the height the renderer reports for its content, so the
* fixed-width HUD shows neither dead space (short states) nor a clipped row (when the
* doctor reveals an extra model row). The width is preserved; the top-left is kept so
* it grows/shrinks downward. `resizable:false` blocks user resizing but not this
* programmatic sizing — on platforms that also block that, we briefly re-enable it.
*/
export function fitRecorderHeight(win: BrowserWindow, contentHeight: number): void {
if (win.isDestroyed() || !Number.isFinite(contentHeight)) return;
const target = Math.round(
Math.max(RECORDER_MIN_HEIGHT, Math.min(RECORDER_MAX_HEIGHT, contentHeight)),
);
const [width, current] = win.getContentSize();
if (Math.abs(current - target) < 1) return;
const wasResizable = win.isResizable();
if (!wasResizable) win.setResizable(true);
win.setContentSize(width, target);
if (!wasResizable) win.setResizable(false);
}

/** Always-on-top recording transport shown without stealing focus from the task. */
export function createRecordingControlsWindow(): BrowserWindow {
let bounds: Bounds;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"check:lockfile": "node scripts/check-lockfile-portability.mjs",
"typecheck": "tsc --noEmit",
"typecheck:evals": "tsc --noEmit -p evals/tsconfig.json",
"test": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs --test evals/builder-imports.test.ts common/architecture-registry.test.ts electron/architectures/catalogue-registry.test.ts common/audio.test.ts common/microphone.test.ts common/narration.test.ts common/sensitive.test.ts electron/recording-controls-bounds.test.ts electron/recording-privacy.test.ts electron/crash-guards.test.ts electron/recorder/controller.test.ts electron/recorder/session-store.test.ts electron/frames/extractor.test.ts electron/narration/audio-analysis.test.ts electron/narration/analyze-gate.test.ts electron/narration/transcribe.test.ts electron/narration/whisper.test.ts electron/sensitive/scanner.test.ts electron/sensitive/secrets.test.ts electron/sensitive/tessdata-source.test.ts electron/sensitive/ocr.test.ts electron/sensitive/frame-redact.test.ts electron/sensitive/frame-heuristics.test.ts electron/sessions.test.ts electron/debug-bundle.test.ts electron/skillbuilder/placement.test.ts src/skill-placement.test.ts scripts/compliance.test.mjs",
"test": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs --test evals/builder-imports.test.ts common/architecture-registry.test.ts electron/architectures/catalogue-registry.test.ts common/audio.test.ts common/microphone.test.ts common/narration.test.ts common/sensitive.test.ts electron/recording-controls-bounds.test.ts electron/recorder-window-sizing.test.ts electron/recording-privacy.test.ts electron/crash-guards.test.ts electron/recorder/controller.test.ts electron/recorder/session-store.test.ts electron/frames/extractor.test.ts electron/narration/audio-analysis.test.ts electron/narration/analyze-gate.test.ts electron/narration/transcribe.test.ts electron/narration/whisper.test.ts electron/sensitive/scanner.test.ts electron/sensitive/secrets.test.ts electron/sensitive/tessdata-source.test.ts electron/sensitive/ocr.test.ts electron/sensitive/frame-redact.test.ts electron/sensitive/frame-heuristics.test.ts electron/sessions.test.ts electron/debug-bundle.test.ts electron/skillbuilder/placement.test.ts src/skill-placement.test.ts scripts/compliance.test.mjs",
"eval": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs evals/run.ts",
"eval:builder": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs evals/builder/run.ts",
"eval:skill": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs evals/skillbuilder/run.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/Recorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ function SensitiveModelRow({
return <Row label="advanced protection" status="warn" note={note} />;
}
if (status.ocr === "ready") {
return <Row label="advanced protection" status="good" note="on-device · multilingual" />;
return <Row label="advanced protection" status="good" note="on-device" />;
}
const failed = status.ocr === "error";
return (
Expand Down
Loading