Fix restarted recordings dropping clicks / stopping after one click
Root cause: warm-before-hide kept the window visible during backend warmup, and on a restart that warmup could take several seconds (the stream backend start waits up to 8s). During that visible window, clicks over the app were skipped by the userIsInApp guard and clicks elsewhere were shot post-click, so a restarted session looked like it stopped after one click. - Recording is now 'armed' only after the window is hidden and the buffer is primed. A new warmingUp flag makes onOsClick ignore clicks during warmup (the window is covering the user's work anyway) instead of mishandling them. Cleared on pause/finish. - armRecording caps the warmup wait (WARMUP_MAX_MS=1500): the window hides and the session arms even if the backend start hangs, so it can never sit visible for seconds dropping clicks. The backend keeps coming up in the background; the first click or two may take the fresh-shot fallback. - A generation token invalidates an in-flight backend start whose session has since finished, so a slow start can't install into a new session or leave the starting-guard stuck and block the restart from starting one. Tests: 4 new behavioral capture tests (warmup ignores clicks; pause/finish clear it; armRecording warms-then-hides-then-arms; a hung start still arms within the cap; a stale start is discarded and frees the guard) plus a new end-to-end self-test scenario (warmup click ignored, first armed click captured). 152 unit tests + all repo checks pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+28
-18
@@ -195,11 +195,22 @@ function createWindow() {
|
||||
console.log('CLICK-SELFTEST burst:', burstSteps, 'of', burstCount,
|
||||
burstSteps === burstCount ? 'OK — no clicks dropped on finish' : 'FAIL — clicks lost');
|
||||
|
||||
// Helper: wait until armRecording has finished warming (window
|
||||
// hidden, buffer primed) so an injected click counts as a real
|
||||
// recording click rather than being ignored as a warmup click.
|
||||
const waitArmed = async () => {
|
||||
for (let i = 0; i < 80 && capture.warmingUp; i++) {
|
||||
await new Promise((res) => setTimeout(res, 50));
|
||||
}
|
||||
};
|
||||
|
||||
// Third scenario: the real "Start recording" path. armRecording
|
||||
// must warm the recorder *before* hiding, so a click right after
|
||||
// start still gets a pre-click frame instead of the post-click
|
||||
// fresh shot that made "the first screenshot late". (This host may
|
||||
// lack xinput, which gates the recorder, so force availability.)
|
||||
// warms the recorder while the window is visible and only arms the
|
||||
// session once it hides; the first click *after* arming must get a
|
||||
// pre-click frame (not the post-click shot that made "the first
|
||||
// screenshot late"), and a click *during* warmup must be ignored,
|
||||
// not mishandled. (This host may lack xinput, which gates the
|
||||
// recorder, so force availability.)
|
||||
const armGuide = store.createGuide({ title: 'arm selftest' });
|
||||
mainWindow.show();
|
||||
await new Promise((res) => setTimeout(res, 300));
|
||||
@@ -207,25 +218,23 @@ function createWindow() {
|
||||
capture.stopClickWatcher();
|
||||
capture.clickCaptureAvailable = () => true;
|
||||
capture.hiddenForSession = true; // window was visible at session start
|
||||
capture.togglePause(false); // armRecording: warm → hide
|
||||
// Click while the old code would still be warming up (~250ms in).
|
||||
await new Promise((res) => setTimeout(res, 250));
|
||||
capture.togglePause(false); // armRecording: warm → hide → arm
|
||||
// A click during warmup must be ignored (window still visible).
|
||||
await new Promise((res) => setTimeout(res, 200));
|
||||
const warmupClicks = store.getGuide(armGuide.guideId).stepsOrder.length;
|
||||
capture.onOsClick(Date.now(), toPhysical({ x: bounds.x + 100, y: bounds.y + 100 }), 'button-1');
|
||||
await waitArmed();
|
||||
const armPoint = {
|
||||
x: Math.round(bounds.x + bounds.width * 0.4),
|
||||
y: Math.round(bounds.y + bounds.height * 0.4),
|
||||
};
|
||||
const armClickAt = Date.now();
|
||||
capture.onOsClick(armClickAt, toPhysical(armPoint), 'button-1');
|
||||
capture.onOsClick(Date.now(), toPhysical(armPoint), 'button-1');
|
||||
await capture.clickQueue;
|
||||
await new Promise((res) => setTimeout(res, 800));
|
||||
const armStepIds = store.getGuide(armGuide.guideId).stepsOrder;
|
||||
let armPreClick = false;
|
||||
if (armStepIds.length) {
|
||||
// A pre-click frame is the win; the log line shows the margin.
|
||||
armPreClick = true;
|
||||
}
|
||||
console.log('CLICK-SELFTEST arm: first click ->', armStepIds.length,
|
||||
'step(s)', armPreClick ? '(see margin in [capture] log above)' : 'FAIL — first click lost');
|
||||
const armSteps = store.getGuide(armGuide.guideId).stepsOrder.length;
|
||||
console.log('CLICK-SELFTEST arm: warmup-click steps', warmupClicks,
|
||||
'-> after-arm steps', armSteps,
|
||||
armSteps === 1 ? 'OK — warmup click ignored, first armed click captured' : 'FAIL');
|
||||
capture.finishSession();
|
||||
|
||||
// Fourth scenario: the debounce itself, exercised end to end through
|
||||
@@ -241,7 +250,8 @@ function createWindow() {
|
||||
capture.hiddenForSession = true;
|
||||
capture.togglePause(false);
|
||||
await capture.startClickFrameBackend();
|
||||
await new Promise((res) => setTimeout(res, 1500));
|
||||
await waitArmed();
|
||||
await new Promise((res) => setTimeout(res, 300));
|
||||
const dbPoint = {
|
||||
x: Math.round(bounds.x + bounds.width * 0.55),
|
||||
y: Math.round(bounds.y + bounds.height * 0.55),
|
||||
|
||||
Reference in New Issue
Block a user