import * as react from 'react'; import { ReactNode } from 'react'; /** Entry kind — regular file or directory. */ type FileKind = "file" | "dir"; /** * A single file-system entry. JSON-friendly by design so agents and remote * hosts can emit entries directly (over MCP, a relay, or a WebSocket). * `path` is the stable identity — POSIX-style, never an index. */ interface FileEntry { /** Stable identity — POSIX-style path (e.g. `"/src/App.tsx"`). */ path: string; /** Display name (usually the last path segment). */ name: string; /** `"file"` or `"dir"`. */ kind: FileKind; /** Size in bytes (optional; shown for files and used by size sorting). */ size?: number; /** Last-modified timestamp, ISO 8601 (optional; used by mtime sorting). */ mtime?: string; /** * Dirs only: `false` = known-empty (no expand affordance), `true` = has * children, `undefined` = unknown (expandable when a provider is present). */ hasChildren?: boolean; /** Disabled entries render dimmed and cannot be selected, expanded, or navigated into. */ disabled?: boolean; } /** * JSON-friendly snapshot node — a {@link FileEntry} plus optionally * materialized children. `children: undefined` on a dir = unknown depth (a * provider fills it lazily in hybrid mode); `children: []` = known-empty. */ interface FileSnapshotNode extends FileEntry { children?: FileSnapshotNode[]; } /** Async data source for provider mode. Works against local FS, HTTP, MCP bridges, SSH adapters — anything that resolves a listing. */ interface FileBrowserProvider { /** * Load the direct children of `path`. Called lazily — only for the current * directory and explicitly expanded folders. The component never walks the * tree eagerly. */ loadChildren: (path: string) => Promise; } /** Which entry kinds are selectable. */ type FileSelectMode = "file" | "directory" | "both"; type FileSortField = "name" | "size" | "mtime"; type FileSortDirection = "asc" | "desc"; /** Sort order for directory listings. Directories always sort before files. */ interface FileSort { by: FileSortField; direction: FileSortDirection; } /** Per-path load lifecycle for provider mode. */ type FileLoadStatus = "idle" | "loading" | "loaded" | "error"; interface FileBrowserProps { /** Async data source (provider mode). Folders load on first expand — never an eager walk. */ provider?: FileBrowserProvider; /** * JSON-friendly tree value (snapshot mode). Replace or patch it from outside * as stream chunks land — the component treats it as the source of truth for * every path it covers. May be combined with `provider` (hybrid): the * snapshot seeds, the provider fills unknown-depth folders. */ snapshot?: FileSnapshotNode[]; /** Which entry kinds are selectable (default `"file"`). Non-selectable entries stay browsable. */ select?: FileSelectMode; /** Allow selecting multiple entries; `value` becomes `string[]` (default `false`). */ multiple?: boolean; /** Controlled selection — a path, an array of paths (`multiple`), or `null`. */ value?: string | string[] | null; /** Initial selection (uncontrolled). */ defaultValue?: string | string[] | null; /** Called with the next selection and the matching known entries. */ onChange?: (value: string | string[] | null, entries: FileEntry[]) => void; /** Controlled current directory. */ path?: string; /** Initial current directory (uncontrolled, default `"/"`). */ defaultPath?: string; /** Called when the current directory changes (breadcrumb click, path input, double-clicked folder). */ onPathChange?: (path: string) => void; /** Controlled expanded folder paths. */ expandedPaths?: string[]; /** Initially expanded folder paths (uncontrolled). */ defaultExpandedPaths?: string[]; /** Called when the expanded set changes. */ onExpandedChange?: (paths: string[]) => void; /** Controlled sort order. */ sort?: FileSort; /** Initial sort order (uncontrolled, default `{ by: "name", direction: "asc" }`). */ defaultSort?: FileSort; /** Called when the sort order changes. */ onSortChange?: (sort: FileSort) => void; /** Controlled name filter — a client-side substring match over loaded nodes. */ filter?: string; /** Initial name filter (uncontrolled). */ defaultFilter?: string; /** Called when the name filter changes. */ onFilterChange?: (filter: string) => void; /** Called when a provider load rejects; the failed folder shows an inline error with a retry. */ onError?: (path: string, error: unknown) => void; /** * Create a folder. **Supplying this is the opt-in** — the toolbar renders a * "New folder" button only when it is present, so a visible button and a * working one cannot disagree. * * Receives the directory currently being viewed and the trimmed name, both * already validated against what is in that directory. Reject the promise to * surface a message on the form; in provider mode a resolved promise reloads * the directory so the folder appears. */ onCreateFolder?: (input: { parentPath: string; name: string; }) => void | Promise; /** Indent per nesting level in px (default `16`). */ indentSize?: number; /** Show file/folder icons (default `true`). */ showIcons?: boolean; /** Custom className for the outer shell. */ className?: string; /** * Custom layout. When omitted, renders the default * `` + `` + ``. */ children?: ReactNode; } /** A flattened, visible row — the keyboard-navigation order of the tree pane. */ interface FileBrowserRow { entry: FileEntry; depth: number; /** Whether the row shows an expand affordance. */ expandable: boolean; /** Whether the row is currently expanded. */ expanded: boolean; /** Path of the parent row, or `null` for top-level rows of the current directory. */ parentPath: string | null; } interface FileBrowserContextValue { /** Known children of a path (snapshot first, then provider cache), or `undefined` when not yet loaded. */ entriesFor: (path: string) => FileEntry[] | undefined; /** Children of a path after the name filter + sort are applied (empty when unknown). */ visibleChildrenFor: (path: string) => FileEntry[]; statusFor: (path: string) => FileLoadStatus; errorFor: (path: string) => string | undefined; /** Request a lazy load of a path's children (no-op without a provider or when already known/loading). */ loadPath: (path: string, options?: { reload?: boolean; }) => void; hasProvider: boolean; path: string; /** Change the current directory (also clears the name filter and resets roving focus). */ navigate: (path: string) => void; expandedPaths: string[]; toggleExpanded: (path: string) => void; select: FileSelectMode; multiple: boolean; selectedPaths: string[]; isSelected: (path: string) => boolean; isSelectable: (entry: FileEntry) => boolean; selectEntry: (entry: FileEntry) => void; sort: FileSort; setSort: (sort: FileSort) => void; filter: string; setFilter: (filter: string) => void; visibleRows: FileBrowserRow[]; focusedPath: string | null; setFocusedPath: (path: string | null) => void; /** The row that currently owns `tabIndex={0}`. */ tabFocusPath: string | null; focusRow: (path: string) => void; registerRow: (path: string, el: HTMLElement | null) => void; indentSize: number; showIcons: boolean; /** Present only when the host opted in. See {@link FileBrowserProps.onCreateFolder}. */ onCreateFolder?: (input: { parentPath: string; name: string; }) => void | Promise; } interface FileBrowserPathBarProps { /** Allow switching to the editable path input (default `true`). */ editable?: boolean; /** Placeholder for the path input. */ placeholder?: string; className?: string; } interface FileBrowserToolbarProps { /** Placeholder for the name filter input (default `"Filter"`). */ filterPlaceholder?: string; className?: string; } interface FileBrowserTreeProps { /** Accessible label for the tree (default `"Files"`). */ ariaLabel?: string; className?: string; } interface FileBrowserNodeProps { entry: FileEntry; depth: number; } /** * Breadcrumb trail over the current directory plus an editable path input — * type a POSIX-style path and press Enter to navigate (lazy-loading it in * provider mode). Escape or blur cancels the edit. */ declare function FileBrowserPathBar({ editable, placeholder, className, }: FileBrowserPathBarProps): react.JSX.Element; declare namespace FileBrowserPathBar { var displayName: string; } /** * Toolbar: client-side name filter (over loaded nodes only — never triggers * loads) plus a dirs-first sort control. Clicking the active sort field flips * its direction. */ declare function FileBrowserToolbar({ filterPlaceholder, className }: FileBrowserToolbarProps): react.JSX.Element; declare namespace FileBrowserToolbar { var displayName: string; } /** * The tree pane: ARIA `tree` semantics, roving tabindex, and full keyboard * navigation (arrows / Enter / Space / Home / End) over the flattened row * order. Lazy loads fire on expand only. */ declare function FileBrowserTree({ ariaLabel, className }: FileBrowserTreeProps): react.JSX.Element; declare namespace FileBrowserTree { var displayName: string; } declare function FileBrowserNode({ entry, depth }: FileBrowserNodeProps): react.JSX.Element; declare namespace FileBrowserNode { var displayName: string; } /** * FileBrowser — remote-capable file/folder browser + directory picker. * * Browses any file tree the host can describe — local FS, HTTP, SSH, or a * remote machine streaming snapshots — via two feeding modes that may be * combined: * * - **Provider mode (lazy pull):** pass `provider.loadChildren(path)`. Folders * load on first expand with per-node loading + error states; the tree is * never walked eagerly. * - **Snapshot mode (streamed push):** pass a JSON-friendly `snapshot` tree * and replace/patch it as chunks arrive (relay / WebSocket / MCP). The * snapshot is the source of truth for every path it covers; with a provider * also present, unknown-depth folders stay lazily loadable (hybrid). * * Fully controlled per the Human+ component contract (`value`/`onChange`, * `path`/`onPathChange`, `expandedPaths`/`onExpandedChange`, plus * `sort`/`filter`), with uncontrolled `defaultX` fallbacks. Every row carries * a `data-path` attribute — paths are the stable handles agents target, never * indexes or generated ids. * * Agent bridge sketch (ships later in `@particle-academy/agent-integrations`): * `registerFilesBridge(server, { adapter })` will expose MCP tools over this * surface — `files_list(path)`, `files_expand(path)` / `files_collapse(path)`, * `files_select(paths)`, `files_navigate(path)`, and * `files_request_snapshot(path, depth)` — with each mutation emitting an * `AgentActivity` event so presence, undo, and coaching layers compose. The * adapter maps those tools onto the same controlled props * (`value`/`path`/`expandedPaths`) and provider/snapshot contract; no DOM * scraping required. * * Read-only in v1: no content preview (pair with fancy-code's `FileViewer`) * and no write operations — rename/delete/upload arrive later behind a * `pendingMode`-gated iteration. */ declare function FileBrowserRoot({ provider, snapshot, select, multiple, value, defaultValue, onChange, path, defaultPath, onPathChange, expandedPaths, defaultExpandedPaths, onExpandedChange, sort, defaultSort, onSortChange, filter, defaultFilter, onFilterChange, onError, onCreateFolder, indentSize, showIcons, className, children, }: FileBrowserProps): react.JSX.Element; declare namespace FileBrowserRoot { var displayName: string; } declare const FileBrowser: typeof FileBrowserRoot & { PathBar: typeof FileBrowserPathBar; Toolbar: typeof FileBrowserToolbar; Tree: typeof FileBrowserTree; Node: typeof FileBrowserNode; }; declare function useFileBrowser(): FileBrowserContextValue; /** * The "New folder" affordance — rendered only when the host opted in by * supplying `onCreateFolder`. * * ## Why an inline input and not `window.prompt` * * A prompt blocks the event loop, cannot be styled or themed, is unusable on * mobile, and — the one that matters for this suite — cannot be driven by an * agent. The Human+ contract asks that every interactive element carry a stable * handle an agent can target; a native dialog has none. * * ## Why validation lives here * * The browser already knows what is in the current directory. The host would * have to round-trip to find out, so a duplicate name becomes a failed write * and an error toast instead of a message before anything is attempted. Name * rules that are about PATHS rather than policy — separators, `.`, `..` — are * checked here too: those are not names, and forwarding them makes the host * decide what a traversal attempt means. * * Anything else is the host's call. This does not guess at case-sensitivity, * reserved Windows device names, or length limits, because the answer depends * on a filesystem the browser cannot see. */ declare function FileBrowserNewFolder({ className }: { className?: string; }): react.JSX.Element | null; declare namespace FileBrowserNewFolder { var displayName: string; } /** * Why a proposed folder name cannot be used, or `null` if it can. * * Only checks what the BROWSER can know: that the name is a name rather than a * path, and that nothing in this directory already answers to it. Case * sensitivity, reserved device names and length limits depend on a filesystem * this component cannot see, so they stay the host's to enforce — and the host * rejecting a create is a supported outcome, surfaced on the form. * * A collision counts against FILES too. `mkdir` fails the same way either way, * and "there is already an index.ts here" is the useful message. */ declare function validateFolderName(raw: string, siblings: ReadonlyArray<{ name: string; }>): string | null; export { FileBrowser, type FileBrowserContextValue, FileBrowserNewFolder, type FileBrowserNodeProps, type FileBrowserPathBarProps, type FileBrowserProps, type FileBrowserProvider, type FileBrowserRow, type FileBrowserToolbarProps, type FileBrowserTreeProps, type FileEntry, type FileKind, type FileLoadStatus, type FileSelectMode, type FileSnapshotNode, type FileSort, type FileSortDirection, type FileSortField, useFileBrowser, validateFolderName };