/** * @fileoverview File tree data layer (v2.0.0). * * Pure functions that turn a flat list of git-tracked paths into a * hierarchical tree suitable for the per-task detail page sidebar. * * Design context: see the Feishu design doc (`roy-plugin-task-show * v2.0.0 设计方案`) §2.3 文件树组件 and §5.1 关键组件拆分. We keep * the data layer framework-free so the SSR side can serialize it as * a `data-files` JSON attribute on the sidebar container and the * client (`public/file-tree.js`) can hydrate the rendered DOM. * * Conventions: * - Paths use forward slashes (POSIX). Git always emits `/`. * - Paths are relative to the repo root (the worktree). No leading * `/`. The frontend never sees the absolute filesystem path. * - Tree nodes carry enough metadata for the renderer to decide * highlight / collapse / scroll behaviour without a second pass. * * The renderer side (vanilla JS in `public/file-tree.js`) handles: * - chevron toggle (▶ / ▼), * - default expansion depth = 2, * - keyboard navigation (↑ / ↓ / Enter), * - scroll-into-view when a mermaid node is clicked. * * The data side (this file) handles: * - nesting, * - sorting (directories first, then alphabetical), * - dedup, * - lookup helpers (find by path, collect all paths). */ /** * A single node in the file tree. * * `type` distinguishes leaves (files) from interior nodes (directories) * so the renderer can pick the right glyph + click handler without * inspecting `children`. * * `depth` is the 0-indexed level relative to the tree root. Roots are * always depth 0, even if the input path is `"a/b/c.ts"` (the `a` * directory becomes a depth-0 root). */ export interface TreeNode { /** Basename (last path segment). */ name: string; /** Full path with `/` separators, identical to the input. */ path: string; /** `"file"` for leaves, `"directory"` for interior nodes. */ type: "file" | "directory"; /** 0-indexed depth from the tree root. */ depth: number; /** Empty for leaves. Always defined (possibly empty) for directories. */ children: TreeNode[]; /** * v2.0.0 highlight flag, set by the caller (the per-task renderer) * to indicate the file is touched by the currently-selected tool * call. The data layer never sets it. */ highlighted?: boolean; } /** * Threshold above which the renderer switches to windowed virtualization. * * 500 was chosen because it's the inflection point where most browsers * start to lag on `scrollIntoView` + addEventListener for the whole * tree. We surface the constant so tests can lock it down and the * renderer side can import the same value without duplication. */ export declare const MAX_NODES_BEFORE_VIRTUAL = 500; /** * Build a nested tree from a flat list of paths. * * Behaviour: * - Empty input → `[]`. * - Duplicate paths → silently deduped. * - Sorting at every level: directories first, then alphabetical * by basename (case-sensitive, identical to git's default sort). * - The `path` field on each node is the full slash-joined path. * * Complexity: O(N * D) where N is the number of paths and D is the * average depth. We use a `Map` keyed by path to avoid O(N²) lookups * during tree assembly. */ export declare function buildFileTree(paths: readonly string[]): TreeNode[]; /** * Extract the set of files referenced by an array of tool-call records. * * Mirrors `extractArgPath()` but returns ALL paths (not just the first * match), deduped. Used by the renderer to compute the "highlight" * set for the file-tree sidebar. * * Inputs are typed loosely so the function is callable from both the * SSR side (where we have `ToolCallRecord[]`) and tests (where we * usually pass `{ args: { file_path: "..." } }` shims). */ export declare function extractAffectedPaths(toolCalls: ReadonlyArray<{ args?: Record; }>): string[]; /** * Find a node by its full path. Returns null when the path doesn't * appear in the tree. Performs a depth-first walk — acceptable because * the renderer only calls this when a mermaid node is clicked (one * lookup per click), and the tree depth is bounded by typical repo * depth (≤ 10 levels). */ export declare function findNodeByPath(roots: readonly TreeNode[], target: string): TreeNode | null; /** * Collect every leaf (file) path from the tree, in DFS pre-order. * Used by the keyboard-search index — the renderer keeps a flat array * so search-by-prefix is O(N) over a reasonable file count. */ export declare function collectAllPaths(roots: readonly TreeNode[]): string[]; /** * Subset of `child_process` we use. Inlined here so tests can pass a * fake `runner` without importing `node:child_process`. */ export interface GitRunner { (args: readonly string[], opts?: { cwd?: string; }): { stdout: string; stderr: string; status: number; }; } /** * Default runner: shells out to `git ls-files` via the system PATH. * Uses `-z` to NUL-separate paths so filenames with embedded newlines * survive the round-trip, AND `--full-name` so paths are emitted * relative to the worktree / repo root (NOT the current working * directory). This keeps the file-tree stable when the plugin is * started from a subdirectory — important because `args.file_path` * from the host typically uses worktree-root-relative paths. * * On non-zero exit (e.g. cwd is not a git repo) returns `stdout: ""` * so callers can degrade gracefully — the file-tree panel renders * empty rather than blowing up the page. */ export declare const defaultGitLsFilesRunner: GitRunner; /** * Run `git ls-files` and return the parsed file list. * * Behaviour: * - Splits on NUL (the `-z` separator). * - Filters out empty segments. * - On non-zero exit (not a git repo, git missing) returns `[]` * instead of throwing — the renderer should still render an * empty (but valid) tree. * - Honors a 30-second in-memory cache keyed by cwd. This keeps * SSR renders cheap — without it every page refresh would * spawn `git` synchronously. The cache is bounded to 8 entries * (FIFO drop). Exported as a separate `fileTreeCacheTestOnly` * getter for test injection; production callers should never * touch the cache directly. * * The runner is injected so tests can swap it for a fake; production * callers pass `defaultGitLsFilesRunner`. */ export declare function gitLsFiles(runner?: GitRunner, opts?: { cwd?: string; }): string[]; /** * In-memory TTL cache used by `gitLsFiles()`. Exported as a const so * tests can clear it between cases; production callers should never * touch it directly. */ export declare const fileTreeCache: Map; /** v2.0.0 TTL: 30s — same window as the SSE stale cache. */ export declare const FILE_TREE_CACHE_TTL_MS = 30000; /** v2.0.0 max cached cwds (worktree + main + a few tests). */ export declare const FILE_TREE_CACHE_MAX = 8; /** * Parse the raw output of `git ls-files -z`. Public so the server-side * cache layer (and tests) can re-parse a captured snapshot without * re-spawning git. * * `git ls-files -z` emits paths separated by NUL bytes; the very last * segment has no trailing NUL. We split on NUL and drop empties. */ export declare function parseLsFiles(raw: string): string[]; /** * Minimal shape we depend on for resolving a worktree path. Typed * structurally so the helper is callable from anywhere (server.ts, * tests, future plugins) without pulling in the full `TaskSession` * shape. `context.worktree` is the only field we read. */ export interface WorktreeContextSource { context?: { worktree?: unknown; }; } /** * Resolve the directory `git ls-files` should be invoked against for a * given task session. * * Resolution order (v2.3.0): * 1. `session.context.worktree` — when the host (the agent runner) * tells us which worktree the task is running in. This is the * accurate answer: the file tree should match the files the * agent is editing, not the plugin's process cwd. * 2. `process.cwd()` — fallback when the host didn't supply the * field (pre-v2.3.0 hosts, hand-rolled tests, etc.). Keeps the * legacy project-wide behaviour intact. * * Defensive parsing: * - Treats `""`, `" "`, non-string values, and `undefined` as * "missing" → falls back to cwd. A blank or whitespace-only path * would silently break `spawnSync("git", ...)` so we sanitise * up-front. * - Trims surrounding whitespace so host payloads with stray * newlines / padding don't leak into the spawn. * * Pure function: takes the session and the current cwd (defaulted * via the optional second arg). The caller passes cwd when it has a * different reference frame (e.g. the SSR tests); production code * uses the default of `process.cwd()`. */ export declare function resolveWorktreePath(session: WorktreeContextSource | null | undefined, cwd?: string): string; /** * v2.5.12 (Task #2951): comprehensive project-files root resolver. * * `resolveWorktreePath()` only looks at `session.context.worktree` * and `process.cwd()`. That misses a common case: the live session * collector often has no `context.worktree` because the host's * `tool:after.execute` payload doesn't carry it, but the operations * cache has `task.projectPath` (set by the host when the task was * created via `task_create`). Falling back to that field lets the * sidebar track the right repo even when the collector's session is * light on context. * * Resolution order (most preferred → least preferred): * 1. `cfg.projectFilesRoot` — explicit plugin config override * (escape hatch when neither the session nor the cache has the * right path). * 2. `session.context.worktree` — host-supplied per-session worktree * (the v2.3.0 contract). * 3. `cachedTask.projectPath` — host-supplied per-task project path * (from the operations cache; survives when the live session has * been evicted or never had a worktree field). * 4. `process.cwd()` — last-resort fallback (legacy behaviour). * * All string inputs are trimmed and treated as "missing" when empty / * whitespace-only, matching `resolveWorktreePath`'s defensive parse. */ export interface ProjectFilesRootOptions { /** Optional session (live collector record). */ session?: WorktreeContextSource | null; /** Optional cached task envelope (from operationsCache). */ cachedTask?: { projectPath?: unknown; } | null; /** Optional plugin config (carries `projectFilesRoot` override). */ cfg?: { projectFilesRoot?: unknown; } | null; /** Override for tests; defaults to `process.cwd()`. */ cwd?: string; } export declare function resolveProjectFilesRoot(opts?: ProjectFilesRootOptions): string; /** * v2.3.0: aggregate everything the file-tree sidebar needs in one * place. Both `renderTaskPage` (SSR) and `handleFileTree` (HTTP * endpoint) call this so the sidebar stays consistent between the * initial paint and any client-side hydration fetch. * * Returns the resolved worktree, the raw `git ls-files` output, the * nested `TreeNode[]`, and the affected path set. We surface all * four because callers render different parts (SSR uses * `tree + affectedPaths`, the endpoint exposes `files`). * * @param session the task whose files we want to render * @param runner optional injected git runner (default = * `defaultGitLsFilesRunner`); tests pass fakes */ export declare function buildSessionFileTree(session: (WorktreeContextSource & { toolCalls?: ReadonlyArray<{ args?: Record; }>; }) | null | undefined, runner?: GitRunner, opts?: { /** * v2.5.12 (Task #2951): the operations cache's view of the task. * When the live session has no `context.worktree` (e.g. the host's * `tool:after.execute` payload didn't carry one), the cache's * `task.projectPath` is a more accurate fallback than the plugin * host's `process.cwd()`. */ cachedTask?: { projectPath?: unknown; } | null; /** v2.5.12 (Task #2951): explicit override (plugin config). */ cfg?: { projectFilesRoot?: unknown; } | null; }): { cwd: string; files: string[]; tree: TreeNode[]; affectedPaths: string[]; }; //# sourceMappingURL=file-tree.d.ts.map