@nguyenquangthai/pi-todo 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +12 -5
- package/package.json +1 -1
- package/src/format.ts +73 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.4 (2026-07-15)
|
|
4
|
+
|
|
5
|
+
- README: add npm/CI/license badges.
|
|
6
|
+
- Add GitHub Actions CI workflow (test + typecheck on push).
|
|
7
|
+
- Overlay: sort items by priority (high → medium → low) within each status group.
|
|
8
|
+
- Overlay: add progress bar (`████░░░░ 60%`) in heading.
|
|
9
|
+
- Overlay: collapse completed/cancelled items into `+N done` when space is tight.
|
|
10
|
+
- README: update overlay description + tool names to match v0.2.3.
|
|
11
|
+
|
|
3
12
|
## 0.2.3 (2026-07-15)
|
|
4
13
|
|
|
5
14
|
- Rename tools to snake_case: `todowrite` → `todo_write`, `todoread` → `todo_read` (pi convention).
|
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# pi-todo
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@nguyenquangthai/pi-todo)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://github.com/QuangThai/pi-todo/actions)
|
|
6
|
+
|
|
3
7
|
OpenCode-style session todo checklist for the [pi coding agent](https://pi.dev).
|
|
4
8
|
|
|
5
|
-
Adds `
|
|
9
|
+
Adds `todo_write` / `todo_read`, a live `# Todos` overlay above the editor (`[ ]` / `[•]` / `[✓]` / `[×]`), and branch-replay persistence (survives `/reload`, tree nav, and custom-entry durability across compaction).
|
|
6
10
|
|
|
7
11
|
## Install
|
|
8
12
|
|
|
@@ -22,7 +26,7 @@ Then restart pi or run `/reload`.
|
|
|
22
26
|
|
|
23
27
|
## Tools
|
|
24
28
|
|
|
25
|
-
### `
|
|
29
|
+
### `todo_write`
|
|
26
30
|
|
|
27
31
|
Full-replace the session todo list. Each call must pass the **complete** list.
|
|
28
32
|
|
|
@@ -44,9 +48,9 @@ Rules enforced by the tool:
|
|
|
44
48
|
- Status: `pending` | `in_progress` | `completed` | `cancelled`
|
|
45
49
|
- Tool text echo caps at **40** lines (`+N more` in the text body; full list still in `details` / JSON)
|
|
46
50
|
|
|
47
|
-
### `
|
|
51
|
+
### `todo_read`
|
|
48
52
|
|
|
49
|
-
Returns the current list as text + JSON. Prefer the overlay for at-a-glance status; avoid calling it in the same parallel batch as `
|
|
53
|
+
Returns the current list as text + JSON. Prefer the overlay for at-a-glance status; avoid calling it in the same parallel batch as `todo_write`.
|
|
50
54
|
|
|
51
55
|
## Overlay
|
|
52
56
|
|
|
@@ -54,11 +58,14 @@ Shown above the editor while any **open** todo remains (`pending` / `in_progress
|
|
|
54
58
|
|
|
55
59
|
Hidden when the list is empty or every item is `completed` / `cancelled`.
|
|
56
60
|
|
|
57
|
-
Heading shows open + running counts, e.g. `# Todos (2 open, 1 running)
|
|
61
|
+
Heading shows open + running counts + progress bar, e.g. `# Todos (2 open, 1 running) ████░░░░ 60%`:
|
|
58
62
|
|
|
59
63
|
- **open** = `pending` + `in_progress`
|
|
60
64
|
- **running** = `in_progress` only (0 or 1 after a valid write)
|
|
65
|
+
- **progress bar** = completed / total (adaptive width)
|
|
61
66
|
|
|
67
|
+
Items within each status group are sorted by priority (high → medium → low).
|
|
68
|
+
When space is tight, completed/cancelled items collapse into `+N done`.
|
|
62
69
|
A blank line separates the heading from the first todo row for visual breathing room.
|
|
63
70
|
|
|
64
71
|
## Development
|
package/package.json
CHANGED
package/src/format.ts
CHANGED
|
@@ -4,6 +4,16 @@ import type { TodoItem, TodoStatus } from "./types.js";
|
|
|
4
4
|
import { MAX_OVERLAY_LINES, MAX_RESULT_LINES } from "./types.js";
|
|
5
5
|
import { countOpenTodos, countRunningTodos, hasOpenTodos, isTerminalStatus } from "./validate.js";
|
|
6
6
|
|
|
7
|
+
const PRIORITY_RANK: Record<string, number> = {
|
|
8
|
+
high: 0,
|
|
9
|
+
medium: 1,
|
|
10
|
+
low: 2,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function prioritySort(a: TodoItem, b: TodoItem): number {
|
|
14
|
+
return (PRIORITY_RANK[a.priority] ?? 2) - (PRIORITY_RANK[b.priority] ?? 2);
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
export function getTodoMarker(status: TodoStatus): string {
|
|
8
18
|
switch (status) {
|
|
9
19
|
case "completed":
|
|
@@ -24,11 +34,26 @@ export function formatPlainTodoLine(todo: TodoItem): string {
|
|
|
24
34
|
/** Compact checklist for tool responses; caps lines to keep LLM context small. */
|
|
25
35
|
export function formatTodoListText(todos: readonly TodoItem[], summary: string): string {
|
|
26
36
|
if (todos.length === 0) return summary;
|
|
37
|
+
|
|
38
|
+
// in_progress first, then pending (sorted by priority), then terminal
|
|
39
|
+
const sorted = [...todos].sort((a, b) => {
|
|
40
|
+
const statusRank: Record<string, number> = {
|
|
41
|
+
in_progress: 0,
|
|
42
|
+
pending: 1,
|
|
43
|
+
completed: 2,
|
|
44
|
+
cancelled: 2,
|
|
45
|
+
};
|
|
46
|
+
const diff =
|
|
47
|
+
(statusRank[a.status] ?? 2) - (statusRank[b.status] ?? 2);
|
|
48
|
+
if (diff !== 0) return diff;
|
|
49
|
+
return prioritySort(a, b);
|
|
50
|
+
});
|
|
51
|
+
|
|
27
52
|
if (todos.length <= MAX_RESULT_LINES) {
|
|
28
|
-
return [summary, ...
|
|
53
|
+
return [summary, ...sorted.map(formatPlainTodoLine)].join("\n");
|
|
29
54
|
}
|
|
30
|
-
const shown =
|
|
31
|
-
const hidden =
|
|
55
|
+
const shown = sorted.slice(0, MAX_RESULT_LINES);
|
|
56
|
+
const hidden = sorted.length - MAX_RESULT_LINES;
|
|
32
57
|
return [
|
|
33
58
|
summary,
|
|
34
59
|
...shown.map(formatPlainTodoLine),
|
|
@@ -55,46 +80,54 @@ export function shouldShowOverlay(todos: readonly TodoItem[]): boolean {
|
|
|
55
80
|
export interface OverlayLayout {
|
|
56
81
|
visible: TodoItem[];
|
|
57
82
|
hiddenCount: number;
|
|
83
|
+
terminalCount: number;
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
/**
|
|
61
|
-
* Fit into maxLines (includes heading). On overflow,
|
|
62
|
-
*
|
|
87
|
+
* Fit into maxLines (includes heading). On overflow, collapse terminal items
|
|
88
|
+
* into a "+N done" line. Within each status group, items are sorted by
|
|
89
|
+
* priority (high → medium → low).
|
|
63
90
|
*/
|
|
64
91
|
export function selectOverlayLayout(
|
|
65
92
|
todos: readonly TodoItem[],
|
|
66
93
|
maxLines: number = MAX_OVERLAY_LINES,
|
|
67
94
|
): OverlayLayout {
|
|
68
95
|
if (!shouldShowOverlay(todos)) {
|
|
69
|
-
return { visible: [], hiddenCount: 0 };
|
|
96
|
+
return { visible: [], hiddenCount: 0, terminalCount: 0 };
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
const bodyBudget = Math.max(1, maxLines - 1);
|
|
73
100
|
if (todos.length <= bodyBudget) {
|
|
74
|
-
|
|
101
|
+
// All fit — show everything
|
|
102
|
+
return { visible: [...todos].sort(prioritySort), hiddenCount: 0, terminalCount: 0 };
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
const innerBudget = Math.max(1, bodyBudget - 1);
|
|
78
|
-
const inProgress = todos.filter((t) => t.status === "in_progress");
|
|
79
|
-
const pending = todos.filter((t) => t.status === "pending");
|
|
80
|
-
const terminal = todos.filter((t) => isTerminalStatus(t.status));
|
|
81
|
-
|
|
82
|
-
const picked =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").sort(prioritySort);
|
|
107
|
+
const pending = todos.filter((t) => t.status === "pending").sort(prioritySort);
|
|
108
|
+
const terminal = todos.filter((t) => isTerminalStatus(t.status)).sort(prioritySort);
|
|
109
|
+
|
|
110
|
+
const picked: TodoItem[] = [];
|
|
111
|
+
const addPicked = (items: TodoItem[]) => {
|
|
112
|
+
for (const t of items) {
|
|
113
|
+
if (picked.length >= innerBudget) break;
|
|
114
|
+
picked.push(t);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
addPicked(inProgress);
|
|
119
|
+
addPicked(pending);
|
|
120
|
+
|
|
121
|
+
// Collapse terminal items: only show them individually if there's room
|
|
122
|
+
if (terminal.length > 0 && picked.length < innerBudget) {
|
|
123
|
+
addPicked(terminal);
|
|
94
124
|
}
|
|
95
125
|
|
|
96
|
-
const
|
|
97
|
-
|
|
126
|
+
const terminalCollapsed = terminal.length > 0 && picked.length >= innerBudget;
|
|
127
|
+
const remaining = todos.length - picked.length;
|
|
128
|
+
const hiddenCount = terminalCollapsed ? remaining : Math.max(0, remaining - terminal.length);
|
|
129
|
+
|
|
130
|
+
return { visible: picked, hiddenCount, terminalCount: terminalCollapsed ? terminal.length : 0 };
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
export interface RenderOverlayOptions {
|
|
@@ -104,8 +137,9 @@ export interface RenderOverlayOptions {
|
|
|
104
137
|
/**
|
|
105
138
|
* Heading counts (when overlay is visible):
|
|
106
139
|
* - open = pending + in_progress
|
|
107
|
-
* - running = in_progress only (0 or 1 after a valid
|
|
140
|
+
* - running = in_progress only (0 or 1 after a valid todo_write)
|
|
108
141
|
* Overlay is hidden when open === 0, so "(0 open, 0 running)" is never shown.
|
|
142
|
+
* Includes a visual progress bar: completed / total.
|
|
109
143
|
*/
|
|
110
144
|
export function renderOverlayLines(
|
|
111
145
|
todos: readonly TodoItem[],
|
|
@@ -119,9 +153,18 @@ export function renderOverlayLines(
|
|
|
119
153
|
const truncate = (line: string) => truncateToWidth(line, width, "…");
|
|
120
154
|
const open = countOpenTodos(todos);
|
|
121
155
|
const running = countRunningTodos(todos);
|
|
156
|
+
const total = todos.length;
|
|
157
|
+
const done = total - open;
|
|
158
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
159
|
+
const barWidth = Math.max(4, Math.min(12, Math.floor(width / 8)));
|
|
160
|
+
const filled = Math.round((barWidth * done) / total);
|
|
161
|
+
const bar =
|
|
162
|
+
"█".repeat(filled) + "░".repeat(Math.max(0, barWidth - filled));
|
|
163
|
+
|
|
122
164
|
const heading = truncate(
|
|
123
165
|
theme.fg("accent", theme.bold(`# Todos`)) +
|
|
124
|
-
theme.fg("dim", ` (${open} open, ${running} running)`)
|
|
166
|
+
theme.fg("dim", ` (${open} open, ${running} running)`) +
|
|
167
|
+
theme.fg("muted", ` ${bar} ${pct}%`),
|
|
125
168
|
);
|
|
126
169
|
|
|
127
170
|
// Small gap between heading and first row — budget -1 to account for the blank line
|
|
@@ -131,6 +174,9 @@ export function renderOverlayLines(
|
|
|
131
174
|
for (const todo of layout.visible) {
|
|
132
175
|
lines.push(truncate(formatThemedTodoLine(todo, theme)));
|
|
133
176
|
}
|
|
177
|
+
if (layout.terminalCount > 0) {
|
|
178
|
+
lines.push(truncate(theme.fg("dim", `+${layout.terminalCount} done`)));
|
|
179
|
+
}
|
|
134
180
|
if (layout.hiddenCount > 0) {
|
|
135
181
|
lines.push(truncate(theme.fg("dim", `+${layout.hiddenCount} more`)));
|
|
136
182
|
}
|