The sidebar nested a worktree session under the worktree, so a repo grew a <repo>/worktrees/<branch> folder holding one session of the same name, and the repo stopped being where you look for the work. The daemon recorded whatever base_repo the request named, which is the worktree itself when spawning from inside one; it now records the resolved main checkout. The web tree groups by that instead of the cwd. The TUI sidebar is a flat list with no directory grouping, so it needs no mirror. Closes #99
87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
// Directory grouping for the session catalog. Pure: no DOM, no module state,
|
|
// so it can be tested directly.
|
|
|
|
import type { Session } from "./types.js";
|
|
import { abbreviateCwd, sessionName, sessionTreePath } from "./types.js";
|
|
|
|
export interface TreeFolder {
|
|
kind: "folder";
|
|
name: string;
|
|
path: string;
|
|
children: TreeNode[];
|
|
}
|
|
|
|
export interface TreeLeaf {
|
|
kind: "leaf";
|
|
session: Session;
|
|
}
|
|
|
|
export type TreeNode = TreeFolder | TreeLeaf;
|
|
|
|
export function compareNodes(a: TreeNode, b: TreeNode): number {
|
|
if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
|
|
const an = a.kind === "folder" ? a.name : sessionName(a.session);
|
|
const bn = b.kind === "folder" ? b.name : sessionName(b.session);
|
|
return an.localeCompare(bn);
|
|
}
|
|
|
|
export function buildTree(sessions: Session[], home: string | undefined, filter: string): TreeNode {
|
|
const root: TreeFolder = { kind: "folder", name: "", path: "", children: [] };
|
|
const lc = filter.trim().toLowerCase();
|
|
|
|
for (const s of sessions) {
|
|
// Grouped by repository, so a worktree session lands under its repo
|
|
// rather than in a folder of its own (#99).
|
|
const cwd = sessionTreePath(s);
|
|
const dir = cwd ? abbreviateCwd(cwd, home) : "";
|
|
if (lc) {
|
|
const haystack = `${sessionName(s)} ${dir}`.toLowerCase();
|
|
if (!haystack.includes(lc)) continue;
|
|
}
|
|
const parts = dir ? dir.split("/").filter(Boolean) : [];
|
|
let cursor = root;
|
|
for (const part of parts) {
|
|
const path = cursor.path ? `${cursor.path}/${part}` : part;
|
|
let next = cursor.children.find(
|
|
(n): n is TreeFolder => n.kind === "folder" && n.name === part,
|
|
);
|
|
if (!next) {
|
|
next = { kind: "folder", name: part, path, children: [] };
|
|
cursor.children.push(next);
|
|
}
|
|
cursor = next;
|
|
}
|
|
cursor.children.push({ kind: "leaf", session: s });
|
|
}
|
|
|
|
const sortRec = (n: TreeFolder) => {
|
|
n.children.sort(compareNodes);
|
|
for (const c of n.children) if (c.kind === "folder") sortRec(c);
|
|
};
|
|
sortRec(root);
|
|
// Collapse linear directory chains so `~/git.naps.pt/naps62/maestro` reads as
|
|
// one compact folder rather than four nested ones.
|
|
root.children = root.children.map((c) => (c.kind === "folder" ? mergeLinear(c) : c));
|
|
return root;
|
|
}
|
|
|
|
/** Fold a folder with a single folder-child into one node (`a` + `b` → `a/b`),
|
|
* repeating down the chain, then recurse into the surviving branches. The merged
|
|
* node keeps the deepest path so collapse/expand state stays stable as it grows. */
|
|
export function mergeLinear(folder: TreeFolder): TreeFolder {
|
|
let cur = folder;
|
|
while (cur.children.length === 1 && cur.children[0].kind === "folder") {
|
|
const child = cur.children[0];
|
|
cur = {
|
|
kind: "folder",
|
|
name: `${cur.name}/${child.name}`,
|
|
path: child.path,
|
|
children: child.children,
|
|
};
|
|
}
|
|
cur.children = cur.children.map((c) =>
|
|
c.kind === "folder" ? mergeLinear(c) : c,
|
|
);
|
|
return cur;
|
|
}
|