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”.
islandflow-199
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
- Added desktop runtime helpers in
apps/web/app/desktop-ai.tsxto detect Electron shell presence separately from bridge presence. - Changed the desktop AI provider to poll briefly for late bridge availability instead of giving up after the first missing-read.
- Reworked the provider’s unavailable/error state so desktop sessions show bridge-focused recovery guidance instead of browser-only copy.
- Updated settings and Copilot action panels in
apps/web/app/desktop-ai-panels.tsxto gate actions on bridge availability while only showing the browser fallback when the shell is genuinely absent. - Added regression coverage in
apps/web/app/desktop-ai.test.tsfor Electron detection, bridge-missing fallback state, and action helper copy. - Filed follow-up issue
islandflow-c8ffor the unrelated shared-types import-path problem currently blockingbun --cwd=apps/web run build.
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
- Shell detection: the renderer now treats an Electron user-agent or a working preload bridge as evidence that the desktop shell is present.
- Bridge recovery: the provider polls for up to 20 attempts at 250ms intervals so late preload exposure does not immediately collapse to browser fallback.
- Better state semantics: unavailable-state generation now accepts runtime context and can mark
desktopAvailabletrue while still reporting a bridge error. - UI gating: Settings and Copilot actions key off
bridgeAvailablefor actionable controls, while the “Desktop required” banner keys offshellAvailable. - Recovery copy: action helper text now distinguishes between “open the desktop app”, “reload or restart because the native bridge is missing”, and “connect a ChatGPT or Codex account”.
Relevant Diff Snippets
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);
+ }
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.";
+ }
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
- Desktop users no longer get told to “open Islandflow Desktop” when they are already in it.
- If the preload bridge is delayed, the renderer now has a short recovery window before giving up.
- If the bridge is missing or broken, the UI explains that exact condition and suggests a sensible recovery path.
- Copilot action text now distinguishes between missing desktop shell, missing bridge, and missing account login.
Validation
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.
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
- The provider’s retry window is intentionally short. It helps with delayed bridge injection, but it does not try to hide a truly broken preload indefinitely.
- The first client paint can still momentarily start from the generic unavailable state before the effect resolves runtime details. The mitigation is that the resolved UI now lands on an accurate desktop-shell state instead of a misleading browser fallback.
- The production web build remains red for an unrelated shared-types import-path issue. That blocker is explicitly tracked in
islandflow-c8fso it does not disappear into session history.
Follow-up Work
islandflow-c8f: remove or normalize explicit.tsimport specifiers inpackages/typesso the Next.js production build can complete.- Optionally add desktop-shell telemetry to the topbar summary so bridge-missing sessions are visible outside Settings too.
- If bridge timing remains flaky in practice, consider emitting a preload-ready event instead of relying only on a short polling window.