fix(maestro): group worktree sessions under their repository
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
This commit is contained in:
@@ -95,3 +95,59 @@ describe("mergeLinear", () => {
|
||||
expect(merged.path).toBe("a/b");
|
||||
});
|
||||
});
|
||||
|
||||
describe("worktree sessions group under their repository (#99)", () => {
|
||||
/** A session the daemon spawned into a worktree of `repo`. */
|
||||
function worktreeSession(id: string, repo: string, wt: string): Session {
|
||||
return { ...session(id, wt), worktree: { path: wt, base_repo: repo } } as Session;
|
||||
}
|
||||
|
||||
/** Top-level folder names, after linear chains are collapsed. */
|
||||
function topFolders(root: TreeFolder): string[] {
|
||||
return root.children.filter((c) => c.kind === "folder").map((c) => (c as TreeFolder).name);
|
||||
}
|
||||
|
||||
it("nests under the repo, not the worktree directory", () => {
|
||||
const root = buildTree(
|
||||
[
|
||||
worktreeSession(
|
||||
"a",
|
||||
"/home/n/tea/agent-skills",
|
||||
"/home/n/tea/agent-skills/worktrees/largo-clarinet",
|
||||
),
|
||||
],
|
||||
"/home/n",
|
||||
"",
|
||||
) as TreeFolder;
|
||||
|
||||
expect(topFolders(root)).toEqual(["~/tea/agent-skills"]);
|
||||
const repo = root.children[0] as TreeFolder;
|
||||
expect(repo.children).toHaveLength(1);
|
||||
expect(repo.children[0].kind).toBe("leaf");
|
||||
});
|
||||
|
||||
it("puts a repo's plain and worktree sessions side by side", () => {
|
||||
const root = buildTree(
|
||||
[
|
||||
session("plain", "/home/n/tea/agent-skills"),
|
||||
worktreeSession(
|
||||
"wt",
|
||||
"/home/n/tea/agent-skills",
|
||||
"/home/n/tea/agent-skills/worktrees/largo-clarinet",
|
||||
),
|
||||
],
|
||||
"/home/n",
|
||||
"",
|
||||
) as TreeFolder;
|
||||
|
||||
expect(topFolders(root)).toEqual(["~/tea/agent-skills"]);
|
||||
const repo = root.children[0] as TreeFolder;
|
||||
expect(repo.children).toHaveLength(2);
|
||||
expect(repo.children.every((c) => c.kind === "leaf")).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves a session without a worktree where it is", () => {
|
||||
const root = buildTree([session("a", "/home/n/tea/arr")], "/home/n", "") as TreeFolder;
|
||||
expect(topFolders(root)).toEqual(["~/tea/arr"]);
|
||||
});
|
||||
});
|
||||
|
||||
+4
-2
@@ -2,7 +2,7 @@
|
||||
// so it can be tested directly.
|
||||
|
||||
import type { Session } from "./types.js";
|
||||
import { abbreviateCwd, sessionCwd, sessionName } from "./types.js";
|
||||
import { abbreviateCwd, sessionName, sessionTreePath } from "./types.js";
|
||||
|
||||
export interface TreeFolder {
|
||||
kind: "folder";
|
||||
@@ -30,7 +30,9 @@ export function buildTree(sessions: Session[], home: string | undefined, filter:
|
||||
const lc = filter.trim().toLowerCase();
|
||||
|
||||
for (const s of sessions) {
|
||||
const cwd = sessionCwd(s);
|
||||
// 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();
|
||||
|
||||
+22
-1
@@ -36,6 +36,16 @@ export interface Session {
|
||||
* without a worktree; all counts default to 0 on the wire for older
|
||||
* daemons. */
|
||||
blocked_work?: InTheWay | null;
|
||||
/** The worktree the daemon created for this session, absent when it did not
|
||||
* create one. `base_repo` is the repository's main checkout, which is what
|
||||
* the session tree groups by (#99). */
|
||||
worktree?: SessionWorktree | null;
|
||||
}
|
||||
|
||||
/** A session's daemon-created worktree (`SessionWorktree` on the wire). */
|
||||
export interface SessionWorktree {
|
||||
path: string;
|
||||
base_repo: string;
|
||||
}
|
||||
|
||||
/** One directory's immediate subdirectories (NAP-329). */
|
||||
@@ -231,12 +241,23 @@ export function sessionName(s: Session): string {
|
||||
}
|
||||
|
||||
/** The directory a session lives in: its live `current_cwd` if the daemon has
|
||||
* observed one, else the spawn `cwd`. Drives the directory-based session tree. */
|
||||
* observed one, else the spawn `cwd`. */
|
||||
export function sessionCwd(s: Session): string | undefined {
|
||||
if (typeof s.current_cwd === "string" && s.current_cwd) return s.current_cwd;
|
||||
return s.cwd || undefined;
|
||||
}
|
||||
|
||||
/** Where a session sits in the directory tree. A worktree session belongs to
|
||||
* its repository, not to the worktree (#99): otherwise `~/tea/agent-skills`
|
||||
* grows a `~/tea/agent-skills/worktrees/largo-clarinet` folder holding one
|
||||
* session of the same name, and the repository stops being where you look for
|
||||
* the work. `base_repo` is the main checkout — the daemon resolves it. */
|
||||
export function sessionTreePath(s: Session): string | undefined {
|
||||
const repo = s.worktree?.base_repo;
|
||||
if (typeof repo === "string" && repo) return repo;
|
||||
return sessionCwd(s);
|
||||
}
|
||||
|
||||
/** Collapse a daemon's home dir to `~`. `home` comes from `DaemonInfo.home_dir`;
|
||||
* absent/empty leaves the path untouched. Mirrors the CLI's `abbreviate_dir`. */
|
||||
export function abbreviateCwd(cwd: string, home?: string): string {
|
||||
|
||||
Reference in New Issue
Block a user