fix electron desktop ai runtime fallback detection
This commit is contained in:
parent
a54e847c8e
commit
d15f96d7be
4 changed files with 390 additions and 47 deletions
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue