import { BrowserWindow, ipcMain } from 'electron' import type { OrcaRuntimeService } from '../runtime/orca-runtime' import type { RuntimeBrowserDriverState, RuntimeStatus, RuntimeSyncWindowGraphResult, RuntimeSyncWindowGraph, RuntimeTerminalDriverState } from '../../shared/runtime-types' import type { RuntimeRpcResponse } from '../../shared/runtime-rpc-envelope' import { TERMINAL_FIT_RESTORE_DEADLINE_MS } from '../../shared/terminal-fit-restore-deadline' import { RpcDispatcher } from '../runtime/rpc/dispatcher' import { ALL_RPC_METHODS } from '../runtime/rpc/methods' function boundTerminalFitRestore(pending: Promise): Promise { let timer: ReturnType | undefined const deadline = new Promise((resolve) => { timer = setTimeout(() => resolve(false), TERMINAL_FIT_RESTORE_DEADLINE_MS) timer.unref?.() }) return Promise.race([pending, deadline]).finally(() => clearTimeout(timer)) } export function registerRuntimeHandlers(runtime: OrcaRuntimeService): void { const pendingTerminalFitRestores = new Map>() ipcMain.removeHandler('runtime:syncWindowGraph') ipcMain.removeHandler('runtime:getStatus') ipcMain.removeHandler('runtime:call') ipcMain.handle( 'runtime:syncWindowGraph', (event, graph: RuntimeSyncWindowGraph): RuntimeSyncWindowGraphResult => { const window = BrowserWindow.fromWebContents(event.sender) if (!window) { throw new Error('Runtime graph sync must originate from a BrowserWindow') } return runtime.syncWindowGraph(window.id, graph) } ) ipcMain.handle('runtime:getStatus', (): RuntimeStatus => { return runtime.getStatus() }) ipcMain.handle( 'runtime:call', async ( _event, args: { method: string; params?: unknown } ): Promise> => { return (await new RpcDispatcher({ runtime, methods: ALL_RPC_METHODS }).dispatch({ id: 'desktop-ipc', authToken: 'desktop-ipc', method: args.method, params: args.params })) as RuntimeRpcResponse } ) ipcMain.removeHandler('runtime:getTerminalFitOverrides') ipcMain.handle( 'runtime:getTerminalFitOverrides', (): { ptyId: string mode: 'mobile-fit' | 'remote-desktop-fit' cols: number rows: number }[] => { const overrides = runtime.getAllTerminalFitOverrides() return Array.from(overrides.entries()).map(([ptyId, override]) => ({ ptyId, ...override })) } ) ipcMain.removeHandler('runtime:getTerminalDrivers') ipcMain.handle( 'runtime:getTerminalDrivers', (): { ptyId: string; driver: RuntimeTerminalDriverState }[] => { const drivers = runtime.getAllTerminalDrivers() return Array.from(drivers.entries()).map(([ptyId, driver]) => ({ ptyId, driver })) } ) ipcMain.removeHandler('runtime:getBrowserDrivers') ipcMain.handle( 'runtime:getBrowserDrivers', (): { browserPageId: string; driver: RuntimeBrowserDriverState }[] => { const drivers = runtime.getAllBrowserDrivers() return Array.from(drivers.entries()).map(([browserPageId, driver]) => ({ browserPageId, driver })) } ) // Why: the desktop "Restore" button sets the display mode to 'desktop' and // applies it, which restores the PTY to its original dimensions and emits // a 'resized' event to any active mobile subscriber. This uses the same // code path as the mobile toggle button (terminal.setDisplayMode RPC). ipcMain.removeHandler('runtime:restoreTerminalFit') ipcMain.handle('runtime:restoreTerminalFit', async (_event, args: { ptyId: string }) => { // Why: this IPC powers the desktop "Take back" button. Beyond restoring // PTY dims (the original semantic), it now also reclaims the input // floor for the desktop via the driver state machine. The lock banner // unmounts and desktop input/resize are unblocked until the next // mobile interaction takes the floor again. See // docs/mobile-presence-lock.md. // // Why async: reclaimTerminalForDesktop awaits applyMobileDisplayMode's // PTY-resize chain. Returning the unresolved Promise to ipcMain made // Electron try to structured-clone a Promise — "An object could not // be cloned" error — and the renderer's restoreTerminalFit() rejected // with no useful info. // Why: keep one underlying reclaim per PTY even after callers time out; // layout serialization means a retry cannot bypass the wedged operation. let pending = pendingTerminalFitRestores.get(args.ptyId) if (!pending) { try { let tracked!: Promise const clearTrackedRestore = (): void => { if (pendingTerminalFitRestores.get(args.ptyId) === tracked) { pendingTerminalFitRestores.delete(args.ptyId) } } tracked = runtime.reclaimTerminalForDesktop(args.ptyId).then( (restored) => { clearTrackedRestore() return restored }, () => { clearTrackedRestore() return false } ) pending = tracked pendingTerminalFitRestores.set(args.ptyId, pending) } catch { return { restored: false } } } return { restored: await boundTerminalFitRestore(pending) } }) ipcMain.removeHandler('runtime:reclaimBrowserForDesktop') ipcMain.handle( 'runtime:reclaimBrowserForDesktop', (_event, args: { browserPageId: string }): { reclaimed: boolean } => { try { return { reclaimed: runtime.reclaimBrowserForDesktop(args.browserPageId) } } catch { return { reclaimed: false } } } ) }