From 8609d389a31be308fe58a7e26b2a4c80d3a17a24 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 15 Sep 2026 00:00:59 +0530 Subject: [PATCH 1/2] feat: add typing-aware auto zoom suggestions --- electron/electron-env.d.ts | 3 +- electron/ipc/cursor/interaction.test.ts | 11 ++ electron/ipc/cursor/interaction.ts | 51 +++++++++ electron/ipc/cursor/monitor.ts | 8 +- electron/ipc/cursor/telemetry.test.ts | 10 ++ electron/ipc/cursor/telemetry.ts | 3 +- electron/ipc/types.ts | 25 +++-- electron/native/NativeCursorMonitor.swift | 11 ++ .../recordly-native-cursor-monitor | Bin 100576 -> 100640 bytes .../darwin-x64/recordly-native-cursor-monitor | Bin 73248 -> 73312 bytes .../timeline/zoomSuggestionUtils.test.ts | 73 +++++++++++++ .../timeline/zoomSuggestionUtils.ts | 98 ++++++++++++++++-- src/components/video-editor/types.ts | 3 +- 13 files changed, 277 insertions(+), 19 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 1a926718f..4ae95662f 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -956,7 +956,8 @@ interface CursorTelemetryPoint { | "double-click" | "right-click" | "middle-click" - | "mouseup"; + | "mouseup" + | "keydown"; cursorType?: | "arrow" | "text" diff --git a/electron/ipc/cursor/interaction.test.ts b/electron/ipc/cursor/interaction.test.ts index 4ea662414..56d2c6c19 100644 --- a/electron/ipc/cursor/interaction.test.ts +++ b/electron/ipc/cursor/interaction.test.ts @@ -12,10 +12,21 @@ vi.mock("electron", () => ({ })); import { + isCandidateTypingKeyEvent, repairBundledUiohookBinaryForCurrentArch, shouldStartGlobalInteractionHook, } from "./interaction"; +describe("isCandidateTypingKeyEvent", () => { + it("accepts ordinary keys but filters modifiers and Ctrl/Meta shortcuts", () => { + expect(isCandidateTypingKeyEvent({ keycode: 0x001e })).toBe(true); + expect(isCandidateTypingKeyEvent({ keycode: 0x002a })).toBe(false); // Shift + expect(isCandidateTypingKeyEvent({ keycode: 0x003a })).toBe(false); // Caps Lock + expect(isCandidateTypingKeyEvent({ keycode: 0x001e, ctrlKey: true })).toBe(false); + expect(isCandidateTypingKeyEvent({ keycode: 0x001e, metaKey: true })).toBe(false); + }); +}); + describe("shouldStartGlobalInteractionHook", () => { it("does not start the synchronous uiohook event tap on macOS", () => { expect(shouldStartGlobalInteractionHook("darwin")).toBe(false); diff --git a/electron/ipc/cursor/interaction.ts b/electron/ipc/cursor/interaction.ts index 47c42437f..1080fb898 100644 --- a/electron/ipc/cursor/interaction.ts +++ b/electron/ipc/cursor/interaction.ts @@ -13,6 +13,7 @@ import { } from "../state"; import type { CursorInteractionType, + HookKeyboardEvent, HookMouseEvent, UiohookLike, UiohookModuleNamespace, @@ -233,6 +234,47 @@ export function recordCursorMouseUp() { pushCursorSample(point.cx, point.cy, getCursorCaptureElapsedMs(), "mouseup"); } +/** uiohook virtual key codes for non-typing modifier/toggle keys. */ +const MODIFIER_ONLY_KEY_CODES = new Set([ + 0x002a, // left Shift + 0x0036, // right Shift + 0x001d, // left Control + 0x0e1d, // right Control + 0x0038, // left Alt + 0x0e38, // right Alt + 0x0e5b, // left Meta / Command + 0x0e5c, // right Meta / Command + 0x003a, // Caps Lock + 0x0045, // Num Lock + 0x0046, // Scroll Lock +]); + +/** + * Keeps keyboard telemetry privacy-preserving: this decision uses only a + * modifier state and key code to decide whether to record an anonymous + * timestamp + cursor position. It never reads or stores key characters. + */ +export function isCandidateTypingKeyEvent(event: HookKeyboardEvent): boolean { + if (event.ctrlKey || event.metaKey) { + return false; + } + + return typeof event.keycode === "number" && !MODIFIER_ONLY_KEY_CODES.has(event.keycode); +} + +export function recordCursorKeyDown() { + if (!isCursorCaptureActive || isCursorCapturePaused()) { + return; + } + + const point = getNormalizedCursorPoint(); + if (!point) { + return; + } + + pushCursorSample(point.cx, point.cy, getCursorCaptureElapsedMs(), "keydown"); +} + export async function startInteractionCapture() { if (!isCursorCaptureActive) { return; @@ -276,6 +318,12 @@ export async function startInteractionCapture() { recordCursorMouseUp(); }; + const onKeyDown = (event: HookKeyboardEvent) => { + if (isCandidateTypingKeyEvent(event)) { + recordCursorKeyDown(); + } + }; + const onMouseMove = (event: HookMouseEvent) => { if (process.platform !== "linux" || !isCursorCaptureActive || isCursorCapturePaused()) { return; @@ -291,6 +339,7 @@ export async function startInteractionCapture() { hook.on("mousedown", onMouseDown); hook.on("mouseup", onMouseUp); + hook.on("keydown", onKeyDown); if (process.platform === "linux") { hook.on("mousemove", onMouseMove); } @@ -300,12 +349,14 @@ export async function startInteractionCapture() { if (typeof hook.off === "function") { hook.off("mousedown", onMouseDown); hook.off("mouseup", onMouseUp); + hook.off("keydown", onKeyDown); if (process.platform === "linux") { hook.off("mousemove", onMouseMove); } } else if (typeof hook.removeListener === "function") { hook.removeListener("mousedown", onMouseDown); hook.removeListener("mouseup", onMouseUp); + hook.removeListener("keydown", onKeyDown); if (process.platform === "linux") { hook.removeListener("mousemove", onMouseMove); } diff --git a/electron/ipc/cursor/monitor.ts b/electron/ipc/cursor/monitor.ts index 8a507d56d..819ef2fc4 100644 --- a/electron/ipc/cursor/monitor.ts +++ b/electron/ipc/cursor/monitor.ts @@ -12,7 +12,7 @@ import { setNativeCursorMonitorProcess, } from "../state"; import type { CursorVisualType } from "../types"; -import { recordCursorMouseDown, recordCursorMouseUp } from "./interaction"; +import { recordCursorKeyDown, recordCursorMouseDown, recordCursorMouseUp } from "./interaction"; export function emitCursorStateChanged(cursorType: CursorVisualType) { BrowserWindow.getAllWindows().forEach((window) => { @@ -28,9 +28,11 @@ export function handleCursorMonitorStdout(chunk: Buffer) { setNativeCursorMonitorOutputBuffer(lines.pop() ?? ""); for (const line of lines) { - const interactionMatch = line.match(/^INTERACTION:(mousedown|mouseup)(?::([123]))?$/); + const interactionMatch = line.match(/^INTERACTION:(mousedown|mouseup|keydown)(?::([123]))?$/); if (interactionMatch) { - if (interactionMatch[1] === "mouseup") { + if (interactionMatch[1] === "keydown") { + recordCursorKeyDown(); + } else if (interactionMatch[1] === "mouseup") { recordCursorMouseUp(); } else { const button = Number(interactionMatch[2]); diff --git a/electron/ipc/cursor/telemetry.test.ts b/electron/ipc/cursor/telemetry.test.ts index 03613859b..0c2ac06cd 100644 --- a/electron/ipc/cursor/telemetry.test.ts +++ b/electron/ipc/cursor/telemetry.test.ts @@ -113,6 +113,16 @@ describe("cursor telemetry pause clock", () => { expect(rm).not.toHaveBeenCalled(); }); + it("preserves anonymous keydown timing telemetry", () => { + const samples = normalizeCursorTelemetrySamples([ + { timeMs: 42, cx: 0.25, cy: 0.75, interactionType: "keydown" }, + ]); + + expect(samples).toEqual([ + { timeMs: 42, cx: 0.25, cy: 0.75, interactionType: "keydown", cursorType: undefined }, + ]); + }); + it("removes the sidecar when saving an empty cursor telemetry payload", async () => { await writeCursorTelemetry("/tmp/recording.mp4", []); diff --git a/electron/ipc/cursor/telemetry.ts b/electron/ipc/cursor/telemetry.ts index 73f62714e..c937cedde 100644 --- a/electron/ipc/cursor/telemetry.ts +++ b/electron/ipc/cursor/telemetry.ts @@ -60,7 +60,8 @@ export function normalizeCursorTelemetrySamples(rawSamples: unknown): CursorTele point.interactionType === "right-click" || point.interactionType === "middle-click" || point.interactionType === "move" || - point.interactionType === "mouseup" + point.interactionType === "mouseup" || + point.interactionType === "keydown" ? point.interactionType : undefined, cursorType: diff --git a/electron/ipc/types.ts b/electron/ipc/types.ts index 58f5425bd..450d6de49 100644 --- a/electron/ipc/types.ts +++ b/electron/ipc/types.ts @@ -96,7 +96,8 @@ export type CursorInteractionType = | "double-click" | "right-click" | "middle-click" - | "mouseup"; + | "mouseup" + | "keydown"; export interface CursorTelemetryPoint { timeMs: number; @@ -120,7 +121,8 @@ export type NativeMacWindowSource = { height?: number; }; -export type HookEventName = "mousedown" | "mouseup" | "mousemove"; +export type HookMouseEventName = "mousedown" | "mouseup" | "mousemove"; +export type HookEventName = HookMouseEventName | "keydown"; export type HookMouseEvent = { button?: number; @@ -139,12 +141,23 @@ export type HookMouseEvent = { }; }; -export type HookEventListener = (event: HookMouseEvent) => void; +/** + * Deliberately excludes key characters and text. Keyboard capture is used only + * to identify the timing of likely typing activity. + */ +export type HookKeyboardEvent = { + keycode?: number; + ctrlKey?: boolean; + metaKey?: boolean; +}; export type UiohookLike = { - on: (eventName: HookEventName, listener: HookEventListener) => void; - off?: (eventName: HookEventName, listener: HookEventListener) => void; - removeListener?: (eventName: HookEventName, listener: HookEventListener) => void; + on(eventName: HookMouseEventName, listener: (event: HookMouseEvent) => void): void; + on(eventName: "keydown", listener: (event: HookKeyboardEvent) => void): void; + off?(eventName: HookMouseEventName, listener: (event: HookMouseEvent) => void): void; + off?(eventName: "keydown", listener: (event: HookKeyboardEvent) => void): void; + removeListener?(eventName: HookMouseEventName, listener: (event: HookMouseEvent) => void): void; + removeListener?(eventName: "keydown", listener: (event: HookKeyboardEvent) => void): void; start: () => void; stop?: () => void; }; diff --git a/electron/native/NativeCursorMonitor.swift b/electron/native/NativeCursorMonitor.swift index 18c644150..37de6d2a6 100644 --- a/electron/native/NativeCursorMonitor.swift +++ b/electron/native/NativeCursorMonitor.swift @@ -415,6 +415,16 @@ func mouseInteractionCallback( let action: String let button: Int switch type { + case .keyDown: + // Timing-only telemetry: never emit the key code, character, or text. + // Command/Control combinations are app shortcuts rather than typing. + let flags = event.flags + guard !flags.contains(.maskCommand), !flags.contains(.maskControl) else { + return Unmanaged.passUnretained(event) + } + print("INTERACTION:keydown") + fflush(stdout) + return Unmanaged.passUnretained(event) case .leftMouseDown: action = "mousedown" button = 1 @@ -455,6 +465,7 @@ let mouseEventTypes: [CGEventType] = [ .rightMouseUp, .otherMouseDown, .otherMouseUp, + .keyDown, ] let mouseEventMask = mouseEventTypes.reduce(CGEventMask(0)) { mask, type in mask | (CGEventMask(1) << type.rawValue) diff --git a/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor b/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor index 192f8719f847a9142a09867c308f87e2d05b128b..b390d5b5d5946e1e3a580f2ca520c927933daeba 100755 GIT binary patch delta 17970 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 ZK-ki(s+SIoUi)$6)}3#qCCgRj{{ZAW@Gt-X diff --git a/electron/native/bin/darwin-x64/recordly-native-cursor-monitor b/electron/native/bin/darwin-x64/recordly-native-cursor-monitor index b375827a8a07f2de3c166e914507e7239fbc970e..b9849a4deb139bf01cf15554ff67385b2ac8fa86 100755 GIT binary patch delta 17596 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 delta 17280 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 Kg)XV8@_zuNrtL5Q diff --git a/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts b/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts index 5f237d5eb..40669e744 100644 --- a/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts +++ b/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts @@ -20,6 +20,10 @@ function makeMove(timeMs: number, cx = 0.5, cy = 0.5): CursorTelemetryPoint { return { timeMs, cx, cy, interactionType: "move" }; } +function makeKeyDown(timeMs: number, cx = 0.5, cy = 0.5): CursorTelemetryPoint { + return { timeMs, cx, cy, interactionType: "keydown" }; +} + /** Wraps click samples with surrounding move events to mimic real mixed telemetry. */ function withMoves(clicks: CursorTelemetryPoint[], totalMs: number): CursorTelemetryPoint[] { return [makeMove(0), ...clicks, makeMove(totalMs)]; @@ -48,6 +52,75 @@ describe("shouldAutoApplyFreshRecordingZoomsForSource", () => { }); describe("buildInteractionZoomSuggestions (click-cluster logic)", () => { + it("extends a text-field click through a valid typing burst", () => { + const result = buildInteractionZoomSuggestions({ + cursorTelemetry: withMoves( + [ + makeClick(5_000, 0.3, 0.4), + makeKeyDown(5_200, 0.3, 0.4), + makeKeyDown(5_350, 0.3, 0.4), + makeKeyDown(5_500, 0.3, 0.4), + makeKeyDown(5_650, 0.3, 0.4), + ], + TOTAL_MS, + ), + totalMs: TOTAL_MS, + defaultDurationMs: 3_000, + }); + + expect(result.status).toBe("ok"); + expect(result.suggestions).toEqual([ + { start: 4_500, end: 6_150, focus: { cx: 0.3, cy: 0.4 } }, + ]); + }); + + it("keeps a normal click window for isolated key activity", () => { + const result = buildInteractionZoomSuggestions({ + cursorTelemetry: withMoves([makeClick(5_000), makeKeyDown(5_200)], TOTAL_MS), + totalMs: TOTAL_MS, + defaultDurationMs: 3_000, + }); + + expect(result.suggestions).toEqual([ + { start: 4_500, end: 5_500, focus: { cx: 0.5, cy: 0.5 } }, + ]); + }); + + it("merges nearby typing bursts but splits bursts separated by a long pause", () => { + const shortPause = buildInteractionZoomSuggestions({ + cursorTelemetry: withMoves( + [ + makeClick(1_000), + makeKeyDown(1_100), makeKeyDown(1_200), makeKeyDown(1_300), + makeKeyDown(3_500), makeKeyDown(3_600), makeKeyDown(3_700), + ], + TOTAL_MS, + ), + totalMs: TOTAL_MS, + defaultDurationMs: 3_000, + }); + expect(shortPause.suggestions).toHaveLength(1); + expect(shortPause.suggestions[0]).toMatchObject({ start: 500, end: 4_200 }); + + const longPause = buildInteractionZoomSuggestions({ + cursorTelemetry: withMoves( + [ + makeClick(1_000), + makeKeyDown(1_100), makeKeyDown(1_200), makeKeyDown(1_300), + makeKeyDown(4_000), makeKeyDown(4_100), makeKeyDown(4_200), + ], + TOTAL_MS, + ), + totalMs: TOTAL_MS, + defaultDurationMs: 3_000, + }); + expect(longPause.suggestions).toHaveLength(2); + expect(longPause.suggestions.map(({ start, end }) => ({ start, end }))).toEqual([ + { start: 500, end: 1_800 }, + { start: 3_500, end: 4_700 }, + ]); + }); + it("creates one zoom track for a single isolated click with 500ms padding", () => { const telemetry = withMoves([makeClick(5_000)], TOTAL_MS); diff --git a/src/components/video-editor/timeline/zoomSuggestionUtils.ts b/src/components/video-editor/timeline/zoomSuggestionUtils.ts index fbdc5a68b..a8199c867 100644 --- a/src/components/video-editor/timeline/zoomSuggestionUtils.ts +++ b/src/components/video-editor/timeline/zoomSuggestionUtils.ts @@ -12,6 +12,8 @@ export interface ZoomDwellCandidate { } export interface CursorInteractionCandidate extends ZoomDwellCandidate { + startMs?: number; + endMs?: number; kind: | "dwell" | "click-like" @@ -19,7 +21,8 @@ export interface CursorInteractionCandidate extends ZoomDwellCandidate { | "text-focus-like" | "dropdown-open" | "text-selection" - | "text-field-click"; + | "text-field-click" + | "typing-burst"; source: "explicit" | "heuristic"; } @@ -71,6 +74,10 @@ export function shouldAutoApplyFreshRecordingZoomsForSource( export const CLICK_CLUSTER_MERGE_GAP_MS = 2500; /** Padding added before the first click and after the last click in a cluster. */ export const CLICK_CLUSTER_PAD_MS = 500; +export const TYPING_BURST_MAX_IDLE_MS = 2000; +export const MIN_TYPING_BURST_KEY_COUNT = 3; +export const MAX_CLICK_TO_TYPING_DELAY_MS = 2000; +export const MAX_CLICK_TO_TYPING_DISTANCE = 0.08; const EXPLICIT_CLICK_TYPES = new Set>([ "click", "double-click", @@ -137,6 +144,16 @@ export function normalizeCursorTelemetry( continue; } + if (candidate.kind === "typing-burst") { + applyCursorTypeInRange( + normalized, + (candidate.startMs ?? candidate.centerTimeMs) - 100, + (candidate.endMs ?? candidate.centerTimeMs) + 500, + "text", + ); + continue; + } + if (candidate.kind === "text-field-click" || candidate.kind === "text-focus-like") { applyCursorTypeInRange( normalized, @@ -224,6 +241,59 @@ export function detectZoomDwellCandidates(samples: CursorTelemetryPoint[]): Zoom return dwellCandidates; } +type TypingBurst = { startMs: number; endMs: number }; + +function findTypingBurstsAfterClick( + samples: CursorTelemetryPoint[], + clickSample: CursorTelemetryPoint, +): TypingBurst[] { + const nextClickTime = samples.find( + (sample) => isExplicitClickType(sample.interactionType) && sample.timeMs > clickSample.timeMs, + )?.timeMs; + const keydowns = samples.filter( + (sample) => + sample.interactionType === "keydown" && + sample.timeMs > clickSample.timeMs && + (nextClickTime === undefined || sample.timeMs < nextClickTime), + ); + const bursts: TypingBurst[] = []; + let current: CursorTelemetryPoint[] = []; + + const commit = () => { + if (current.length < MIN_TYPING_BURST_KEY_COUNT) return; + const first = current[0]; + const last = current[current.length - 1]; + const startsSoonEnough = + bursts.length > 0 || first.timeMs - clickSample.timeMs <= MAX_CLICK_TO_TYPING_DELAY_MS; + const relevantSamples = samples.filter( + (sample) => sample.timeMs >= clickSample.timeMs && sample.timeMs <= last.timeMs, + ); + const stayedNearField = relevantSamples.every( + (sample) => + Math.hypot(sample.cx - clickSample.cx, sample.cy - clickSample.cy) <= + MAX_CLICK_TO_TYPING_DISTANCE, + ); + const hasTextContext = relevantSamples.some((sample) => sample.cursorType === "text"); + + if (startsSoonEnough && (stayedNearField || hasTextContext)) { + bursts.push({ startMs: first.timeMs, endMs: last.timeMs }); + } + }; + + for (const keydown of keydowns) { + if ( + current.length > 0 && + keydown.timeMs - current[current.length - 1].timeMs > TYPING_BURST_MAX_IDLE_MS + ) { + commit(); + current = []; + } + current.push(keydown); + } + commit(); + return bursts; +} + export function detectInteractionCandidates( samples: CursorTelemetryPoint[], ): CursorInteractionCandidate[] { @@ -254,6 +324,18 @@ export function detectInteractionCandidates( kind, source: "explicit", }); + + for (const [index, burst] of findTypingBurstsAfterClick(samples, clickSample).entries()) { + explicitInteractionCandidates.push({ + centerTimeMs: Math.round((burst.startMs + burst.endMs) / 2), + focus: { cx: clickSample.cx, cy: clickSample.cy }, + strength: 1400, + startMs: index === 0 ? clickSample.timeMs : burst.startMs, + endMs: burst.endMs, + kind: "typing-burst", + source: "explicit", + }); + } } // --- Phase 2: Dwell-based heuristic candidates --- @@ -315,8 +397,8 @@ function buildClickClusters( const sorted = [...clicks].sort((a, b) => a.centerTimeMs - b.centerTimeMs); const clusters: Array<{ firstMs: number; lastMs: number; focus: ZoomFocus }> = []; - let clusterStart = sorted[0].centerTimeMs; - let clusterEnd = sorted[0].centerTimeMs; + let clusterStart = sorted[0].startMs ?? sorted[0].centerTimeMs; + let clusterEnd = sorted[0].endMs ?? sorted[0].centerTimeMs; let bestStrength = sorted[0].strength; let bestFocus = sorted[0].focus; let sumCx = sorted[0].focus.cx; @@ -325,11 +407,13 @@ function buildClickClusters( for (let i = 1; i < sorted.length; i++) { const click = sorted[i]; - const gap = click.centerTimeMs - clusterEnd; + const clickStart = click.startMs ?? click.centerTimeMs; + const clickEnd = click.endMs ?? click.centerTimeMs; + const gap = clickStart - clusterEnd; if (gap <= mergeGapMs) { // Extend current cluster - clusterEnd = Math.max(clusterEnd, click.centerTimeMs); + clusterEnd = Math.max(clusterEnd, clickEnd); if (click.strength > bestStrength) { bestStrength = click.strength; bestFocus = click.focus; @@ -344,8 +428,8 @@ function buildClickClusters( lastMs: clusterEnd, focus: bestFocus ?? { cx: sumCx / count, cy: sumCy / count }, }); - clusterStart = click.centerTimeMs; - clusterEnd = click.centerTimeMs; + clusterStart = clickStart; + clusterEnd = clickEnd; bestStrength = click.strength; bestFocus = click.focus; sumCx = click.focus.cx; diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index 55de007bb..92892c1b8 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -27,7 +27,8 @@ export interface CursorTelemetryPoint { | "double-click" | "right-click" | "middle-click" - | "mouseup"; + | "mouseup" + | "keydown"; cursorType?: | "arrow" | "text" From 968e14e77b436908602e8582416fb8d8eb1a3d10 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 15 Sep 2026 09:24:59 +0530 Subject: [PATCH 2/2] fix: scope typing text context to current burst --- .../timeline/zoomSuggestionUtils.test.ts | 30 +++++++++++++++++++ .../timeline/zoomSuggestionUtils.ts | 5 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts b/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts index 40669e744..45e28dc3a 100644 --- a/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts +++ b/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts @@ -121,6 +121,36 @@ describe("buildInteractionZoomSuggestions (click-cluster logic)", () => { ]); }); + it("does not let a later burst inherit text context from an earlier burst", () => { + const result = buildInteractionZoomSuggestions({ + cursorTelemetry: withMoves( + [ + makeClick(2_600, 0.3, 0.4), + makeKeyDown(2_700, 0.3, 0.4), + makeKeyDown(2_800, 0.3, 0.4), + makeKeyDown(2_900, 0.3, 0.4), + // This later click lands within the first burst's text-cursor + // annotation window, but its typing happens away from that field. + makeClick(3_300, 0.7, 0.7), + makeMove(3_450, 0.75, 0.75), + makeMove(3_500, 0.8, 0.7), + makeMove(3_550, 0.75, 0.8), + makeKeyDown(3_600, 0.8, 0.8), + makeKeyDown(3_700, 0.8, 0.8), + makeKeyDown(3_800, 0.8, 0.8), + ], + TOTAL_MS, + ), + totalMs: TOTAL_MS, + defaultDurationMs: 3_000, + }); + + expect(result.status).toBe("ok"); + expect(result.suggestions).toEqual([ + { start: 2_100, end: 3_800, focus: { cx: 0.3, cy: 0.4 } }, + ]); + }); + it("creates one zoom track for a single isolated click with 500ms padding", () => { const telemetry = withMoves([makeClick(5_000)], TOTAL_MS); diff --git a/src/components/video-editor/timeline/zoomSuggestionUtils.ts b/src/components/video-editor/timeline/zoomSuggestionUtils.ts index a8199c867..f93552320 100644 --- a/src/components/video-editor/timeline/zoomSuggestionUtils.ts +++ b/src/components/video-editor/timeline/zoomSuggestionUtils.ts @@ -268,12 +268,15 @@ function findTypingBurstsAfterClick( const relevantSamples = samples.filter( (sample) => sample.timeMs >= clickSample.timeMs && sample.timeMs <= last.timeMs, ); + const burstSamples = samples.filter( + (sample) => sample.timeMs >= first.timeMs && sample.timeMs <= last.timeMs, + ); const stayedNearField = relevantSamples.every( (sample) => Math.hypot(sample.cx - clickSample.cx, sample.cy - clickSample.cy) <= MAX_CLICK_TO_TYPING_DISTANCE, ); - const hasTextContext = relevantSamples.some((sample) => sample.cursorType === "text"); + const hasTextContext = burstSamples.some((sample) => sample.cursorType === "text"); if (startsSoonEnough && (stayedNearField || hasTextContext)) { bursts.push({ startMs: first.timeMs, endMs: last.timeMs });