feat(overlay): activate main window on orb click (#605)#611
Conversation
…ai#605) Exposes the existing show_main_window helper as a Tauri command so the overlay webview can bring the main window to front. The command is whitelisted in allow-core-process.toml so the overlay window capability can invoke it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The overlay orb had no click behavior. Now clicking it in idle mode invokes activate_main_window, bringing the main app window to the front (mirrors the tray icon flow). Since the overlay is an NSPanel NonactivatingPanel on macOS, React's synthesized onClick does not fire. Instead we record the press position on mousedown and emulate click on mouseup when the pointer stayed within a 4px slop. Dragging is deferred to mousemove past the slop so startDragging doesn't swallow the mouseup event. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a new Tauri command Changes
Sequence DiagramsequenceDiagram
actor User
participant Overlay as "OverlayApp.tsx"
participant Tauri as "Tauri Runtime"
participant Backend as "lib.rs::activate_main_window"
participant Window as "Main Window"
User->>Overlay: Pointer down / move / up on orb
Overlay->>Overlay: Distinguish click vs drag (pressRef, CLICK_SLOP_PX)
alt Click (no drag) & mode == idle
Overlay->>Tauri: invoke("activate_main_window")
Tauri->>Backend: activate_main_window(app)
Backend->>Backend: show_main_window(&app) -> Result
Backend->>Window: show(), unminimize(), set_focus()
Window-->>User: Window becomes frontmost / focused
else Drag or non-idle
Overlay->>Overlay: start drag or perform idle/non-activate action
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src-tauri/src/lib.rs`:
- Around line 409-412: activate_main_window currently swallows all errors;
change its signature to return a Result<(), String> and propagate any failures
from locating the "main" window and from calling Window::show(),
Window::unminimize(), and Window::set_focus() (or from the helper
show_main_window(&app)) by converting their errors into an Err(String) (e.g. via
map_err or ? with From) so the Tauri invoke handler receives a rejected promise;
ensure the invoke registration/handler expects Result and forwards the returned
error to JavaScript.
In `@app/src/overlay/OverlayApp.tsx`:
- Around line 473-483: handleMouseUp currently calls handleOrbClick
synchronously which causes a single-click to fire before the
onDoubleClick={resetPosition}, so change handleMouseUp to schedule the click
action with a short timeout (e.g., 150–250ms) instead of invoking handleOrbClick
immediately: if e.button !== 0 or pressRef.current is null or
pressRef.current.dragStarted then do nothing; otherwise start a timeout (store
it in a ref like singleClickTimerRef) that calls handleOrbClick after the delay.
In resetPosition (the onDoubleClick handler) clear that singleClickTimerRef so
the pending click is cancelled on a double-click; also clear the timer on
unmount and when press becomes null to avoid leaks.
- Around line 444-457: Stale press state can persist if the user releases the
mouse off the orb; update handleMouseMove to detect that the button is no longer
down (check e.buttons === 0 or (e.buttons & 1) === 0) and if so clear
pressRef.current = null and return before any slop checks, and also ensure the
existing onMouseUp handler clears pressRef.current (set to null) so press state
is always reset when the button is released; modify handleDragStart,
handleMouseMove, and onMouseUp references to pressRef and any call to
startDragging() accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0c9355d9-8c6b-4500-af6d-0f58b1d8dcf4
📒 Files selected for processing (3)
app/src-tauri/permissions/allow-core-process.tomlapp/src-tauri/src/lib.rsapp/src/overlay/OverlayApp.tsx
…them (tinyhumansai#605) `show_main_window` silently logged failures and returned `()`, so the `activate_main_window` Tauri command could report success on a no-op. Thread `Result<(), String>` through so JS `invoke().catch()` sees real failures, and preserve the previous log-on-error behavior at the tray/Reopen call sites where no caller consumes the result. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ai#605) Two follow-ups to the deferred-drag pattern: 1. Drop stale pressRef when the primary button is no longer held during mousemove. Window-drag / focus changes can steal the mouseup, leaving the ref populated so the next idle hover would start a spurious drag. 2. Debounce the synthetic click by 250 ms so a follow-up dblclick can cancel it — the double-click-to-reset gesture was firing activate + reset together. Clear the timer on dblclick and on unmount. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/overlay/OverlayApp.tsx`:
- Around line 446-450: handleDragStart currently records pressRef but doesn't
cancel any pending single-click timer, so a click scheduled on the previous
mouseup can still fire mid-drag; update handleDragStart to also clear the
pending click timeout (e.g., call clearTimeout on your single-click timer ref
such as singleClickTimeoutRef or clickTimeoutRef and set it to null) when a new
mousedown begins, so handleOrbClick cannot run during a subsequent drag; keep
the existing e.button check and pressRef update.
- Around line 197-203: The handler handleOrbClick currently calls
invoke('activate_main_window') unconditionally; wrap that Tauri command call
with an isTauri() runtime check before invoking to ensure it only runs in Tauri
environments (keep the existing .catch error handling). Update the conditional
branch where mode === 'idle' inside handleOrbClick to call
invoke('activate_main_window') only if isTauri() returns true, otherwise skip
the invoke (or use the non-Tauri fallback if needed).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1422a2ad-c71e-4976-bbe7-b135ea142c54
📒 Files selected for processing (2)
app/src-tauri/src/lib.rsapp/src/overlay/OverlayApp.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src-tauri/src/lib.rs
| /** Click handler for the orb: idle → bring main window to front; active → dismiss bubble. */ | ||
| const handleOrbClick = useCallback(() => { | ||
| if (mode === 'idle') { | ||
| console.debug('[overlay] orb clicked while idle — activating main window'); | ||
| invoke('activate_main_window').catch(err => { | ||
| console.error('[overlay] failed to activate main window:', err); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check the current state of the file at the relevant lines
cat -n app/src/overlay/OverlayApp.tsx | sed -n '190,210p'Repository: tinyhumansai/openhuman
Length of output: 910
🏁 Script executed:
# Check if isTauri is imported and used in the file
rg -A2 -B2 'isTauri' app/src/overlay/OverlayApp.tsx | head -30Repository: tinyhumansai/openhuman
Length of output: 395
🏁 Script executed:
# Check the tauriCommands wrapper pattern to see if activate_main_window should be wrapped
cat -n app/src/utils/tauriCommands.tsRepository: tinyhumansai/openhuman
Length of output: 128
🏁 Script executed:
# Check for any existing wrapper for activate_main_window
rg 'activate_main_window' app/src/Repository: tinyhumansai/openhuman
Length of output: 148
🏁 Script executed:
# Check the CLAUDE.md for actual coding guidelines about Tauri guards
cat CLAUDE.md 2>/dev/null | head -100Repository: tinyhumansai/openhuman
Length of output: 5935
Guard the new Tauri command call with isTauri().
Line 201 invokes activate_main_window unconditionally. The file already imports isTauri and uses it for similar Tauri calls in resolveCoreSocketUrl(). Per coding guidelines, guard all Tauri usage with explicit runtime checks instead of relying on command failure handling.
Suggested fix
const handleOrbClick = useCallback(() => {
if (mode === 'idle') {
+ if (!isTauri()) {
+ console.debug('[overlay] orb clicked while idle outside Tauri — ignoring');
+ return;
+ }
console.debug('[overlay] orb clicked while idle — activating main window');
- invoke('activate_main_window').catch(err => {
+ void invoke('activate_main_window').catch(err => {
console.error('[overlay] failed to activate main window:', err);
});
} else {
goIdle();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/overlay/OverlayApp.tsx` around lines 197 - 203, The handler
handleOrbClick currently calls invoke('activate_main_window') unconditionally;
wrap that Tauri command call with an isTauri() runtime check before invoking to
ensure it only runs in Tauri environments (keep the existing .catch error
handling). Update the conditional branch where mode === 'idle' inside
handleOrbClick to call invoke('activate_main_window') only if isTauri() returns
true, otherwise skip the invoke (or use the non-Tauri fallback if needed).
| /** Record mouse-down position; defer drag until the pointer actually moves. */ | ||
| const handleDragStart = useCallback((e: React.MouseEvent) => { | ||
| if (e.button !== 0) return; | ||
| pressRef.current = { x: e.screenX, y: e.screenY, dragStarted: false }; | ||
| }, []); |
There was a problem hiding this comment.
Cancel any pending single-click when a new press starts.
A click scheduled on the previous mouseup can still fire after the next mousedown. If the user starts a drag within the 250ms debounce window, handleOrbClick() can run mid-drag and break the preserved drag-to-reposition flow.
Suggested fix
const handleDragStart = useCallback((e: React.MouseEvent) => {
if (e.button !== 0) return;
+ if (clickTimerRef.current !== null) {
+ window.clearTimeout(clickTimerRef.current);
+ clickTimerRef.current = null;
+ }
pressRef.current = { x: e.screenX, y: e.screenY, dragStarted: false };
}, []);📝 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.
| /** Record mouse-down position; defer drag until the pointer actually moves. */ | |
| const handleDragStart = useCallback((e: React.MouseEvent) => { | |
| if (e.button !== 0) return; | |
| pressRef.current = { x: e.screenX, y: e.screenY, dragStarted: false }; | |
| }, []); | |
| /** Record mouse-down position; defer drag until the pointer actually moves. */ | |
| const handleDragStart = useCallback((e: React.MouseEvent) => { | |
| if (e.button !== 0) return; | |
| if (clickTimerRef.current !== null) { | |
| window.clearTimeout(clickTimerRef.current); | |
| clickTimerRef.current = null; | |
| } | |
| pressRef.current = { x: e.screenX, y: e.screenY, dragStarted: false }; | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/overlay/OverlayApp.tsx` around lines 446 - 450, handleDragStart
currently records pressRef but doesn't cancel any pending single-click timer, so
a click scheduled on the previous mouseup can still fire mid-drag; update
handleDragStart to also clear the pending click timeout (e.g., call clearTimeout
on your single-click timer ref such as singleClickTimeoutRef or clickTimeoutRef
and set it to null) when a new mousedown begins, so handleOrbClick cannot run
during a subsequent drag; keep the existing e.button check and pressRef update.
…show-window # Conflicts: # app/src-tauri/src/lib.rs
…inyhumansai#611) * feat(tauri): add activate_main_window command for overlay (tinyhumansai#605) Exposes the existing show_main_window helper as a Tauri command so the overlay webview can bring the main window to front. The command is whitelisted in allow-core-process.toml so the overlay window capability can invoke it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(overlay): activate main window on orb click (tinyhumansai#605) The overlay orb had no click behavior. Now clicking it in idle mode invokes activate_main_window, bringing the main app window to the front (mirrors the tray icon flow). Since the overlay is an NSPanel NonactivatingPanel on macOS, React's synthesized onClick does not fire. Instead we record the press position on mousedown and emulate click on mouseup when the pointer stayed within a 4px slop. Dragging is deferred to mousemove past the slop so startDragging doesn't swallow the mouseup event. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(window): propagate show_main_window errors instead of swallowing them (tinyhumansai#605) `show_main_window` silently logged failures and returned `()`, so the `activate_main_window` Tauri command could report success on a no-op. Thread `Result<(), String>` through so JS `invoke().catch()` sees real failures, and preserve the previous log-on-error behavior at the tray/Reopen call sites where no caller consumes the result. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(overlay): stabilize orb click vs drag vs double-click (tinyhumansai#605) Two follow-ups to the deferred-drag pattern: 1. Drop stale pressRef when the primary button is no longer held during mousemove. Window-drag / focus changes can steal the mouseup, leaving the ref populated so the next idle hover would start a spurious drag. 2. Debounce the synthetic click by 250 ms so a follow-up dblclick can cancel it — the double-click-to-reset gesture was firing activate + reset together. Clear the timer on dblclick and on unmount. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
activate_main_windowas a Tauri command so the overlay webview can reuse the existingshow_main_windowhelper (the same one the tray icon uses).clickevents by emulating click onmouseupwhen the pointer stays within a 4px slop.Problem
The overlay orb is always visible but clicking it had no effect — the main window stayed wherever it was (hidden, minimized, or behind other apps). Users had no way to surface the main window from the orb short of using the Dock / tray icon. See #605.
Two layered root causes had to be fixed:
show_main_windowhelper inlib.rswas only reachable from tray / Reopen handlers.NSPanelwithNonactivatingPanelstyle (from [Bug] Voice overlay orb disappears behind fullscreen applications #528, required for fullscreen visibility). That panel doesn't become key on click, so React's synthesizedonClicknever fires on the orb. Naively adding anonMouseUphandler also didn't work because the existingonMouseDownimmediately callsstartDragging(), which takes over the native event loop and preventsmouseupfrom reaching the webview.Solution
Backend (
app/src-tauri/src/lib.rs,app/src-tauri/permissions/allow-core-process.toml)#[tauri::command] fn activate_main_window(app: AppHandle)that delegates to the existingshow_main_window(&app)helper — same show / unminimize / set_focus flow that the tray icon and macOS Reopen event use.generate_handler![]and added it to theallow-core-processcapability so the overlay window can invoke it. Without the capability entry,invoke()from the overlay is silently rejected.Frontend (
app/src/overlay/OverlayApp.tsx)handleOrbClick: idle →invoke('activate_main_window'); active → dismiss bubble (oldgoIdlebehavior).onMouseDownjust records the press position — nostartDragging()yet.onMouseMoveescalates tostartDragging()only once the pointer moves past a 4px slop.onMouseUpfireshandleOrbClickif no drag was initiated.Submission Checklist
///doc comment on the new Rust command; JSDoc-style comments on the new TS handlers; a block comment atpressRefexplains why drag is deferred.pressRefcaptures the NSPanel +startDragging()constraint.Impact
NSPanelso click would work there regardless; the new handlers also work on those.allow-core-processcapability; only themainandoverlaywindows can invoke it.Related
Summary by CodeRabbit
New Features
Improvements
Permissions