fix(widget): hide on app load, bottom-center position, exclude from window-state restore - #104
Conversation
…indow-state restore Three issues with the dictation pill (Whisper-Flow / Ghost-Pepper style): 1. Pill appeared on app load even though `.visible(false)` and the global shortcut hadn't been pressed. 2. When shown, it positioned at top-center instead of bottom-center. 3. The "Ready — hold shortcut to speak" idle label rendered inside the pill even when no recording was active. Root causes & fixes: **(1) `tauri-plugin-window-state` was restoring widget visibility.** If the user had the widget visible when they quit the app (mid-dictation, or by clicking the tray's "Start Dictation" while a window was up), the plugin saved `visible: true` and restored it on next launch — overriding the `WebviewWindowBuilder.visible(false)`. Fix: add the widget label to the plugin's denylist, so its state is never persisted. Belt-and-braces: explicit `win.hide()` on the widget during studio-mode and pill-mode startup, so any other plugin or stale state can't sneak the window in. **(2) Position was hard-coded to top-center.** Changed `LogicalPosition::new(x, 60.0)` (top) to a computed bottom-center position: `y = logical_screen_height - 64 - 80` (80 px margin clears macOS dock + Windows taskbar + most Linux panels). Same math in all three places it's set (global-shortcut handler, tray dictate, pill-mode pre-position) — identical behavior on macOS/Windows/Linux per the new CLAUDE.md "default features work on every platform" rule. **(3) Idle label rendered visually.** `CaptureWidget.jsx` now returns `null` when `state === 'idle'`. Listeners stay mounted (hold-to-talk wiring is preserved), only the visual pill DOM disappears. The slide-in animation triggers on the natural unmount→mount when state flips out of idle. Also: lock in two durable rules surfaced in this session: - CLAUDE.md: "Default features must work on every platform" — platform- divergent defaults are a P0 bug; platform-only features must go behind explicit opt-in. - CLAUDE.md: "No RC, no ceremony" — v0.3.0 ships continuous-to-main; tag when actually useful; no v0.4 deferrals while v0.3.0 is open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughProject constraints add cross-platform default behavior rules and update release policy. Tauri backend now excludes the widget window from state persistence, positions it at computed bottom-center coordinates when activated via global shortcut or tray menu, and defensively hides it on startup. Frontend component returns null when idle, removing the "ready" pill UI until state transitions, with animation keyframe adjusted to match new positioning. ChangesWidget Visibility, Positioning, and Idle Handling
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src-tauri/src/lib.rs`:
- Around line 201-207: The computed x/y for positioning the pill can be negative
and is duplicated; create a small helper (e.g., fn clamp_pill_position(size:
tauri::PhysicalSize, scale: f64, pill_w: f64, pill_h: f64) -> (f64,f64)) that
computes logical_w/logical_h, derives desired center-based x and bottom-based y,
then clamps x to the range [0.0, logical_w - pill_w] and y to [0.0, logical_h -
pill_h] before returning a LogicalPosition; replace the inline logic around
win.set_position(...) (the block using logical_w/logical_h, x/y and
tauri::LogicalPosition::new) in all three places with a call to this helper so
positioning is centralized and never places the pill off-screen.
🪄 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 Plus
Run ID: 10f2ca96-c761-4fad-828e-b7d057542294
📒 Files selected for processing (4)
CLAUDE.mdfrontend/src-tauri/src/lib.rsfrontend/src/components/CaptureWidget.cssfrontend/src/components/CaptureWidget.jsx
| let logical_w = size.width as f64 / scale; | ||
| let logical_h = size.height as f64 / scale; | ||
| let x = (logical_w / 2.0 - 150.0) as i32; | ||
| let y = (logical_h - 64.0 - 80.0) as i32; | ||
| let _ = win.set_position(tauri::Position::Logical( | ||
| tauri::LogicalPosition::new(x as f64, 60.0), | ||
| tauri::LogicalPosition::new(x as f64, y as f64), | ||
| )); |
There was a problem hiding this comment.
Clamp and centralize widget coordinates before set_position.
The computed x/y can go negative on small or highly scaled displays, which may place the pill partially/off-screen. This same logic is repeated in three places, so it can drift.
Proposed fix
+fn widget_bottom_center_position(logical_w: f64, logical_h: f64) -> tauri::LogicalPosition<f64> {
+ let x = (logical_w / 2.0 - 150.0).max(0.0);
+ let y = (logical_h - 64.0 - 80.0).max(0.0);
+ tauri::LogicalPosition::new(x, y)
+}- let x = (logical_w / 2.0 - 150.0) as i32;
- let y = (logical_h - 64.0 - 80.0) as i32;
- let _ = win.set_position(tauri::Position::Logical(
- tauri::LogicalPosition::new(x as f64, y as f64),
- ));
+ let pos = widget_bottom_center_position(logical_w, logical_h);
+ let _ = win.set_position(tauri::Position::Logical(pos));Also applies to: 359-365, 431-437
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src-tauri/src/lib.rs` around lines 201 - 207, The computed x/y for
positioning the pill can be negative and is duplicated; create a small helper
(e.g., fn clamp_pill_position(size: tauri::PhysicalSize, scale: f64, pill_w:
f64, pill_h: f64) -> (f64,f64)) that computes logical_w/logical_h, derives
desired center-based x and bottom-based y, then clamps x to the range [0.0,
logical_w - pill_w] and y to [0.0, logical_h - pill_h] before returning a
LogicalPosition; replace the inline logic around win.set_position(...) (the
block using logical_w/logical_h, x/y and tauri::LogicalPosition::new) in all
three places with a call to this helper so positioning is centralized and never
places the pill off-screen.
Three issues with the dictation pill (Whisper-Flow / Ghost-Pepper style):
Root causes & fixes
1.
tauri-plugin-window-statewas restoring widget visibilityIf the user had the widget visible when they quit (mid-dictation, or via the tray "Start Dictation" with main window also up), the plugin saved `visible: true` and restored it on next launch — overriding `WebviewWindowBuilder.visible(false)`.
Fix: add the widget label to the plugin's denylist; its state is never persisted. Belt-and-braces: explicit `win.hide()` on the widget during studio-mode and pill-mode startup, so any other plugin or stale state can't sneak the window in.
2. Position was hard-coded to top-center
Changed `LogicalPosition::new(x, 60.0)` (top) to a computed bottom-center position: `y = logical_screen_height - 64 - 80` (80 px margin clears macOS dock + Windows taskbar + most Linux panels). Same math in all three places it's set — identical behavior on every platform per the new CLAUDE.md "default features work on every platform" rule.
3. Idle label rendered visually
`CaptureWidget.jsx` now returns `null` when `state === 'idle'`. Listeners stay mounted (hold-to-talk wiring is preserved), only the visual pill DOM disappears.
Plus: two durable rules locked into CLAUDE.md
These surfaced in conversation but are durable across all future work:
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes