@nguyenquangthai/pi-todo 0.2.3 → 0.2.5

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.5 (2026-07-15)
4
+
5
+ - Fix progress bar: use single-width ASCII (`#`/`·`) instead of double-width Unicode (`█`/`░`) to avoid alignment issues in terminal.
6
+ - README: update progress bar example to match ASCII chars.
7
+
8
+ ## 0.2.4 (2026-07-15)
9
+
10
+ - README: add npm/CI/license badges.
11
+ - Add GitHub Actions CI workflow (test + typecheck on push).
12
+ - Overlay: sort items by priority (high → medium → low) within each status group.
13
+ - Overlay: add progress bar (`████░░░░ 60%`) in heading.
14
+ - Overlay: collapse completed/cancelled items into `+N done` when space is tight.
15
+ - README: update overlay description + tool names to match v0.2.3.
16
+
3
17
  ## 0.2.3 (2026-07-15)
4
18
 
5
19
  - 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
+ [![npm version](https://img.shields.io/npm/v/@nguyenquangthai/pi-todo?color=blue)](https://www.npmjs.com/package/@nguyenquangthai/pi-todo)
4
+ [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
5
+ [![Tests](https://github.com/QuangThai/pi-todo/actions/workflows/ci.yml/badge.svg)](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 `todowrite` / `todoread`, a live `# Todos` overlay above the editor (`[ ]` / `[•]` / `[✓]` / `[×]`), and branch-replay persistence (survives `/reload`, tree nav, and custom-entry durability across compaction).
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
- ### `todowrite`
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
- ### `todoread`
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 `todowrite`.
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 (3 open, 1 running) ####···· 25%`:
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, single-width ASCII)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nguyenquangthai/pi-todo",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "OpenCode-like session todo checklist for the pi coding agent — todowrite/todoread with a live TUI overlay",
5
5
  "type": "module",
6
6
  "author": "QuangThai",
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, ...todos.map(formatPlainTodoLine)].join("\n");
53
+ return [summary, ...sorted.map(formatPlainTodoLine)].join("\n");
29
54
  }
30
- const shown = todos.slice(0, MAX_RESULT_LINES);
31
- const hidden = todos.length - MAX_RESULT_LINES;
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, drop terminal first,
62
- * then pending from the end; keep in_progress. Preserve original order.
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
- return { visible: [...todos], hiddenCount: 0 };
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 = new Set<TodoItem>();
83
- for (const t of inProgress) {
84
- if (picked.size >= innerBudget) break;
85
- picked.add(t);
86
- }
87
- for (const t of pending) {
88
- if (picked.size >= innerBudget) break;
89
- picked.add(t);
90
- }
91
- for (const t of terminal) {
92
- if (picked.size >= innerBudget) break;
93
- picked.add(t);
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 visible = todos.filter((t) => picked.has(t));
97
- return { visible, hiddenCount: todos.length - visible.length };
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 todowrite)
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,17 @@ 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 = "#".repeat(filled) + "·".repeat(Math.max(0, barWidth - filled));
162
+
122
163
  const heading = truncate(
123
164
  theme.fg("accent", theme.bold(`# Todos`)) +
124
- theme.fg("dim", ` (${open} open, ${running} running)`),
165
+ theme.fg("dim", ` (${open} open, ${running} running)`) +
166
+ theme.fg("muted", ` ${bar} ${pct}%`),
125
167
  );
126
168
 
127
169
  // Small gap between heading and first row — budget -1 to account for the blank line
@@ -131,6 +173,9 @@ export function renderOverlayLines(
131
173
  for (const todo of layout.visible) {
132
174
  lines.push(truncate(formatThemedTodoLine(todo, theme)));
133
175
  }
176
+ if (layout.terminalCount > 0) {
177
+ lines.push(truncate(theme.fg("dim", `+${layout.terminalCount} done`)));
178
+ }
134
179
  if (layout.hiddenCount > 0) {
135
180
  lines.push(truncate(theme.fg("dim", `+${layout.hiddenCount} more`)));
136
181
  }