@nguyenquangthai/pi-todo 0.3.3 → 0.3.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 +139 -118
- package/README.md +38 -38
- package/package.json +1 -1
- package/src/format.ts +76 -75
- package/src/prompt.ts +86 -86
- package/src/tools/todoupdate.ts +55 -43
- package/src/validate.ts +28 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,118 +1,139 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.3.
|
|
4
|
-
|
|
5
|
-
###
|
|
6
|
-
|
|
7
|
-
- **
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- **ID
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- **
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- **
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
## 0.2.
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.5 (2026-07-17)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Root cause of intermittent `todo_update` ID errors**: LLMs frequently
|
|
8
|
+
mistype 36-char UUIDs even when IDs are visible in tool output. New todos
|
|
9
|
+
now get short sequential IDs (`t1`, `t2`, …) that are trivial to copy.
|
|
10
|
+
- **Clearer mismatch errors**: `todo_update` lists current IDs when a patch
|
|
11
|
+
targets an unknown ID, so the model can self-correct without guessing.
|
|
12
|
+
- Legacy UUID IDs in existing sessions remain valid; only newly assigned IDs
|
|
13
|
+
use the short format.
|
|
14
|
+
|
|
15
|
+
## 0.3.4 (2026-07-17)
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **IDs visible in tool text**: `formatPlainTodoLine` and `todo_update` results
|
|
20
|
+
now include stable IDs so models can copy them for follow-up patches.
|
|
21
|
+
- **`todo_update` guidance**: Description warns that IDs can change after
|
|
22
|
+
`todo_write`; always `todo_read` before patching when unsure.
|
|
23
|
+
|
|
24
|
+
## 0.3.3 (2026-07-16)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Overlay heading**: Shows open, running, and completed counts. Completed
|
|
29
|
+
counts include only `completed` todos; `cancelled` todos remain excluded.
|
|
30
|
+
- **Simplified status display**: Removed the ANSI progress bar and `done/total`
|
|
31
|
+
counter from the overlay.
|
|
32
|
+
- **Documentation and screenshot**: Updated the README, screenshot PNG, and
|
|
33
|
+
screenshot HTML source to match the new heading.
|
|
34
|
+
|
|
35
|
+
## 0.3.2 (2026-07-16)
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **Todo ID integrity**: Prevented id-less todos from taking IDs explicitly
|
|
40
|
+
retained by another todo during a full `todo_write` replacement.
|
|
41
|
+
- **Diagnostics**: Added ID-integrity reporting and `repair_needed` status to
|
|
42
|
+
`todo_diagnose`.
|
|
43
|
+
- **Mutation bounds**: Capped todo mutations and persisted snapshots at 200
|
|
44
|
+
items.
|
|
45
|
+
|
|
46
|
+
## 0.3.1 (2026-07-15)
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **Durable `todo_update` replay**: Replays successful `todo_update` tool results
|
|
51
|
+
as a fallback when compaction retains tool messages but prunes older custom
|
|
52
|
+
state entries. Verified in a live Pi `write → update → /compact → /reload →
|
|
53
|
+
todo_diagnose` lifecycle.
|
|
54
|
+
- **Completion reminders**: Completion and cadence reminders now direct agents
|
|
55
|
+
to patch known IDs with `todo_update`, rather than unnecessarily replacing
|
|
56
|
+
the full checklist. A successful update also clears pending intent nudges.
|
|
57
|
+
- **Timeline overlay boundaries**: The overlay preserves checklist order,
|
|
58
|
+
pins an out-of-view active item, and never renders more than `maxLines`.
|
|
59
|
+
|
|
60
|
+
## 0.3.0 (2026-07-15)
|
|
61
|
+
|
|
62
|
+
- **ID invariant hardening**: `validateTodoWrite` rejects items with explicit `id`
|
|
63
|
+
that doesn't exist in current list. New items must omit `id` (auto-assign).
|
|
64
|
+
- **`ensureTodoIds` tuple matching**: Matches by `(content+status+priority)` first,
|
|
65
|
+
then by unique content-only. Duplicate-content items get fresh UUIDs instead
|
|
66
|
+
of risking mis-assignment.
|
|
67
|
+
- **`todo_update` atomicity**: Stale-ctx returns error, real errors propagate,
|
|
68
|
+
setTodos after appendEntry — matching todo_write guarantees.
|
|
69
|
+
- **Status-first shared sort**: `statusPrioritySort()` extracted and shared by
|
|
70
|
+
`formatTodoListText` (tool output) and `selectOverlayLayout` (overlay) —
|
|
71
|
+
in_progress → pending → terminal, regardless of overflow.
|
|
72
|
+
- **Fixed double-count bug**: Terminal items no longer counted in both `+N done`
|
|
73
|
+
AND `+N more` on overlay overflow.
|
|
74
|
+
- **Lifecycle E2E test**: Write → update → tree → compact → shutdown → restart,
|
|
75
|
+
verifying stable IDs and status throughout.
|
|
76
|
+
- **125 tests**, typecheck clean, CI green (Node 20 + 22).
|
|
77
|
+
|
|
78
|
+
## 0.2.9 (2026-07-15)
|
|
79
|
+
|
|
80
|
+
- **Atomic write ordering**: `setTodos()` happens *after* `pi.appendEntry()`
|
|
81
|
+
succeeds. If appendEntry throws (stale-ctx or persistence error), the in-memory
|
|
82
|
+
store is never mutated — no more desync between live state and durable state.
|
|
83
|
+
- **Stale-ctx now returns error**: Instead of silently swallowing "stale after
|
|
84
|
+
session replacement", the tool returns an error so the LLM knows the write
|
|
85
|
+
was not committed.
|
|
86
|
+
- **4 atomicity tests**: Mock appendEntry rejects with stale-ctx / persistence
|
|
87
|
+
errors and asserts store is unchanged. 102/102 tests passing.
|
|
88
|
+
|
|
89
|
+
## 0.2.8 (2026-07-15)
|
|
90
|
+
|
|
91
|
+
- **Replay**: revert broken timestamp-based hardening; Pi guarantees getBranch() is chronological, so original last-entry-wins algorithm is correct and simpler.
|
|
92
|
+
- **appendEntry**: only swallow stale-ctx errors; real persistence/disk errors now propagate instead of being silently caught.
|
|
93
|
+
- **Tests**: remove 6 broken mock-timestamp tests that never reflected Pi runtime (timestamp is ISO string, not number). Keep 9 correct persistence roundtrip tests.
|
|
94
|
+
- **Tests**: 96/96 pass, typecheck clean.
|
|
95
|
+
|
|
96
|
+
## 0.2.7 (2026-07-15)
|
|
97
|
+
|
|
98
|
+
- Background-color progress bar using ANSI reverse video — theme-driven (accent/muted), no alignment issues.
|
|
99
|
+
- Progress format: done/total with background-color bar.
|
|
100
|
+
- 14 integration tests for write->read->overlay roundtrip.
|
|
101
|
+
|
|
102
|
+
## 0.2.6 (2026-07-15)
|
|
103
|
+
|
|
104
|
+
## 0.2.5 (2026-07-15)
|
|
105
|
+
|
|
106
|
+
- Rework overlay heading: remove progress bar entirely, simplify to count-only.
|
|
107
|
+
- Fix docstring examples to match new heading format.
|
|
108
|
+
- README: update overlay section to remove progress bar mentions.
|
|
109
|
+
|
|
110
|
+
## 0.2.4 (2026-07-15)
|
|
111
|
+
|
|
112
|
+
- Priority sorting in overlay (high→medium→low).
|
|
113
|
+
- Progress bar in heading (Unicode blocks).
|
|
114
|
+
- Collapsible completed items (+N done).
|
|
115
|
+
- CI: GitHub Actions (test + typecheck on push, Node 18/20/22).
|
|
116
|
+
- README: npm version, license, CI badges.
|
|
117
|
+
|
|
118
|
+
## 0.2.3 (2026-07-15)
|
|
119
|
+
|
|
120
|
+
- Tool rename: todowrite → todo_write, todoread → todo_read (snake_case).
|
|
121
|
+
- renderCall/renderResult polish: accent color for item count.
|
|
122
|
+
- Overlay heading: "X open / Y total" instead of "N todo(s)".
|
|
123
|
+
|
|
124
|
+
## 0.2.2 (2026-07-15)
|
|
125
|
+
|
|
126
|
+
- Fix idle reminder text: "Open items still pending" instead of "0 open".
|
|
127
|
+
- Reminder hides for all-terminal lists.
|
|
128
|
+
- completion update reminder copy polish.
|
|
129
|
+
|
|
130
|
+
## 0.2.1 (2026-07-15)
|
|
131
|
+
|
|
132
|
+
- First usable release.
|
|
133
|
+
- todo_write, todo_read tools.
|
|
134
|
+
- Overlay with OpenCode-style markers.
|
|
135
|
+
- Persistence via custom entry + toolResult details.
|
|
136
|
+
- Cold start heuristics (multi-step, VI prompts, fix/polish/setup).
|
|
137
|
+
- Completion nudge when user says done.
|
|
138
|
+
- Idle reminder cadence every ~4 turns.
|
|
139
|
+
- 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_update` / `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).
|
|
9
|
+
Adds `todo_write` / `todo_update` / `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
|
|
|
@@ -45,35 +45,35 @@ Rules enforced by the tool:
|
|
|
45
45
|
- Exactly **one** `in_progress` allowed (hard reject if more)
|
|
46
46
|
- `content` required (non-empty after sanitize); max **500** chars (longer values truncated)
|
|
47
47
|
- `priority` required: `high` | `medium` | `low`
|
|
48
|
-
- Status: `pending` | `in_progress` | `completed` | `cancelled`
|
|
49
|
-
- **ID rule:** omit `id` for a new item; the system assigns
|
|
50
|
-
- For changed, repeated, or long/truncated content, include the exact existing ID rather than relying on automatic content matching.
|
|
51
|
-
- Do not call `todo_write` and a `todo_update` that needs its IDs in the same parallel batch. Wait for the write result, then use returned IDs or call `todo_read`.
|
|
52
|
-
- A mutation can contain at most **200** todos/updates.
|
|
53
|
-
- Array order is the workflow timeline. Keep existing positions when statuses change; only add or reorder items intentionally.
|
|
48
|
+
- Status: `pending` | `in_progress` | `completed` | `cancelled`
|
|
49
|
+
- **ID rule:** omit `id` for a new item; the system assigns a short sequential ID (`t1`, `t2`, …). Only include an ID returned by `todo_read` when retaining an existing item. Never invent an ID. Replacing the list does not inherently reset IDs: matching existing items can retain them.
|
|
50
|
+
- For changed, repeated, or long/truncated content, include the exact existing ID rather than relying on automatic content matching.
|
|
51
|
+
- Do not call `todo_write` and a `todo_update` that needs its IDs in the same parallel batch. Wait for the write result, then use returned IDs or call `todo_read`.
|
|
52
|
+
- A mutation can contain at most **200** todos/updates.
|
|
53
|
+
- Array order is the workflow timeline. Keep existing positions when statuses change; only add or reorder items intentionally.
|
|
54
54
|
- Tool text echo caps at **40** lines (`+N more` in the text body; full list still in `details` / JSON)
|
|
55
55
|
|
|
56
|
-
### `todo_update`
|
|
57
|
-
|
|
58
|
-
Patch existing todos by stable ID without replacing the list or changing its order. `id` is required, must be a non-empty string, and must match a current todo; use `todo_read`
|
|
59
|
-
|
|
60
|
-
If an older session returns a todo without `id`, it cannot be patched with `todo_update`. Call `todo_write` with that item but omit `id` to assign one, then use `todo_update` normally.
|
|
61
|
-
|
|
62
|
-
```json
|
|
63
|
-
{
|
|
64
|
-
"updates": [
|
|
65
|
-
{ "id": "
|
|
66
|
-
]
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### `todo_read`
|
|
71
|
-
|
|
72
|
-
Returns the current list as text + JSON. Prefer the overlay for at-a-glance status; use it to obtain stable IDs before `todo_update`, and avoid calling it in the same parallel batch as a todo mutation.
|
|
73
|
-
|
|
74
|
-
### `todo_diagnose`
|
|
75
|
-
|
|
76
|
-
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`, `mismatch`, or `repair_needed` when duplicate/missing IDs are found; it never changes todos.
|
|
56
|
+
### `todo_update`
|
|
57
|
+
|
|
58
|
+
Patch existing todos by short stable ID (`t1`, `t2`, …) without replacing the list or changing its order. `id` is required, must be a non-empty string, and must match a current todo exactly; use `todo_read` (or the IDs shown in the last write/update text) first. This tool never deletes items.
|
|
59
|
+
|
|
60
|
+
If an older session returns a todo without `id`, it cannot be patched with `todo_update`. Call `todo_write` with that item but omit `id` to assign one, then use `todo_update` normally.
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"updates": [
|
|
65
|
+
{ "id": "t1", "status": "completed" }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `todo_read`
|
|
71
|
+
|
|
72
|
+
Returns the current list as text + JSON. Prefer the overlay for at-a-glance status; use it to obtain stable IDs before `todo_update`, and avoid calling it in the same parallel batch as a todo mutation.
|
|
73
|
+
|
|
74
|
+
### `todo_diagnose`
|
|
75
|
+
|
|
76
|
+
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`, `mismatch`, or `repair_needed` when duplicate/missing IDs are found; it never changes todos.
|
|
77
77
|
|
|
78
78
|
## Overlay
|
|
79
79
|
|
|
@@ -81,16 +81,16 @@ Shown above the editor while any **open** todo remains (`pending` / `in_progress
|
|
|
81
81
|
|
|
82
82
|
Hidden when the list is empty or every item is `completed` / `cancelled`.
|
|
83
83
|
|
|
84
|
-
Heading shows open, running, and completed counts, e.g. `# Todos (3 open, 1 running, 1 completed)`:
|
|
85
|
-
|
|
86
|
-
- **open** = `pending` + `in_progress`
|
|
87
|
-
- **running** = `in_progress` only (0 or 1 after a valid write)
|
|
88
|
-
- **completed** = `completed` only; `cancelled` todos are not counted
|
|
89
|
-
|
|
90
|
-
Items always stay in the array's workflow order; status changes only their marker/color.
|
|
91
|
-
When space is tight, the overlay shows the earliest checklist items and `+N more`. If the active item is outside that prefix, it is repeated as `Active: [•] …` rather than moved ahead of earlier work.
|
|
92
|
-
A blank line separates the heading from the first todo row for visual breathing room.
|
|
93
|
-
Successful `todo_write` and `todo_update` results display `✓ Saved`, meaning the durable checkpoint was accepted before the in-memory snapshot was updated.
|
|
84
|
+
Heading shows open, running, and completed counts, e.g. `# Todos (3 open, 1 running, 1 completed)`:
|
|
85
|
+
|
|
86
|
+
- **open** = `pending` + `in_progress`
|
|
87
|
+
- **running** = `in_progress` only (0 or 1 after a valid write)
|
|
88
|
+
- **completed** = `completed` only; `cancelled` todos are not counted
|
|
89
|
+
|
|
90
|
+
Items always stay in the array's workflow order; status changes only their marker/color.
|
|
91
|
+
When space is tight, the overlay shows the earliest checklist items and `+N more`. If the active item is outside that prefix, it is repeated as `Active: [•] …` rather than moved ahead of earlier work.
|
|
92
|
+
A blank line separates the heading from the first todo row for visual breathing room.
|
|
93
|
+
Successful `todo_write` and `todo_update` results display `✓ Saved`, meaning the durable checkpoint was accepted before the in-memory snapshot was updated.
|
|
94
94
|
|
|
95
95
|
## Development
|
|
96
96
|
|
package/package.json
CHANGED
package/src/format.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { TodoItem, TodoStatus } from "./types.js";
|
|
4
|
-
import { MAX_OVERLAY_LINES, MAX_RESULT_LINES } from "./types.js";
|
|
5
|
-
import { countCompletedTodos, countOpenTodos, countRunningTodos, hasOpenTodos } from "./validate.js";
|
|
3
|
+
import type { TodoItem, TodoStatus } from "./types.js";
|
|
4
|
+
import { MAX_OVERLAY_LINES, MAX_RESULT_LINES } from "./types.js";
|
|
5
|
+
import { countCompletedTodos, countOpenTodos, countRunningTodos, hasOpenTodos } from "./validate.js";
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
export function getTodoMarker(status: TodoStatus): string {
|
|
@@ -19,22 +19,23 @@ export function getTodoMarker(status: TodoStatus): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function formatPlainTodoLine(todo: TodoItem): string {
|
|
22
|
-
|
|
22
|
+
const prefix = todo.id ? `${todo.id} ` : "";
|
|
23
|
+
return `${getTodoMarker(todo.status)} ${prefix}${todo.content}`;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** Compact checklist for tool responses; caps lines to keep LLM context small. */
|
|
26
27
|
export function formatTodoListText(todos: readonly TodoItem[], summary: string): string {
|
|
27
28
|
if (todos.length === 0) return summary;
|
|
28
29
|
|
|
29
|
-
// The list is a workflow timeline. Never reshuffle completed work after the
|
|
30
|
-
// next task just because its status changed.
|
|
31
|
-
const ordered = [...todos];
|
|
30
|
+
// The list is a workflow timeline. Never reshuffle completed work after the
|
|
31
|
+
// next task just because its status changed.
|
|
32
|
+
const ordered = [...todos];
|
|
32
33
|
|
|
33
34
|
if (todos.length <= MAX_RESULT_LINES) {
|
|
34
|
-
return [summary, ...ordered.map(formatPlainTodoLine)].join("\n");
|
|
35
|
+
return [summary, ...ordered.map(formatPlainTodoLine)].join("\n");
|
|
35
36
|
}
|
|
36
|
-
const shown = ordered.slice(0, MAX_RESULT_LINES);
|
|
37
|
-
const hidden = ordered.length - MAX_RESULT_LINES;
|
|
37
|
+
const shown = ordered.slice(0, MAX_RESULT_LINES);
|
|
38
|
+
const hidden = ordered.length - MAX_RESULT_LINES;
|
|
38
39
|
return [
|
|
39
40
|
summary,
|
|
40
41
|
...shown.map(formatPlainTodoLine),
|
|
@@ -58,58 +59,58 @@ export function shouldShowOverlay(todos: readonly TodoItem[]): boolean {
|
|
|
58
59
|
return hasOpenTodos(todos);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
export interface OverlayLayout {
|
|
62
|
-
visible: TodoItem[];
|
|
63
|
-
/** Active item repeated below the timeline when it is outside the visible prefix. */
|
|
64
|
-
pinnedActive?: TodoItem;
|
|
65
|
-
hiddenCount: number;
|
|
66
|
-
/** Retained for consumers of the layout API; terminal items are no longer regrouped. */
|
|
67
|
-
terminalCount: number;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Fit the checklist timeline into the overlay without sorting by status or
|
|
72
|
-
* priority. When the active task falls outside the visible prefix, repeat it
|
|
73
|
-
* as a pinned "Active:" row rather than moving it ahead of earlier work.
|
|
74
|
-
*/
|
|
62
|
+
export interface OverlayLayout {
|
|
63
|
+
visible: TodoItem[];
|
|
64
|
+
/** Active item repeated below the timeline when it is outside the visible prefix. */
|
|
65
|
+
pinnedActive?: TodoItem;
|
|
66
|
+
hiddenCount: number;
|
|
67
|
+
/** Retained for consumers of the layout API; terminal items are no longer regrouped. */
|
|
68
|
+
terminalCount: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fit the checklist timeline into the overlay without sorting by status or
|
|
73
|
+
* priority. When the active task falls outside the visible prefix, repeat it
|
|
74
|
+
* as a pinned "Active:" row rather than moving it ahead of earlier work.
|
|
75
|
+
*/
|
|
75
76
|
export function selectOverlayLayout(
|
|
76
77
|
todos: readonly TodoItem[],
|
|
77
|
-
maxLines: number = MAX_OVERLAY_LINES,
|
|
78
|
-
): OverlayLayout {
|
|
79
|
-
if (!shouldShowOverlay(todos)) {
|
|
80
|
-
return { visible: [], hiddenCount: 0, terminalCount: 0 };
|
|
78
|
+
maxLines: number = MAX_OVERLAY_LINES,
|
|
79
|
+
): OverlayLayout {
|
|
80
|
+
if (!shouldShowOverlay(todos)) {
|
|
81
|
+
return { visible: [], hiddenCount: 0, terminalCount: 0 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const bodyBudget = Math.max(1, maxLines - 1);
|
|
85
|
+
if (todos.length <= bodyBudget) {
|
|
86
|
+
// All fit — show the canonical checklist sequence unchanged.
|
|
87
|
+
return { visible: [...todos], hiddenCount: 0, terminalCount: 0 };
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
visible = todos.slice(0, visibleCapacity);
|
|
99
|
-
pinnedActive = active;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const hiddenCount = todos.length - visible.length - (pinnedActive ? 1 : 0);
|
|
103
|
-
return { visible, pinnedActive, hiddenCount, terminalCount: 0 };
|
|
90
|
+
const active = todos.find((todo) => todo.status === "in_progress");
|
|
91
|
+
// Reserve one row for the overflow summary. If the active task lies outside
|
|
92
|
+
// the timeline prefix, reserve another row to pin it without reordering.
|
|
93
|
+
let visibleCapacity = Math.max(0, bodyBudget - 1);
|
|
94
|
+
let visible = todos.slice(0, visibleCapacity);
|
|
95
|
+
let pinnedActive = active && !visible.includes(active) ? active : undefined;
|
|
96
|
+
|
|
97
|
+
if (pinnedActive) {
|
|
98
|
+
visibleCapacity = Math.max(0, bodyBudget - 2);
|
|
99
|
+
visible = todos.slice(0, visibleCapacity);
|
|
100
|
+
pinnedActive = active;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const hiddenCount = todos.length - visible.length - (pinnedActive ? 1 : 0);
|
|
104
|
+
return { visible, pinnedActive, hiddenCount, terminalCount: 0 };
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
export interface RenderOverlayOptions {
|
|
107
108
|
maxLines?: number;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
/**
|
|
111
|
-
* The overlay is hidden when no work remains open.
|
|
112
|
-
*/
|
|
111
|
+
/**
|
|
112
|
+
* The overlay is hidden when no work remains open.
|
|
113
|
+
*/
|
|
113
114
|
export function renderOverlayLines(
|
|
114
115
|
todos: readonly TodoItem[],
|
|
115
116
|
theme: Theme,
|
|
@@ -118,33 +119,33 @@ export function renderOverlayLines(
|
|
|
118
119
|
): string[] {
|
|
119
120
|
if (!shouldShowOverlay(todos)) return [];
|
|
120
121
|
|
|
121
|
-
const maxLines = Math.max(1, options.maxLines ?? MAX_OVERLAY_LINES);
|
|
122
|
-
const truncate = (line: string) => truncateToWidth(line, width, "…");
|
|
123
|
-
const open = countOpenTodos(todos);
|
|
124
|
-
const running = countRunningTodos(todos);
|
|
125
|
-
const completed = countCompletedTodos(todos);
|
|
126
|
-
const heading = truncate(
|
|
127
|
-
theme.fg("accent", theme.bold("# Todos")) +
|
|
128
|
-
theme.fg("dim", ` (${open} open, ${running} running, ${completed} completed)`),
|
|
129
|
-
);
|
|
130
|
-
|
|
122
|
+
const maxLines = Math.max(1, options.maxLines ?? MAX_OVERLAY_LINES);
|
|
123
|
+
const truncate = (line: string) => truncateToWidth(line, width, "…");
|
|
124
|
+
const open = countOpenTodos(todos);
|
|
125
|
+
const running = countRunningTodos(todos);
|
|
126
|
+
const completed = countCompletedTodos(todos);
|
|
127
|
+
const heading = truncate(
|
|
128
|
+
theme.fg("accent", theme.bold("# Todos")) +
|
|
129
|
+
theme.fg("dim", ` (${open} open, ${running} running, ${completed} completed)`),
|
|
130
|
+
);
|
|
131
|
+
|
|
131
132
|
// Small gap between heading and first row — budget -1 to account for the blank line
|
|
132
133
|
const layout = selectOverlayLayout(todos, Math.max(3, maxLines - 1));
|
|
133
134
|
const lines: string[] = [heading, ""];
|
|
134
135
|
|
|
135
|
-
for (const todo of layout.visible) {
|
|
136
|
-
lines.push(truncate(formatThemedTodoLine(todo, theme)));
|
|
137
|
-
}
|
|
138
|
-
if (layout.pinnedActive) {
|
|
139
|
-
lines.push(
|
|
140
|
-
truncate(theme.fg("warning", `Active: ${formatPlainTodoLine(layout.pinnedActive)}`)),
|
|
141
|
-
);
|
|
142
|
-
}
|
|
136
|
+
for (const todo of layout.visible) {
|
|
137
|
+
lines.push(truncate(formatThemedTodoLine(todo, theme)));
|
|
138
|
+
}
|
|
139
|
+
if (layout.pinnedActive) {
|
|
140
|
+
lines.push(
|
|
141
|
+
truncate(theme.fg("warning", `Active: ${formatPlainTodoLine(layout.pinnedActive)}`)),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
143
144
|
if (layout.hiddenCount > 0) {
|
|
144
145
|
lines.push(truncate(theme.fg("dim", `+${layout.hiddenCount} more`)));
|
|
145
146
|
}
|
|
146
|
-
lines.push("");
|
|
147
|
-
// Layout reserves content rows, but the heading and trailing spacer are
|
|
148
|
-
// rendered here. Enforce the public maxLines contract at the final boundary.
|
|
149
|
-
return lines.slice(0, maxLines);
|
|
150
|
-
}
|
|
147
|
+
lines.push("");
|
|
148
|
+
// Layout reserves content rows, but the heading and trailing spacer are
|
|
149
|
+
// rendered here. Enforce the public maxLines contract at the final boundary.
|
|
150
|
+
return lines.slice(0, maxLines);
|
|
151
|
+
}
|
package/src/prompt.ts
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
export const TODOWRITE_DESCRIPTION = `Create and maintain a structured task list for the current coding session. Tracks progress, organizes multi-step work, and surfaces status in an overlay above the editor.
|
|
2
|
-
|
|
3
|
-
## When to Use This Tool
|
|
4
|
-
|
|
5
|
-
Use this tool **proactively** — call todo_write **before** starting the work in these scenarios:
|
|
6
|
-
|
|
7
|
-
- Complex multi-step tasks — when work needs 3+ distinct steps
|
|
8
|
-
- Non-trivial work that benefits from planning (implement, refactor, debug, audit)
|
|
9
|
-
- **Explain / explore / review a codebase or feature** across multiple files or modules — break the walkthrough into todos first
|
|
10
|
-
- User provides multiple tasks (numbered or comma-separated) or asks for a todo list
|
|
11
|
-
- After receiving new instructions — capture requirements as todos before coding or explaining
|
|
12
|
-
- When you start a task — mark it \`in_progress\` (exactly ONE) BEFORE beginning work
|
|
13
|
-
- When you finish a task — mark it \`completed\` immediately (same turn you finish), then advance the next pending item to \`in_progress\`
|
|
14
|
-
- When the user says work is done / approved / finished — update the matching item to \`completed\` via todo_write before continuing
|
|
15
|
-
|
|
16
|
-
## When NOT to Use This Tool
|
|
17
|
-
|
|
18
|
-
Skip when:
|
|
19
|
-
- There is only a single, straightforward step (one file glance, one short answer)
|
|
20
|
-
- A one-line factual question with no multi-step plan
|
|
21
|
-
- Tracking truly adds no organizational value
|
|
22
|
-
|
|
23
|
-
Do **not** skip just because the request is "explain" or "review" — if the answer needs multiple steps or sections, use todo_write.
|
|
24
|
-
|
|
25
|
-
## States
|
|
26
|
-
|
|
27
|
-
- \`pending\` — not started
|
|
28
|
-
- \`in_progress\` — actively working (exactly ONE at a time; the tool rejects >1)
|
|
29
|
-
- \`completed\` — finished successfully
|
|
30
|
-
- \`cancelled\` — no longer needed
|
|
31
|
-
|
|
32
|
-
## Rules
|
|
33
|
-
|
|
34
|
-
- Each call **REPLACES** the entire list (full replace). Always pass the complete todos array.
|
|
35
|
-
- **ID rule:** omit \`id\` for every new item — the system assigns
|
|
36
|
-
- Do not call \`todo_write\` and a dependent \`todo_update\` in the same parallel batch. Complete the write, then use its returned IDs (or \`todo_read\`) for the update.
|
|
37
|
-
- Update status in real time; don't batch completions across multiple finished steps.
|
|
38
|
-
- Mark \`completed\` only after the work is actually done (including verification) — never on intent alone.
|
|
39
|
-
- Keep exactly one \`in_progress\` while actively working. Never leave a stale \`in_progress\` after that step is finished.
|
|
40
|
-
- When using \`todo_write\`, after marking an item \`completed\`, set the next actionable item to \`in_progress\` in the same full-replace call when you are continuing.
|
|
41
|
-
- Array order is the workflow timeline. Preserve existing item positions as statuses change; add or reorder items only deliberately.
|
|
42
|
-
- If blocked, keep the active item \`in_progress\` and add a follow-up todo for the blocker.
|
|
43
|
-
- Items should be specific and actionable.
|
|
44
|
-
- Do not call todo_write and todo_read in the same parallel tool batch — write first, then read later if needed.
|
|
45
|
-
- Prefer the live overlay for status; use todo_read only when you need the JSON snapshot.`;
|
|
46
|
-
|
|
47
|
-
export const TODOREAD_DESCRIPTION =
|
|
48
|
-
"Read the current session todo list. Returns JSON of all todos with status, priority, and stable IDs. Prefer the overlay for at-a-glance status; call after todo_write settles (not in the same parallel batch). A legacy item without an ID cannot be patched by todo_update; rewrite it with todo_write and omit id to assign one.";
|
|
49
|
-
|
|
50
|
-
export const TODODIAGNOSE_DESCRIPTION =
|
|
51
|
-
"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.";
|
|
52
|
-
|
|
53
|
-
export const TODOWRITE_GUIDELINES = [
|
|
54
|
-
"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.",
|
|
55
|
-
"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.",
|
|
56
|
-
"ID rule: for todo_write, omit id for new items so the system assigns
|
|
57
|
-
"Never call todo_write and a todo_update that depends on its IDs in the same parallel batch. Wait for todo_write to finish, then use its returned IDs or call todo_read. If todo_read shows a legacy item without id, rewrite it with todo_write and omit id to assign one before using todo_update.",
|
|
58
|
-
"When finishing a step or when the user confirms done, use todo_update for an ID-based patch when possible; use todo_write only when replacing the full checklist. Advance the next pending item in the same mutation if work continues.",
|
|
59
|
-
"Treat todo array order as the workflow timeline: preserve positions when updating statuses, and only add or reorder items intentionally.",
|
|
60
|
-
"Use todo_write for full replacement and todo_update for ID-based patches; todo_read exposes IDs and todo_diagnose is read-only troubleshooting.",
|
|
61
|
-
"Do not call todo_write and todo_read in the same parallel tool batch.",
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Appended to the system prompt on every agent start (pi-todotools pattern).
|
|
66
|
-
* Fixes cold-start: tool description alone is easy for models to ignore.
|
|
67
|
-
* Keep short — token cost every turn.
|
|
68
|
-
*/
|
|
69
|
-
export const TASK_MANAGEMENT_SECTION = `
|
|
70
|
-
<Task_Management>
|
|
71
|
-
todo_write/todo_update/todo_read are the coordination layer for multi-step work. Use todo_write for the initial/full checklist and todo_update for a targeted patch by stable ID. In todo_write, omit id for new items; only retain an exact ID returned by todo_read for an existing item. In todo_update, id is required and must match a current todo. Do not call a write and an update that depends on its IDs in the same parallel batch: wait for the write result first. The TUI overlay only appears after a todo mutation — without it the user cannot see plan/progress.
|
|
72
|
-
|
|
73
|
-
Required cold start: for explain/explore/review/implement/refactor/audit/debug/fix/polish/setup of a codebase, feature, UI, or multi-file ask — your FIRST tool call must be todo_write with a short checklist (WHAT/WHERE), exactly one in_progress, then continue. Do not start with read/bash/search alone on those asks.
|
|
74
|
-
|
|
75
|
-
Ongoing: mark completed immediately when a step finishes; use todo_update for a targeted ID-based patch or todo_write for a full replacement. Advance the next pending item in the same mutation; when the user says done/approved, update that turn.
|
|
76
|
-
|
|
77
|
-
Skip only single trivial Q&A (one short fact, greeting). Multi-step Vietnamese or English asks (giải thích, chỉnh, bổ sung, help me, …) still need todos first.
|
|
78
|
-
</Task_Management>
|
|
79
|
-
`;
|
|
80
|
-
|
|
81
|
-
/** Extra systemPrompt boost when prompt-intent detects multi-step + empty list. */
|
|
82
|
-
export const COLD_START_BOOST = `
|
|
83
|
-
<Task_Management_Priority>
|
|
84
|
-
COLD START ACTIVE: the current user message is multi-step and there is no open todo list. Call todo_write before any other tool. Prefer 3–8 concrete checklist items. Keep exactly one in_progress.
|
|
85
|
-
</Task_Management_Priority>
|
|
86
|
-
`;
|
|
1
|
+
export const TODOWRITE_DESCRIPTION = `Create and maintain a structured task list for the current coding session. Tracks progress, organizes multi-step work, and surfaces status in an overlay above the editor.
|
|
2
|
+
|
|
3
|
+
## When to Use This Tool
|
|
4
|
+
|
|
5
|
+
Use this tool **proactively** — call todo_write **before** starting the work in these scenarios:
|
|
6
|
+
|
|
7
|
+
- Complex multi-step tasks — when work needs 3+ distinct steps
|
|
8
|
+
- Non-trivial work that benefits from planning (implement, refactor, debug, audit)
|
|
9
|
+
- **Explain / explore / review a codebase or feature** across multiple files or modules — break the walkthrough into todos first
|
|
10
|
+
- User provides multiple tasks (numbered or comma-separated) or asks for a todo list
|
|
11
|
+
- After receiving new instructions — capture requirements as todos before coding or explaining
|
|
12
|
+
- When you start a task — mark it \`in_progress\` (exactly ONE) BEFORE beginning work
|
|
13
|
+
- When you finish a task — mark it \`completed\` immediately (same turn you finish), then advance the next pending item to \`in_progress\`
|
|
14
|
+
- When the user says work is done / approved / finished — update the matching item to \`completed\` via todo_write before continuing
|
|
15
|
+
|
|
16
|
+
## When NOT to Use This Tool
|
|
17
|
+
|
|
18
|
+
Skip when:
|
|
19
|
+
- There is only a single, straightforward step (one file glance, one short answer)
|
|
20
|
+
- A one-line factual question with no multi-step plan
|
|
21
|
+
- Tracking truly adds no organizational value
|
|
22
|
+
|
|
23
|
+
Do **not** skip just because the request is "explain" or "review" — if the answer needs multiple steps or sections, use todo_write.
|
|
24
|
+
|
|
25
|
+
## States
|
|
26
|
+
|
|
27
|
+
- \`pending\` — not started
|
|
28
|
+
- \`in_progress\` — actively working (exactly ONE at a time; the tool rejects >1)
|
|
29
|
+
- \`completed\` — finished successfully
|
|
30
|
+
- \`cancelled\` — no longer needed
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- Each call **REPLACES** the entire list (full replace). Always pass the complete todos array.
|
|
35
|
+
- **ID rule:** omit \`id\` for every new item — the system assigns a short ID (\`t1\`, \`t2\`, …). Only preserve an \`id\` returned by \`todo_read\` for an item that already exists; never invent an ID. For changed, repeated, or long/truncated content, preserve the exact existing ID instead of relying on content matching. Full replacement does not inherently reset IDs: matching existing items can retain them.
|
|
36
|
+
- Do not call \`todo_write\` and a dependent \`todo_update\` in the same parallel batch. Complete the write, then use its returned IDs (or \`todo_read\`) for the update.
|
|
37
|
+
- Update status in real time; don't batch completions across multiple finished steps.
|
|
38
|
+
- Mark \`completed\` only after the work is actually done (including verification) — never on intent alone.
|
|
39
|
+
- Keep exactly one \`in_progress\` while actively working. Never leave a stale \`in_progress\` after that step is finished.
|
|
40
|
+
- When using \`todo_write\`, after marking an item \`completed\`, set the next actionable item to \`in_progress\` in the same full-replace call when you are continuing.
|
|
41
|
+
- Array order is the workflow timeline. Preserve existing item positions as statuses change; add or reorder items only deliberately.
|
|
42
|
+
- If blocked, keep the active item \`in_progress\` and add a follow-up todo for the blocker.
|
|
43
|
+
- Items should be specific and actionable.
|
|
44
|
+
- Do not call todo_write and todo_read in the same parallel tool batch — write first, then read later if needed.
|
|
45
|
+
- Prefer the live overlay for status; use todo_read only when you need the JSON snapshot.`;
|
|
46
|
+
|
|
47
|
+
export const TODOREAD_DESCRIPTION =
|
|
48
|
+
"Read the current session todo list. Returns JSON of all todos with status, priority, and stable IDs. Prefer the overlay for at-a-glance status; call after todo_write settles (not in the same parallel batch). A legacy item without an ID cannot be patched by todo_update; rewrite it with todo_write and omit id to assign one.";
|
|
49
|
+
|
|
50
|
+
export const TODODIAGNOSE_DESCRIPTION =
|
|
51
|
+
"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.";
|
|
52
|
+
|
|
53
|
+
export const TODOWRITE_GUIDELINES = [
|
|
54
|
+
"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.",
|
|
55
|
+
"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.",
|
|
56
|
+
"ID rule: for todo_write, omit id for new items so the system assigns a short ID (t1, t2, …). Supply an id only to preserve an existing item, using the exact ID from todo_read; never invent IDs. For changed, repeated, or long/truncated content, preserve that exact ID rather than relying on content matching. For todo_update, id is required and must match a current todo.",
|
|
57
|
+
"Never call todo_write and a todo_update that depends on its IDs in the same parallel batch. Wait for todo_write to finish, then use its returned IDs or call todo_read. If todo_read shows a legacy item without id, rewrite it with todo_write and omit id to assign one before using todo_update.",
|
|
58
|
+
"When finishing a step or when the user confirms done, use todo_update for an ID-based patch when possible; use todo_write only when replacing the full checklist. Advance the next pending item in the same mutation if work continues.",
|
|
59
|
+
"Treat todo array order as the workflow timeline: preserve positions when updating statuses, and only add or reorder items intentionally.",
|
|
60
|
+
"Use todo_write for full replacement and todo_update for ID-based patches; todo_read exposes IDs and todo_diagnose is read-only troubleshooting.",
|
|
61
|
+
"Do not call todo_write and todo_read in the same parallel tool batch.",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Appended to the system prompt on every agent start (pi-todotools pattern).
|
|
66
|
+
* Fixes cold-start: tool description alone is easy for models to ignore.
|
|
67
|
+
* Keep short — token cost every turn.
|
|
68
|
+
*/
|
|
69
|
+
export const TASK_MANAGEMENT_SECTION = `
|
|
70
|
+
<Task_Management>
|
|
71
|
+
todo_write/todo_update/todo_read are the coordination layer for multi-step work. Use todo_write for the initial/full checklist and todo_update for a targeted patch by stable ID. In todo_write, omit id for new items (system assigns t1, t2, …); only retain an exact ID returned by todo_read for an existing item. In todo_update, id is required and must match a current todo. Do not call a write and an update that depends on its IDs in the same parallel batch: wait for the write result first. The TUI overlay only appears after a todo mutation — without it the user cannot see plan/progress.
|
|
72
|
+
|
|
73
|
+
Required cold start: for explain/explore/review/implement/refactor/audit/debug/fix/polish/setup of a codebase, feature, UI, or multi-file ask — your FIRST tool call must be todo_write with a short checklist (WHAT/WHERE), exactly one in_progress, then continue. Do not start with read/bash/search alone on those asks.
|
|
74
|
+
|
|
75
|
+
Ongoing: mark completed immediately when a step finishes; use todo_update for a targeted ID-based patch or todo_write for a full replacement. Advance the next pending item in the same mutation; when the user says done/approved, update that turn.
|
|
76
|
+
|
|
77
|
+
Skip only single trivial Q&A (one short fact, greeting). Multi-step Vietnamese or English asks (giải thích, chỉnh, bổ sung, help me, …) still need todos first.
|
|
78
|
+
</Task_Management>
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
/** Extra systemPrompt boost when prompt-intent detects multi-step + empty list. */
|
|
82
|
+
export const COLD_START_BOOST = `
|
|
83
|
+
<Task_Management_Priority>
|
|
84
|
+
COLD START ACTIVE: the current user message is multi-step and there is no open todo list. Call todo_write before any other tool. Prefer 3–8 concrete checklist items. Keep exactly one in_progress.
|
|
85
|
+
</Task_Management_Priority>
|
|
86
|
+
`;
|
package/src/tools/todoupdate.ts
CHANGED
|
@@ -1,43 +1,55 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { formatTodoListText } from "../format.js";
|
|
4
|
+
import { TodoUpdateParams } from "../schema.js";
|
|
5
|
+
import { getTodos, setTodos, withStoreLock } from "../store.js";
|
|
6
|
+
import type { TodoWriteDetails } from "../types.js";
|
|
7
|
+
import { TODO_STATE_ENTRY_TYPE, TOOL_UPDATE } from "../types.js";
|
|
8
|
+
import { countOpenTodos, ensureTodoIds, validateTodoUpdate } from "../validate.js";
|
|
9
|
+
|
|
10
|
+
export function registerTodoUpdateTool(pi: ExtensionAPI, options: { onCommit?: () => void }): void {
|
|
11
|
+
pi.registerTool({
|
|
12
|
+
name: TOOL_UPDATE,
|
|
13
|
+
label: "Todo Update",
|
|
14
|
+
description:
|
|
15
|
+
"Patch existing todos by short stable ID (t1, t2, …) without replacing the full list. " +
|
|
16
|
+
"id is required and must match a current todo exactly — copy it from todo_read / the previous tool result; " +
|
|
17
|
+
"never invent or approximate an ID. This tool never deletes items.\n\n" +
|
|
18
|
+
"Note: always call todo_read (or use IDs from the last write/update text) before todo_update.",
|
|
19
|
+
promptGuidelines: [
|
|
20
|
+
"Copy the exact short ID (t1, t2, …) from todo_read or the last todo_write/todo_update text. Never invent, truncate, or approximate IDs.",
|
|
21
|
+
],
|
|
22
|
+
promptSnippet: "Patch one or more existing todos by ID without replacing the full list",
|
|
23
|
+
parameters: TodoUpdateParams,
|
|
24
|
+
async execute(_toolCallId, params) {
|
|
25
|
+
return withStoreLock(() => {
|
|
26
|
+
const current = getTodos();
|
|
27
|
+
const result = validateTodoUpdate(params.updates, current);
|
|
28
|
+
if (!result.ok) return { content: [{ type: "text", text: `Error: ${result.error}` }], details: { todos: current, error: result.error } as TodoWriteDetails };
|
|
29
|
+
const todos = ensureTodoIds(result.todos, current);
|
|
30
|
+
if (!result.unchanged) {
|
|
31
|
+
try { pi.appendEntry(TODO_STATE_ENTRY_TYPE, { todos }); }
|
|
32
|
+
catch (e) {
|
|
33
|
+
if (/stale after session replacement/i.test(String(e))) {
|
|
34
|
+
return { content: [{ type: "text", text: "Error: session was replaced — state not committed. Please retry todo_update." }], details: { todos: current, error: "stale session replacement" } as TodoWriteDetails };
|
|
35
|
+
}
|
|
36
|
+
throw e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
setTodos(todos);
|
|
40
|
+
options.onCommit?.();
|
|
41
|
+
const open = countOpenTodos(todos);
|
|
42
|
+
const text = result.unchanged
|
|
43
|
+
? "No change"
|
|
44
|
+
: `Updated ${params.updates.length} todo(s)\n\n${formatTodoListText(todos, `${open} open / ${todos.length} total`)}`;
|
|
45
|
+
return { content: [{ type: "text", text }], details: { todos, ...(result.unchanged ? { unchanged: true } : {}) } as TodoWriteDetails };
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
renderCall(args, theme) { return new Text(theme.fg("toolTitle", theme.bold("todo_update ")) + theme.fg("accent", `${Array.isArray(args.updates) ? args.updates.length : 0} patch(es)`), 0, 0); },
|
|
49
|
+
renderResult(result, _opts, theme) {
|
|
50
|
+
const details = result.details as TodoWriteDetails | undefined;
|
|
51
|
+
if (details?.error) return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
52
|
+
return new Text(theme.fg("success", details?.unchanged ? "No change" : "✓ Saved"), 0, 0);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { sanitizeTodoText } from "./sanitize.js";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
2
|
import type { TodoItem, TodoPriority, TodoStatus } from "./types.js";
|
|
4
3
|
import { MAX_CONTENT_LENGTH, MAX_TODO_ITEMS, TODO_PRIORITIES, TODO_STATUSES } from "./types.js";
|
|
5
4
|
|
|
5
|
+
/** Short sequential IDs (`t1`, `t2`, …) — easy for LLMs to copy; avoids UUID typos. */
|
|
6
|
+
export function nextShortTodoId(used: ReadonlySet<string>): string {
|
|
7
|
+
let n = 1;
|
|
8
|
+
while (used.has(`t${n}`)) n += 1;
|
|
9
|
+
return `t${n}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatAvailableIds(current: readonly TodoItem[]): string {
|
|
13
|
+
const ids = current.map((t) => t.id).filter((id): id is string => typeof id === "string" && id.length > 0);
|
|
14
|
+
if (ids.length === 0) return "(none)";
|
|
15
|
+
return ids.join(", ");
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
export type ValidateOk = { ok: true; todos: TodoItem[]; unchanged: boolean };
|
|
7
19
|
export type ValidateErr = { ok: false; error: string };
|
|
8
20
|
export type ValidateResult = ValidateOk | ValidateErr;
|
|
@@ -117,13 +129,16 @@ export function validateTodoWrite(
|
|
|
117
129
|
* an ID that another incoming item explicitly preserves.
|
|
118
130
|
* - Items without `id` try to match a prior item: first by full tuple
|
|
119
131
|
* (content+status+priority), then by unique content. If ambiguous (multiple
|
|
120
|
-
* same-content items) or no prior match, a new
|
|
132
|
+
* same-content items) or no prior match, a new short ID (`tN`) is generated.
|
|
121
133
|
*/
|
|
122
134
|
export function ensureTodoIds(todos: readonly TodoItem[], current: readonly TodoItem[]): TodoItem[] {
|
|
123
135
|
// Reserve every explicit ID up front. Incoming order must not decide whether an
|
|
124
136
|
// id-less item steals an ID that a later item explicitly retains.
|
|
125
137
|
const reserved = new Set(todos.flatMap((todo) => todo.id ? [todo.id] : []));
|
|
126
138
|
const claimed = new Set<string>();
|
|
139
|
+
// Avoid reusing any current ID in this pass — a later id-less item may still
|
|
140
|
+
// match it by tuple/content. Freed IDs become reusable on the next write.
|
|
141
|
+
const currentIds = new Set(current.flatMap((todo) => todo.id ? [todo.id] : []));
|
|
127
142
|
|
|
128
143
|
// Pre-index content uniqueness: content that appears only once in `current`
|
|
129
144
|
// is safe for content-only fallback.
|
|
@@ -154,10 +169,11 @@ export function ensureTodoIds(todos: readonly TodoItem[], current: readonly Todo
|
|
|
154
169
|
if (byContent) id = byContent.id;
|
|
155
170
|
}
|
|
156
171
|
}
|
|
157
|
-
// 3. No match or ambiguous → fresh ID
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
172
|
+
// 3. No match or ambiguous → fresh short sequential ID
|
|
173
|
+
if (!id) {
|
|
174
|
+
const used = new Set([...reserved, ...claimed, ...currentIds]);
|
|
175
|
+
id = nextShortTodoId(used);
|
|
176
|
+
}
|
|
161
177
|
}
|
|
162
178
|
claimed.add(id);
|
|
163
179
|
return { ...todo, id };
|
|
@@ -197,7 +213,12 @@ export function validateTodoUpdate(rawUpdates: unknown, current: readonly TodoIt
|
|
|
197
213
|
if (seen.has(rec.id)) return { ok: false, error: `updates[${i}].id is duplicated` };
|
|
198
214
|
seen.add(rec.id);
|
|
199
215
|
const target = next.find((todo) => todo.id === rec.id);
|
|
200
|
-
if (!target)
|
|
216
|
+
if (!target) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: `updates[${i}].id "${rec.id}" does not match an existing todo; current IDs: ${formatAvailableIds(current)}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
201
222
|
if (rec.content === undefined && rec.status === undefined && rec.priority === undefined) {
|
|
202
223
|
return { ok: false, error: `updates[${i}] must change content, status, or priority` };
|
|
203
224
|
}
|