From 2dfdc6f63b1e00e70b3069c2013ca72a6e0c8da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1l=20Akp=C4=B1nar?= <4ni1ak@gmail.com> Date: Tue, 1 Sep 2026 00:39:13 +0300 Subject: [PATCH 1/2] fix(overlay): keep the Actions Ring above panels on Linux/Wayland MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plain WindowKind::PopUp is just another toplevel to a Wayland compositor — nothing stops it being drawn under a panel/dock's own always-on-top surface, so the ring could open hidden behind one. Anchor it as an AnchoredPopup off an invisible full-screen Layer::Overlay host instead: that layer is guaranteed above everything else, including panels. The host also fixes cursor placement as a side effect. openlogi_hook's Linux cursor_position() falls back to XWayland's X11 query_pointer under a Wayland session — not a real Wayland pointer, and it doesn't track one, so it reports a stale, display-ambiguous coordinate on a Wayland-native desktop. The host surface gets the compositor's own first pointer-motion delivery instead (with a bounded fallback to the old X11-based guess if none arrives), which is accurate regardless of which monitor the cursor is on. Every dismissal path (slot/cancel/root click, click-away, the display-lifetime timeout) now closes the host alongside the ring, and clicking the host itself dismisses the ring — click-away-to-dismiss Linux never had, since the existing native monitor is macOS-only. Falls back to the previous plain-PopUp behavior on macOS, Windows, Linux/X11, and Wayland compositors without zwlr_layer_shell_v1. Fixes #1210 --- crates/openlogi-overlay/src/main.rs | 67 ++--- crates/openlogi-overlay/src/ring.rs | 339 ++++++++++++++++++++++++- crates/openlogi-overlay/src/session.rs | 8 +- 3 files changed, 363 insertions(+), 51 deletions(-) diff --git a/crates/openlogi-overlay/src/main.rs b/crates/openlogi-overlay/src/main.rs index 403325f66..306a8b564 100644 --- a/crates/openlogi-overlay/src/main.rs +++ b/crates/openlogi-overlay/src/main.rs @@ -22,15 +22,13 @@ mod session; use std::sync::Arc; use anyhow::Result; -use gpui::AppContext as _; use tracing::warn; use tracing_subscriber::EnvFilter; use openlogi_core::action_ring::DISPLAY_LIFETIME; use crate::agent::{Ipc, OverlayCommand, spawn_ipc}; -use crate::platform::RingPlacement; -use crate::ring::RingView; +use crate::ring::open_ring; use crate::session::{ClickAwaySession, claim_the_role, spawn_click_away_dismissal}; fn main() -> Result<()> { @@ -73,48 +71,37 @@ fn main() -> Result<()> { for handle in cx.windows() { let _ = handle.update(cx, |_, window, _| window.remove_window()); } - let placement = match RingPlacement::capture(cx) { - Ok(placement) => placement, - Err(error) => { - warn!(%error, "could not locate Actions Ring display"); - let _ = commands.send(OverlayCommand::Cancel { - session_id: invocation.session_id, - }); - return; - } - }; - let commands = commands.clone(); - let timeout_commands = commands.clone(); - let session_id = invocation.session_id; - match cx.open_window(placement.window_options(), |_, cx| { - cx.new(|_| RingView::new(invocation, commands, &live_session)) - }) { - Ok(handle) => { - if let Err(error) = handle - .update(cx, |_, window, _| placement.show(window)) - .and_then(std::convert::identity) + }); + let commands = commands.clone(); + let timeout_commands = commands.clone(); + let session_id = invocation.session_id; + // Awaited, not wrapped in `cx.update`: on Linux/Wayland this + // opens the ring's layer-shell host and then genuinely waits + // on the compositor's first pointer-motion delivery to it + // before placing the ring — see `ring::linux_wayland_ring_host`. + match open_ring(cx, invocation, commands, Arc::clone(&live_session)).await { + Ok((handle, host)) => { + cx.spawn(async move |cx| { + cx.background_executor().timer(DISPLAY_LIFETIME).await; + if handle + .update(cx, |_, window, _| window.remove_window()) + .is_ok() { - warn!(%error, "could not position Actions Ring window"); - let _ = handle.update(cx, |_, window, _| window.remove_window()); let _ = timeout_commands.send(OverlayCommand::Cancel { session_id }); - return; } - cx.spawn(async move |cx| { - cx.background_executor().timer(DISPLAY_LIFETIME).await; - if handle - .update(cx, |_, window, _| window.remove_window()) - .is_ok() - { - let _ = timeout_commands - .send(OverlayCommand::Cancel { session_id }); - } - }) - .detach(); - } - Err(error) => warn!(%error, "could not open Actions Ring window"), + // The ring's own dismissal paths (slot/cancel/root click, + // click-away) already close the host alongside it — this + // timeout path is the one exit that doesn't run through any + // of those, so it must close the host itself. + if let Some(host) = host { + let _ = host.update(cx, |_, window, _| window.remove_window()); + } + }) + .detach(); } - }); + Err(error) => warn!(%error, "could not open Actions Ring window"), + } } }) .detach(); diff --git a/crates/openlogi-overlay/src/ring.rs b/crates/openlogi-overlay/src/ring.rs index ab585fb82..76cfdb06e 100644 --- a/crates/openlogi-overlay/src/ring.rs +++ b/crates/openlogi-overlay/src/ring.rs @@ -4,21 +4,21 @@ //! clamped to the display it came up on, so a ring raised near a screen edge //! stays whole instead of being cut off. -#[cfg(any(not(target_os = "windows"), test))] -use gpui::{Bounds, Pixels, Point, Size, point}; use gpui::{ - Context, Hsla, InteractiveElement, IntoElement, ParentElement, Render, SharedString, - StatefulInteractiveElement as _, Styled, Window, WindowBackgroundAppearance, WindowKind, - WindowOptions, div, prelude::FluentBuilder as _, px, svg, + AppContext as _, Bounds, Context, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, + Point, Render, SharedString, Size, StatefulInteractiveElement as _, Styled, Window, + WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, + prelude::FluentBuilder as _, px, svg, }; use openlogi_core::binding::{Action, ActionRingSlot}; use openlogi_ipc::ActionRingInvocation; use openlogi_ui::action_icons::RING_CANCEL_ICON; use openlogi_ui::color; use std::sync::Arc; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, oneshot}; use crate::agent::OverlayCommand; +use crate::platform; use crate::session::{ClickAwaySession, ShowingRing}; pub(crate) const WINDOW_SIZE: f32 = 360.0; @@ -57,6 +57,13 @@ pub(crate) struct RingView { hovered: Option, /// Publishes click-away identity for exactly this view's lifetime. _showing: ShowingRing, + /// The invisible full-screen Wayland layer-shell window this ring is an + /// `AnchoredPopup` child of, if [`open_ring`] opened one — `None` + /// everywhere else (macOS, Windows, Linux/X11, or Wayland without + /// `zwlr_layer_shell_v1`). Every place that closes this window must also + /// close this one, or it lingers as an invisible click-blocking layer + /// over the whole screen (#1206). + host: Option, } impl RingView { @@ -65,6 +72,7 @@ impl RingView { invocation: ActionRingInvocation, commands: mpsc::UnboundedSender, live: &Arc, + host: Option, ) -> Self { let showing = live.showing(invocation.session_id); Self { @@ -72,6 +80,7 @@ impl RingView { commands, hovered: None, _showing: showing, + host, } } @@ -80,6 +89,11 @@ impl RingView { self.invocation.session_id } + /// The host window ([`Self::host`]) a click-away dismissal must also close. + pub(crate) const fn host(&self) -> Option { + self.host + } + /// Report this ring cancelled. The window is closed by the caller, which /// is the only one holding the handle. pub(crate) fn cancel(&self) { @@ -99,6 +113,7 @@ impl RingView { let (left, top) = slot.placement(WINDOW_SIZE, RADIUS, SLOT_SIZE); let session_id = self.invocation.session_id; let activate = self.commands.clone(); + let host = self.host; Some( div() .id(("ring-slot", slot.index())) @@ -138,7 +153,7 @@ impl RingView { .on_click(move |_, window, cx| { cx.stop_propagation(); let _ = activate.send(OverlayCommand::Activate { session_id, slot }); - window.remove_window(); + close_ring(window, cx, host); }) .into_any_element(), ) @@ -150,6 +165,8 @@ impl Render for RingView { let session_id = self.invocation.session_id; let root_commands = self.commands.clone(); let center_commands = self.commands.clone(); + let root_host = self.host; + let center_host = self.host; let hovered_label = self.hovered.and_then(|slot| { let presentation = self.invocation.slots.get(&slot)?; // User-authored labels render verbatim: passing them through the @@ -202,7 +219,7 @@ impl Render for RingView { .on_click(move |_, window, cx| { cx.stop_propagation(); let _ = center_commands.send(OverlayCommand::Cancel { session_id }); - window.remove_window(); + close_ring(window, cx, center_host); }), ) .when_some(hovered_label, |ring, label| { @@ -218,9 +235,9 @@ impl Render for RingView { .child(label), ) }) - .on_click(move |_, window, _| { + .on_click(move |_, window, cx| { let _ = root_commands.send(OverlayCommand::Cancel { session_id }); - window.remove_window(); + close_ring(window, cx, root_host); }) } } @@ -241,6 +258,308 @@ pub(crate) fn ring_window_options() -> WindowOptions { } } +/// Close a ring window and, if it has one, its Wayland layer-shell host +/// together — every dismissal path but the display-lifetime timeout (which +/// closes the host itself, see `main.rs`) goes through here. +fn close_ring(window: &mut Window, cx: &mut gpui::App, host: Option) { + window.remove_window(); + if let Some(host) = host { + let _ = host.update(cx, |_, window, _| window.remove_window()); + } +} + +/// Open the Actions Ring for `invocation`, reporting interactions through +/// `commands`. Returns the ring's window handle and, on Linux/Wayland, the +/// invisible layer-shell host it opened alongside it (every dismissal path +/// must close that host together with the ring — see [`RingView::host`]). +/// +/// A plain `WindowKind::PopUp` (used everywhere else, and as the Linux +/// fallback when no Wayland layer-shell is available) is just another +/// toplevel to a Wayland compositor — nothing stops it from being drawn under +/// a panel/dock's own always-on-top surface. Anchoring the ring as a popup off +/// a `Layer::Overlay` host instead inherits that host's guaranteed +/// above-everything stacking (#1206). Away from that host, placement goes +/// through [`platform::RingPlacement`], which also carries the macOS/Windows +/// native-display and DPI corrections. +pub(crate) async fn open_ring( + cx: &mut gpui::AsyncApp, + invocation: ActionRingInvocation, + commands: mpsc::UnboundedSender, + live_session: Arc, +) -> anyhow::Result<(gpui::WindowHandle, Option)> { + let size = Size::new(px(WINDOW_SIZE), px(WINDOW_SIZE)); + let host = linux_wayland_ring_host(cx, invocation.session_id).await; + // Only captured (and only consulted for window_options below) when no + // layer-shell host was opened — the host's own anchor already carries + // exact placement, so there is nothing for `RingPlacement` to add there. + let placement = match host { + Some(_) => None, + None => Some(cx.update(platform::RingPlacement::capture)?), + }; + let options = match (&host, &placement) { + (Some((host, cursor)), _) => WindowOptions { + // AnchoredPopup ignores window_bounds' origin (see its own + // doc comment) — only the size matters here, the host's + // anchor_rect carries the position. + window_bounds: Some(WindowBounds::Windowed(Bounds::new(Point::default(), size))), + titlebar: None, + focus: false, + show: true, + kind: WindowKind::AnchoredPopup(gpui::popup::PopupOptions { + parent: *host, + // Center anchor + center gravity centers the popup + // exactly on the anchor point — no manual half-size + // offset to get subtly wrong (#1206). + anchor_rect: Bounds::new(*cursor, Size::default()), + anchor: gpui::popup::PopupAnchor::Center, + gravity: gpui::popup::PopupGravity::Center, + constraint_adjustment: gpui::popup::PopupConstraintAdjustment::SLIDE_X + | gpui::popup::PopupConstraintAdjustment::SLIDE_Y, + offset: Point::default(), + grab: false, + }), + is_movable: false, + is_resizable: false, + is_minimizable: false, + display_id: None, + window_background: WindowBackgroundAppearance::Transparent, + app_id: Some("openlogi-action-ring".to_string()), + ..WindowOptions::default() + }, + (None, Some(placement)) => placement.window_options(), + (None, None) => unreachable!("placement is always captured when no host was opened"), + }; + let host = host.map(|(host, _)| host); + let opened = cx.update(|cx| { + cx.open_window(options, move |_, cx| { + cx.new(|_| RingView::new(invocation, commands, &live_session, host)) + }) + }); + let handle = match opened { + Ok(handle) => handle, + Err(error) => { + // No RingView exists for this host yet, so nothing else will ever + // close it — leaving it open would block clicks across the whole + // screen until the next ring invocation or process exit. + if let Some(host) = host { + cx.update(|cx| { + let _ = host.update(cx, |_, window, _| window.remove_window()); + }); + } + return Err(error); + } + }; + if let Some(placement) = placement { + // Windows must apply its DPI-aware geometry against the live HWND; + // everywhere else this just tidies the window's native style. + let shown = cx.update(|cx| { + handle + .update(cx, |_, window, _| placement.show(window)) + .and_then(std::convert::identity) + }); + if let Err(error) = shown { + tracing::warn!(%error, "could not position Actions Ring window"); + cx.update(|cx| { + let _ = handle.update(cx, |_, window, _| window.remove_window()); + }); + return Err(error); + } + } + Ok((handle, host)) +} + +/// Open the invisible full-screen `Layer::Overlay` window the ring anchors to +/// on Linux/Wayland, so the ring inherits guaranteed above-panel stacking, and +/// read the real cursor position from it. `None` on every other platform, on +/// Linux/X11, or when the compositor doesn't support `zwlr_layer_shell_v1` — +/// the ring then falls back to a plain `WindowKind::PopUp` placed by +/// [`platform::RingPlacement`], exactly as before this host existed. +/// +/// Wayland deliberately has no query for "where is the pointer right now" — +/// [`openlogi_hook::cursor_position`] falls back to XWayland's X11 +/// `query_pointer` under a Wayland session, which is not a real Wayland +/// pointer and does not track one; it reports whatever coordinate it last +/// held (often stale, and always display-ambiguous on a multi-monitor +/// desktop). This host sidesteps that entirely: it opens, then waits for the +/// compositor's own first pointer-move delivery — this surface covers the +/// whole desktop with no other `zwlr_layer_shell_v1` surface competing for +/// it, so that motion event's position is the real cursor position, already +/// local to this surface (#1206). [`gpui::Window::mouse_position`] read +/// immediately after `open_window` returns is **not** equivalent: the +/// platform round-trip that carries the compositor's first `enter`/`motion` +/// hasn't necessarily happened yet, so that read can (and, observed live, +/// intermittently does) return a stale value from whatever this process last +/// knew — capped at [`CURSOR_WAIT`] and falling back to +/// [`linux_fallback_origin`] so a slow or absent compositor still eventually +/// opens *a* ring rather than hanging. +/// +/// No visible content otherwise: its only job is being a layer-shell surface +/// the ring can be a positioned `AnchoredPopup` child of, and a click-away +/// target (see [`RingHostView`]'s own doc comment). Its output is left to the +/// compositor's own choice (no `display_id`) rather than guessed from the +/// same broken cursor position — most compositors, including KWin, place an +/// unrequested layer-shell surface on the output the pointer is currently +/// over, which is exactly the output this ring needs to open on. +#[cfg(target_os = "linux")] +const CURSOR_WAIT: std::time::Duration = std::time::Duration::from_millis(250); + +/// The no-pointer-motion fallback origin for [`linux_wayland_ring_host`], +/// local to the display it lands on — `platform::RingPlacement` doesn't +/// expose this on its own since only this Linux-only, host-local anchor +/// needs both the ring's global origin and the display's own origin to +/// convert it. `platform::display_containing` never resolves natively on +/// Linux, so this is exactly the fallback branch `RingPlacement::capture` +/// itself takes there. +#[cfg(target_os = "linux")] +#[expect( + clippy::cast_possible_truncation, + reason = "native cursor coordinates are screen-sized and exactly usable as GPUI f32 pixels" +)] +fn linux_fallback_origin(cx: &mut gpui::App) -> (Point, Option>) { + let cursor = openlogi_hook::cursor_position(); + let size = Size::new(px(WINDOW_SIZE), px(WINDOW_SIZE)); + let cursor_point = cursor + .as_ref() + .map(|cursor| point(px(cursor.x as f32), px(cursor.y as f32))); + let display = cursor_point + .and_then(|cursor| { + cx.displays() + .into_iter() + .find(|display| display.bounds().contains(&cursor)) + }) + .or_else(|| cx.primary_display()); + let center = cursor_point + .or_else(|| display.as_ref().map(|display| display.bounds().center())) + .unwrap_or_default(); + let bounds = display.as_ref().map(|display| display.bounds()); + let desired_origin = point(center.x - size.width / 2.0, center.y - size.height / 2.0); + let origin = bounds.map_or(desired_origin, |bounds| { + clamp_window_origin(desired_origin, size, bounds) + }); + (origin, bounds) +} + +#[cfg(target_os = "linux")] +async fn linux_wayland_ring_host( + cx: &mut gpui::AsyncApp, + session_id: u64, +) -> Option<(gpui::AnyWindowHandle, Point)> { + if gpui::guess_compositor() != "Wayland" { + return None; + } + let (cursor_tx, cursor_rx) = oneshot::channel(); + let opened = cx.update(|cx| { + // A placeholder size only: anchoring to all four edges makes the + // compositor's own configure response the real, authoritative size + // regardless of what's requested here. + let size = cx + .primary_display() + .map_or_else(|| Size::new(px(1920.0), px(1080.0)), |d| d.bounds().size); + let options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::new(Point::default(), size))), + titlebar: None, + focus: false, + show: true, + kind: WindowKind::LayerShell(gpui::layer_shell::LayerShellOptions { + namespace: "openlogi-action-ring-host".to_string(), + layer: gpui::layer_shell::Layer::Overlay, + anchor: gpui::layer_shell::Anchor::TOP + | gpui::layer_shell::Anchor::BOTTOM + | gpui::layer_shell::Anchor::LEFT + | gpui::layer_shell::Anchor::RIGHT, + exclusive_zone: None, + exclusive_edge: None, + margin: None, + keyboard_interactivity: gpui::layer_shell::KeyboardInteractivity::None, + }), + is_movable: false, + is_resizable: false, + is_minimizable: false, + display_id: None, + window_background: WindowBackgroundAppearance::Transparent, + app_id: Some("openlogi-action-ring-host".to_string()), + ..WindowOptions::default() + }; + cx.open_window(options, |_, cx| { + cx.new(|_| RingHostView { + session_id, + cursor_tx: std::rc::Rc::new(std::cell::RefCell::new(Some(cursor_tx))), + }) + }) + }); + let handle = match opened { + Ok(handle) => handle, + Err(error) => { + tracing::warn!( + %error, + "could not open the Actions Ring's layer-shell host — \ + falling back to a plain popup, which a panel may draw over" + ); + return None; + } + }; + let cursor = tokio::select! { + cursor = cursor_rx => cursor.ok(), + () = cx.background_executor().timer(CURSOR_WAIT) => { + tracing::warn!( + "no pointer motion on the Actions Ring's layer-shell host within {CURSOR_WAIT:?} \ + — opening at a best-effort guess instead" + ); + None + } + }; + let cursor = match cursor { + Some(cursor) => cursor, + None => cx.update(|cx| { + let (origin, display_bounds) = linux_fallback_origin(cx); + display_bounds.map_or(origin, |bounds| origin - bounds.origin) + }), + }; + Some((handle.into(), cursor)) +} + +#[cfg(not(target_os = "linux"))] +async fn linux_wayland_ring_host( + _cx: &mut gpui::AsyncApp, + _session_id: u64, +) -> Option<(gpui::AnyWindowHandle, Point)> { + None +} + +/// The invisible layer-shell window [`linux_wayland_ring_host`] opens. A +/// click anywhere on it (i.e. anywhere outside the ring it hosts) dismisses +/// that ring, and its first mouse-move reports the real cursor position back +/// to whoever is waiting on [`Self::cursor_tx`] — see +/// [`linux_wayland_ring_host`]'s own doc comment for both. +#[cfg(target_os = "linux")] +struct RingHostView { + session_id: u64, + /// Taken and fired on this view's first mouse-move; `None` after. Shared + /// (not owned outright) because [`Render::render`] hands a fresh + /// `on_mouse_move` closure to a new element tree on every call, and each + /// must see whether an earlier one already fired it. + cursor_tx: std::rc::Rc>>>>, +} + +#[cfg(target_os = "linux")] +impl Render for RingHostView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let session_id = self.session_id; + let cursor_tx = std::rc::Rc::clone(&self.cursor_tx); + div() + .id("ring-host") + .size_full() + .on_mouse_move(move |event, _window, _cx| { + if let Some(tx) = cursor_tx.borrow_mut().take() { + let _ = tx.send(event.position); + } + }) + .on_click(move |_, _window, cx| { + crate::session::dismiss_click_away(cx, session_id); + }) + } +} + #[cfg(any(not(target_os = "windows"), test))] pub(crate) fn clamp_window_origin( desired: Point, diff --git a/crates/openlogi-overlay/src/session.rs b/crates/openlogi-overlay/src/session.rs index 5ab182da6..996e01610 100644 --- a/crates/openlogi-overlay/src/session.rs +++ b/crates/openlogi-overlay/src/session.rs @@ -112,12 +112,18 @@ pub(crate) fn dismiss_click_away(cx: &mut gpui::App, session_id: u64) { let Some(ring) = handle.downcast::() else { continue; }; - let _ = ring.update(cx, |view, window, _| { + let _ = ring.update(cx, |view, window, cx| { if !click_away_targets(session_id, view.session_id()) { return; } view.cancel(); window.remove_window(); + // The ring's own Wayland layer-shell host (if any) doesn't close + // itself — every dismissal path must close it alongside the ring + // (#1206). + if let Some(host) = view.host() { + let _ = host.update(cx, |_, window, _| window.remove_window()); + } }); } } From 6e0bc21847d1ddcbb3b0c0d0e11c5fe67a85b572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1l=20Akp=C4=B1nar?= <4ni1ak@gmail.com> Date: Tue, 1 Sep 2026 01:13:27 +0300 Subject: [PATCH 2/2] fix(overlay): cross-platform build + fallback-anchor + leak fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gate the oneshot import and silence the non-Linux stub's unused_async Linux-only — cargo clippy/test --workspace only ran the Linux cfg branch locally, so the non-Linux stub's unused import/async broke the macOS and Windows CI jobs. - The 250ms no-pointer-motion fallback in linux_wayland_ring_host was reusing ring_placement()'s global-coordinate origin as a host-local anchor — exactly the coordinate-space bug the host exists to avoid, reintroduced on a secondary display. Convert it the same way the main PopUp fallback does. - open_ring leaked the host window when the anchored popup itself failed to open: no RingView exists yet for any dismissal path to find, so the invisible full-screen host would sit there blocking clicks until the next ring invocation. Close it explicitly on that error path. Both anchor/leak issues via Greptile review on #1211. --- crates/openlogi-overlay/src/ring.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/openlogi-overlay/src/ring.rs b/crates/openlogi-overlay/src/ring.rs index 76cfdb06e..e9ab821b0 100644 --- a/crates/openlogi-overlay/src/ring.rs +++ b/crates/openlogi-overlay/src/ring.rs @@ -15,7 +15,9 @@ use openlogi_ipc::ActionRingInvocation; use openlogi_ui::action_icons::RING_CANCEL_ICON; use openlogi_ui::color; use std::sync::Arc; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::mpsc; +#[cfg(target_os = "linux")] +use tokio::sync::oneshot; use crate::agent::OverlayCommand; use crate::platform; @@ -518,7 +520,14 @@ async fn linux_wayland_ring_host( Some((handle.into(), cursor)) } +// `async` only to match the Linux implementation's signature, which the +// caller `.await`s unconditionally — this stub has nothing to await. #[cfg(not(target_os = "linux"))] +#[expect(clippy::allow_attributes, reason = "see below")] +#[allow( + clippy::unused_async, + reason = "kept async to match the Linux implementation's signature" +)] async fn linux_wayland_ring_host( _cx: &mut gpui::AsyncApp, _session_id: u64,