Collapse completed todo subtrees in widget
This commit is contained in:
@@ -10,19 +10,52 @@ export const STATUS_ICONS = {
|
||||
done: "✓",
|
||||
} as const;
|
||||
|
||||
interface DescendantStats {
|
||||
done: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface TreeRow {
|
||||
task: Task;
|
||||
prefix: string;
|
||||
collapsed?: DescendantStats;
|
||||
}
|
||||
|
||||
export function orderedTreeRows(tasks: Task[]): TreeRow[] {
|
||||
function descendantStats(task: Task, byId: Map<number, Task>, memo: Map<number, DescendantStats>): DescendantStats {
|
||||
const cached = memo.get(task.id);
|
||||
if (cached) return cached;
|
||||
|
||||
let done = 0;
|
||||
let total = 0;
|
||||
for (const childId of task.childrenIds) {
|
||||
const child = byId.get(childId);
|
||||
if (!child) continue;
|
||||
|
||||
total += 1;
|
||||
if (child.status === "done") done += 1;
|
||||
const nested = descendantStats(child, byId, memo);
|
||||
done += nested.done;
|
||||
total += nested.total;
|
||||
}
|
||||
|
||||
const stats = { done, total };
|
||||
memo.set(task.id, stats);
|
||||
return stats;
|
||||
}
|
||||
|
||||
function buildTreeRows(tasks: Task[], collapseCompleted: boolean): TreeRow[] {
|
||||
const byId = new Map(tasks.map((task) => [task.id, task]));
|
||||
const memo = new Map<number, DescendantStats>();
|
||||
const rows: TreeRow[] = [];
|
||||
|
||||
const visit = (task: Task, ancestorHasNext: boolean[], isLast: boolean, depth: number) => {
|
||||
const ancestors = ancestorHasNext.map((hasNext) => (hasNext ? "│ " : " ")).join("");
|
||||
const connector = depth === 0 ? "" : isLast ? "└─ " : "├─ ";
|
||||
rows.push({ task, prefix: `${ancestors}${connector}` });
|
||||
const stats = collapseCompleted ? descendantStats(task, byId, memo) : undefined;
|
||||
const collapsed = stats && stats.total > 0 && stats.done === stats.total ? stats : undefined;
|
||||
rows.push({ task, prefix: `${ancestors}${connector}`, ...(collapsed ? { collapsed } : {}) });
|
||||
if (collapsed) return;
|
||||
|
||||
const children = task.childrenIds.map((id) => byId.get(id)).filter((child): child is Task => child !== undefined);
|
||||
children.forEach((child, index) => {
|
||||
const childAncestors = depth === 0 ? [] : [...ancestorHasNext, !isLast];
|
||||
@@ -35,6 +68,10 @@ export function orderedTreeRows(tasks: Task[]): TreeRow[] {
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function orderedTreeRows(tasks: Task[]): TreeRow[] {
|
||||
return buildTreeRows(tasks, false);
|
||||
}
|
||||
|
||||
function styleTaskName(task: Task, theme: Theme): string {
|
||||
if (task.status === "done") return theme.fg("dim", theme.strikethrough(task.name));
|
||||
if (task.status === "in-progress") return theme.fg("warning", task.name);
|
||||
@@ -59,7 +96,13 @@ export function formatDuration(durationMs: number): string {
|
||||
return hours === 0 ? minuteClock : `${String(hours).padStart(2, "0")}:${minuteClock}`;
|
||||
}
|
||||
|
||||
export function renderTaskLine(task: Task, prefix: string, theme: Theme, width: number): string {
|
||||
function renderTaskLineWithSuffix(
|
||||
task: Task,
|
||||
prefix: string,
|
||||
theme: Theme,
|
||||
width: number,
|
||||
suffix?: string,
|
||||
): string {
|
||||
const line =
|
||||
theme.fg("dim", prefix) +
|
||||
styleStatusIcon(task, theme) +
|
||||
@@ -67,19 +110,36 @@ export function renderTaskLine(task: Task, prefix: string, theme: Theme, width:
|
||||
theme.fg("accent", `${task.id}.`) +
|
||||
" " +
|
||||
styleTaskName(task, theme);
|
||||
if (task.durationMs === undefined) return truncateToWidth(line, Math.max(1, width));
|
||||
const trailing = [
|
||||
suffix === undefined ? undefined : theme.fg("dim", ` · ${suffix}`),
|
||||
task.durationMs === undefined ? undefined : theme.fg("dim", ` · ${formatDuration(task.durationMs)}`),
|
||||
]
|
||||
.filter((part): part is string => part !== undefined)
|
||||
.join("");
|
||||
if (trailing.length === 0) return truncateToWidth(line, Math.max(1, width));
|
||||
|
||||
const duration = theme.fg("dim", ` · ${formatDuration(task.durationMs)}`);
|
||||
const durationWidth = visibleWidth(duration);
|
||||
if (width <= durationWidth + 6) return truncateToWidth(line, Math.max(1, width));
|
||||
const taskWidth = Math.max(1, width - durationWidth);
|
||||
return truncateToWidth(line, taskWidth) + duration;
|
||||
const trailingWidth = visibleWidth(trailing);
|
||||
const minimumTaskWidth = suffix === undefined ? 6 : 12;
|
||||
if (width <= trailingWidth + minimumTaskWidth) return truncateToWidth(line, Math.max(1, width));
|
||||
const taskWidth = Math.max(1, width - trailingWidth);
|
||||
return truncateToWidth(line, taskWidth) + trailing;
|
||||
}
|
||||
|
||||
export function renderTaskLine(task: Task, prefix: string, theme: Theme, width: number): string {
|
||||
return renderTaskLineWithSuffix(task, prefix, theme, width);
|
||||
}
|
||||
|
||||
export function renderTree(tasks: Task[], theme: Theme, width: number, padding = " "): string[] {
|
||||
return orderedTreeRows(tasks).map(({ task, prefix }) => renderTaskLine(task, `${padding}${prefix}`, theme, width));
|
||||
}
|
||||
|
||||
export function renderWidgetTree(tasks: Task[], theme: Theme, width: number, padding = " "): string[] {
|
||||
return buildTreeRows(tasks, true).map(({ task, prefix, collapsed }) => {
|
||||
const summary = collapsed === undefined ? undefined : `${collapsed.done}/${collapsed.total} done`;
|
||||
return renderTaskLineWithSuffix(task, `${padding}${prefix}`, theme, width, summary);
|
||||
});
|
||||
}
|
||||
|
||||
export function progressLabel(stats: TaskStats): string {
|
||||
const blocked = stats.blocked > 0 ? ` · !${stats.blocked}` : "";
|
||||
return ` Tasks ${stats.done}/${stats.total}${blocked}`;
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import type { TaskState } from "./state.ts";
|
||||
import { renderProgressHeader, renderTree } from "./ui.ts";
|
||||
import { renderProgressHeader, renderWidgetTree } from "./ui.ts";
|
||||
|
||||
const WIDGET_ID = "pi-todo-herdr";
|
||||
const TIMER_REFRESH_MS = 1000;
|
||||
@@ -31,7 +31,7 @@ export function updateTaskWidget(
|
||||
return {
|
||||
render(width: number): string[] {
|
||||
const currentTasks = state.list();
|
||||
return [renderProgressHeader(state.stats(), theme, width), ...renderTree(currentTasks, theme, width)];
|
||||
return [renderProgressHeader(state.stats(), theme, width), ...renderWidgetTree(currentTasks, theme, width)];
|
||||
},
|
||||
invalidate(): void {},
|
||||
dispose(): void {
|
||||
|
||||
Reference in New Issue
Block a user