import type { FileSystemRouteIdentity, FsReadResponse, FsWriteResponse, OpenGeniRequestOptions, SessionCapabilities, SessionEvent, WorkspaceCaptureManifest } from "@opengeni/sdk"; import { type ClientOverride } from "../provider.js"; import { type OpenGeniText } from "../messages/runtime.js"; /** The git-status overlay a file row may carry (tints modified files in the tree). */ export type FileTreeStatus = "added" | "modified" | "deleted" | "renamed" | "untracked"; export type CapturedFileUnavailableReason = "not-captured" | "too-large" | "content-missing"; /** A cold capture can index a path without retaining its contents. Components * use this typed result to offer an explicit wake action instead of silently * turning passive review into a live sandbox read. */ export declare class CapturedFileUnavailableError extends Error { readonly path: string; readonly reason: CapturedFileUnavailableReason; readonly code = "captured_file_unavailable"; constructor(path: string, reason: CapturedFileUnavailableReason, t?: OpenGeniText); } export declare class FileWriteConflictError extends Error { readonly path: string; readonly expectedContent: string; readonly liveContent: string; readonly code = "file_write_conflict"; constructor(path: string, expectedContent: string, liveContent: string, t?: OpenGeniText); } export type SandboxWriteFileOptions = { /** Exact text loaded into the editor. The live file is re-read before write; * divergence fails closed with FileWriteConflictError. */ expectedContent?: string | undefined; /** Deliberately bypass the expected-content guard after a visible conflict. */ force?: boolean | undefined; }; /** A node in the Pierre file tree. `children === undefined` ⇒ an unexpanded dir * (lazy treeMode); `children: []` ⇒ an expanded-but-empty dir. */ export type FileTreeNode = { /** Opaque path returned by the selected target's FileSystem boundary. */ path: string; name: string; kind: "file" | "dir"; children?: FileTreeNode[] | undefined; size?: number | null | undefined; status?: FileTreeStatus | undefined; /** A residue dir the capture COLLAPSED (node_modules, .git, dist, …): its * contents were never indexed. Cold, expanding it can't list children — the * UI shows an inline "contents on machine" row until the box is warm. */ truncated?: boolean | undefined; }; export type UseSandboxFilesOptions = ClientOverride & { /** Live event log (usually `useSessionEvents().events`) — drives auto-refresh * on `fs.changed` / `git.changed`. */ events?: SessionEvent[] | undefined; /** Initial path to list (workspace root by default). */ rootPath?: string | undefined; /** Exact capability identity for the selected filesystem route. A route/root * change resets this hook and makes stale provider requests fail retryably. */ route?: FileSystemRouteIdentity | undefined; /** Hold off the initial list (e.g. panel collapsed). Default true. */ enabled?: boolean | undefined; /** Pause live reads/invalidation while preserving the last rendered result. */ active?: boolean | undefined; /** Repository roots advertised by capabilities, relative to the workspace. */ repoPaths?: readonly string[] | undefined; /** The lease liveness. `null` means capability negotiation has not produced * an authoritative lifecycle value yet and therefore blocks live I/O. Omitting * the option preserves the legacy direct-hook behavior. The structured FileSystem * capability is advertised even on a COLD box, so the mount-time list can race * the box: it lists before the box is warm, gets an empty/errored result, and * (with no `fs.changed` event) never re-lists. Passing liveness re-lists when * the box first becomes warm, so the tree populates as soon as the box is up. */ liveness?: SessionCapabilities["liveness"] | null | undefined; /** The latest turn-end workspace capture (from `useWorkspaceCapture`). The tree * paints immediately from this durable index, then a warm box reconciles live * in place. If that live read fails, the capture remains a truthful read-only * fallback instead of turning into an empty error surface. */ capture?: WorkspaceCaptureManifest | null | undefined; /** Called when an OPTIMISTIC mutation is reverted because its background * Channel-A op failed (e.g. a 409 rename collision). The host wires this to a * toast — the tree silently rolls the node back, the user sees why. */ onMutationError?: ((error: Error, op: string) => void) | undefined; }; export type UseSandboxFilesResult = { /** The tree roots (the listed root's children). */ tree: FileTreeNode[]; /** Lazy-expand a directory node in place (lists its immediate children). */ expand: (path: string) => Promise; /** Paths whose lazy `fs.list` is currently in flight — the FileBrowser shows a * spinner on these nodes so a 2-3s Channel-A list never looks frozen. */ expandingPaths: Set; /** Read a file for the preview pane (text or base64-for-binary, size-capped). */ readFile: (path: string, options?: OpenGeniRequestOptions) => Promise; /** Write a file (overwrite, last-writer-wins) — the editor save path. * Optimistic: a brand-new file is spliced into the tree immediately and the * Channel-A write runs in the background; on failure the splice is reverted. */ writeFile: (path: string, content: string, options?: SandboxWriteFileOptions) => Promise; /** Create a new empty file (refuses to clobber an existing path: overwrite=false). */ createFile: (path: string) => Promise; /** Create a directory (recursive by default). */ createDir: (path: string) => Promise; /** Delete a path (pass recursive=true for a non-empty directory). */ deleteEntry: (path: string, recursive?: boolean) => Promise; /** Move / rename a path (rename == move). Refuses to clobber unless overwrite=true. */ moveEntry: (path: string, newPath: string, opts?: { overwrite?: boolean; }) => Promise; /** Re-list the whole tree from the root. */ refresh: () => Promise; /** Monotonic invalidation token for open file previews. Advances when an * external filesystem change or completed agent tool may have changed bytes. */ contentRevision?: number | undefined; /** Which source the tree is currently served from: the live box, the turn-end * capture (cold/offline), or neither yet. M5's source badge reads this. */ source: "live" | "capture" | null; /** When the served capture was taken (ISO), when `source === "capture"`. */ capturedAt: string | null; /** Lightweight repository context loaded by the same status request that tints files. */ gitSummary?: SandboxFilesGitSummary | null | undefined; /** True only while the initial repository-status overlay is still loading. */ gitLoading?: boolean | undefined; loading: boolean; error: Error | null; }; export type SandboxFilesGitSummary = { branch: string | null; isRepo: boolean; repoCount: number; ahead: number; behind: number; dirtyCount: number; }; /** * Project the FileSystem service into a lazy-loaded Pierre tree. The initial * list pulls one level (depth 1); `expand(path)` lists a directory's immediate * children on demand (the fast lazy-tree UX). A git-status overlay tints * modified files. Auto-refreshes when an `fs.changed` / `git.changed` event * arrives on the live log. */ export declare function useSandboxFiles(sessionId: string | null | undefined, options?: UseSandboxFilesOptions): UseSandboxFilesResult;