Turn Record · 2026-05-20 18:34 EDT

Fix Desktop Copilot Shell Detection

This turn fixed the case where Islandflow could be running inside the Electron desktop shell but still show the browser-only Copilot fallback. The renderer now distinguishes between “not in desktop”, “desktop shell detected but native bridge missing”, and “desktop bridge present but initial state failed”.

Main Issue islandflow-199
Files Changed 3
Validation Targeted tests passed, web build blocked by existing repo issue

Summary

The Copilot settings surface no longer assumes that any failure to load desktop AI state means the app is running in a browser. Instead, it recovers late bridge injection for a short window, exposes better shell-vs- bridge state to the UI, and gives the user actionable recovery copy when the native bridge is missing.

Changes Made

Context

The reported symptom was a Settings screen that still rendered “Browser-only fallback” even though the user was visibly inside the Islandflow desktop app. The pre-fix renderer had a single blunt interpretation: if the preload bridge was not immediately readable, the state fell back to the same model used for a real browser session.

That collapsed three different states into one misleading message:

  • Regular browser session, where desktop AI truly is unavailable.
  • Electron shell present, but the bridge appeared slightly later than the first effect pass.
  • Electron shell and bridge both present, but the initial getState() call failed.

Important Implementation Details

Electron shell detection Late bridge retry Accurate fallback messaging Regression tests

Relevant Diff Snippets

Diff snippets are rendered as plain patch strings compatible with the format documented by diffs.com.

Renderer runtime detection and bridge polling
diff --git a/apps/web/app/desktop-ai.tsx b/apps/web/app/desktop-ai.tsx
@@
+const BRIDGE_POLL_INTERVAL_MS = 250;
+const BRIDGE_POLL_MAX_ATTEMPTS = 20;
+const ELECTRON_USER_AGENT_PATTERN = /\bElectron\/\S+/i;
+
+export const detectDesktopShell = (userAgent: string | null | undefined): boolean =>
+  Boolean(userAgent && ELECTRON_USER_AGENT_PATTERN.test(userAgent));
+
+export const resolveDesktopAiRuntime = (...) => {
+  const bridge = value?.islandflowDesktop?.ai ? value.islandflowDesktop : null;
+  const bridgeAvailable = Boolean(bridge?.ai);
+  const shellAvailable = bridgeAvailable || detectDesktopShell(value?.navigator?.userAgent);
+  return { shellAvailable, bridgeAvailable, bridge };
+};
@@
+    if (!syncRuntime()) {
+      const pollForBridge = () => {
+        attempts += 1;
+        if (syncRuntime() || attempts >= BRIDGE_POLL_MAX_ATTEMPTS) {
+          return;
+        }
+        pollTimer = window.setTimeout(pollForBridge, BRIDGE_POLL_INTERVAL_MS);
+      };
+      pollTimer = window.setTimeout(pollForBridge, BRIDGE_POLL_INTERVAL_MS);
+    }
Settings and task panels stop calling the desktop shell a browser
diff --git a/apps/web/app/desktop-ai-panels.tsx b/apps/web/app/desktop-ai-panels.tsx
@@
-  const actionsDisabled = busyAction !== null || !state.desktopAvailable;
+  const actionsDisabled = busyAction !== null || !bridgeAvailable;
@@
-      {!state.desktopAvailable ? (
+      {!shellAvailable ? (
         <CopilotPane title="Desktop required" eyebrow="Browser-only fallback" wide>
@@
-const requireDesktopActionCopy = (desktopAvailable: boolean, loggedIn: boolean): string => {
-  if (!desktopAvailable) {
+export const requireDesktopActionCopy = (shellAvailable: boolean, bridgeAvailable: boolean, loggedIn: boolean): string => {
+  if (!shellAvailable) {
     return "This control is desktop-only. Open Islandflow Desktop to run Copilot tasks.";
   }
+  if (!bridgeAvailable) {
+    return "Islandflow Desktop is open, but this window is missing the native AI bridge. Reload the window or restart the app.";
+  }
Regression coverage for shell-vs-bridge behavior
diff --git a/apps/web/app/desktop-ai.test.ts b/apps/web/app/desktop-ai.test.ts
new file mode 100644
@@
+it("recognizes Electron user agents before the bridge is available", () => {
+  const runtime = resolveDesktopAiRuntime({ navigator: { userAgent: "... Electron/39.0.0 ..." } });
+  expect(runtime.shellAvailable).toBe(true);
+  expect(runtime.bridgeAvailable).toBe(false);
+});
+
+it("reports desktop-shell bridge failures without pretending the app is a browser", () => {
+  const state = createUnavailableState({ shellAvailable: true });
+  expect(state.desktopAvailable).toBe(true);
+  expect(state.transportError).toContain("native AI bridge");
+});

Expected Impact for End-Users

Validation

Passed bun test apps/web/app/desktop-ai.test.ts apps/web/app/routes.test.ts apps/web/app/terminal.test.ts

All targeted renderer tests passed, including the new desktop shell and bridge regression coverage.

Blocked By Existing Repo Issue bun --cwd=apps/web run build

The build still fails during shared package type-checking because packages/types/src/desktop-ai.ts imports sibling files with explicit .ts extensions. Follow-up issue: islandflow-c8f.

Issues, Limitations, and Mitigations

Follow-up Work