Summary
On Windows with fractional display scaling (verified at 125% / DPI 120), the recorder HUD widens continuously from the moment it launches, with no user interaction, until it spans the display.
Environment
- Windows 11 x64, display scaling 125% (
GetDpiForWindow returns 120)
- Commit
87fdf397ea85f07595b2645bc52cdbcda14eb36f, installed via install.ps1
- Electron 43.1.1, Node 24.19.0
Steps to reproduce
- Set Windows display scaling to 125%.
- Launch Skill Recorder.
- Watch the HUD window. No clicks required.
Observed
Polling GetWindowRect on the HUD window:
| elapsed |
outer size |
| 0.0s |
401 x 500 |
| 1.2s |
418 x 494 |
| 5.0s |
507 x 494 |
| 20.0s |
1306 x 494 |
| 45.0s |
1403 x 494 (still growing) |
Height stays at 494. Only the width grows, at roughly 15-20 px/s.
Analysis
fitRecorderHeight in electron/recorder-window-sizing.ts reads the outer width via getSize() and feeds it straight back into setSize():
const [outerWidth, outerHeight] = win.getSize();
// ...
win.setSize(outerWidth, targetOuterHeight);
Both APIs operate in DIPs. Under fractional scaling the DIP -> physical -> DIP round trip is lossy, so every call widens the window slightly. The width change reflows the renderer, the ResizeObserver in src/Recorder.tsx fires again, and fitRecorderHeight runs again. The result is a self-sustaining rAF-paced loop.
At 100% scaling the round trip is lossless and the height early-return terminates the loop, which is likely why 1a582cc ("fix(ui): prevent recorder window width growth") appeared to resolve this.
Related: the same rounding perturbs the resulting content height, so Math.abs(currentContentHeight - targetContentHeight) < 1 may never hold and the loop never settles on its own.
Suggested fix
Pin the outer width to its first observed value rather than re-reading it on each call, and give the height comparison a small tolerance:
const pinnedOuterWidths = new WeakMap<RecorderWindowSizingTarget, number>();
const HEIGHT_TOLERANCE = 2;
// ...
if (Math.abs(currentContentHeight - targetContentHeight) <= HEIGHT_TOLERANCE) return;
const [outerWidth, outerHeight] = win.getSize();
let pinnedWidth = pinnedOuterWidths.get(win);
if (pinnedWidth === undefined) {
pinnedWidth = outerWidth;
pinnedOuterWidths.set(win, pinnedWidth);
}
// ...
win.setSize(pinnedWidth, targetOuterHeight);
With this applied locally the window settles at 402 x 494 and stays there (verified over 45s), and recorder-window-sizing.test.ts still passes.
Note that the existing test double models a lossless DIP mapping, so it cannot catch this class of bug. A case whose setSize simulates fractional-scale rounding would help.
Summary
On Windows with fractional display scaling (verified at 125% / DPI 120), the recorder HUD widens continuously from the moment it launches, with no user interaction, until it spans the display.
Environment
GetDpiForWindowreturns 120)87fdf397ea85f07595b2645bc52cdbcda14eb36f, installed viainstall.ps1Steps to reproduce
Observed
Polling
GetWindowRecton the HUD window:Height stays at 494. Only the width grows, at roughly 15-20 px/s.
Analysis
fitRecorderHeightinelectron/recorder-window-sizing.tsreads the outer width viagetSize()and feeds it straight back intosetSize():Both APIs operate in DIPs. Under fractional scaling the DIP -> physical -> DIP round trip is lossy, so every call widens the window slightly. The width change reflows the renderer, the
ResizeObserverinsrc/Recorder.tsxfires again, andfitRecorderHeightruns again. The result is a self-sustaining rAF-paced loop.At 100% scaling the round trip is lossless and the height early-return terminates the loop, which is likely why 1a582cc ("fix(ui): prevent recorder window width growth") appeared to resolve this.
Related: the same rounding perturbs the resulting content height, so
Math.abs(currentContentHeight - targetContentHeight) < 1may never hold and the loop never settles on its own.Suggested fix
Pin the outer width to its first observed value rather than re-reading it on each call, and give the height comparison a small tolerance:
With this applied locally the window settles at 402 x 494 and stays there (verified over 45s), and
recorder-window-sizing.test.tsstill passes.Note that the existing test double models a lossless DIP mapping, so it cannot catch this class of bug. A case whose
setSizesimulates fractional-scale rounding would help.