Skip to content

Overlay floating control [Improvements to #892]#907

Merged
yujonglee merged 7 commits intofastrepl:overlay-floating-controlfrom
plyght:overlay-floating-control
Jun 2, 2025
Merged

Overlay floating control [Improvements to #892]#907
yujonglee merged 7 commits intofastrepl:overlay-floating-controlfrom
plyght:overlay-floating-control

Conversation

@plyght
Copy link
Contributor

@plyght plyght commented Jun 1, 2025

Summary by CodeRabbit

  • New Features

    • Enhanced control bar with advanced recording and audio controls, including pause/resume, microphone and speaker mute toggles, and synchronized audio state across windows.
    • Added a configurable settings popup for recording preferences and window behavior.
    • Transparent background support for the desktop app interface.
    • New commands and permissions for managing "fake window bounds" and overlays.
  • Improvements

    • More responsive and visually improved toolbar with updated icons, loading indicators, and drag handle enhancements.
    • Centralized and optimized event handling for session and audio state updates.
    • Improved overlay bounds logic for better window management and reduced redundant state changes.
  • Documentation

    • Updated plugin permissions documentation to reflect new window management capabilities.
  • Style

    • Consistent code formatting and styling improvements in plugin bindings and related files.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 1, 2025

Walkthrough

This update introduces new commands and state management for handling "fake window bounds" in the Windows plugin, centralizes and improves overlay and audio state synchronization, and refactors UI components to support richer recording and audio controls. Permissions and documentation are updated to reflect the new commands, and platform-specific window handling is improved, especially for macOS.

Changes

Files/Groups Change Summary
apps/desktop/index.html Added inline CSS to enforce transparent backgrounds for html, body, and #root elements.
apps/desktop/src/components/editor-area/note-header/listen-button.tsx Added inter-window audio mute state synchronization using Tauri events; refactored mute toggles to emit/listen for events and update UI accordingly.
apps/desktop/src/routes/__root.tsx Added effect to listen for "debug" events from the control window and log payloads to the console.
apps/desktop/src/routes/app.control.tsx Major refactor: integrated listener plugin for audio/recording state, added event-driven state sync, enhanced overlay bounds logic, added settings popup, improved UI, and expanded recording/audio controls.
packages/utils/src/stores/ongoing-session.ts Centralized session event handling with a global listener, added cleanup action, removed redundant local listener setup.
plugins/windows/build.rs Added "set_fake_window_bounds" and "remove_fake_window" command strings to the commands array.
plugins/windows/js/bindings.gen.ts Reformatted file for consistency; added setFakeWindowBounds and removeFakeWindow commands to the JS bindings.
plugins/windows/permissions/autogenerated/commands/remove_fake_window.toml
.../set_fake_window_bounds.toml
Added new TOML permission files for remove_fake_window and set_fake_window_bounds commands.
plugins/windows/permissions/autogenerated/reference.md Updated documentation to include new permissions for setting/removing (fake) window bounds.
plugins/windows/permissions/default.toml Added new default permissions for overlay and fake window bounds commands.
plugins/windows/permissions/schemas/schema.json Extended schema with new permission constants for fake window bounds commands and updated descriptions.
plugins/windows/src/commands.rs Refactored overlay bounds logic into helpers; replaced old commands with new ones using FakeWindowBounds; added set_fake_window_bounds and remove_fake_window commands.
plugins/windows/src/ext.rs Refactored Control window creation; improved macOS support by hiding window buttons and customizing appearance with Cocoa APIs.
plugins/windows/src/lib.rs Added public re-exports for FakeWindowBounds and OverlayBound; registered new commands and managed state for fake window bounds in the plugin setup.
plugins/windows/src/overlay.rs Added Clone/Copy to OverlayBound; introduced FakeWindowBounds wrapper; optimized overlay listener logic to minimize redundant state changes and calls.

Sequence Diagram(s)

Overlay/Fake Window Bounds Update Flow

sequenceDiagram
    participant App as App/Frontend
    participant Tauri as Tauri Plugin
    participant State as FakeWindowBounds State

    App->>Tauri: set_fake_window_bounds(name, bounds)
    Tauri->>State: update_bounds(name, bounds)
    State-->>Tauri: State updated
    Tauri-->>App: Success/Failure

    App->>Tauri: remove_fake_window(name)
    Tauri->>State: remove_bounds(name)
    State-->>Tauri: State updated
    Tauri-->>App: Success/Failure
Loading

Audio Mute State Synchronization

sequenceDiagram
    participant WindowA as Window A
    participant WindowB as Window B
    participant Tauri as Tauri Event Bus

    WindowA->>Tauri: Emit audio-mic-state-changed (muted/unmuted)
    Tauri-->>WindowB: Receive audio-mic-state-changed
    WindowB->>WindowB: Refetch mic mute state, update UI
Loading

Poem

🐇
With overlays clear and windows bright,
Fake bounds now set, and states just right.
Audio mutes sync across the land,
While rabbits tweak with gentle hand.
Mac buttons vanish, settings bloom—
The code hops forward, making room!

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@plyght
Copy link
Contributor Author

plyght commented Jun 1, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 1, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🔭 Outside diff range comments (1)
apps/desktop/src/routes/app.control.tsx (1)

210-224: 🛠️ Refactor suggestion

⚠️ Potential issue

useEffect cleanup fires on every drag-state change – leaks + flicker risk

The effect that installs the mousemove / mouseup handlers is re-executed whenever isDragging or dragOffset change.
That means its cleanup — including windowsCommands.removeFakeWindow("control") — runs many times during a single drag cycle.
Side-effects:

  • The fake window is destroyed/re-created repeatedly → possible flicker / CPU overhead.
  • Listeners are detached/attached unnecessarily.
  • If removeFakeWindow throws, the user will get an error for every state change.

Move the removeFakeWindow call to an unmount-only effect and keep the pointer-event listeners in their own useEffect that depends on isDragging only when truly needed.

-useEffect(() => {
+// 1️⃣ install / remove global listeners
+useEffect(() => {
   ...
-  window.removeEventListener("mouseup", handleMouseUp);
-  windowsCommands.removeFakeWindow("control");
+  window.removeEventListener("mouseup", handleMouseUp);
 }, []);
+
+// 2️⃣ run once on unmount to clean up fake window
+useEffect(() => {
+  return () => {
+    windowsCommands.removeFakeWindow("control");
+  };
+}, []);
🧹 Nitpick comments (16)
apps/desktop/index.html (1)

11-16: LGTM! Transparent backgrounds support overlay functionality.

The inline CSS styles correctly set transparent backgrounds for overlay functionality. The !important declarations ensure these styles override any conflicting styles, which is likely necessary for proper overlay rendering.

Consider moving these styles to an external CSS file if more overlay-specific styles are added in the future to maintain better separation of concerns.

apps/desktop/src/routes/__root.tsx (1)

70-81: LGTM! Debug event listener properly implemented.

The debug event listener is well-implemented with proper cleanup in the useEffect hook. The logging format with "[Control Debug]" prefix provides clear identification of debug messages from the control window.

Consider adding error handling around the event listener for production robustness:

listen<string>("debug", (event) => {
  console.log(`[Control Debug] ${event.payload}`);
-}).then((fn) => {
+}).then((fn) => {
  unlisten = fn;
+}).catch((error) => {
+  console.error("[Control Debug] Failed to setup debug listener:", error);
});
plugins/windows/permissions/autogenerated/reference.md (1)

31-82: Consider maintaining alphabetical order in the permission table.

The new permission entries are complete and well-documented. However, they appear to be inserted at the beginning of the table, which breaks the alphabetical ordering pattern used elsewhere in the documentation.

Consider placing these entries in alphabetical order within the table for better organization and consistency.

apps/desktop/src/components/editor-area/note-header/listen-button.tsx (2)

448-456: Remove noisy console logs before production

console.log("[Main Window] Received … is helpful while developing but easily spams the dev-tools for every audio toggle across windows.

-  console.log(`[Main Window] Received mic state change:`, payload);

Consider guarding with if (import.meta.env.DEV) or removing.


476-482: Duplicate logic – extract helper to DRY mutations

Both toggle mutations share identical code except the getter/setter & event name. A small helper would reduce duplication:

function makeToggle(
  getter: () => Promise<boolean>,
  setter: (v: boolean) => Promise<void>,
  eventName: string,
) {
  return useMutation(async () => {
    const current = await getter();
    await setter(!current);
    await emit(eventName, { muted: !current });
    return !current;
  });
}

This keeps the two mutations to one-liners and future-proofs against copy-paste errors.

plugins/windows/src/overlay.rs (4)

13-17: Remove dead OverlayState to avoid warnings & confusion

OverlayState is no longer referenced after the introduction of FakeWindowBounds. Keeping it around will trigger an “unused type” warning and makes it harder for new contributors to grasp the active state-handling path.

-#[derive(Default)]
-pub struct OverlayState {
-    pub bounds: Arc<RwLock<HashMap<String, HashMap<String, OverlayBound>>>>,
-}

34-47: last_focus_state is never reset when all bounds are removed

When map.get(..) returns None the cursor-ignore flag is restored, but last_focus_state stays untouched.
If the window happened to be focused in a previous iteration the local flag remains true, which silences the focus-restore logic once new bounds arrive.

Consider resetting last_focus_state alongside last_ignore_state here to keep the finite-state-machine consistent.


56-66: Potential focus desync on cursor-position/scale-factor errors

In this early-continue branch the cursor-ignore flag is corrected, but the focus flag again stays stale.
Same recommendation as above: reset last_focus_state = false to guarantee the next successful iteration can trigger set_focus().


34-37: Use tokio::time::interval for cleaner, drift-free polling

Manually sleeping inside an infinite loop accumulates drift over time and is harder to read. interval(Duration::from_millis(100)) produces the same 10 Hz cadence without drift and automatically yields between ticks.

packages/utils/src/stores/ongoing-session.ts (2)

86-91: Ensure cleanup() idempotency

After invoking the unlisten callback you should null-out the reference so that:

  1. A second cleanup() call does not throw.
  2. A new listener can be safely attached later.
 if (sessionEventUnlisten) {
   sessionEventUnlisten();
+  set((state) =>
+    mutate(state, (draft) => {
+      draft.sessionEventUnlisten = undefined;
+    })
+  );
 }

42-81: Handle .listen() rejection

.listen() returns a Promise<UnlistenFn>. Network / IPC issues can make this promise reject.
Add a .catch() to log and surface the problem, otherwise the store will silently miss all future events.

plugins/windows/src/commands.rs (3)

1-2: Remove unused HashMap import

std::collections::HashMap is no longer used directly in this module; keeping it will emit an “unused import” warning.

-use std::collections::HashMap;

161-198: Four public commands wrap the same helpers – consider consolidation

window_set_overlay_bounds/window_remove_overlay_bounds and set_fake_window_bounds/remove_fake_window expose identical behaviour under two naming schemes.
Unless both names are truly required for API compatibility, keeping only one pair reduces maintenance overhead and the permission surface.


123-137: Hold write-lock only as long as necessary

The println! debug statements run while the write lock is held, slightly extending the critical section. Move the prints outside the locked scope to shorten contention if these commands become hot.

apps/desktop/src/routes/app.control.tsx (2)

50-71: Guard against state-update-after-unmount in initializeState

If the component unmounts before the async getState() / audio queries resolve, the subsequent set* calls will warn (Can't perform a React state update on an unmounted component).
Add an isMounted flag:

useEffect(() => {
  let mounted = true;
  (async () => {
     try {
       const currentState = await listenerCommands.getState();
       if (mounted) setRecordingStatus(currentState as RecordingStatus);
       ...
     } finally {
       // any finally work
     }
  })();
  return () => { mounted = false };
}, []);

491-510: Add accessibility label to IconButton

Buttons are rendered with only an SVG – screen-reader users have no context. Re-emit the tooltip as aria-label and hide decorative icons:

-<button
-  onClick={handleClick}
-  disabled={disabled}
-  className="p-2 ..."
-  title={tooltip}
->
+<button
+  onClick={handleClick}
+  disabled={disabled}
+  className="p-2 ..."
+  title={tooltip}
+  aria-label={tooltip}
+>

Also consider adding role="img" aria-hidden="true" on the SVGs for complete semantics.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb7172a and c398bec.

📒 Files selected for processing (16)
  • apps/desktop/index.html (1 hunks)
  • apps/desktop/src/components/editor-area/note-header/listen-button.tsx (4 hunks)
  • apps/desktop/src/routes/__root.tsx (2 hunks)
  • apps/desktop/src/routes/app.control.tsx (5 hunks)
  • packages/utils/src/stores/ongoing-session.ts (3 hunks)
  • plugins/windows/build.rs (1 hunks)
  • plugins/windows/js/bindings.gen.ts (1 hunks)
  • plugins/windows/permissions/autogenerated/commands/remove_fake_window.toml (1 hunks)
  • plugins/windows/permissions/autogenerated/commands/set_fake_window_bounds.toml (1 hunks)
  • plugins/windows/permissions/autogenerated/reference.md (2 hunks)
  • plugins/windows/permissions/default.toml (1 hunks)
  • plugins/windows/permissions/schemas/schema.json (2 hunks)
  • plugins/windows/src/commands.rs (3 hunks)
  • plugins/windows/src/ext.rs (2 hunks)
  • plugins/windows/src/lib.rs (3 hunks)
  • plugins/windows/src/overlay.rs (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
plugins/windows/src/overlay.rs (1)
plugins/windows/js/bindings.gen.ts (1)
  • OverlayBound (112-117)
plugins/windows/js/bindings.gen.ts (2)
plugins/windows/src/events.rs (2)
  • window (9-9)
  • window (22-22)
plugins/db/js/bindings.gen.ts (1)
  • Result (187-189)
plugins/windows/src/commands.rs (2)
plugins/windows/js/bindings.gen.ts (2)
  • HyprWindow (96-105)
  • OverlayBound (112-117)
plugins/windows/src/ext.rs (1)
  • label (94-96)
🔇 Additional comments (16)
plugins/windows/build.rs (1)

15-16: LGTM! Command additions follow established patterns.

The new commands for fake window bounds functionality are properly integrated into the COMMANDS array and follow the existing naming convention. These align well with the overlay improvements mentioned in the PR objectives.

plugins/windows/permissions/default.toml (1)

14-17: LGTM! Permissions properly secure new functionality.

The new permissions correctly correspond to the fake window bounds and overlay functionality, following the established "allow-" naming convention. This ensures proper security for the new commands.

apps/desktop/src/routes/__root.tsx (1)

8-8: LGTM! Import addition supports debug event handling.

The import of listen from Tauri API is correctly added to support the new debug event functionality.

plugins/windows/permissions/autogenerated/commands/set_fake_window_bounds.toml (1)

1-14: LGTM! Standard permission configuration file structure.

The permission configuration follows Tauri's standard format with proper allow/deny entries, clear descriptions, and correct schema reference. The autogenerated nature ensures consistency with the plugin's command structure.

plugins/windows/permissions/autogenerated/commands/remove_fake_window.toml (1)

1-14: LGTM! Consistent permission configuration structure.

The permission configuration for remove_fake_window follows the same proper structure as the set_fake_window_bounds permissions, ensuring consistency across the plugin's permission model.

plugins/windows/src/lib.rs (3)

11-11: LGTM! Proper type exports for the new functionality.

The public exports of FakeWindowBounds and OverlayBound provide necessary type access for external consumers of the plugin.


75-78: LGTM! Proper state management setup.

The initialization of FakeWindowBounds managed state follows the established pattern in the plugin setup. The default initialization and state management are correctly implemented.


51-52:

Details

✅ Verification successful

Verify that the new commands are properly implemented.

The new commands set_fake_window_bounds and remove_fake_window are registered in the plugin's command list. Ensure these commands are implemented in the commands module.


🏁 Script executed:

#!/bin/bash
# Description: Verify that the new commands are implemented in the commands module
# Expected: Find implementations of both commands

echo "Searching for set_fake_window_bounds implementation:"
ast-grep --pattern 'pub fn set_fake_window_bounds($_) {
  $$$
}'

echo -e "\nSearching for remove_fake_window implementation:"
ast-grep --pattern 'pub fn remove_fake_window($_) {
  $$$
}'

Length of output: 357


#!/bin/bash
# Find commands module files under plugins/windows/src
echo "Searching for commands module files:"
find plugins/windows/src -type f \( -name "commands.rs" -o -name "mod.rs" \) | grep -E "commands\.rs|commands/mod\.rs" || true

echo -e "\nSearching for a commands directory and listing its Rust files:"
if [ -d plugins/windows/src/commands ]; then
  ls plugins/windows/src/commands/*.rs || echo "No .rs files in plugins/windows/src/commands/"
else
  echo "No commands directory found at plugins/windows/src/commands/"
fi

#!/bin/bash
# List all files under the plugin to locate the commands module
echo "=== All files in plugins/windows/src ==="
find plugins/windows/src -type f

# Look for any commands module declarations
echo -e "\n=== Searching for `mod commands` declarations ==="
grep -R "mod commands" -n plugins/windows/src || echo "No `mod commands` declaration found."

# Locate commands.rs or commands directory
echo -e "\n=== Locating commands.rs or commands directory ==="
find plugins/windows/src -type f \( -name "commands.rs" -o -name "mod.rs" \) | grep -E "commands\.rs|commands/mod\.rs" || true
if [ -d plugins/windows/src/commands ]; then
  echo -e "\n=== Listing .rs files in plugins/windows/src/commands ==="
  ls plugins/windows/src/commands/*.rs || echo "No .rs files in plugins/windows/src/commands/"
else
  echo -e "\nNo commands/ subdirectory under plugins/windows/src"
fi

# Search entire plugin for command implementations
echo -e "\n=== Searching for `set_fake_window_bounds` in codebase ==="
grep -R "set_fake_window_bounds" -n plugins/windows/src || echo "set_fake_window_bounds not found."

echo -e "\n=== Searching for `remove_fake_window` in codebase ==="
grep -R "remove_fake_window" -n plugins/windows/src || echo "remove_fake_window not found."

🏁 Script executed:

#!/bin/bash
# List all files under the Windows plugin to locate where commands might live
echo "=== All files in plugins/windows/src ==="
find plugins/windows/src -type f

# Search for any mentions or definitions of the new commands
echo -e "\n=== Searching for set_fake_window_bounds ==="
grep -R -n "set_fake_window_bounds" plugins/windows/src || echo "No occurrences of set_fake_window_bounds found"

echo -e "\n=== Searching for remove_fake_window ==="
grep -R -n "remove_fake_window" plugins/windows/src || echo "No occurrences of remove_fake_window found"

Length of output: 902


Commands Implementations Verified

Both set_fake_window_bounds and remove_fake_window are defined in plugins/windows/src/commands.rs:

  • pub async fn set_fake_window_bounds(...) at line 181
  • pub async fn remove_fake_window(...) at line 192

No further action required.

plugins/windows/permissions/autogenerated/reference.md (1)

17-20: LGTM! Complete default permission list updates.

The default permission list correctly includes all four new permissions for the fake window bounds functionality, maintaining consistency with the plugin's permission model.

plugins/windows/permissions/schemas/schema.json (2)

297-320: New permission variants are correctly added and follow the existing pattern

The four new PermissionKind constants (allow|deny-remove-fake-window, allow|deny-set-fake-window-bounds) are declared consistently with the rest of the schema (description + markdownDescription + const).
I don’t see any duplication, typos, or ordering issues that would break the oneOf validation.

Looks good!


478-482: Default permission set updated – double-check downstream docs/guides

default now advertises the two new allow-*fake-window* permissions.
Please confirm the generated docs (e.g. reference.md) and end-user examples reflect the same list so copy/paste users don’t get out-of-sync examples.

plugins/windows/js/bindings.gen.ts (1)

69-77: Generated file edited – ensure regeneration stays deterministic

setFakeWindowBounds / removeFakeWindow look correct, but remember this file is auto-generated by tauri-specta.
If the Rust side changes again, a full regeneration will overwrite manual tweaks (e.g. comment style, import spacing).
Unless you patched the generator, keep manual edits out of generated artefacts.

plugins/windows/src/ext.rs (3)

356-386: Builder chain split improves readability – tiny portability tweak

Good call extracting the builder into a mutable variable and applying platform specific modifiers.
On non-macOS you now apply .decorations(false) after .transparent(true). Some Wayland/X11 WMs ignore transparency if decorations are disabled first; to be safe, call .decorations(false) before .transparent(true) (no-op on macOS).


387-388: spawn_overlay_listener still called – verify it now consumes FakeWindowBounds

Since the state holder moved from OverlayStateFakeWindowBounds, confirm spawn_overlay_listener signature was updated correspondingly; otherwise this will compile but the listener will ignore fake-window updates.


399-421: ⚠️ Potential issue

unsafe Cocoa block – add objc! nil checks for robustness

Inside the unsafe block you cast the raw pointer and immediately call standardWindowButton_.
If ns_window were unexpectedly nil, the Obj-C message send would seg-fault. You already guard the outer Option, but adding if ns_window.is_null() { return; } costs nothing.

- let ns_window: id = ns_window as *mut _ as id;
+ let ns_window: id = ns_window as *mut _ as id;
+ if ns_window.is_null() {
+   return;
+ }

Likewise a try-catch style macro (objc_exception!) would avoid panics when Apple changes API names.

Likely an incorrect or invalid review comment.

plugins/windows/src/overlay.rs (1)

95-101: last_focus_state mirrors an assumption, not the real focus

window.is_focused() is queried each pass, but last_focus_state is updated only by our own calls, not by the actual focus state.
If another app steals focus the flag may be wrong and set_focus() will never fire again.
You can close the gap by always syncing last_focus_state = focused after the check.

Comment on lines +446 to +463
// Listen for audio state changes from other windows
useEffect(() => {
const unsubscribeMicState = listen<{ muted: boolean }>("audio-mic-state-changed", ({ payload }) => {
console.log(`[Main Window] Received mic state change:`, payload);
refetchMicMuted();
});

const unsubscribeSpeakerState = listen<{ muted: boolean }>("audio-speaker-state-changed", ({ payload }) => {
console.log(`[Main Window] Received speaker state change:`, payload);
refetchSpeakerMuted();
});

return () => {
unsubscribeMicState.then(unlisten => unlisten());
unsubscribeSpeakerState.then(unlisten => unlisten());
};
}, [refetchMicMuted, refetchSpeakerMuted]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Potential memory-leak & race in event listener cleanup

listen returns Promise<UnlistenFn>.
Storing the promise and later calling .then(unlisten => unlisten()) inside the cleanup works, but if the promise rejects (e.g. window closed) the .catch() branch is swallowed and the listener may linger.
A safer pattern is:

useEffect(() => {
-  const unsub = listen<Payload>("evt", handler);
+  let unlisten: (() => void) | undefined;
+  const unsubPromise = listen<Payload>("evt", handler)
+    .then(fn => (unlisten = fn))
+    .catch(console.error);

  return () => {
-    unsub.then(u => u()).catch(console.error);
+    if (unlisten) unlisten();
  };
}, []);

This guarantees that the actual unlisten function is called or the error is surfaced.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src/components/editor-area/note-header/listen-button.tsx around
lines 446 to 463, the cleanup function for event listeners uses .then() on
promises returned by listen(), but does not handle promise rejections, risking
unhandled errors and lingering listeners. To fix this, convert the cleanup
function to async, await the promises from listen(), and call the unlisten
functions inside a try-catch block to properly handle any errors during
unsubscription.

Comment on lines +465 to 472
mutationFn: async () => {
const newMuted = !isMicMuted;
await listenerCommands.setMicMuted(newMuted);
// Emit event to synchronize with other windows
await emit("audio-mic-state-changed", { muted: newMuted });
return newMuted;
},
onSuccess: () => refetchMicMuted(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Undefined state can flip mic unintentionally

isMicMuted can be undefined until the first query resolves.
!undefined evaluates to true, so the very first click may always mute, regardless of the actual device state.

- const newMuted = !isMicMuted;
+ if (isMicMuted === undefined) {
+   return; // or disable the button until loaded
+ }
+ const newMuted = !isMicMuted;

The same applies to toggleSpeakerMuted.
Also consider passing disabled={isMicMuted === undefined} to <AudioControlButton> to prevent premature clicks.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mutationFn: async () => {
const newMuted = !isMicMuted;
await listenerCommands.setMicMuted(newMuted);
// Emit event to synchronize with other windows
await emit("audio-mic-state-changed", { muted: newMuted });
return newMuted;
},
onSuccess: () => refetchMicMuted(),
mutationFn: async () => {
if (isMicMuted === undefined) {
return; // or disable the button until loaded
}
const newMuted = !isMicMuted;
await listenerCommands.setMicMuted(newMuted);
// Emit event to synchronize with other windows
await emit("audio-mic-state-changed", { muted: newMuted });
return newMuted;
},
onSuccess: () => refetchMicMuted(),
🤖 Prompt for AI Agents
In apps/desktop/src/components/editor-area/note-header/listen-button.tsx around
lines 465 to 472, the isMicMuted state can be undefined initially, causing the
toggle logic to incorrectly flip the mic state on the first click. To fix this,
add a guard to check if isMicMuted is undefined before toggling and prevent the
mutation if so. Also, pass disabled={isMicMuted === undefined} to the
<AudioControlButton> component to disable the button until the state is
resolved. Apply the same fix for toggleSpeakerMuted to handle its undefined
state similarly.

Comment on lines +42 to +81
// Set up global session event listener
listenerEvents.sessionEvent.listen(({ payload }) => {
if (payload.type === "audioAmplitude") {
set((state) =>
mutate(state, (draft) => {
draft.amplitude = {
mic: payload.mic,
speaker: payload.speaker,
};
})
);
} else if (payload.type === "running_active") {
set((state) =>
mutate(state, (draft) => {
draft.status = "running_active";
draft.loading = false;
})
);
} else if (payload.type === "running_paused") {
set((state) =>
mutate(state, (draft) => {
draft.status = "running_paused";
draft.loading = false;
})
);
} else if (payload.type === "inactive") {
set((state) =>
mutate(state, (draft) => {
draft.status = "inactive";
draft.loading = false;
})
);
}
}).then((unlisten) => {
set((state) =>
mutate(state, (draft) => {
draft.sessionEventUnlisten = unlisten;
})
);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Event listener is registered for every store instance ⇒ memory-leak & duplicate updates

listenerEvents.sessionEvent.listen() is executed during store creation. If createOngoingSessionStore() is ever called twice (e.g. in tests, storybook, hot-reload), you will end up with N listeners all mutating the same state.

Guard against multiple registrations or move the listener outside the factory, e.g.:

-let unlistenPromise: Promise<() => void> | undefined;
+let unlistenPromise: Promise<() => void> | undefined;

 export const createOngoingSessionStore = (…) => {
   return createStore<State & Actions>((set, get) => {
-    listenerEvents.sessionEvent.listen(…)
+    if (!unlistenPromise) {
+      unlistenPromise = listenerEvents.sessionEvent.listen(…);
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/utils/src/stores/ongoing-session.ts between lines 42 and 81, the
session event listener is registered inside the store creation function, causing
multiple listeners to accumulate if the store is created multiple times, leading
to memory leaks and duplicate state updates. To fix this, move the listener
registration outside the store factory function so it only registers once, or
add a guard to ensure the listener is only registered a single time regardless
of how many store instances are created.

Comment on lines +75 to +101
// Listen for session events
const unsubscribeSession = listenerEvents.sessionEvent.listen(({ payload }) => {
console.log(`[Control Bar] Session event:`, payload);

if (payload.type === "inactive" || payload.type === "running_active" || payload.type === "running_paused") {
setRecordingStatus(payload.type);
setRecordingLoading(false);
}
});

// Listen for audio state changes from other windows
const unsubscribeMicState = listen<{ muted: boolean }>("audio-mic-state-changed", ({ payload }) => {
console.log(`[Control Bar] Received mic state change:`, payload);
setMicMuted(payload.muted);
});

const unsubscribeSpeakerState = listen<{ muted: boolean }>("audio-speaker-state-changed", ({ payload }) => {
console.log(`[Control Bar] Received speaker state change:`, payload);
setSpeakerMuted(payload.muted);
});

return () => {
unsubscribeSession.then(unlisten => unlisten());
unsubscribeMicState.then(unlisten => unlisten());
unsubscribeSpeakerState.then(unlisten => unlisten());
};
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Promise-based unlisten pattern is error-prone

listenerEvents.sessionEvent.listen() returns Promise<UnlistenFn>.
Storing the promise in unsubscribeSession and then calling
unsubscribeSession.then(unlisten => unlisten()) inside cleanup:

  1. Loses type-safety (TS thinks unsubscribeSession is a promise, not a fn).
  2. Makes it easy to miss awaited errors.
  3. Doubles the .then ceremony for every listener.

Prefer await inside the mounting phase, keep the unlisten function directly:

useEffect(() => {
  let unlistenSession: () => void;

  (async () => {
    unlistenSession = await listenerEvents.sessionEvent.listen(({ payload }) => {
      ...
    });
  })();

  return () => {
    unlistenSession?.();
  };
}, []);
🤖 Prompt for AI Agents
In apps/desktop/src/routes/app.control.tsx around lines 75 to 101, the current
pattern stores promises returned by listenerEvents.sessionEvent.listen() and
listen() calls, then calls .then() on them in the cleanup function, which loses
type safety and complicates error handling. Refactor the useEffect to use an
async IIFE that awaits each listen call and stores the unlisten functions
directly in variables. Then, in the cleanup function, call these unlisten
functions directly without using .then(), ensuring proper type safety and
simpler error handling.

Comment on lines +196 to 207
// Clamp position to keep toolbar on screen
const clampedX = Math.max(0, Math.min(window.innerWidth - toolbarWidth, e.clientX - dragOffset.x));
const clampedY = Math.max(0, Math.min(window.innerHeight - toolbarHeight, e.clientY - dragOffset.y));

const newPosition = {
x: clampedX,
y: clampedY,
};
setPosition(newPosition);
// Update bounds immediately during drag for smooth interaction
setTimeout(updateOverlayBounds, 0);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

updateOverlayBounds is invoked on every mouse-move – throttle it

Inside the drag handler we call
setTimeout(updateOverlayBounds, 0) for each mousemove.
On a typical drag this fires hundreds of async RPC calls per second to setFakeWindowBounds, overwhelming the Tauri bridge and wasting battery.

Replace with a throttled / requestAnimationFrame approach so the call runs at most once per frame:

-        setTimeout(updateOverlayBounds, 0);
+        if (!updateOverlayBoundsPending.current) {
+          updateOverlayBoundsPending.current = true;
+          requestAnimationFrame(() => {
+            updateOverlayBounds();
+            updateOverlayBoundsPending.current = false;
+          });
+        }
const updateOverlayBoundsPending = useRef(false);
🤖 Prompt for AI Agents
In apps/desktop/src/routes/app.control.tsx around lines 196 to 207, the
updateOverlayBounds function is called via setTimeout on every mousemove event
during dragging, causing excessive asynchronous calls that overwhelm the Tauri
bridge. To fix this, implement a throttling mechanism using a useRef boolean
flag (e.g., updateOverlayBoundsPending) to ensure updateOverlayBounds is only
called once per animation frame. Replace the setTimeout call with a
requestAnimationFrame callback that checks and resets this flag, preventing
multiple invocations within the same frame.

@yujonglee yujonglee linked an issue Jun 2, 2025 that may be closed by this pull request
@yujonglee yujonglee merged commit c398bec into fastrepl:overlay-floating-control Jun 2, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Jul 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement floating meeting indicator

2 participants