Indent deepest task leaves
This commit is contained in:
@@ -98,13 +98,13 @@ A task starts timing the first time it enters `in-progress`. Leaving that state
|
|||||||
|
|
||||||
## Pi Widget
|
## Pi Widget
|
||||||
|
|
||||||
The widget appears above Pi's editor while todos exist. Run `/todo` to hide or show it for the current Pi runtime. Ten seconds after every task is completed, the task tree is automatically deleted and task IDs reset to `1`; adding or reopening unfinished work during that delay cancels the clear. Its header is the virtual task-tree root, with connectors extending from it through every top-level task so the display always remains a tree. The outline uses only the labels needed by the deepest task: one-level trees render every task as an unlabeled leaf, two-level trees use numbered parents and unlabeled leaves, and three-level trees use Roman-numeral categories, numbered items, and unlabeled leaves. Internal task IDs remain unchanged and visible in tool output. In-progress circles rotate through `◐ ◓ ◑ ◒` twice per second; other status symbols remain still. The widget shows at most 12 task rows plus a compact count of hidden tasks, so larger plans do not crowd the editor out of an 80×24 terminal.
|
The widget appears above Pi's editor while todos exist. Run `/todo` to hide or show it for the current Pi runtime. Ten seconds after every task is completed, the task tree is automatically deleted and task IDs reset to `1`; adding or reopening unfinished work during that delay cancels the clear. Its header is the virtual task-tree root, with connectors extending from it through every top-level task so the display always remains a tree. Content at the deepest leaf level is indented one additional tree step in both the widget and expanded list output so shallower numbered items remain aligned. The outline uses only the labels needed by the deepest task: one-level trees render every task as an unlabeled leaf, two-level trees use numbered parents and unlabeled leaves, and three-level trees use Roman-numeral categories, numbered items, and unlabeled leaves. Internal task IDs remain unchanged and visible in tool output. In-progress circles rotate through `◐ ◓ ◑ ◒` twice per second; other status symbols remain still. The widget shows at most 12 task rows plus a compact count of hidden tasks, so larger plans do not crowd the editor out of an 80×24 terminal.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Todos 2/6 · !1 ━━━━━────────
|
Todos 2/6 · !1 ━━━━━────────
|
||||||
├─ I. ◐ Build auth · 12:08
|
├─ I. ◐ Build auth · 12:08
|
||||||
│ ├─ 1. ! Resolve token issue · 03:41
|
│ ├─ 1. ! Resolve token issue · 03:41
|
||||||
│ │ └─ ○ Inspect provider response
|
│ │ └─ ○ Inspect provider response
|
||||||
│ └─ 2. ○ Add middleware
|
│ └─ 2. ○ Add middleware
|
||||||
└─ II. ✓ Define schema · 04:31
|
└─ II. ✓ Define schema · 04:31
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -151,8 +151,32 @@ export function renderTaskLine(task: Task, prefix: string, theme: Theme, width:
|
|||||||
return renderTaskLineWithSuffix(task, prefix, theme, width);
|
return renderTaskLineWithSuffix(task, prefix, theme, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STATUS_ICON_WIDTH = 1;
|
||||||
|
const TRUNCATED_STATUS_WIDTH = STATUS_ICON_WIDTH + 3;
|
||||||
|
|
||||||
|
function indentDeepestLeaf(
|
||||||
|
prefix: string,
|
||||||
|
depth: number,
|
||||||
|
maximumDepth: number,
|
||||||
|
width: number,
|
||||||
|
minimumContentWidth: number,
|
||||||
|
): string {
|
||||||
|
if (depth === 0 || depth !== maximumDepth) return prefix;
|
||||||
|
|
||||||
|
const indentedPrefix = `${prefix} `;
|
||||||
|
return visibleWidth(indentedPrefix) + minimumContentWidth <= width ? indentedPrefix : prefix;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderTree(tasks: Task[], theme: Theme, width: number, padding = " "): string[] {
|
export function renderTree(tasks: Task[], theme: Theme, width: number, padding = " "): string[] {
|
||||||
return orderedTreeRows(tasks).map(({ task, prefix }) => renderTaskLine(task, `${padding}${prefix}`, theme, width));
|
const maximumDepth = maximumTaskDepth(tasks);
|
||||||
|
return orderedTreeRows(tasks).map(({ task, prefix, depth }) =>
|
||||||
|
renderTaskLine(
|
||||||
|
task,
|
||||||
|
indentDeepestLeaf(`${padding}${prefix}`, depth, maximumDepth, width, TRUNCATED_STATUS_WIDTH),
|
||||||
|
theme,
|
||||||
|
width,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRomanNumeral(value: number): string {
|
function toRomanNumeral(value: number): string {
|
||||||
@@ -214,7 +238,16 @@ export function renderWidgetTree(
|
|||||||
const lines = visibleRows.map(({ task, prefix, depth, siblingIndex, collapsed }) => {
|
const lines = visibleRows.map(({ task, prefix, depth, siblingIndex, collapsed }) => {
|
||||||
const summary = collapsed === undefined ? undefined : `${collapsed.done}/${collapsed.total} done`;
|
const summary = collapsed === undefined ? undefined : `${collapsed.done}/${collapsed.total} done`;
|
||||||
const identifier = widgetOutlineLabel(depth, siblingIndex, maximumDepth);
|
const identifier = widgetOutlineLabel(depth, siblingIndex, maximumDepth);
|
||||||
return renderTaskLineWithSuffix(task, `${padding}${prefix}`, theme, width, summary, identifier, true, inProgressFrame);
|
return renderTaskLineWithSuffix(
|
||||||
|
task,
|
||||||
|
indentDeepestLeaf(`${padding}${prefix}`, depth, maximumDepth, width, STATUS_ICON_WIDTH),
|
||||||
|
theme,
|
||||||
|
width,
|
||||||
|
summary,
|
||||||
|
identifier,
|
||||||
|
true,
|
||||||
|
inProgressFrame,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
if (rows.length <= MAX_WIDGET_TASK_ROWS) return lines;
|
if (rows.length <= MAX_WIDGET_TASK_ROWS) return lines;
|
||||||
|
|
||||||
|
|||||||
+12
-8
@@ -62,7 +62,7 @@ test("renders lightweight connectors without a detached root continuation", () =
|
|||||||
assert.deepEqual(output, [
|
assert.deepEqual(output, [
|
||||||
" ◉ 1. Build auth · 00:00",
|
" ◉ 1. Build auth · 00:00",
|
||||||
" ├─ ! 2. Resolve token issue",
|
" ├─ ! 2. Resolve token issue",
|
||||||
" │ └─ ○ 3. Inspect provider response",
|
" │ └─ ○ 3. Inspect provider response",
|
||||||
" └─ ○ 4. Add middleware",
|
" └─ ○ 4. Add middleware",
|
||||||
" ✓ 5. ~Define schema~",
|
" ✓ 5. ~Define schema~",
|
||||||
]);
|
]);
|
||||||
@@ -95,7 +95,7 @@ test("collapses completed widget subtrees while keeping list trees expanded", ()
|
|||||||
" ├─ I. ○ Release",
|
" ├─ I. ○ Release",
|
||||||
" │ ├─ 1. ○ Completed branch · 2/2 done",
|
" │ ├─ 1. ○ Completed branch · 2/2 done",
|
||||||
" │ └─ 2. ○ Open branch",
|
" │ └─ 2. ○ Open branch",
|
||||||
" │ └─ ○ Open leaf",
|
" │ └─ ○ Open leaf",
|
||||||
" └─ II. ○ Pending parent · 1/1 done",
|
" └─ II. ○ Pending parent · 1/1 done",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -124,9 +124,9 @@ test("uses only the outline levels needed while leaving leaf tasks unlabeled", (
|
|||||||
]);
|
]);
|
||||||
assert.deepEqual(renderWidgetTree(twoLevelState.list(), plainTheme as never, 80).map(stripAnsi), [
|
assert.deepEqual(renderWidgetTree(twoLevelState.list(), plainTheme as never, 80).map(stripAnsi), [
|
||||||
" ├─ 1. ○ First group",
|
" ├─ 1. ○ First group",
|
||||||
" │ └─ ○ First nested leaf",
|
" │ └─ ○ First nested leaf",
|
||||||
" └─ 2. ○ Second group",
|
" └─ 2. ○ Second group",
|
||||||
" └─ ○ Second nested leaf",
|
" └─ ○ Second nested leaf",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const state = new TaskState(() => 0);
|
const state = new TaskState(() => 0);
|
||||||
@@ -151,7 +151,7 @@ test("uses only the outline levels needed while leaving leaf tasks unlabeled", (
|
|||||||
" ├─ III. ○ Category three",
|
" ├─ III. ○ Category three",
|
||||||
" ├─ IV. ○ Category four",
|
" ├─ IV. ○ Category four",
|
||||||
" │ ├─ 1. ○ First item",
|
" │ ├─ 1. ○ First item",
|
||||||
" │ │ └─ ○ Leaf sub-item",
|
" │ │ └─ ○ Leaf sub-item",
|
||||||
" │ └─ 2. ○ Second item",
|
" │ └─ 2. ○ Second item",
|
||||||
" └─ V. ○ Category five",
|
" └─ V. ○ Category five",
|
||||||
" └─ 1. ○ Reset item numbering",
|
" └─ 1. ○ Reset item numbering",
|
||||||
@@ -164,6 +164,10 @@ test("uses only the outline levels needed while leaving leaf tasks unlabeled", (
|
|||||||
assert.match(statusPriority[3] ?? "", /○/);
|
assert.match(statusPriority[3] ?? "", /○/);
|
||||||
const childStatusPriority = renderWidgetTree(state.list(), plainTheme as never, 10).map(stripAnsi);
|
const childStatusPriority = renderWidgetTree(state.list(), plainTheme as never, 10).map(stripAnsi);
|
||||||
assert.match(childStatusPriority[4] ?? "", /○/);
|
assert.match(childStatusPriority[4] ?? "", /○/);
|
||||||
|
const leafStatusPriority = renderWidgetTree(state.list(), plainTheme as never, 12).map(stripAnsi);
|
||||||
|
assert.match(leafStatusPriority[5] ?? "", /○/);
|
||||||
|
const listLeafStatusPriority = renderTree(state.list(), plainTheme as never, 12).map(stripAnsi);
|
||||||
|
assert.match(listLeafStatusPriority[5] ?? "", /○/);
|
||||||
|
|
||||||
const timedState = new TaskState(() => 0);
|
const timedState = new TaskState(() => 0);
|
||||||
timedState.add([
|
timedState.add([
|
||||||
@@ -214,10 +218,10 @@ test("renumbers widget labels after task reordering and reparenting", () => {
|
|||||||
const lines = renderWidgetTree(state.list(), plainTheme as never, 80).map(stripAnsi);
|
const lines = renderWidgetTree(state.list(), plainTheme as never, 80).map(stripAnsi);
|
||||||
assert.deepEqual(lines, [
|
assert.deepEqual(lines, [
|
||||||
" ├─ 1. ○ Category B",
|
" ├─ 1. ○ Category B",
|
||||||
" │ ├─ ○ A2",
|
" │ ├─ ○ A2",
|
||||||
" │ └─ ○ B1",
|
" │ └─ ○ B1",
|
||||||
" └─ 2. ○ Category A",
|
" └─ 2. ○ Category A",
|
||||||
" └─ ○ A1",
|
" └─ ○ A1",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user