import { invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; /** * Remote sessions: a profile opened on a leased Windows, macOS or Linux host. * * The desktop holds no part of the session's life. It asks the backend to * start or stop one and is told what happened; the hosts, the session cap, * the profile lock and the billing all live server-side. */ /** * `provisioning` -> `ready` -> `live` -> `closed`, plus `error` for a session * that failed on the fleet. * * Kept open rather than a closed union: the state machine is the server's, and * a state added there must render as itself instead of failing to decode. */ export type RemoteSessionPhase = string; export interface RemoteSessionState { session_id: string; profile_id?: string | null; platform?: string | null; /** Named to match the field the API returns. */ state: RemoteSessionPhase; /** The relay is up, so the session can actually be driven. */ cdp_ready: boolean; /** `interactive` or `cookie_bot`. */ kind?: string | null; /** Set when the session belongs to a cookie-bot run. */ run_id?: string | null; /** The team the hours are attributed to. */ team_id?: string | null; started_at?: string | null; /** When it finished. One timestamp, not a ready/closed pair. */ ended_at?: string | null; /** Why it ended, e.g. `stopped_by_user`, `max_duration`. */ close_reason?: string | null; /** What it has cost so far — a running figure while it is live. */ billed_seconds?: number | null; } export interface RemoteSessionEnded { session_id: string; status: string; billed_seconds: number; } /** * Why a profile cannot be opened on this computer right now. * * - `running`: a browser is open on the fleet holding this profile. * - `pending_sync`: a session has finished and what it wrote is still being * pulled down. Opening the local copy now would make the local files look * newer than the host's push, and the next sync would then upload the stale * copy over the session's work and delete the rest of it. * * Both states are temporary and neither is an error. */ export type RemoteHandoffState = "running" | "pending_sync"; /** * States a session cannot leave under its own steam. * * `error` is terminal just as `closed` is, and there is never a `closed` frame * after one — the server keeps a fleet-reported failure visible rather than * flattening it into "finished". A consumer that only watched for `closed` * would leave a failed session pinned in its live list for ever, and the * profile would look busy until the app restarted. */ export function isSessionOver(session: RemoteSessionState): boolean { return session.state === "closed" || session.state === "error"; } /** Everything the caller owns, pushed once when the stream connects. */ export interface RemoteSessionSnapshot { sessions: RemoteSessionState[]; } /** Whether the desktop is currently receiving transitions. */ export interface RemoteSessionStreamStatus { connected: boolean; reason?: string | null; } /** * Tauri event names. A transition arrives here rather than being polled for: * `POST /api/remote-sessions` answers `provisioning` and nothing more, so * without the stream the desktop is blind between launch and stop. */ export const REMOTE_SESSION_EVENTS = { /** One session changed. Payload: `RemoteSessionState`. */ state: "remote-session-state", /** Connect snapshot. Payload: `RemoteSessionSnapshot`. */ snapshot: "remote-session-snapshot", /** Stream connectivity. Payload: `RemoteSessionStreamStatus`. */ stream: "remote-session-stream", /** * The set of profiles that cannot be launched locally changed. * Payload: `Record`. */ handoff: "remote-handoff-changed", } as const; /** Which profiles are blocked from launching locally, and why. */ export function getRemoteHandoffStates(): Promise< Record > { return invoke>( "get_remote_handoff_states", ); } export function onRemoteHandoffChanged( handler: (states: Record) => void, ): Promise { return listen>( REMOTE_SESSION_EVENTS.handoff, (event) => handler(event.payload), ); } export function listRemoteSessions(): Promise { return invoke("list_remote_sessions"); } export function getRemoteSession( sessionId: string, ): Promise { return invoke("get_remote_session", { sessionId }); } export function stopRemoteSession( sessionId: string, ): Promise { return invoke("stop_remote_session", { sessionId }); } /** Subscribe to transitions. Idempotent; call once the user is signed in. */ export function startRemoteSessionEvents(): Promise { return invoke("start_remote_session_events"); } /** Unsubscribe. Call on sign-out. */ export function stopRemoteSessionEvents(): Promise { return invoke("stop_remote_session_events"); } /** Whether the subscriber is alive, for a UI that mounted after it started. */ export function getRemoteSessionEventsStatus(): Promise { return invoke("get_remote_session_events_status"); } export function onRemoteSessionState( handler: (session: RemoteSessionState) => void, ): Promise { return listen(REMOTE_SESSION_EVENTS.state, (event) => handler(event.payload), ); } export function onRemoteSessionSnapshot( handler: (snapshot: RemoteSessionSnapshot) => void, ): Promise { return listen( REMOTE_SESSION_EVENTS.snapshot, (event) => handler(event.payload), ); } export function onRemoteSessionStream( handler: (status: RemoteSessionStreamStatus) => void, ): Promise { return listen( REMOTE_SESSION_EVENTS.stream, (event) => handler(event.payload), ); }