fix desktop preload bridge loading

This commit is contained in:
dirtydishes 2026-05-20 19:20:25 -04:00
parent 17b030f01f
commit ebdc4ab8e6
3 changed files with 334 additions and 23 deletions

View file

@ -1,3 +1,4 @@
{"_type":"issue","id":"islandflow-hj3","title":"Fix Electron preload for desktop AI bridge","description":"## Why\\nThe desktop settings page reports the native AI bridge as unavailable because Electron fails to load the preload script in local dev.\\n\\n## What\\nUpdate the desktop preload implementation/build so Electron can execute it, restore window.islandflowDesktop, and verify the Copilot settings panel detects the bridge again.\\n\\n## Acceptance Criteria\\n- Electron no longer logs a preload syntax error\\n- window.islandflowDesktop is available in the desktop renderer\\n- The settings page no longer shows bridge unavailable solely because preload failed\\n- Relevant desktop/web tests pass","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T23:16:39Z","created_by":"dirtydishes","updated_at":"2026-05-20T23:20:20Z","started_at":"2026-05-20T23:16:48Z","closed_at":"2026-05-20T23:20:20Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-199","title":"fix desktop copilot fallback inside electron","description":"## Why\\nThe settings page can render the browser-only fallback even when Islandflow is running inside the Electron desktop shell.\\n\\n## What\\nSeparate desktop-shell detection from desktop AI transport state, make the provider recover if the bridge appears late or initial state loading fails, and cover the regression with tests.\\n\\n## Acceptance Criteria\\n- The desktop shell no longer shows the browser-only fallback solely because initial bridge state failed or arrived late\\n- Desktop-only actions can distinguish between missing Electron bridge and transport/auth problems\\n- Automated tests cover the recovery behavior","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T22:30:16Z","created_by":"dirtydishes","updated_at":"2026-05-20T22:37:21Z","started_at":"2026-05-20T22:30:23Z","closed_at":"2026-05-20T22:37:21Z","close_reason":"Fixed desktop-shell Copilot fallback handling, added bridge recovery logic, updated desktop-vs-bridge UI messaging, and added regression tests. Follow-up tracked in islandflow-c8f for unrelated web build blocker.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-yza","title":"Persist historical flow packets for alert detail replay","description":"## Why\nAlert details can show a missing persisted flow packet when the packet is no longer present in the Redis hot cache, even though the associated historical alert and evidence were loaded from ClickHouse.\n\n## What needs to be done\nTrace the API path that resolves alert detail flow packets, compare Redis hot-cache lookups with ClickHouse historical fetches, and ensure historical flow packet payloads are treated as first-class persisted data with context preserved when replaying or loading older alerts.\n\n## Acceptance Criteria\n- Alert detail flow packets load for historical alerts even when the packet is absent from Redis hot cache\n- Historical ClickHouse-backed flow packet responses preserve the context required by the UI\n- Relevant automated tests cover the regression or the gap is explicitly documented","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-20T06:52:04Z","created_by":"dirtydishes","updated_at":"2026-05-20T06:59:26Z","started_at":"2026-05-20T06:52:09Z","closed_at":"2026-05-20T06:59:26Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-jor","title":"Support Forgejo pull request status in desktop git panel","description":"The desktop app currently reports pull request status unavailable when a repository only has a Forgejo remote. Add native Forgejo/Gitea-style remote detection and pull request status lookup so Forgejo-only repositories can show PR state in the Codex app git panel.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-19T20:55:15Z","created_by":"dirtydishes","updated_at":"2026-05-19T20:59:46Z","started_at":"2026-05-19T20:55:25Z","closed_at":"2026-05-19T20:59:46Z","close_reason":"Patched the installed Codex desktop app bundle with a Forgejo PR status fallback and documented the local change.","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -1,34 +1,34 @@
import { contextBridge, ipcRenderer } from "electron";
import type {
IslandflowAiReasoningEffort,
IslandflowAiState,
IslandflowAiTaskRequest
} from "@islandflow/types";
import {
DESKTOP_AI_CANCEL_LOGIN,
DESKTOP_AI_GET_STATE,
DESKTOP_AI_LOGIN_BROWSER,
DESKTOP_AI_LOGIN_DEVICE,
DESKTOP_AI_LOGOUT,
DESKTOP_AI_RUN_TASK,
DESKTOP_AI_STATE_CHANNEL,
DESKTOP_AI_UPDATE_PREFERENCES
} from "./desktop-ai-ipc.js";
const { contextBridge, ipcRenderer } = require("electron");
const DESKTOP_AI_STATE_CHANNEL = "islandflow:desktop-ai:state";
const DESKTOP_AI_GET_STATE = "islandflow:desktop-ai:get-state";
const DESKTOP_AI_LOGIN_BROWSER = "islandflow:desktop-ai:login-browser";
const DESKTOP_AI_LOGIN_DEVICE = "islandflow:desktop-ai:login-device";
const DESKTOP_AI_CANCEL_LOGIN = "islandflow:desktop-ai:cancel-login";
const DESKTOP_AI_LOGOUT = "islandflow:desktop-ai:logout";
const DESKTOP_AI_UPDATE_PREFERENCES = "islandflow:desktop-ai:update-preferences";
const DESKTOP_AI_RUN_TASK = "islandflow:desktop-ai:run-task";
type DesktopAiState = any;
type DesktopAiTaskRequest = any;
type DesktopAiPreferenceUpdate = Partial<{
model: string | null;
reasoningEffort: string | null;
}>;
const bridge = {
ai: {
getState: (): Promise<IslandflowAiState> => ipcRenderer.invoke(DESKTOP_AI_GET_STATE),
getState: (): Promise<DesktopAiState> => ipcRenderer.invoke(DESKTOP_AI_GET_STATE),
loginWithBrowser: (): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_LOGIN_BROWSER),
loginWithDeviceCode: (): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_LOGIN_DEVICE),
cancelLogin: (): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_CANCEL_LOGIN),
logout: (): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_LOGOUT),
updatePreferences: (
next: Partial<{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }>
): Promise<void> => ipcRenderer.invoke(DESKTOP_AI_UPDATE_PREFERENCES, next),
runTask: (request: IslandflowAiTaskRequest): Promise<{ taskId: string }> =>
updatePreferences: (next: DesktopAiPreferenceUpdate): Promise<void> =>
ipcRenderer.invoke(DESKTOP_AI_UPDATE_PREFERENCES, next),
runTask: (request: DesktopAiTaskRequest): Promise<{ taskId: string }> =>
ipcRenderer.invoke(DESKTOP_AI_RUN_TASK, request),
subscribe: (listener: (state: IslandflowAiState) => void): (() => void) => {
const handler = (_event: Electron.IpcRendererEvent, state: IslandflowAiState) => {
subscribe: (listener: (state: DesktopAiState) => void): (() => void) => {
const handler = (_event: Electron.IpcRendererEvent, state: DesktopAiState) => {
listener(state);
};

View file

@ -0,0 +1,310 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>2026-05-20 · Fix Electron Preload Bridge</title>
<style>
:root {
color-scheme: dark;
--bg: oklch(0.11 0.01 250);
--panel: oklch(0.16 0.013 250 / 0.92);
--panel-2: oklch(0.14 0.012 250 / 0.9);
--text: oklch(0.93 0.014 250);
--muted: oklch(0.74 0.018 250);
--faint: oklch(0.6 0.016 250);
--line: oklch(0.75 0.014 250 / 0.14);
--accent: oklch(0.79 0.12 74);
--accent-soft: oklch(0.79 0.12 74 / 0.12);
--green-soft: oklch(0.75 0.12 151 / 0.12);
--red-soft: oklch(0.72 0.14 28 / 0.12);
--shadow: 0 24px 80px oklch(0.02 0.01 250 / 0.45);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
background:
radial-gradient(circle at top left, oklch(0.8 0.12 74 / 0.09), transparent 28%),
linear-gradient(180deg, oklch(0.15 0.012 250) 0%, oklch(0.1 0.01 250) 100%);
color: var(--text);
}
main {
width: min(1160px, calc(100vw - 40px));
margin: 0 auto;
padding: 36px 0 64px;
}
h1, h2, h3, .eyebrow, .meta, code, pre {
font-family: "IBM Plex Mono", ui-monospace, monospace;
}
h1, h2, h3 { margin: 0; }
p, li { color: var(--muted); line-height: 1.7; }
a { color: inherit; }
main > * + * { margin-top: 22px; }
.hero, section {
border: 1px solid var(--line);
border-radius: 24px;
box-shadow: var(--shadow);
}
.hero {
display: grid;
gap: 20px;
padding: 28px;
background:
linear-gradient(135deg, oklch(0.2 0.017 250 / 0.96), oklch(0.14 0.012 250 / 0.98)),
var(--panel);
}
.eyebrow {
margin: 0 0 10px;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 0.76rem;
}
.hero h1 {
font-size: clamp(2rem, 3.4vw, 3.5rem);
line-height: 0.96;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.hero-copy { max-width: 72ch; margin: 14px 0 0; }
.hero-grid, .two-col, .validation-grid {
display: grid;
gap: 18px;
}
.hero-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.two-col { grid-template-columns: minmax(0, 1.15fr) minmax(320px, 0.85fr); }
.validation-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.stat, .validation-card, .callout {
padding: 16px 18px;
border-radius: 16px;
border: 1px solid var(--line);
background: oklch(0.12 0.01 250 / 0.5);
}
.stat span, .validation-card span {
display: block;
margin-bottom: 8px;
color: var(--faint);
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.68rem;
}
.validation-card.good { background: var(--green-soft); }
.validation-card.warn { background: var(--red-soft); }
.callout { background: var(--accent-soft); }
section {
padding: 24px;
background: var(--panel-2);
}
section h2 {
margin-bottom: 14px;
font-size: 1rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
ul { margin: 0; padding-left: 20px; }
li + li { margin-top: 10px; }
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid oklch(0.8 0.12 74 / 0.28);
background: var(--accent-soft);
font-size: 0.74rem;
}
.meta { color: var(--faint); font-size: 0.82rem; }
pre.diff {
margin: 0;
padding: 16px 18px;
overflow: auto;
border-radius: 16px;
border: 1px solid var(--line);
background: oklch(0.08 0.008 250 / 0.95);
color: var(--text);
line-height: 1.5;
font-size: 0.78rem;
}
.diff-title {
margin-bottom: 10px;
color: var(--faint);
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
@media (max-width: 860px) {
main {
width: min(100vw - 24px, 1160px);
padding-top: 18px;
}
.hero-grid, .two-col, .validation-grid {
grid-template-columns: minmax(0, 1fr);
}
}
</style>
</head>
<body>
<main>
<article class="hero">
<div>
<p class="eyebrow">Repository Turn Document</p>
<h1>Fix Electron Preload Bridge</h1>
<p class="hero-copy">
Repaired the desktop ChatGPT login bridge by making the Electron preload
script emit a classic runnable script instead of ESM. Local desktop
settings now reconnect to the native bridge and show the managed
account, transport readiness, and model controls again.
</p>
</div>
<div class="hero-grid">
<div class="stat">
<span>Area</span>
<strong>Electron desktop shell</strong>
</div>
<div class="stat">
<span>Issue</span>
<strong>`islandflow-hj3`</strong>
</div>
<div class="stat">
<span>Validated</span>
<strong>Build, tests, live desktop restart</strong>
</div>
</div>
</article>
<section>
<h2>Summary</h2>
<p>
The desktop Copilot settings page was not failing because login was broken.
Electron was failing before the bridge could even load: the preload bundle
contained runtime `import` syntax, so the renderer never got
`window.islandflowDesktop`. After switching the preload implementation to
CommonJS-compatible runtime code, the local Electron shell resumed exposing
the bridge and the settings page recovered immediately after restart.
</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Rewrote the desktop preload runtime imports to use `require("electron")`.</li>
<li>Inlined the IPC channel names inside the preload file so no runtime module imports remain.</li>
<li>Removed the type-only module usage that caused TypeScript to emit a trailing `export {}`.</li>
<li>Verified the generated `apps/desktop/dist/preload.js` is now a plain script.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>
The UI state on `localhost:3000/settings` was already designed to distinguish
between “not in desktop” and “desktop shell detected but native bridge
missing.” In this case the latter message was accurate but incomplete: Electron
was present, yet the preload never executed because the emitted file was not
valid for the preload runtime. That left the renderer with an Electron user
agent but no bridge object.
</p>
</section>
<section class="two-col">
<div>
<h2>Important Implementation Details</h2>
<ul>
<li>Electron logged `Unable to load preload script` followed by `Cannot use import statement outside a module` during local startup.</li>
<li>The desktop main process configuration was already pointing at the correct preload path, so the failure was in emitted file format, not window wiring.</li>
<li>Keeping the preload free of runtime imports ensures Electron can execute it even though the desktop workspace is otherwise ESM-oriented.</li>
<li>The bridge API shape exposed to the web app did not change, so no renderer updates were required.</li>
</ul>
</div>
<div class="callout">
<h2>Relevant Runtime Signal</h2>
<p class="meta">Observed in Electron startup logs during reproduction</p>
<p>
`Unable to load preload script: .../dist/preload.js` and
`SyntaxError: Cannot use import statement outside a module`
were the decisive clues that isolated the problem to preload emission.
</p>
</div>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<div class="diff-title">apps/desktop/src/preload.ts</div>
<pre class="diff"><code class="language-diff">--- a/apps/desktop/src/preload.ts
+++ b/apps/desktop/src/preload.ts
@@
-import { contextBridge, ipcRenderer } from "electron";
-import { ... } from "./desktop-ai-ipc.js";
+const { contextBridge, ipcRenderer } = require("electron");
+const DESKTOP_AI_STATE_CHANNEL = "islandflow:desktop-ai:state";
+const DESKTOP_AI_GET_STATE = "islandflow:desktop-ai:get-state";
+const DESKTOP_AI_LOGIN_BROWSER = "islandflow:desktop-ai:login-browser";
+const DESKTOP_AI_LOGIN_DEVICE = "islandflow:desktop-ai:login-device";
+const DESKTOP_AI_CANCEL_LOGIN = "islandflow:desktop-ai:cancel-login";
+const DESKTOP_AI_LOGOUT = "islandflow:desktop-ai:logout";
+const DESKTOP_AI_UPDATE_PREFERENCES = "islandflow:desktop-ai:update-preferences";
+const DESKTOP_AI_RUN_TASK = "islandflow:desktop-ai:run-task";
@@
- updatePreferences: (
- next: Partial&lt;{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }&gt;
- ): Promise&lt;void&gt; =&gt; ipcRenderer.invoke(DESKTOP_AI_UPDATE_PREFERENCES, next),
+ updatePreferences: (next: DesktopAiPreferenceUpdate): Promise&lt;void&gt; =&gt;
+ ipcRenderer.invoke(DESKTOP_AI_UPDATE_PREFERENCES, next),
</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>
Local Islandflow Desktop sessions no longer get stuck in the misleading
“Bridge unavailable in this window” state when the actual problem is preload
execution. Users should see their managed ChatGPT session reconnect, transport
status move to <strong>Ready</strong>, and model controls become interactive
after launching or restarting the desktop shell.
</p>
</section>
<section>
<h2>Validation</h2>
<div class="validation-grid">
<div class="validation-card good">
<span>Desktop Build</span>
<strong>`bun --cwd=apps/desktop run build` succeeded and emitted a plain preload script.</strong>
</div>
<div class="validation-card good">
<span>Desktop Tests</span>
<strong>`bun --cwd=apps/desktop test` passed.</strong>
</div>
<div class="validation-card good">
<span>Web Tests</span>
<strong>`bun test apps/web/app/desktop-ai.test.ts` passed.</strong>
</div>
<div class="validation-card good">
<span>Manual Verification</span>
<strong>Restarted `bun run dev:desktop` and confirmed settings showed the connected account, ready transport, and populated model controls.</strong>
</div>
</div>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>The preload now duplicates the IPC channel strings instead of importing them. That is intentional to keep the emitted file import-free.</li>
<li>The desktop startup logs still show unrelated remote plugin 403 warnings from the managed app-server environment. They did not block bridge recovery in this turn.</li>
<li>The preload uses loose local types to avoid turning the file back into a runtime module. If preload complexity grows, a dedicated CommonJS-safe shared typing strategy may be worth adding later.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<div class="chip-row">
<span class="chip">No immediate follow-up required</span>
<span class="chip">Possible hardening: build-time preload output guard</span>
</div>
</section>
</main>
</body>
</html>