From d8dc4cd924125e55e873e2e8ad66e19e2ea1f6dc Mon Sep 17 00:00:00 2001 From: Philipp Winterle Date: Thu, 17 Sep 2026 15:53:24 +0200 Subject: [PATCH] feat: Declare the desktop app as the preferred bridge channel The server's UI operation bridge picks whichever UI connected first as the channel that answers every MCP and CTL operation. With the desktop app and a browser tab both open, which one that is comes down to startup order, and the user has no way to steer it. The bridge now accepts an optional priority in the operations message. Sending 1 from the desktop shell and 0 from a browser tab makes the app the user is actually driving win, whatever the connection order was. isDesktopShell() reads the injected desktop API directly, because it has to answer synchronously here - desktopVersion never resolves in a browser, so it can't tell the two apart. --- src/services/desktop-api.ts | 5 +++++ src/services/ui-api/server-operation-bridge.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/services/desktop-api.ts b/src/services/desktop-api.ts index 1d2dfd878..0bb4c5728 100644 --- a/src/services/desktop-api.ts +++ b/src/services/desktop-api.ts @@ -94,6 +94,11 @@ const global = typeof globalThis !== 'undefined' export const DesktopApi: DesktopApi = global.desktopApi ?? {}; +// The desktop shell injects its API into the page before the UI loads. A browser +// tab pointed at the same server never has it, so this tells the two apart +// synchronously, unlike desktopVersion which never resolves in a browser. +export const isDesktopShell = (): boolean => !!global.desktopApi; + const DEFAULT_SERVER_PORT = 45457; const DEFAULT_MOCKTTP_PORT = 45456; diff --git a/src/services/ui-api/server-operation-bridge.ts b/src/services/ui-api/server-operation-bridge.ts index 920b65f18..81466a8a3 100644 --- a/src/services/ui-api/server-operation-bridge.ts +++ b/src/services/ui-api/server-operation-bridge.ts @@ -1,6 +1,6 @@ import { autorun, IReactionDisposer } from 'mobx'; import { AccountStore } from '../../model/account/account-store'; -import { getServerPort } from '../desktop-api'; +import { getServerPort, isDesktopShell } from '../desktop-api'; import { OperationRegistry } from './api-registry'; const RECONNECT_BASE_MS = 1_000; @@ -83,7 +83,11 @@ export function startServerOperationBridge( disposers.push(autorun(() => { ws!.send(JSON.stringify({ type: 'operations', - operations: registry.getDefinitions() + operations: registry.getDefinitions(), + // The desktop app is the UI the user is actually driving, so it + // takes the bridge's primary role from any browser tab that + // happens to be pointed at the same server too. + priority: isDesktopShell() ? 1 : 0 })); })); }