@nguyenquangthai/pi-todo 0.2.7 → 0.2.9
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 +43 -29
- package/README.md +8 -3
- package/package.json +4 -2
- package/src/index.ts +5 -3
- package/src/prompt.ts +4 -1
- package/src/replay.ts +11 -6
- package/src/schema.ts +4 -1
- package/src/tools/tododiagnose.ts +55 -0
- package/src/tools/todowrite.ts +27 -7
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,50 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.9 (2026-07-15)
|
|
4
|
+
|
|
5
|
+
- **Atomic write ordering**: `setTodos()` happens *after* `pi.appendEntry()`
|
|
6
|
+
succeeds. If appendEntry throws (stale-ctx or persistence error), the in-memory
|
|
7
|
+
store is never mutated — no more desync between live state and durable state.
|
|
8
|
+
- **Stale-ctx now returns error**: Instead of silently swallowing "stale after
|
|
9
|
+
session replacement", the tool returns an error so the LLM knows the write
|
|
10
|
+
was not committed.
|
|
11
|
+
- **4 atomicity tests**: Mock appendEntry rejects with stale-ctx / persistence
|
|
12
|
+
errors and asserts store is unchanged. 102/102 tests passing.
|
|
13
|
+
|
|
14
|
+
## 0.2.8 (2026-07-15)
|
|
15
|
+
|
|
16
|
+
- **Replay**: revert broken timestamp-based hardening; Pi guarantees getBranch() is chronological, so original last-entry-wins algorithm is correct and simpler.
|
|
17
|
+
- **appendEntry**: only swallow stale-ctx errors; real persistence/disk errors now propagate instead of being silently caught.
|
|
18
|
+
- **Tests**: remove 6 broken mock-timestamp tests that never reflected Pi runtime (timestamp is ISO string, not number). Keep 9 correct persistence roundtrip tests.
|
|
19
|
+
- **Tests**: 96/96 pass, typecheck clean.
|
|
20
|
+
|
|
3
21
|
## 0.2.7 (2026-07-15)
|
|
4
22
|
|
|
5
|
-
-
|
|
6
|
-
- Progress
|
|
7
|
-
-
|
|
8
|
-
- Clean up heading: remove all Unicode block characters, simple and reliable.
|
|
23
|
+
- Background-color progress bar using ANSI reverse video — theme-driven (accent/muted), no alignment issues.
|
|
24
|
+
- Progress format: done/total with background-color bar.
|
|
25
|
+
- 14 integration tests for write->read->overlay roundtrip.
|
|
9
26
|
|
|
10
27
|
## 0.2.6 (2026-07-15)
|
|
11
28
|
|
|
12
|
-
- Restore Unicode block chars (`█`/`░`) for progress bar — looks cleaner.
|
|
13
|
-
- Fix alignment: halve bar width (double-width chars) + pad percentage to 3 chars so heading never shifts when % changes.
|
|
14
|
-
- README: update progress bar example to match.
|
|
15
|
-
|
|
16
29
|
## 0.2.5 (2026-07-15)
|
|
17
30
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
31
|
+
- Rework overlay heading: remove progress bar entirely, simplify to count-only.
|
|
32
|
+
- Fix docstring examples to match new heading format.
|
|
33
|
+
- README: update overlay section to remove progress bar mentions.
|
|
20
34
|
|
|
21
35
|
## 0.2.4 (2026-07-15)
|
|
22
36
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- README: update overlay description + tool names to match v0.2.3.
|
|
37
|
+
- Priority sorting in overlay (high→medium→low).
|
|
38
|
+
- Progress bar in heading (Unicode blocks).
|
|
39
|
+
- Collapsible completed items (+N done).
|
|
40
|
+
- CI: GitHub Actions (test + typecheck on push, Node 18/20/22).
|
|
41
|
+
- README: npm version, license, CI badges.
|
|
29
42
|
|
|
30
43
|
## 0.2.3 (2026-07-15)
|
|
31
44
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
45
|
+
- Tool rename: todowrite → todo_write, todoread → todo_read (snake_case).
|
|
46
|
+
- renderCall/renderResult polish: accent color for item count.
|
|
47
|
+
- Overlay heading: "X open / Y total" instead of "N todo(s)".
|
|
35
48
|
|
|
36
49
|
## 0.2.2 (2026-07-15)
|
|
37
50
|
|
|
38
|
-
-
|
|
51
|
+
- Fix idle reminder text: "Open items still pending" instead of "0 open".
|
|
52
|
+
- Reminder hides for all-terminal lists.
|
|
53
|
+
- completion update reminder copy polish.
|
|
39
54
|
|
|
40
55
|
## 0.2.1 (2026-07-15)
|
|
41
56
|
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
- Add blank line under overlay heading for visual spacing.
|
|
57
|
+
- First usable release.
|
|
58
|
+
- todo_write, todo_read tools.
|
|
59
|
+
- Overlay with OpenCode-style markers.
|
|
60
|
+
- Persistence via custom entry + toolResult details.
|
|
61
|
+
- Cold start heuristics (multi-step, VI prompts, fix/polish/setup).
|
|
62
|
+
- Completion nudge when user says done.
|
|
63
|
+
- Idle reminder cadence every ~4 turns.
|
|
64
|
+
- State-aware reminder with open items list.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
OpenCode-style session todo checklist for the [pi coding agent](https://pi.dev).
|
|
8
8
|
|
|
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).
|
|
9
|
+
Adds `todo_write` / `todo_read` / `todo_diagnose`, a live `# Todos` overlay above the editor (`[ ]` / `[•]` / `[✓]` / `[×]`), and branch-replay persistence (survives `/reload`, tree nav, and custom-entry durability across compaction).
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -50,7 +50,11 @@ Rules enforced by the tool:
|
|
|
50
50
|
|
|
51
51
|
### `todo_read`
|
|
52
52
|
|
|
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`.
|
|
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`.
|
|
54
|
+
|
|
55
|
+
### `todo_diagnose`
|
|
56
|
+
|
|
57
|
+
Read-only persistence check for suspected reload, tree-navigation, or compaction drift. It compares the live in-memory snapshot against a replay of the durable session branch and reports `consistent` or `mismatch`; it never changes todos.
|
|
54
58
|
|
|
55
59
|
## Overlay
|
|
56
60
|
|
|
@@ -66,7 +70,8 @@ Heading shows open + running counts + background-color progress bar, e.g. `# Tod
|
|
|
66
70
|
|
|
67
71
|
Items within each status group are sorted by priority (high → medium → low).
|
|
68
72
|
When space is tight, completed/cancelled items collapse into `+N done`.
|
|
69
|
-
A blank line separates the heading from the first todo row for visual breathing room.
|
|
73
|
+
A blank line separates the heading from the first todo row for visual breathing room.
|
|
74
|
+
Successful `todo_write` results display `✓ Saved`, meaning the durable checkpoint was accepted before the in-memory snapshot was updated.
|
|
70
75
|
|
|
71
76
|
## Development
|
|
72
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nguyenquangthai/pi-todo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
"prepublishOnly": "npm test && npm run typecheck"
|
|
42
42
|
},
|
|
43
43
|
"pi": {
|
|
44
|
-
"extensions": [
|
|
44
|
+
"extensions": [
|
|
45
|
+
"./src/index.ts"
|
|
46
|
+
],
|
|
45
47
|
"image": "https://raw.githubusercontent.com/QuangThai/pi-todo/main/media/screenshot.png"
|
|
46
48
|
},
|
|
47
49
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-todo — OpenCode-like session todo checklist for Pi.
|
|
3
3
|
*
|
|
4
|
-
* Tools: todo_write (full replace), todo_read
|
|
4
|
+
* Tools: todo_write (full replace), todo_read, todo_diagnose (read-only)
|
|
5
5
|
* Overlay: # Todos with [ ]/[•]/[✓]/[×] above the editor
|
|
6
6
|
* Persistence: toolResult details + custom entry, replayed from branch
|
|
7
7
|
* Reminder: pi-tasks-style cadence → transient <system-reminder> via context
|
|
@@ -31,11 +31,12 @@ import {
|
|
|
31
31
|
import { replayFromBranch } from "./replay.js";
|
|
32
32
|
import { clearTodos, getTodos, setTodos } from "./store.js";
|
|
33
33
|
import { registerTodoReadTool } from "./tools/todoread.js";
|
|
34
|
+
import { registerTodoDiagnoseTool } from "./tools/tododiagnose.js";
|
|
34
35
|
import { registerTodoWriteTool } from "./tools/todowrite.js";
|
|
35
|
-
import { TOOL_READ, TOOL_WRITE } from "./types.js";
|
|
36
|
+
import { TOOL_DIAGNOSE, TOOL_READ, TOOL_WRITE } from "./types.js";
|
|
36
37
|
import { hasOpenTodos, isOpenTodo } from "./validate.js";
|
|
37
38
|
|
|
38
|
-
const TODO_TOOL_NAMES = new Set([TOOL_WRITE, TOOL_READ]);
|
|
39
|
+
const TODO_TOOL_NAMES = new Set([TOOL_WRITE, TOOL_READ, TOOL_DIAGNOSE]);
|
|
39
40
|
|
|
40
41
|
const cadenceConfig: CadenceConfig = {
|
|
41
42
|
reminderInterval: REMINDER_INTERVAL,
|
|
@@ -87,6 +88,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
87
88
|
|
|
88
89
|
registerTodoWriteTool(pi, { onCommit: refreshOverlay });
|
|
89
90
|
registerTodoReadTool(pi);
|
|
91
|
+
registerTodoDiagnoseTool(pi);
|
|
90
92
|
|
|
91
93
|
// Cold-start + completion nudges. Tool description alone is often ignored;
|
|
92
94
|
// prompt-aware section + one-shot context reminder (pi-tasks style) is stronger.
|
package/src/prompt.ts
CHANGED
|
@@ -44,11 +44,14 @@ Do **not** skip just because the request is "explain" or "review" — if the ans
|
|
|
44
44
|
export const TODOREAD_DESCRIPTION =
|
|
45
45
|
"Read the current session todo list. Returns JSON of all todos with status and priority. Prefer the overlay for at-a-glance status; call after todo_write settles (not in the same parallel batch).";
|
|
46
46
|
|
|
47
|
+
export const TODODIAGNOSE_DESCRIPTION =
|
|
48
|
+
"Read-only persistence diagnostic. Compares the current in-memory todo snapshot with the durable session-branch replay. Use only to investigate suspected reload, tree-navigation, or compaction state drift; it never changes todos.";
|
|
49
|
+
|
|
47
50
|
export const TODOWRITE_GUIDELINES = [
|
|
48
51
|
"For multi-step work (3+ steps), codebase explain/explore/review, or when the user gives a list of tasks, call todo_write BEFORE starting — do not skip tracking.",
|
|
49
52
|
"Pass the full list every todo_write call (full replace). Keep exactly one todo in_progress; mark completed immediately when a step finishes — never leave a stale in_progress.",
|
|
50
53
|
"When finishing a step or when the user confirms done, call todo_write in that same turn to mark completed and advance the next pending item if work continues.",
|
|
51
|
-
|
|
54
|
+
"Do not invent TaskCreate/TaskUpdate tools — use todo_write/todo_read; todo_diagnose is read-only troubleshooting.",
|
|
52
55
|
"Do not call todo_write and todo_read in the same parallel tool batch.",
|
|
53
56
|
];
|
|
54
57
|
|
package/src/replay.ts
CHANGED
|
@@ -33,9 +33,14 @@ function isWriteDetails(value: unknown): value is TodoWriteDetails {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Replay todo state from the session branch.
|
|
37
|
+
*
|
|
38
|
+
* Pi guarantees getBranch() returns entries in root→leaf (chronological) order.
|
|
39
|
+
* Last valid entry wins — custom `pi-todo.state` entries and `todo_write`
|
|
40
|
+
* toolResult details are both tracked.
|
|
41
|
+
*
|
|
42
|
+
* Error envelopes (e.g. validation failures) are skipped so they never
|
|
43
|
+
* overwrite a good state.
|
|
39
44
|
*/
|
|
40
45
|
export function replayFromBranch(ctx: {
|
|
41
46
|
sessionManager: { getBranch(): Iterable<unknown> };
|
|
@@ -53,12 +58,12 @@ export function replayFromBranch(ctx: {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
if (e.type !== "message" || !isRecord(e.message)) continue;
|
|
56
|
-
const msg = e.message
|
|
61
|
+
const msg = e.message as Record<string, unknown>;
|
|
57
62
|
if (msg.role !== "toolResult" || msg.toolName !== TOOL_WRITE) continue;
|
|
58
63
|
if (!isWriteDetails(msg.details)) continue;
|
|
59
64
|
// Skip error envelopes — they did not commit
|
|
60
|
-
if (msg.details.error) continue;
|
|
61
|
-
todos = msg.details.todos.map((t) => ({ ...t }));
|
|
65
|
+
if ((msg.details as TodoWriteDetails).error) continue;
|
|
66
|
+
todos = (msg.details as TodoWriteDetails).todos.map((t) => ({ ...t }));
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
return todos;
|
package/src/schema.ts
CHANGED
|
@@ -18,7 +18,10 @@ export const TodoWriteParams = Type.Object({
|
|
|
18
18
|
}),
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
export const TodoReadParams = Type.Object({});
|
|
21
|
+
export const TodoReadParams = Type.Object({});
|
|
22
|
+
|
|
23
|
+
/** Read-only persistence check; deliberately accepts no mutation input. */
|
|
24
|
+
export const TodoDiagnoseParams = Type.Object({});
|
|
22
25
|
|
|
23
26
|
export type TodoWriteInput = Static<typeof TodoWriteParams>;
|
|
24
27
|
export type TodoItemInput = Static<typeof TodoItemSchema>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { TODODIAGNOSE_DESCRIPTION } from "../prompt.js";
|
|
4
|
+
import { replayFromBranch } from "../replay.js";
|
|
5
|
+
import { TodoDiagnoseParams } from "../schema.js";
|
|
6
|
+
import { getTodos, withStoreLock } from "../store.js";
|
|
7
|
+
import type { TodoItem } from "../types.js";
|
|
8
|
+
import { TOOL_DIAGNOSE } from "../types.js";
|
|
9
|
+
import { todosEqual } from "../validate.js";
|
|
10
|
+
|
|
11
|
+
interface TodoDiagnoseDetails {
|
|
12
|
+
status: "consistent" | "mismatch";
|
|
13
|
+
storeTodos: TodoItem[];
|
|
14
|
+
replayedTodos: TodoItem[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Registers a read-only comparison between live state and durable branch replay. */
|
|
18
|
+
export function registerTodoDiagnoseTool(pi: ExtensionAPI): void {
|
|
19
|
+
pi.registerTool({
|
|
20
|
+
name: TOOL_DIAGNOSE,
|
|
21
|
+
label: "Todo Diagnose",
|
|
22
|
+
description: TODODIAGNOSE_DESCRIPTION,
|
|
23
|
+
promptSnippet: "Diagnose todo persistence without changing todos",
|
|
24
|
+
parameters: TodoDiagnoseParams,
|
|
25
|
+
|
|
26
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
27
|
+
return withStoreLock(() => {
|
|
28
|
+
const storeTodos = getTodos();
|
|
29
|
+
const replayedTodos = replayFromBranch(ctx);
|
|
30
|
+
const status = todosEqual(storeTodos, replayedTodos) ? "consistent" : "mismatch";
|
|
31
|
+
const details: TodoDiagnoseDetails = { status, storeTodos, replayedTodos };
|
|
32
|
+
const text =
|
|
33
|
+
status === "consistent"
|
|
34
|
+
? "Persistence check: consistent — current todo snapshot matches durable branch replay."
|
|
35
|
+
: "Persistence check: MISMATCH — current todo snapshot differs from durable branch replay. No state was changed.";
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: `${text}\n\n${JSON.stringify(details, null, 2)}` }],
|
|
39
|
+
details,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
renderCall(_args, theme) {
|
|
45
|
+
return new Text(theme.fg("toolTitle", theme.bold("todo_diagnose")), 0, 0);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
renderResult(result, _opts, theme) {
|
|
49
|
+
const details = result.details as TodoDiagnoseDetails | undefined;
|
|
50
|
+
const text = details?.status === "consistent" ? "✓ Consistent" : "! Mismatch";
|
|
51
|
+
const color = details?.status === "consistent" ? "success" : "warning";
|
|
52
|
+
return new Text(theme.fg(color, text), 0, 0);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
package/src/tools/todowrite.ts
CHANGED
|
@@ -33,18 +33,34 @@ export function registerTodoWriteTool(
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
// Skip no-op rewrites to avoid session noise.
|
|
36
|
+
// 1. Persist durable state BEFORE updating in-memory store.
|
|
37
|
+
// If appendEntry fails (stale ctx, persistence error), we abort
|
|
38
|
+
// so in-memory store never diverges from durable state.
|
|
40
39
|
if (!result.unchanged) {
|
|
41
40
|
try {
|
|
42
41
|
pi.appendEntry(TODO_STATE_ENTRY_TYPE, { todos: result.todos });
|
|
43
|
-
} catch {
|
|
44
|
-
//
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// Stale session: discard this write entirely — returning error
|
|
44
|
+
// so the LLM knows the state was not committed.
|
|
45
|
+
if (/stale after session replacement/i.test(String(e))) {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: "Error: session was replaced — state not committed. Please retry todo_write.",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
details: { todos: current, error: "stale session replacement" } satisfies TodoWriteDetails,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Real persistence/disk/runtime error: propagate.
|
|
57
|
+
throw e;
|
|
45
58
|
}
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
// 2. Durable write succeeded (or no-op) — now update in-memory store.
|
|
62
|
+
setTodos(result.todos);
|
|
63
|
+
|
|
48
64
|
options.onCommit?.();
|
|
49
65
|
|
|
50
66
|
const open = countOpenTodos(result.todos);
|
|
@@ -87,7 +103,11 @@ export function registerTodoWriteTool(
|
|
|
87
103
|
if (!details?.error && !details?.unchanged) {
|
|
88
104
|
const open = details?.todos ? details.todos.filter((t: any) => t.status === "pending" || t.status === "in_progress").length : 0;
|
|
89
105
|
const total = details?.todos?.length ?? 0;
|
|
90
|
-
|
|
106
|
+
return new Text(
|
|
107
|
+
theme.fg("success", "✓ Saved") + theme.fg("muted", ` · ${open} open / ${total} total`),
|
|
108
|
+
0,
|
|
109
|
+
0,
|
|
110
|
+
);
|
|
91
111
|
}
|
|
92
112
|
const msg = text?.type === "text" ? text.text.split("\n")[0] ?? "Updated" : "Updated";
|
|
93
113
|
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", msg), 0, 0);
|
package/src/types.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface TodoWriteDetails {
|
|
|
21
21
|
export const TODO_STATE_ENTRY_TYPE = "pi-todo.state";
|
|
22
22
|
export const TOOL_WRITE = "todo_write";
|
|
23
23
|
export const TOOL_READ = "todo_read";
|
|
24
|
+
export const TOOL_DIAGNOSE = "todo_diagnose";
|
|
24
25
|
export const WIDGET_KEY = "pi-todo";
|
|
25
26
|
export const MAX_OVERLAY_LINES = 12;
|
|
26
27
|
/** Max characters per todo content after sanitize (context/tool safety). */
|