From 2f7283d41e8b5d6578445274e4e325b7706a0f4a Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Mon, 3 Aug 2026 21:04:22 +0800 Subject: [PATCH] fix(collab): defer queued actions until the gesture's capture closes A pointer press opens the gesture's local-edit capture and queues the panel action it hit, but the same press's frame drained that action immediately. The session's document actor refuses to act while a capture is open, so every in-panel admission approval failed with LocalEditAlreadyActive and surfaced as a bogus rejection on both peers. Wait for the matching release, and let a release that leaves an action pending schedule the frame that drains it. --- .../src/app_handler/pointer_events.rs | 13 ++++++++++++- crates/op-host-desktop/src/app_handler/redraw.rs | 11 ++++++++++- .../src/collab_runtime/local_edit.rs | 10 ++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/op-host-desktop/src/app_handler/pointer_events.rs b/crates/op-host-desktop/src/app_handler/pointer_events.rs index 14b8c74c8..70e2040c3 100644 --- a/crates/op-host-desktop/src/app_handler/pointer_events.rs +++ b/crates/op-host-desktop/src/app_handler/pointer_events.rs @@ -400,7 +400,18 @@ impl DesktopApp { .apply_release_with_viewport(self.viewport_width, self.viewport_height); self.collab_runtime.finish_local_edit(&mut self.host); self.sync_native_ime(); - if consumed { + // The redraw pass defers collaboration actions queued mid-gesture + // until the capture closes, so a release that leaves one pending must + // schedule the frame that drains it — even when the release itself + // changed nothing on screen. + let drain_pending = self + .host + .editor_state() + .editor_ui + .collab + .pending_action + .is_some(); + if consumed || drain_pending { self.request_redraw(true); } } diff --git a/crates/op-host-desktop/src/app_handler/redraw.rs b/crates/op-host-desktop/src/app_handler/redraw.rs index 193adf115..5549b9470 100644 --- a/crates/op-host-desktop/src/app_handler/redraw.rs +++ b/crates/op-host-desktop/src/app_handler/redraw.rs @@ -47,7 +47,16 @@ impl DesktopApp { } else { true }; - if collaboration_transition_ready && self.collab_runtime.drain_ui_action(&mut self.host) { + // A press that queues an action also opens the gesture's local-edit + // capture, and the session's document actor refuses to activate a + // peer (or run any other document transition) while that capture is + // open. Draining here would turn every in-panel approval into a + // spurious failure, so wait for the matching release — which finishes + // the capture and requests another frame. + if collaboration_transition_ready + && !self.collab_runtime.local_edit_in_flight() + && self.collab_runtime.drain_ui_action(&mut self.host) + { self.redraw_dirty = true; } if self.collab_runtime.take_save_as_fork_request() { diff --git a/crates/op-host-desktop/src/collab_runtime/local_edit.rs b/crates/op-host-desktop/src/collab_runtime/local_edit.rs index 2cc705463..d093b9199 100644 --- a/crates/op-host-desktop/src/collab_runtime/local_edit.rs +++ b/crates/op-host-desktop/src/collab_runtime/local_edit.rs @@ -10,6 +10,16 @@ use super::types::CollabRuntimeError; use super::DesktopCollabRuntime; impl DesktopCollabRuntime { + /// Whether a GUI edit capture is open right now. + /// + /// A pointer gesture is one transaction: press opens the capture, release + /// closes it. Actions queued by that same press (approving an admission, + /// for example) must not be drained mid-gesture, because the session's + /// document actor refuses to act while a capture is in flight. + pub(crate) const fn local_edit_in_flight(&self) -> bool { + self.transaction_active + } + /// Start one GUI-owned edit capture; `false` means busy or no live session is bound. pub(crate) fn begin_local_edit(&mut self, host: &mut WidgetHostNative) -> bool { if self.transaction_active {