Files
fusion/packages/dashboard/app/utils/worktreeGrouping.ts
Dustin Byrne c802108a02 refactor(HAI-116): rename kb to hai across all packages, CLI, and docs
- Rename npm packages from @kb/* to @hai/* and update all workspace references
- Rename CLI binary from kb to hai and config directory from .kb to .hai
- Update dashboard UI branding, titles, and references from kb to hai
- Update all test files, CI workflows, and documentation to reflect new naming
- Run comprehensive grep verification to ensure no stale kb references remain
2026-03-26 22:44:11 -04:00

126 lines
3.7 KiB
TypeScript

import type { Task } from "@kb/core";
export interface WorktreeGroupData {
label: string;
activeTasks: Task[];
queuedTasks: Task[];
}
/**
* Extract a clean display name from a worktree path.
* e.g. ".worktrees/KB-001" → "KB-001", "/path/to/kb/kb-001" → "kb-001"
*/
export function getWorktreeLabel(worktreePath: string): string {
// Take the last segment of the path
const segments = worktreePath.replace(/\/+$/, "").split("/");
return segments[segments.length - 1] || worktreePath;
}
/**
* Topological sort of tasks by dependency order.
* Mirrors resolveDependencyOrder from @kb/core but inlined to avoid
* build alias issues (Vite aliases @kb/core to types.ts only).
*/
function resolveDependencyOrder(tasks: Task[]): string[] {
const taskMap = new Map(tasks.map((t) => [t.id, t]));
const ordered: string[] = [];
const visited = new Set<string>();
const visiting = new Set<string>();
function visit(id: string): void {
if (visited.has(id)) return;
if (visiting.has(id)) return;
visiting.add(id);
const task = taskMap.get(id);
if (task) {
for (const depId of task.dependencies || []) {
if (taskMap.has(depId)) visit(depId);
}
}
visiting.delete(id);
visited.add(id);
ordered.push(id);
}
for (const task of tasks) visit(task.id);
return ordered;
}
/**
* Group in-progress tasks by worktree and collect queued todo tasks
* as visual previews in the "Up Next" group.
*
* Queued tasks (eligible "todo" tasks whose dependencies are all satisfied)
* are always placed in the "Up Next" group — they are never distributed
* to worktree-specific groups since they have no worktree assignment yet.
* The number of queued tasks shown is capped at `maxConcurrent`.
*/
export function groupByWorktree(
inProgressTasks: Task[],
allTasks: Task[],
maxConcurrent: number,
): WorktreeGroupData[] {
// Separate assigned vs unassigned in-progress tasks
const assigned = inProgressTasks.filter((t) => t.worktree);
const unassigned = inProgressTasks.filter((t) => !t.worktree);
// Group assigned tasks by worktree
const worktreeMap = new Map<string, Task[]>();
for (const task of assigned) {
const key = task.worktree!;
const list = worktreeMap.get(key) || [];
list.push(task);
worktreeMap.set(key, list);
}
// Find queued todo tasks: "todo" tasks with all deps satisfied (done or in-review)
const taskById = new Map(allTasks.map((t) => [t.id, t]));
const todoTasks = allTasks.filter((t) => t.column === "todo");
const eligible = todoTasks.filter((t) =>
!t.paused &&
(t.dependencies || []).every((depId) => {
const dep = taskById.get(depId);
return dep && (dep.column === "done" || dep.column === "in-review");
}),
);
// Order eligible tasks by dependency order
const orderedIds = resolveDependencyOrder(eligible);
const orderedEligible = orderedIds
.map((id) => taskById.get(id))
.filter((t): t is Task => t !== undefined && eligible.includes(t));
// Build groups from worktree map
const groups: WorktreeGroupData[] = [];
const worktreeKeys = Array.from(worktreeMap.keys());
for (const key of worktreeKeys) {
groups.push({
label: getWorktreeLabel(key),
activeTasks: worktreeMap.get(key)!,
queuedTasks: [],
});
}
// Add unassigned group if needed
if (unassigned.length > 0) {
groups.push({
label: "Unassigned",
activeTasks: unassigned,
queuedTasks: [],
});
}
// All eligible queued tasks go into the "Up Next" group (capped at maxConcurrent)
const queued = orderedEligible.slice(0, maxConcurrent);
if (queued.length > 0) {
groups.push({
label: "Up Next",
activeTasks: [],
queuedTasks: queued,
});
}
return groups;
}