import type { GetWorkspaceCaptureResponse, SessionEvent, WorkspaceCaptureDegradedReason, WorkspaceCaptureManifest, WorkspaceRevisionCapturedPayload, WorkspaceRevisionDegradedPayload, } from "@opengeni/sdk"; import { useCallback, useEffect, useRef, useState } from "react"; import { useOpenGeni, type ClientOverride } from "../provider"; /** The announce event M1 emits at turn end (metadata only, never content). */ const REVISION_CAPTURED = "workspace.revision.captured"; const REVISION_DEGRADED = "workspace.revision.degraded"; const MANIFEST_FETCH_TIMEOUT_MS = 15_000; type AvailableCaptureResponse = Extract; const CAPTURE_STATS_NUMBER_FIELDS = [ "repoCount", "fileCount", "additions", "deletions", "totalBytes", "tooLargeCount", "binaryCount", "treeEntryCount", "durationMs", ] as const; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } /** * Signed manifests bypass the authenticated JSON client's wire decoder. Treat * the blob as untrusted until its outer schema and exact revision identity match * the metadata response that minted the URL. The API performs the full contract * parse; this client-side boundary prevents a corrupt/mis-keyed response from * being presented as a different authoritative capture. */ function captureManifestForResponse( value: unknown, response: AvailableCaptureResponse, ): WorkspaceCaptureManifest { if ( !isRecord(value) || value.version !== 1 || !Number.isSafeInteger(value.revision) || typeof value.capturedAt !== "string" || !(value.turnId === null || typeof value.turnId === "string") || !Number.isSafeInteger(value.leaseEpoch) || !isRecord(value.treeIndex) || typeof value.treeTruncated !== "boolean" || !Array.isArray(value.repos) || !Array.isArray(value.files) || !isRecord(value.stats) ) { throw new Error("Workspace capture manifest failed validation."); } if ( value.revision !== response.revision || value.capturedAt !== response.capturedAt || value.turnId !== response.turnId || value.leaseEpoch !== response.leaseEpoch ) { throw new Error("Workspace capture manifest identity did not match its response."); } for (const field of CAPTURE_STATS_NUMBER_FIELDS) { if (!Number.isSafeInteger(value.stats[field]) || value.stats[field] !== response.stats[field]) { throw new Error("Workspace capture manifest statistics did not match its response."); } } if ( typeof value.stats.treeTruncated !== "boolean" || value.stats.treeTruncated !== response.stats.treeTruncated || (value.stats.fingerprint ?? null) !== (response.stats.fingerprint ?? null) || value.repos.length !== response.stats.repoCount || value.files.length !== response.stats.fileCount || value.treeTruncated !== response.stats.treeTruncated ) { throw new Error("Workspace capture manifest statistics did not match its response."); } return value as WorkspaceCaptureManifest; } export type UseWorkspaceCaptureOptions = ClientOverride & { /** Live event log (usually `useSessionEvents().events`) — a * `workspace.revision.captured` for a NEWER revision refreshes the manifest. */ events?: SessionEvent[] | undefined; /** Hold off the mount fetch (e.g. the workbench panel is collapsed). Default true. */ enabled?: boolean | undefined; }; export type UseWorkspaceCaptureResult = { /** The resolved manifest (tree index + per-repo diffs + file refs), or null when * no capture exists (falls back to the live/wake path — status quo). */ capture: WorkspaceCaptureManifest | null; /** The loaded capture's monotonic revision, or null when unavailable. */ revision: number | null; /** When the loaded capture was taken (ISO), for the "as of