Turn Document · 2026-05-20 18:59 EDT

Clarify Desktop AI Settings Bridge State

The /settings desktop AI surface now explains why ChatGPT login and model controls are unavailable when a desktop window loses its native bridge, instead of looking like broken controls.

Issue islandflow-dy2
Primary Surface apps/web/app/desktop-ai-panels.tsx
User Outcome Actionable bridge recovery state

Summary

The settings page previously showed disabled ChatGPT login buttons and sparse model controls whenever the Electron shell was open but its native AI bridge was missing. That looked indistinguishable from broken UI. The fix makes the unavailable state explicit, swaps the dead login affordance for a recovery action, and gives the model area state-aware empty copy.

Changes Made

  • Added pure helper functions that centralize settings copy for desktop-only, bridge-missing, and logged-out model states.
  • Changed the account panel so a missing bridge shows a prominent warning callout and a Reload window action instead of inert login buttons.
  • Changed the profile slot badge so a selected managed login profile reports Bridge unavailable when the current desktop window cannot use it.
  • Added state-specific labels and empty-state copy for the model selector and model list so they explain whether the app is waiting on desktop, bridge recovery, or account login.
  • Added a warning callout style and unit coverage for the new copy-selection helpers.

Context

This work came directly from a user report against /settings: in a desktop session missing its native bridge, the login controls did not function and the model dropdown area appeared blank or broken.

Why this mattered

The underlying bridge failure was already being detected. The real problem was that the UI expressed that failure as disabled controls without enough explanation, which created the impression that auth itself was broken.

Important Implementation Details

  • The fix stays in the web renderer layer and does not alter Electron auth or preference persistence behavior.
  • updatePreferences still remains bridge-backed and login-independent, so the change focuses on clearer messaging rather than adding new backend gating.
  • The account panel now derives a bridge notice from shellAvailable and bridgeAvailable, which keeps the browser-only and missing-bridge states distinct.
  • The model area uses the same availability helper family, so the disabled select label and the list empty-state copy stay aligned.
  • The UI now prefers truthful status language over optimistic language. A selected profile is no longer presented as fully usable when the active window cannot reach the native bridge.

Expected Impact for End-Users

  • Users on a broken bridge session immediately see that the problem is window connectivity, not a silent ChatGPT login failure.
  • Users get a local recovery path from the settings page through the new Reload window action.
  • Model controls no longer appear mysteriously empty. They explain whether the app is waiting for bridge recovery or for account login.
  • The managed profile card now reflects actual usability in the current window, which reduces false confidence.

Relevant Diff Snippets

These snippets are formatted as unified patches so they can be consumed by Diffs’ parsePatchFiles or PatchDiff flows from diffs.com/docs.

Settings state helpers and bridge recovery action
diff --git a/apps/web/app/desktop-ai-panels.tsx b/apps/web/app/desktop-ai-panels.tsx
@@
+export const getDesktopAiSettingsBridgeNotice = (shellAvailable, bridgeAvailable) => {
+  if (!shellAvailable) {
+    return {
+      title: "Desktop app required",
+      body: "Open Islandflow Desktop to connect ChatGPT, load managed models, and use native Copilot controls."
+    };
+  }
+  if (!bridgeAvailable) {
+    return {
+      title: "Bridge unavailable in this window",
+      body: "This Islandflow Desktop window is missing its native AI bridge, so login actions and model controls stay disabled until the bridge reconnects. Reload the window or restart Islandflow if this keeps happening."
+    };
+  }
+  return null;
+};
@@
-              <button className="terminal-button terminal-button-primary">Browser login</button>
-              <button className="terminal-button">Device code</button>
+              <button
+                className="terminal-button terminal-button-primary"
+                type="button"
+                onClick={() => window.location.reload()}
+              >
+                Reload window
+              </button>
@@
+      {bridgeNotice ? (
+        <div className="copilot-callout copilot-callout-warning">
+          <strong>{bridgeNotice.title}</strong>
+          <p className="copilot-note">{bridgeNotice.body}</p>
+        </div>
+      ) : null}
Model controls copy and empty states
diff --git a/apps/web/app/desktop-ai-panels.tsx b/apps/web/app/desktop-ai-panels.tsx
@@
+const modelSelectLabel = getDesktopAiModelSelectLabel(
+  shellAvailable,
+  bridgeAvailable,
+  state.account.loggedIn,
+  state.models.length
+);
+const modelListEmptyCopy = getDesktopAiModelListEmptyCopy(
+  shellAvailable,
+  bridgeAvailable,
+  state.account.loggedIn
+);
@@
-                <option value="">Use server default</option>
+                <option value="">{modelSelectLabel}</option>
@@
-            {state.models.map((model) => (
-              <div className="copilot-model-row" key={model.id}>...</div>
-            ))}
+            {state.models.length === 0 ? (
+              <p className="copilot-empty">{modelListEmptyCopy}</p>
+            ) : (
+              state.models.map((model) => (
+                <div className="copilot-model-row" key={model.id}>...</div>
+              ))
+            )}
Unit coverage and warning presentation
diff --git a/apps/web/app/desktop-ai.test.ts b/apps/web/app/desktop-ai.test.ts
@@
+describe("desktop ai settings copy", () => {
+  it("explains when the native bridge is missing from the desktop window", () => {
+    expect(getDesktopAiSettingsBridgeNotice(true, false)?.title).toBe(
+      "Bridge unavailable in this window"
+    );
+  });
+
+  it("keeps the model selector explicit while the bridge is disconnected", () => {
+    expect(getDesktopAiModelSelectLabel(true, false, false, 0)).toBe("Bridge unavailable");
+    expect(getDesktopAiModelListEmptyCopy(true, false, false)).toContain(
+      "native AI bridge reconnects"
+    );
+  });
+});
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
@@
+.copilot-callout-warning {
+  border-color: oklch(0.74 0.08 68 / 0.42);
+  background: oklch(0.2 0.03 68 / 0.28);
+}

Validation

Automated bun test apps/web/app/desktop-ai.test.ts

Passed with 14 assertions groups green, including new coverage for bridge-state copy helpers.

Manual Electron app state inspection

Verified the live 127.0.0.1:3000/settings window now shows Reload window, Bridge unavailable, and the new model-controls explanatory copy.

Build bun --cwd=apps/web run build

Still fails on an existing repo-wide TypeScript import-extension problem in packages/types/src/desktop-ai.ts, unrelated to this change.

Issues, Limitations, and Mitigations

  • The fix does not repair the missing native bridge itself. It makes that failure mode explicit and recoverable from the page.
  • Reload window is a best-effort recovery action. If the bridge is absent because of a deeper shell startup issue, the user may still need to restart Islandflow.
  • The production web build could not serve as a final validation gate because of the pre-existing .ts-extension import issue already tracked elsewhere in Beads.

Follow-up Work

  • No new follow-up issue was required for this UX patch. The reported confusion is addressed in islandflow-dy2.
  • The repo-wide Next.js build blocker remains separate work, already represented by islandflow-c8f.
  • If bridge loss keeps happening in practice, the next useful step would be adding an in-app bridge diagnostics surface instead of relying on copy and window reload alone.

Changes at a Glance

settings UX hardening desktop bridge state managed auth recovery explicit model empty states unit test coverage