different-ai/openwork · error

Electron desktop helper is unavailable: ${prop}

Error message

Electron desktop helper is unavailable: ${prop}

What it means

This is the dynamic twin of the typed helper: the DesktopBridge Proxy intercepts every property access and returns an async `fn` that forwards `prop` to `window.__OPENWORK_ELECTRON__.invokeDesktop`. If the Electron bridge is missing when the proxied method is called, it throws the same fail-fast error, embedding the accessed property name so you can see which desktop capability was attempted.

Source

Thrown at apps/app/src/app/lib/desktop.ts:282

// The cast is inherent to the Proxy pattern: the target is an empty cache and
// members are fabricated on access. The contract typing above is what keeps
// it honest (command names + signatures are checked on both sides).
export const desktopBridge = new Proxy(electronBridge, {
  get(target, prop) {
    if (typeof prop !== "string") return undefined;

    // resolveWorkspaceListSelectedId is a pure function, not an IPC call
    if (prop === "resolveWorkspaceListSelectedId") {
      return resolveWorkspaceListSelectedId;
    }

    const cached = target[prop];
    if (cached) return cached;

    const fn = async (...args: unknown[]) => {
      const invokeDesktop = window.__OPENWORK_ELECTRON__?.invokeDesktop;
      if (!invokeDesktop) {
        throw new Error(`Electron desktop helper is unavailable: ${prop}`);
      }
      // The Proxy is the one dynamic point in the bridge: `prop` is whatever
      // property was accessed, already constrained by the DesktopBridge
      // surface this Proxy is exported as.
      return invokeDesktop(
        prop as DesktopCommandName,
        ...(args as DesktopCommandArgs<DesktopCommandName>),
      );
    };
    target[prop] = fn;
    return fn;
  },
}) as unknown as DesktopBridge;

// ---------------------------------------------------------------------------
// desktopFetch — proxies non-loopback requests through the Electron main
// process. Loopback hosts (the local opencode/openwork server) use the
// renderer's own fetch, which works against same-machine services. Cross-origin

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Run the code inside the Electron renderer with the preload loaded, so window.__OPENWORK_ELECTRON__.invokeDesktop is defined.
  2. Wrap proxied bridge calls in an existence check (`window.__OPENWORK_ELECTRON__?.invokeDesktop`) and branch to a web fallback or hide desktop-only UI.
  3. Mock the bridge in tests: `window.__OPENWORK_ELECTRON__ = { invokeDesktop: vi.fn() }` before importing modules that touch the Proxy.
  4. Audit which property name appears in the message — a misspelled/removed command will also surface here if preload exposes invokeDesktop but rejects unknown commands; sync DesktopCommandName with preload definitions.

Example fix

// before
await bridge.downloadFile(url);

// after
if (!window.__OPENWORK_ELECTRON__?.invokeDesktop) {
  throw new Error("Desktop build required for this action");
}
await bridge.downloadFile(url);
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof window === "undefined" || !window.__OPENWORK_ELECTRON__?.invokeDesktop) {
  disableDesktopActions();
}

Type guard

function isDesktopBridgeAvailable(): boolean {
  return typeof window !== "undefined" &&
    typeof window.__OPENWORK_ELECTRON__?.invokeDesktop === "function";
}

Try / catch

try {
  await bridge.someCommand(arg);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("Electron desktop helper is unavailable")) {
    return fallbackForBrowser();
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling any method on the exported desktop bridge Proxy (a DesktopCommandName property not already cached) while `window.__OPENWORK_ELECTRON__` is undefined — web build, SSR/prerender, preload not loaded, or an Electron version whose preload lacks invokeDesktop.

Common situations: The same app bundle served in a browser where a desktop-only code path runs (e.g. auto-start on boot, shell integration); Next.js SSR executing a component that touches the bridge during server render; an Electron upgrade where the preload API was renamed or the preload failed silently; vitest/jsdom runs without a bridge mock.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/e5bdc756c762729e. Report an issue: GitHub.