feat: add desktop teleprompter#2006
Conversation
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
| if (existingWindow) { | ||
| await commands.refreshWindowContentProtection(); | ||
| await existingWindow.unminimize(); | ||
| await existingWindow.show(); | ||
| await existingWindow.setFocus(); | ||
| return; |
There was a problem hiding this comment.
If refreshWindowContentProtection() ever throws (e.g. command not available / transient failure), it’d be nice to still bring the teleprompter to front.
| if (existingWindow) { | |
| await commands.refreshWindowContentProtection(); | |
| await existingWindow.unminimize(); | |
| await existingWindow.show(); | |
| await existingWindow.setFocus(); | |
| return; | |
| try { | |
| await commands.refreshWindowContentProtection(); | |
| } catch (error) { | |
| console.error("Failed to refresh window content protection:", error); | |
| } | |
| await existingWindow.unminimize(); | |
| await existingWindow.show(); | |
| await existingWindow.setFocus(); |
| pub fn set_window_opacity(window: tauri::Window, opacity: f64) { | ||
| let opacity = opacity.clamp(0.45, 1.0); | ||
| let c_window = window.clone(); | ||
| _ = window.run_on_main_thread(move || unsafe { | ||
| use cocoa::base::id; | ||
| use objc::{msg_send, sel, sel_impl}; | ||
|
|
||
| let Ok(ns_win) = c_window.ns_window() else { | ||
| return; | ||
| }; | ||
| let ns_win = ns_win as id; | ||
| let _: () = msg_send![ns_win, setAlphaValue: opacity]; | ||
| }); | ||
| } |
There was a problem hiding this comment.
Minor: run_on_main_thread can fail; logging makes this much easier to debug in the field.
| pub fn set_window_opacity(window: tauri::Window, opacity: f64) { | |
| let opacity = opacity.clamp(0.45, 1.0); | |
| let c_window = window.clone(); | |
| _ = window.run_on_main_thread(move || unsafe { | |
| use cocoa::base::id; | |
| use objc::{msg_send, sel, sel_impl}; | |
| let Ok(ns_win) = c_window.ns_window() else { | |
| return; | |
| }; | |
| let ns_win = ns_win as id; | |
| let _: () = msg_send![ns_win, setAlphaValue: opacity]; | |
| }); | |
| } | |
| pub fn set_window_opacity(window: tauri::Window, opacity: f64) { | |
| let opacity = opacity.clamp(0.45, 1.0); | |
| let c_window = window.clone(); | |
| if let Err(error) = window.run_on_main_thread(move || unsafe { | |
| use cocoa::base::id; | |
| use objc::{msg_send, sel, sel_impl}; | |
| let Ok(ns_win) = c_window.ns_window() else { | |
| return; | |
| }; | |
| let ns_win = ns_win as id; | |
| let _: () = msg_send![ns_win, setAlphaValue: opacity]; | |
| }) { | |
| tracing::warn!(?error, "Failed to set window opacity"); | |
| } | |
| } |
|
hey @greptileai, please re-review the PR |
|
hey @greptileai, please re-review the PR |
|
hey @greptileai, please re-review the PR |
| const nextState = state(); | ||
| clearTimeout(saveTimer); | ||
| saveTimer = setTimeout(() => { | ||
| void teleprompterStore.set(nextState); |
There was a problem hiding this comment.
teleprompterStore.set() can reject here and become an unhandled promise rejection (since it’s inside a setTimeout). Worth catching + logging like the other save paths.
| void teleprompterStore.set(nextState); | |
| saveTimer = setTimeout(() => { | |
| void teleprompterStore.set(nextState).catch((error) => { | |
| console.error("Failed to persist teleprompter settings:", error); | |
| }); | |
| }, 250); |
Summary
Validation
pnpm exec biome check <touched files>pnpm --dir apps/desktop exec tsc --noEmit --project tsconfig.jsonpnpm --dir apps/desktop exec vitest run src/routes/teleprompter-utils.test.tspnpm --dir apps/desktop buildcargo fmt --all -- --checkcargo check -p cap-desktopcargo test -p cap-desktop general_settings --libGreptile Summary
This PR adds a minimal desktop teleprompter window to Cap with smooth scroll-based playback, native macOS Liquid Glass styling, adjustable opacity and always-on-top behavior, and automatic exclusion from Cap recordings. The previously-noted issue with debounced saves being dropped on window close has been resolved —
onCleanupnow flushes the latest state before clearing the timer, andonCloseRequestedpersists state in afinallyblock guarded by anallowCloseflag to prevent double-writes.window_capture_excluded— ensuring content protection is applied reliably.openTeleprompteruses a module-levelcreatingWindowsentinel (set synchronously before any further awaits) to prevent duplicate window creation; the asynconCloseRequestedunlisten race is handled with adisposedflag.teleprompter-utils.tsextracts pure math (word count, speed calculation, sub-pixel position accumulation) with accompanying unit tests covering edge cases including negative inputs and 60-frame accumulation accuracy.Confidence Score: 5/5
Safe to merge — the feature is well-contained, the previous close-handler data-loss issue has been fixed, and all validation steps in the PR description pass.
The teleprompter window lifecycle, persistence, recording exclusion, and animation loop are all implemented correctly. The previously flagged save-on-close bug is fixed. No logic errors, data races, or broken contracts were found in the changed paths.
No files require special attention.
Important Files Changed
Comments Outside Diff (1)
apps/desktop/src/routes/teleprompter.tsx, line 513-535 (link)onCleanupcallsclearTimeout(saveTimer), so any state change made within the 250 ms debounce window before the component unmounts is silently dropped — the store write is cancelled before it executes. For a user who edits their script and then immediately closes the window, the last change (including potentially a large paste) won't be persisted.The fix is to flush the current state in
onCleanupbefore clearing the timer: callvoid teleprompterStore.set(state())beforeclearTimeout(saveTimer)so the latest state is always written on unmount.Prompt To Fix With AI
Reviews (4): Last reviewed commit: "fix: flush pending teleprompter saves" | Re-trigger Greptile