fix electron desktop ai runtime fallback detection

This commit is contained in:
dirtydishes 2026-05-20 20:11:37 -04:00
parent a54e847c8e
commit d15f96d7be
4 changed files with 390 additions and 47 deletions

View file

@ -36,6 +36,12 @@ type DesktopAiRuntime = {
bridge: DesktopAiBridge | null;
};
const BROWSER_RUNTIME: DesktopAiRuntime = {
shellAvailable: false,
bridgeAvailable: false,
bridge: null
};
declare global {
interface Window {
islandflowDesktopRuntime?: {
@ -105,6 +111,17 @@ export const resolveDesktopAiRuntime = (
};
};
export const resolveCurrentDesktopAiRuntime = (): DesktopAiRuntime =>
typeof window === "undefined" ? BROWSER_RUNTIME : resolveDesktopAiRuntime(window);
const isSameDesktopAiRuntime = (
left: DesktopAiRuntime,
right: DesktopAiRuntime
): boolean =>
left.shellAvailable === right.shellAvailable &&
left.bridgeAvailable === right.bridgeAvailable &&
left.bridge === right.bridge;
export const createUnavailableState = (runtime?: Partial<DesktopAiRuntime>): IslandflowAiState => {
const shellAvailable = Boolean(runtime?.shellAvailable || runtime?.bridgeAvailable);
const bridgeAvailable = Boolean(runtime?.bridgeAvailable);
@ -193,9 +210,11 @@ const rejectDesktopOnly = async (): Promise<never> => {
};
export function DesktopAiProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<IslandflowAiState>(() => createUnavailableState());
const [bridge, setBridge] = useState<DesktopAiBridge | null>(null);
const [shellAvailable, setShellAvailable] = useState(false);
const initialRuntime = resolveCurrentDesktopAiRuntime();
const [runtime, setRuntime] = useState<DesktopAiRuntime>(initialRuntime);
const [state, setState] = useState<IslandflowAiState>(() =>
createUnavailableState(initialRuntime)
);
useEffect(() => {
if (typeof window === "undefined") {
@ -203,91 +222,124 @@ export function DesktopAiProvider({ children }: { children: ReactNode }) {
}
let disposed = false;
let activeBridge: DesktopAiBridge | null = null;
let unsubscribe = () => {};
let pollTimer: number | null = null;
let attempts = 0;
const clearPendingPoll = () => {
if (pollTimer !== null) {
window.clearTimeout(pollTimer);
pollTimer = null;
}
};
const connectBridge = (runtime: DesktopAiRuntime): boolean => {
if (!runtime.bridge) {
return false;
const replaceRuntime = (nextRuntime: DesktopAiRuntime) => {
setRuntime((currentRuntime) =>
isSameDesktopAiRuntime(currentRuntime, nextRuntime) ? currentRuntime : nextRuntime
);
};
const attachBridge = (nextBridge: DesktopAiBridge | null, runtimeSnapshot: DesktopAiRuntime) => {
if (activeBridge === nextBridge) {
return;
}
setShellAvailable(runtime.shellAvailable);
setBridge(runtime.bridge);
void runtime.bridge.ai.getState().then(
unsubscribe();
unsubscribe = () => {};
activeBridge = nextBridge;
if (!nextBridge) {
return;
}
const bridgeAtAttach = nextBridge;
void bridgeAtAttach.ai.getState().then(
(nextState) => {
if (!disposed) {
if (!disposed && activeBridge === bridgeAtAttach) {
setState(nextState);
}
},
() => {
if (!disposed) {
setState(createUnavailableState(runtime));
if (!disposed && activeBridge === bridgeAtAttach) {
setState(createUnavailableState(runtimeSnapshot));
}
}
);
unsubscribe = runtime.bridge.ai.subscribe((nextState) => {
if (!disposed) {
unsubscribe = bridgeAtAttach.ai.subscribe((nextState) => {
if (!disposed && activeBridge === bridgeAtAttach) {
setState(nextState);
}
});
return true;
};
const syncRuntime = (): boolean => {
const runtime = resolveDesktopAiRuntime(window);
setShellAvailable(runtime.shellAvailable);
if (connectBridge(runtime)) {
const nextRuntime = resolveDesktopAiRuntime(window);
replaceRuntime(nextRuntime);
if (nextRuntime.bridge) {
attachBridge(nextRuntime.bridge, nextRuntime);
return true;
}
setBridge(null);
setState(createUnavailableState(runtime));
attachBridge(null, nextRuntime);
setState(createUnavailableState(nextRuntime));
return false;
};
if (!syncRuntime()) {
const pollForBridge = () => {
if (disposed) {
return;
}
attempts += 1;
if (syncRuntime() || attempts >= BRIDGE_POLL_MAX_ATTEMPTS) {
return;
}
pollTimer = window.setTimeout(pollForBridge, BRIDGE_POLL_INTERVAL_MS);
};
const pollForBridge = () => {
if (disposed) {
return;
}
attempts += 1;
if (syncRuntime() || attempts >= BRIDGE_POLL_MAX_ATTEMPTS) {
clearPendingPoll();
return;
}
pollTimer = window.setTimeout(pollForBridge, BRIDGE_POLL_INTERVAL_MS);
};
const startRetryWindow = () => {
attempts = 0;
clearPendingPoll();
pollTimer = window.setTimeout(pollForBridge, BRIDGE_POLL_INTERVAL_MS);
};
const onWindowLifecycle = () => {
if (!syncRuntime()) {
startRetryWindow();
}
};
window.addEventListener("focus", onWindowLifecycle);
window.addEventListener("pageshow", onWindowLifecycle);
if (!syncRuntime()) {
startRetryWindow();
}
return () => {
disposed = true;
if (pollTimer !== null) {
window.clearTimeout(pollTimer);
}
clearPendingPoll();
window.removeEventListener("focus", onWindowLifecycle);
window.removeEventListener("pageshow", onWindowLifecycle);
unsubscribe();
};
}, []);
const value = useMemo<DesktopAiContextValue>(
() => ({
bridgeAvailable: Boolean(bridge?.ai),
shellAvailable,
bridgeAvailable: runtime.bridgeAvailable,
shellAvailable: runtime.shellAvailable,
state,
loginWithBrowser: bridge?.ai.loginWithBrowser ?? rejectDesktopOnly,
loginWithDeviceCode: bridge?.ai.loginWithDeviceCode ?? rejectDesktopOnly,
cancelLogin: bridge?.ai.cancelLogin ?? rejectDesktopOnly,
logout: bridge?.ai.logout ?? rejectDesktopOnly,
updatePreferences: bridge?.ai.updatePreferences ?? rejectDesktopOnly,
runTask: bridge?.ai.runTask ?? rejectDesktopOnly,
cancelTask: bridge?.ai.cancelTask ?? rejectDesktopOnly
loginWithBrowser: runtime.bridge?.ai.loginWithBrowser ?? rejectDesktopOnly,
loginWithDeviceCode: runtime.bridge?.ai.loginWithDeviceCode ?? rejectDesktopOnly,
cancelLogin: runtime.bridge?.ai.cancelLogin ?? rejectDesktopOnly,
logout: runtime.bridge?.ai.logout ?? rejectDesktopOnly,
updatePreferences: runtime.bridge?.ai.updatePreferences ?? rejectDesktopOnly,
runTask: runtime.bridge?.ai.runTask ?? rejectDesktopOnly,
cancelTask: runtime.bridge?.ai.cancelTask ?? rejectDesktopOnly
}),
[bridge, shellAvailable, state]
[runtime, state]
);
return <DesktopAiContext.Provider value={value}>{children}</DesktopAiContext.Provider>;