Files
kandev/apps/web/components/task/file-tree-editor-target.ts
T
2026-07-31 09:00:31 +01:00

51 lines
2.3 KiB
TypeScript

export type FileTreeEditorTarget = {
/** Path relative to the resolved worktree root, as the editors API expects. */
filePath: string;
worktreeId?: string;
};
type WorktreeLike = { id: string; path?: string };
function pathBasename(path: string): string {
return path.split(/[\\/]/).filter(Boolean).pop() ?? "";
}
/**
* Maps a Files-panel tree path onto the `{worktreeId, filePath}` pair the
* editors API resolves against, or `null` when the node is not addressable by
* that API.
*
* The Files tree is served either from inside a worktree — in which case node
* paths are already worktree-relative — or from the task workspace root, where
* the first segment names a workspace entry. A task reaches the second shape
* both by holding several worktrees and by having any source attached through
* *Add Repositories to workspace*, which rebinds the session to the task root
* even when it adds no worktree. The worktree count therefore cannot tell the
* two apart; the tree's own root directory name can.
*
* Entries that are not worktrees (an attached plain folder) have no worktree to
* resolve against, and the editors API rejects paths that escape the worktree,
* so those nodes return `null` and callers hide the action.
*/
export function resolveFileTreeEditorTarget(
nodePath: string,
worktrees: WorktreeLike[],
treeRootName?: string,
): FileTreeEditorTarget | null {
const known = worktrees.filter((worktree) => worktree.path);
// Repository-backed sessions without a worktree fall back to the repository
// checkout server-side, and an unloaded tree has no root to compare against.
// Both keep the pass-through behaviour rather than hiding the action.
if (known.length === 0 || !treeRootName) return { filePath: nodePath };
const rooted = known.find((worktree) => pathBasename(worktree.path!) === treeRootName);
if (rooted) return { filePath: nodePath, worktreeId: rooted.id };
const separatorIndex = nodePath.indexOf("/");
const head = separatorIndex === -1 ? nodePath : nodePath.slice(0, separatorIndex);
const rest = separatorIndex === -1 ? "" : nodePath.slice(separatorIndex + 1);
const match = known.find((worktree) => pathBasename(worktree.path!) === head);
if (!match) return null;
return { filePath: rest, worktreeId: match.id };
}