From 2566b27d4682b01773fd34c710f34217a77153c6 Mon Sep 17 00:00:00 2001 From: Giorgio Ughini Date: Thu, 6 Aug 2026 15:50:16 +0200 Subject: [PATCH 1/2] fix(ui): clarify advanced protection status Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b1fb5b3-71e6-4305-8335-9d0327bb0dc0 --- src/Recorder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Recorder.tsx b/src/Recorder.tsx index b301c1f..bf409c4 100644 --- a/src/Recorder.tsx +++ b/src/Recorder.tsx @@ -741,7 +741,7 @@ function SensitiveModelRow({ return ; } if (status.ocr === "ready") { - return ; + return ; } const failed = status.ocr === "error"; return ( From 1a582cc1f76458b35025e3ebc9f9c895d5d38864 Mon Sep 17 00:00:00 2001 From: Giorgio Ughini Date: Thu, 6 Aug 2026 15:51:53 +0200 Subject: [PATCH 2/2] fix(ui): prevent recorder window width growth Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: adfd86dc-6f98-47f3-b598-0fe86ac92b1a --- electron/recorder-window-sizing.test.ts | 74 +++++++++++++++++++++++++ electron/recorder-window-sizing.ts | 40 +++++++++++++ electron/window.ts | 25 +-------- package.json | 2 +- 4 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 electron/recorder-window-sizing.test.ts create mode 100644 electron/recorder-window-sizing.ts diff --git a/electron/recorder-window-sizing.test.ts b/electron/recorder-window-sizing.test.ts new file mode 100644 index 0000000..413b048 --- /dev/null +++ b/electron/recorder-window-sizing.test.ts @@ -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); +}); diff --git a/electron/recorder-window-sizing.ts b/electron/recorder-window-sizing.ts new file mode 100644 index 0000000..27676ac --- /dev/null +++ b/electron/recorder-window-sizing.ts @@ -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); + } +} diff --git a/electron/window.ts b/electron/window.ts index 586b42e..dd77e1e 100644 --- a/electron/window.ts +++ b/electron/window.ts @@ -10,6 +10,7 @@ import { resizeRecordingControlsBounds, } from "./recording-controls-bounds"; import { windowIcon } from "./icons"; +export { fitRecorderHeight } from "./recorder-window-sizing"; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -17,10 +18,6 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); * `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; @@ -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; diff --git a/package.json b/package.json index ba6b2d8..42a5c27 100644 --- a/package.json +++ b/package.json @@ -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",