diff --git a/CHANGELOG.md b/CHANGELOG.md index aa29354..1900ad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [v1.4.1] - 2026-06-08 + +### Bug Fixes + +- The `heph` CLI and `heph-tui` now survive a daemon restart. Previously the unix-socket client connected once and never reconnected, so an opt-in self-update or `heph daemon restart` left every subsequent call failing — `heph-tui` would sit on errors until relaunched. The client now reconnects on a dropped socket: a request that never went out is retried transparently, while a reply lost mid-request is surfaced (not silently retried) so a mutation is never double-applied. A long-running TUI self-heals on its next refresh tick. +- Quick-add popover (⌘'): hand keyboard focus back to the previously active app when it hides, and stop the (now invisible) overlay from intercepting clicks where it used to sit. + + ## [v1.4.0] - 2026-06-08 ### Features diff --git a/Cargo.lock b/Cargo.lock index be8f974..cc9b3a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2237,6 +2237,8 @@ dependencies = [ "heph-core", "hephd", "libc", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "serde_json", "winit", ] diff --git a/crates/heph-quickadd/Cargo.toml b/crates/heph-quickadd/Cargo.toml index 5b1889b..57bbb98 100644 --- a/crates/heph-quickadd/Cargo.toml +++ b/crates/heph-quickadd/Cargo.toml @@ -19,7 +19,16 @@ global-hotkey = "0.8" # macOS-only: winit for the accessory-mode activation policy (no Dock icon), # pinned to the same minor eframe carries so cargo unifies to one winit; libc -# for getppid() (orphan detection — self-exit when the supervising daemon dies). +# for getppid() (orphan detection — self-exit when the supervising daemon dies); +# objc2 + objc2-app-kit to hand keyboard focus back to the previously active app +# when the popover hides (NSApplication.hide:/unhide:). Pinned to the 0.6/0.3 +# line global-hotkey already pulls in, so cargo unifies to one copy. [target.'cfg(target_os = "macos")'.dependencies] winit = "0.30" libc = "0.2" +objc2 = "0.6" +objc2-app-kit = { version = "0.3", default-features = false, features = [ + "std", + "NSApplication", + "NSResponder", +] } diff --git a/crates/heph-quickadd/src/app.rs b/crates/heph-quickadd/src/app.rs index b08bf03..a334b22 100644 --- a/crates/heph-quickadd/src/app.rs +++ b/crates/heph-quickadd/src/app.rs @@ -226,6 +226,9 @@ impl QuickAdd { } fn show(&mut self, ctx: &egui::Context) { + // Undo the app-level hide from the previous `hide()` so we can take focus + // again (no-op the first time / off macOS). + app_take_focus(); self.visible = true; self.focus_pending = true; self.current_hint = random_hint(self.current_hint); @@ -256,6 +259,13 @@ impl QuickAdd { ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(WIN_W, BASE_H))); self.win_h_applied = BASE_H; } + // Hand keyboard focus back to the app underneath us. winit's + // `Visible(false)` alone leaves *us* the active application, so focus + // never returns and the borderless always-on-top overlay can keep eating + // clicks where it used to sit. `NSApplication.hide:` orders our windows + // fully out and activates the next app in line — exactly the one the user + // was in (no-op off macOS). + app_yield_focus(); } /// Optimistic submit: hide now, create in the background. @@ -596,6 +606,39 @@ impl QuickAdd { } } +/// Hide the popover at the *application* level so macOS hands keyboard focus +/// back to the previously active app. `NSApplication.hide:` orders all our +/// windows out and activates the next app in line — the one the user was in — +/// which a plain winit `Visible(false)` does not do. No-op off macOS. +#[cfg(target_os = "macos")] +fn app_yield_focus() { + use objc2::MainThreadMarker; + use objc2_app_kit::NSApplication; + // eframe's `update` runs on the main thread, so this marker is always Some. + if let Some(mtm) = MainThreadMarker::new() { + NSApplication::sharedApplication(mtm).hide(None); + } +} + +#[cfg(not(target_os = "macos"))] +fn app_yield_focus() {} + +/// Undo [`app_yield_focus`]: clear the app-level hidden flag before re-showing, +/// so the window the viewport `Focus` command then makes key actually appears. +/// (`unhide:` also re-activates us; the per-window `Focus`/`Visible` viewport +/// commands do the rest.) No-op off macOS. +#[cfg(target_os = "macos")] +fn app_take_focus() { + use objc2::MainThreadMarker; + use objc2_app_kit::NSApplication; + if let Some(mtm) = MainThreadMarker::new() { + NSApplication::sharedApplication(mtm).unhide(None); + } +} + +#[cfg(not(target_os = "macos"))] +fn app_take_focus() {} + /// The current parent process id, for orphan detection. `None` off macOS (where /// hephd does not supervise a helper — there is no Aqua session to inherit). fn current_parent_pid() -> Option { diff --git a/docs/changelog.d/client-reconnect.bugfix.md b/docs/changelog.d/client-reconnect.bugfix.md deleted file mode 100644 index ae987b8..0000000 --- a/docs/changelog.d/client-reconnect.bugfix.md +++ /dev/null @@ -1 +0,0 @@ -The `heph` CLI and `heph-tui` now survive a daemon restart. Previously the unix-socket client connected once and never reconnected, so an opt-in self-update or `heph daemon restart` left every subsequent call failing — `heph-tui` would sit on errors until relaunched. The client now reconnects on a dropped socket: a request that never went out is retried transparently, while a reply lost mid-request is surfaced (not silently retried) so a mutation is never double-applied. A long-running TUI self-heals on its next refresh tick.