"use client"; import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; import type { IslandflowAiReasoningEffort, IslandflowAiState, IslandflowAiTaskRequest } from "@islandflow/types"; type DesktopAiBridge = { ai: { getState: () => Promise; loginWithBrowser: () => Promise; loginWithDeviceCode: () => Promise; cancelLogin: () => Promise; logout: () => Promise; updatePreferences: ( next: Partial<{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }> ) => Promise; runTask: (request: IslandflowAiTaskRequest) => Promise<{ taskId: string }>; cancelTask: (taskId: string) => Promise; subscribe: (listener: (state: IslandflowAiState) => void) => () => void; }; }; type DesktopAiRuntime = { shellAvailable: boolean; bridgeAvailable: boolean; bridge: DesktopAiBridge | null; }; const BROWSER_RUNTIME: DesktopAiRuntime = { shellAvailable: false, bridgeAvailable: false, bridge: null }; declare global { interface Window { islandflowDesktopRuntime?: { app?: string | null; shell?: string | null; }; islandflowDesktop?: DesktopAiBridge; } } type DesktopAiContextValue = { bridgeAvailable: boolean; shellAvailable: boolean; state: IslandflowAiState; loginWithBrowser: () => Promise; loginWithDeviceCode: () => Promise; cancelLogin: () => Promise; logout: () => Promise; updatePreferences: ( next: Partial<{ model: string | null; reasoningEffort: IslandflowAiReasoningEffort | null }> ) => Promise; runTask: (request: IslandflowAiTaskRequest) => Promise<{ taskId: string }>; cancelTask: (taskId: string) => Promise; }; const BRIDGE_POLL_INTERVAL_MS = 250; const BRIDGE_POLL_MAX_ATTEMPTS = 20; const ELECTRON_USER_AGENT_PATTERN = /\b(?:Electron|IslandflowDesktop)\/\S+/i; export const detectDesktopShell = (userAgent: string | null | undefined): boolean => Boolean(userAgent && ELECTRON_USER_AGENT_PATTERN.test(userAgent)); export const detectDesktopRuntimeMarker = ( runtime: | { app?: string | null; shell?: string | null; } | null | undefined ): boolean => runtime?.app === "islandflow" && runtime.shell === "electron"; export const resolveDesktopAiRuntime = ( value: | { islandflowDesktopRuntime?: { app?: string | null; shell?: string | null; }; islandflowDesktop?: DesktopAiBridge; navigator?: { userAgent?: string | null }; } | null | undefined ): DesktopAiRuntime => { const bridge = value?.islandflowDesktop?.ai ? value.islandflowDesktop : null; const bridgeAvailable = Boolean(bridge?.ai); const shellAvailable = bridgeAvailable || detectDesktopRuntimeMarker(value?.islandflowDesktopRuntime) || detectDesktopShell(value?.navigator?.userAgent); return { shellAvailable, bridgeAvailable, bridge }; }; 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): IslandflowAiState => { const shellAvailable = Boolean(runtime?.shellAvailable || runtime?.bridgeAvailable); const bridgeAvailable = Boolean(runtime?.bridgeAvailable); const transportError = !shellAvailable ? "Desktop AI is only available inside the Islandflow Electron app." : bridgeAvailable ? "The desktop AI bridge loaded, but its initial state could not be read." : "Islandflow Desktop is open, but the native AI bridge is unavailable in this session."; const loginMessage = !shellAvailable ? "Open Islandflow Desktop to connect a ChatGPT or Codex account." : bridgeAvailable ? "The desktop bridge connected, but its initial state did not load. Retry the action or restart Islandflow if this persists." : "This desktop window is missing its native AI bridge. Reload the window or restart Islandflow if this persists."; return { desktopAvailable: shellAvailable, transportStatus: shellAvailable ? "error" : "stopped", transportError, profiles: [ { id: "managed-chatgpt", label: "Managed ChatGPT login", description: shellAvailable ? "Managed ChatGPT login belongs to the desktop shell, but this window is not connected to the native bridge yet." : "Available only in the desktop app.", mode: "managed-chatgpt", enabled: shellAvailable, selected: true, statusLabel: shellAvailable ? "Bridge unavailable" : "Desktop only" } ], selectedProfileId: "managed-chatgpt", account: { loggedIn: false, email: null, planType: null, authMode: null, requiresOpenaiAuth: true, login: { status: "idle", message: loginMessage } }, preferences: { model: null, reasoningEffort: "high" }, models: [], rateLimitsByLimitId: {}, usage: { today: { breakdown: { totalTokens: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0 }, normalizedCostUsd: 0, turnCount: 0, activeDays: 0 }, lifetime: { breakdown: { totalTokens: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0 }, normalizedCostUsd: 0, turnCount: 0, activeDays: 0 }, recentTurns: [] }, tasks: [], updatedAt: Date.now() }; }; const DesktopAiContext = createContext(null); const rejectDesktopOnly = async (): Promise => { throw new Error("Desktop AI is only available inside the Islandflow Electron app."); }; export function DesktopAiProvider({ children }: { children: ReactNode }) { const initialRuntime = resolveCurrentDesktopAiRuntime(); const [runtime, setRuntime] = useState(initialRuntime); const [state, setState] = useState(() => createUnavailableState(initialRuntime) ); useEffect(() => { if (typeof window === "undefined") { return; } 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 replaceRuntime = (nextRuntime: DesktopAiRuntime) => { setRuntime((currentRuntime) => isSameDesktopAiRuntime(currentRuntime, nextRuntime) ? currentRuntime : nextRuntime ); }; const attachBridge = (nextBridge: DesktopAiBridge | null, runtimeSnapshot: DesktopAiRuntime) => { if (activeBridge === nextBridge) { return; } unsubscribe(); unsubscribe = () => {}; activeBridge = nextBridge; if (!nextBridge) { return; } const bridgeAtAttach = nextBridge; void bridgeAtAttach.ai.getState().then( (nextState) => { if (!disposed && activeBridge === bridgeAtAttach) { setState(nextState); } }, () => { if (!disposed && activeBridge === bridgeAtAttach) { setState(createUnavailableState(runtimeSnapshot)); } } ); unsubscribe = bridgeAtAttach.ai.subscribe((nextState) => { if (!disposed && activeBridge === bridgeAtAttach) { setState(nextState); } }); }; const syncRuntime = (): boolean => { const nextRuntime = resolveDesktopAiRuntime(window); replaceRuntime(nextRuntime); if (nextRuntime.bridge) { attachBridge(nextRuntime.bridge, nextRuntime); return true; } attachBridge(null, nextRuntime); setState(createUnavailableState(nextRuntime)); return false; }; 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; clearPendingPoll(); window.removeEventListener("focus", onWindowLifecycle); window.removeEventListener("pageshow", onWindowLifecycle); unsubscribe(); }; }, []); const value = useMemo( () => ({ bridgeAvailable: runtime.bridgeAvailable, shellAvailable: runtime.shellAvailable, state, 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 }), [runtime, state] ); return {children}; } export const useDesktopAi = (): DesktopAiContextValue => { const value = useContext(DesktopAiContext); if (!value) { throw new Error("Desktop AI context missing"); } return value; };