Skip to content

Commit ac33136

Browse files
committed
fix(gateway): wire approval publisher on auto-start + focus window on popup
Two bugs around meta-tool write approvals. 1. Approval publisher missing on auto-start Only the manual `start_gateway` Tauri command attached the broker publisher; the lib.rs auto-start path (which runs on every desktop app launch when auto-start is enabled — i.e. virtually always) never did. Result: even with the desktop app fully running, every write meta tool (`mcpmux_create_feature_set`, `mcpmux_bind_current_workspace`, …) returned `approval_required: no desktop attached to mcpmux gateway`. Factored the publisher wiring into `commands::gateway::attach_approval_publisher` and called it from both paths. Also added the missing `state.approval_broker = Some(...)` in the auto-start block so the desktop's grants-list / revoke commands can reach the broker too. 2. Popups rendered behind other windows When a meta-tool approval request fired or a session reported a root that needs binding, the dialog/sheet rendered in whichever window the user wasn't focused on. Added `focus_main_window(&app)` at two chokepoints: - In the approval publisher closure, before emitting the `meta-tool-approval-request` event. - In the domain-event bridge, when forwarding `WorkspaceNeedsBinding` (which triggers the binding sheet). `unminimize` + `show` + `set_focus` covers minimized, hidden behind another app, and tray-hidden states. Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent c2f02f6 commit ac33136

2 files changed

Lines changed: 76 additions & 24 deletions

File tree

apps/desktop/src-tauri/src/commands/gateway.rs

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,62 @@ pub(crate) async fn shutdown_gateway_handle(mut handle: mcpmux_gateway::GatewayS
114114
}
115115
}
116116

117+
/// Bring the main webview window forward so the user sees a popup the
118+
/// gateway just emitted. Best-effort — silently no-ops when the window
119+
/// doesn't exist (rare, e.g. during teardown). Used by the approval
120+
/// publisher and the WorkspaceNeedsBinding bridge so an LLM tool call or
121+
/// a fresh client connection automatically draws the user's eye to the
122+
/// mcpmux app instead of the dialog rendering invisibly under another
123+
/// window.
124+
pub(crate) fn focus_main_window<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
125+
use tauri::Manager;
126+
let Some(window) = app.get_webview_window("main") else {
127+
return;
128+
};
129+
// unminimize + show + set_focus together cover every state the user
130+
// could have left the window in (minimized, hidden behind another
131+
// app, hidden by user via the close-to-tray flow).
132+
let _ = window.unminimize();
133+
let _ = window.show();
134+
let _ = window.set_focus();
135+
}
136+
137+
/// Wire the meta-tool approval broker to the desktop event bus so write
138+
/// tools (e.g. `mcpmux_bind_current_workspace`) can prompt the React
139+
/// dialog. Both the manual `start_gateway` command and the lib.rs
140+
/// auto-start path must call this — without it the broker stays
141+
/// publisher-less and every write surfaces as
142+
/// `approval_required: no desktop attached to mcpmux gateway`.
143+
pub(crate) async fn attach_approval_publisher<R: tauri::Runtime>(
144+
approval_broker: &Arc<mcpmux_gateway::services::ApprovalBroker>,
145+
app_handle: tauri::AppHandle<R>,
146+
) {
147+
let publisher: mcpmux_gateway::services::meta_tools::ApprovalPublisher = Arc::new(move |req| {
148+
let app_handle = app_handle.clone();
149+
Box::pin(async move {
150+
// Bring the window forward BEFORE emitting so the dialog
151+
// animates into a visible window — otherwise it'd render
152+
// behind whatever the user is currently focused on.
153+
focus_main_window(&app_handle);
154+
// Emit the request; the React layer owns rendering +
155+
// collecting the user's decision. Failure to emit means
156+
// no desktop frontend is listening — broker maps that to
157+
// "approval_required" to the calling tool.
158+
match app_handle.emit("meta-tool-approval-request", &req) {
159+
Ok(()) => true,
160+
Err(e) => {
161+
tracing::warn!(
162+
error = %e,
163+
"[meta-tool] failed to emit approval request"
164+
);
165+
false
166+
}
167+
}
168+
})
169+
});
170+
approval_broker.set_publisher(publisher).await;
171+
}
172+
117173
/// Wires up ServerManager state + the OAuth completion handler + the
118174
/// periodic refresh loop after a GatewayServer has been spawned.
119175
///
@@ -245,6 +301,14 @@ pub fn start_domain_event_bridge(
245301
while let Ok(event) = event_rx.recv().await {
246302
let event_type = event.type_name();
247303

304+
// Some domain events imply a popup the user must see (a workspace
305+
// root needs binding, a backend wants OAuth, etc.). Bring the
306+
// window forward BEFORE emitting so the popup animates into a
307+
// visible window instead of rendering behind another app.
308+
if matches!(event, DomainEvent::WorkspaceNeedsBinding { .. }) {
309+
focus_main_window(&app_handle_clone);
310+
}
311+
248312
// Map domain events to UI channels
249313
let (channel, payload) = map_domain_event_to_ui(&event);
250314

@@ -822,30 +886,7 @@ pub async fn start_gateway(
822886
// Meta-tool approval broker — attach a Tauri-event publisher so
823887
// incoming approval requests reach the React dialog.
824888
let approval_broker = server.approval_broker();
825-
{
826-
let app_handle_for_broker = app_handle.clone();
827-
let publisher: mcpmux_gateway::services::meta_tools::ApprovalPublisher =
828-
std::sync::Arc::new(move |req| {
829-
let app_handle = app_handle_for_broker.clone();
830-
Box::pin(async move {
831-
// Emit the request; the React layer owns rendering +
832-
// collecting the user's decision. Failure to emit means
833-
// no desktop frontend is listening — broker maps that to
834-
// "approval_required" to the calling tool.
835-
match app_handle.emit("meta-tool-approval-request", &req) {
836-
Ok(()) => true,
837-
Err(e) => {
838-
tracing::warn!(
839-
error = %e,
840-
"[meta-tool] failed to emit approval request"
841-
);
842-
false
843-
}
844-
}
845-
})
846-
});
847-
approval_broker.set_publisher(publisher).await;
848-
}
889+
attach_approval_publisher(&approval_broker, app_handle.clone()).await;
849890

850891
// Start domain event bridge (clean architecture)
851892
start_domain_event_bridge(&app_handle, gw_state.clone());

apps/desktop/src-tauri/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,16 @@ pub fn run() {
473473
let event_emitter = server.event_emitter();
474474
let grant_service = server.grant_service();
475475
let session_roots = server.session_roots();
476+
let approval_broker = server.approval_broker();
477+
478+
// Wire the approval broker to the desktop event bus so
479+
// write meta tools can prompt the React dialog. Without
480+
// this, every write surfaces as "no desktop attached".
481+
crate::commands::gateway::attach_approval_publisher(
482+
&approval_broker,
483+
app_handle_for_sm.clone(),
484+
)
485+
.await;
476486

477487
// Start domain event bridge
478488
crate::commands::gateway::start_domain_event_bridge(&app_handle_for_sm, gw_inner_state.clone());
@@ -510,6 +520,7 @@ pub fn run() {
510520
state.feature_service = Some(feature_service);
511521
state.event_emitter = Some(event_emitter);
512522
state.grant_service = Some(grant_service);
523+
state.approval_broker = Some(approval_broker);
513524
state.session_roots = Some(session_roots);
514525

515526
info!(

0 commit comments

Comments
 (0)