There is no ctrl-k on a phone and the web UI is mobile-first, so the field the operator already reaches for becomes the touch entry point: focusing or tapping it dispatches OPEN_COMMAND_BAR_EVENT and the shell opens the bar with an empty query. The field is readonly and blurs itself first, so no keyboard rises over it and the bar can be closed without reopening it. This replaces the inline substring filter — one search affordance in the header, not two. buildTree loses its filter argument, and the tree's "No matches" empty state goes with it; the bar's own no-results state took over. The filter's name-and-directory matching is now covered against the bar. The bar's phone-width sheet moves to the spawn modal's 599px break, with thumb-sized rows and a field that clears the zoom-on-focus threshold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
3.2 KiB
TypeScript
91 lines
3.2 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): TreeNode {
|
|
const root: TreeFolder = { kind: "folder", name: "", path: "", children: [] };
|
|
|
|
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) : "";
|
|
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);
|
|
// The root's sole child (usually `~`) has no sibling to distinguish it
|
|
// from, so it folds into every one of its own children, not just a lone
|
|
// one — unlike the chain merge below, and only here at the very top.
|
|
if (root.children.length === 1 && root.children[0].kind === "folder") {
|
|
const only = root.children[0];
|
|
root.children = only.children.map((c) =>
|
|
c.kind === "folder" ? { ...c, name: `${only.name}/${c.name}` } : c,
|
|
);
|
|
}
|
|
// 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;
|
|
}
|