@nguyenquangthai/pi-todo 0.2.2 → 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 +15 -0
- package/README.md +12 -5
- package/package.json +62 -62
- package/src/format.ts +73 -27
- package/src/index.ts +185 -185
- package/src/prompt-intent.ts +147 -147
- package/src/prompt.ts +77 -77
- package/src/reminder-cadence.ts +116 -116
- package/src/replay.ts +69 -69
- package/src/store.ts +36 -36
- package/src/tools/todoread.ts +8 -3
- package/src/tools/todowrite.ts +6 -1
- package/src/types.ts +29 -29
package/src/index.ts
CHANGED
|
@@ -1,185 +1,185 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-todo — OpenCode-like session todo checklist for Pi.
|
|
3
|
-
*
|
|
4
|
-
* Tools:
|
|
5
|
-
* Overlay: # Todos with [ ]/[•]/[✓]/[×] above the editor
|
|
6
|
-
* Persistence: toolResult details + custom entry, replayed from branch
|
|
7
|
-
* Reminder: pi-tasks-style cadence → transient <system-reminder> via context
|
|
8
|
-
* Cold start: pi-todotools section + prompt-aware one-shot context nudge
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { formatPlainTodoLine } from "./format.js";
|
|
13
|
-
import { TodoOverlay } from "./overlay.js";
|
|
14
|
-
import {
|
|
15
|
-
buildColdStartReminder,
|
|
16
|
-
buildCompletionUpdateReminder,
|
|
17
|
-
shouldNudgeColdStart,
|
|
18
|
-
shouldNudgeCompletionUpdate,
|
|
19
|
-
} from "./prompt-intent.js";
|
|
20
|
-
import { COLD_START_BOOST, TASK_MANAGEMENT_SECTION } from "./prompt.js";
|
|
21
|
-
import {
|
|
22
|
-
buildSystemReminder,
|
|
23
|
-
createCadenceState,
|
|
24
|
-
drainReminderForContext,
|
|
25
|
-
evaluateToolResult,
|
|
26
|
-
onTurnStart,
|
|
27
|
-
REMINDER_INTERVAL,
|
|
28
|
-
resetCadenceState,
|
|
29
|
-
type CadenceConfig,
|
|
30
|
-
} from "./reminder-cadence.js";
|
|
31
|
-
import { replayFromBranch } from "./replay.js";
|
|
32
|
-
import { clearTodos, getTodos, setTodos } from "./store.js";
|
|
33
|
-
import { registerTodoReadTool } from "./tools/todoread.js";
|
|
34
|
-
import { registerTodoWriteTool } from "./tools/todowrite.js";
|
|
35
|
-
import { TOOL_READ, TOOL_WRITE } from "./types.js";
|
|
36
|
-
import { hasOpenTodos, isOpenTodo } from "./validate.js";
|
|
37
|
-
|
|
38
|
-
const TODO_TOOL_NAMES = new Set([TOOL_WRITE, TOOL_READ]);
|
|
39
|
-
|
|
40
|
-
const cadenceConfig: CadenceConfig = {
|
|
41
|
-
reminderInterval: REMINDER_INTERVAL,
|
|
42
|
-
todoToolNames: TODO_TOOL_NAMES,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function isStaleCtxError(e: unknown): boolean {
|
|
46
|
-
return /stale after session replacement/i.test(String(e));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function syncFromSession(ctx: ExtensionContext, overlay: TodoOverlay | undefined): void {
|
|
50
|
-
try {
|
|
51
|
-
setTodos(replayFromBranch(ctx));
|
|
52
|
-
} catch (e) {
|
|
53
|
-
if (!isStaleCtxError(e)) throw e;
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (ctx.hasUI && overlay) {
|
|
57
|
-
overlay.setUICtx(ctx.ui);
|
|
58
|
-
overlay.update();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function injectReminderMessage(
|
|
63
|
-
messages: unknown[],
|
|
64
|
-
text: string,
|
|
65
|
-
): { messages: unknown[] } {
|
|
66
|
-
return {
|
|
67
|
-
messages: [
|
|
68
|
-
...messages,
|
|
69
|
-
{
|
|
70
|
-
role: "user" as const,
|
|
71
|
-
content: [{ type: "text" as const, text }],
|
|
72
|
-
timestamp: Date.now(),
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export default function (pi: ExtensionAPI): void {
|
|
79
|
-
let overlay: TodoOverlay | undefined;
|
|
80
|
-
const cadence = createCadenceState();
|
|
81
|
-
/** One-shot transient nudge for the next LLM context (cold start or completion). */
|
|
82
|
-
let pendingIntentNudge: string | null = null;
|
|
83
|
-
|
|
84
|
-
const refreshOverlay = (): void => {
|
|
85
|
-
overlay?.update();
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
registerTodoWriteTool(pi, { onCommit: refreshOverlay });
|
|
89
|
-
registerTodoReadTool(pi);
|
|
90
|
-
|
|
91
|
-
// Cold-start + completion nudges. Tool description alone is often ignored;
|
|
92
|
-
// prompt-aware section + one-shot context reminder (pi-tasks style) is stronger.
|
|
93
|
-
pi.on("before_agent_start", async (event) => {
|
|
94
|
-
const prompt = typeof event.prompt === "string" ? event.prompt : "";
|
|
95
|
-
const open = hasOpenTodos(getTodos());
|
|
96
|
-
let systemPrompt = `${event.systemPrompt}\n${TASK_MANAGEMENT_SECTION}`;
|
|
97
|
-
|
|
98
|
-
if (shouldNudgeColdStart(prompt, open)) {
|
|
99
|
-
systemPrompt += `\n${COLD_START_BOOST}`;
|
|
100
|
-
pendingIntentNudge = buildColdStartReminder(prompt);
|
|
101
|
-
} else if (shouldNudgeCompletionUpdate(prompt, open)) {
|
|
102
|
-
const openLines = getTodos().filter(isOpenTodo).map(formatPlainTodoLine);
|
|
103
|
-
pendingIntentNudge = buildCompletionUpdateReminder(openLines);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return { systemPrompt };
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
110
|
-
resetCadenceState(cadence);
|
|
111
|
-
pendingIntentNudge = null;
|
|
112
|
-
if (ctx.hasUI && !overlay) {
|
|
113
|
-
overlay = new TodoOverlay();
|
|
114
|
-
}
|
|
115
|
-
syncFromSession(ctx, overlay);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
pi.on("session_tree", async (_event, ctx) => {
|
|
119
|
-
syncFromSession(ctx, overlay);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
pi.on("session_compact", async (_event, ctx) => {
|
|
123
|
-
syncFromSession(ctx, overlay);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
pi.on("session_shutdown", async () => {
|
|
127
|
-
try {
|
|
128
|
-
overlay?.dispose();
|
|
129
|
-
} finally {
|
|
130
|
-
// Clear in-memory store so a stale session_start (early return) cannot
|
|
131
|
-
// briefly expose the previous session's todos to reminders/tools.
|
|
132
|
-
clearTodos();
|
|
133
|
-
overlay = undefined;
|
|
134
|
-
pendingIntentNudge = null;
|
|
135
|
-
resetCadenceState(cadence);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
pi.on("turn_start", async () => {
|
|
140
|
-
onTurnStart(cadence);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Cadence tracking only — never mutate tool result content.
|
|
144
|
-
// Gate on open work (pending/in_progress), not all-terminal lists.
|
|
145
|
-
pi.on("tool_result", async (event) => {
|
|
146
|
-
const isTodoTool = TODO_TOOL_NAMES.has(event.toolName);
|
|
147
|
-
if (
|
|
148
|
-
!isTodoTool &&
|
|
149
|
-
cadence.currentTurn - cadence.lastTodoToolUseTurn < REMINDER_INTERVAL
|
|
150
|
-
) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (!isTodoTool && cadence.reminderInjectedThisCycle) return;
|
|
154
|
-
|
|
155
|
-
const hasOpenWork = isTodoTool ? false : hasOpenTodos(getTodos());
|
|
156
|
-
evaluateToolResult(cadence, event.toolName, hasOpenWork, cadenceConfig);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Transient injection for one LLM call — not persisted in the session store.
|
|
160
|
-
// Intent nudge (cold start / user-said-done) takes priority over idle cadence.
|
|
161
|
-
// Peer typings sometimes omit "context" from the overloaded `on` signature.
|
|
162
|
-
const onEvent = pi.on.bind(pi) as (event: string, handler: (...args: never[]) => unknown) => void;
|
|
163
|
-
onEvent("context", async (event: { messages: unknown[] }) => {
|
|
164
|
-
if (pendingIntentNudge) {
|
|
165
|
-
const text = pendingIntentNudge;
|
|
166
|
-
pendingIntentNudge = null;
|
|
167
|
-
return injectReminderMessage(event.messages, text);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (!drainReminderForContext(cadence)) return;
|
|
171
|
-
|
|
172
|
-
const reminder = buildSystemReminder(getTodos());
|
|
173
|
-
if (!reminder) return;
|
|
174
|
-
|
|
175
|
-
return injectReminderMessage(event.messages, reminder);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// Refresh from in-memory store — do NOT replayFromBranch here (branch is stale).
|
|
179
|
-
// Clear intent nudge once
|
|
180
|
-
pi.on("tool_execution_end", async (event) => {
|
|
181
|
-
if (event.toolName !== TOOL_WRITE || event.isError) return;
|
|
182
|
-
pendingIntentNudge = null;
|
|
183
|
-
overlay?.update();
|
|
184
|
-
});
|
|
185
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* pi-todo — OpenCode-like session todo checklist for Pi.
|
|
3
|
+
*
|
|
4
|
+
* Tools: todo_write (full replace), todo_read
|
|
5
|
+
* Overlay: # Todos with [ ]/[•]/[✓]/[×] above the editor
|
|
6
|
+
* Persistence: toolResult details + custom entry, replayed from branch
|
|
7
|
+
* Reminder: pi-tasks-style cadence → transient <system-reminder> via context
|
|
8
|
+
* Cold start: pi-todotools section + prompt-aware one-shot context nudge
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { formatPlainTodoLine } from "./format.js";
|
|
13
|
+
import { TodoOverlay } from "./overlay.js";
|
|
14
|
+
import {
|
|
15
|
+
buildColdStartReminder,
|
|
16
|
+
buildCompletionUpdateReminder,
|
|
17
|
+
shouldNudgeColdStart,
|
|
18
|
+
shouldNudgeCompletionUpdate,
|
|
19
|
+
} from "./prompt-intent.js";
|
|
20
|
+
import { COLD_START_BOOST, TASK_MANAGEMENT_SECTION } from "./prompt.js";
|
|
21
|
+
import {
|
|
22
|
+
buildSystemReminder,
|
|
23
|
+
createCadenceState,
|
|
24
|
+
drainReminderForContext,
|
|
25
|
+
evaluateToolResult,
|
|
26
|
+
onTurnStart,
|
|
27
|
+
REMINDER_INTERVAL,
|
|
28
|
+
resetCadenceState,
|
|
29
|
+
type CadenceConfig,
|
|
30
|
+
} from "./reminder-cadence.js";
|
|
31
|
+
import { replayFromBranch } from "./replay.js";
|
|
32
|
+
import { clearTodos, getTodos, setTodos } from "./store.js";
|
|
33
|
+
import { registerTodoReadTool } from "./tools/todoread.js";
|
|
34
|
+
import { registerTodoWriteTool } from "./tools/todowrite.js";
|
|
35
|
+
import { TOOL_READ, TOOL_WRITE } from "./types.js";
|
|
36
|
+
import { hasOpenTodos, isOpenTodo } from "./validate.js";
|
|
37
|
+
|
|
38
|
+
const TODO_TOOL_NAMES = new Set([TOOL_WRITE, TOOL_READ]);
|
|
39
|
+
|
|
40
|
+
const cadenceConfig: CadenceConfig = {
|
|
41
|
+
reminderInterval: REMINDER_INTERVAL,
|
|
42
|
+
todoToolNames: TODO_TOOL_NAMES,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function isStaleCtxError(e: unknown): boolean {
|
|
46
|
+
return /stale after session replacement/i.test(String(e));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function syncFromSession(ctx: ExtensionContext, overlay: TodoOverlay | undefined): void {
|
|
50
|
+
try {
|
|
51
|
+
setTodos(replayFromBranch(ctx));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (!isStaleCtxError(e)) throw e;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (ctx.hasUI && overlay) {
|
|
57
|
+
overlay.setUICtx(ctx.ui);
|
|
58
|
+
overlay.update();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function injectReminderMessage(
|
|
63
|
+
messages: unknown[],
|
|
64
|
+
text: string,
|
|
65
|
+
): { messages: unknown[] } {
|
|
66
|
+
return {
|
|
67
|
+
messages: [
|
|
68
|
+
...messages,
|
|
69
|
+
{
|
|
70
|
+
role: "user" as const,
|
|
71
|
+
content: [{ type: "text" as const, text }],
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default function (pi: ExtensionAPI): void {
|
|
79
|
+
let overlay: TodoOverlay | undefined;
|
|
80
|
+
const cadence = createCadenceState();
|
|
81
|
+
/** One-shot transient nudge for the next LLM context (cold start or completion). */
|
|
82
|
+
let pendingIntentNudge: string | null = null;
|
|
83
|
+
|
|
84
|
+
const refreshOverlay = (): void => {
|
|
85
|
+
overlay?.update();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
registerTodoWriteTool(pi, { onCommit: refreshOverlay });
|
|
89
|
+
registerTodoReadTool(pi);
|
|
90
|
+
|
|
91
|
+
// Cold-start + completion nudges. Tool description alone is often ignored;
|
|
92
|
+
// prompt-aware section + one-shot context reminder (pi-tasks style) is stronger.
|
|
93
|
+
pi.on("before_agent_start", async (event) => {
|
|
94
|
+
const prompt = typeof event.prompt === "string" ? event.prompt : "";
|
|
95
|
+
const open = hasOpenTodos(getTodos());
|
|
96
|
+
let systemPrompt = `${event.systemPrompt}\n${TASK_MANAGEMENT_SECTION}`;
|
|
97
|
+
|
|
98
|
+
if (shouldNudgeColdStart(prompt, open)) {
|
|
99
|
+
systemPrompt += `\n${COLD_START_BOOST}`;
|
|
100
|
+
pendingIntentNudge = buildColdStartReminder(prompt);
|
|
101
|
+
} else if (shouldNudgeCompletionUpdate(prompt, open)) {
|
|
102
|
+
const openLines = getTodos().filter(isOpenTodo).map(formatPlainTodoLine);
|
|
103
|
+
pendingIntentNudge = buildCompletionUpdateReminder(openLines);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { systemPrompt };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
110
|
+
resetCadenceState(cadence);
|
|
111
|
+
pendingIntentNudge = null;
|
|
112
|
+
if (ctx.hasUI && !overlay) {
|
|
113
|
+
overlay = new TodoOverlay();
|
|
114
|
+
}
|
|
115
|
+
syncFromSession(ctx, overlay);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
119
|
+
syncFromSession(ctx, overlay);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
123
|
+
syncFromSession(ctx, overlay);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
pi.on("session_shutdown", async () => {
|
|
127
|
+
try {
|
|
128
|
+
overlay?.dispose();
|
|
129
|
+
} finally {
|
|
130
|
+
// Clear in-memory store so a stale session_start (early return) cannot
|
|
131
|
+
// briefly expose the previous session's todos to reminders/tools.
|
|
132
|
+
clearTodos();
|
|
133
|
+
overlay = undefined;
|
|
134
|
+
pendingIntentNudge = null;
|
|
135
|
+
resetCadenceState(cadence);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
pi.on("turn_start", async () => {
|
|
140
|
+
onTurnStart(cadence);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Cadence tracking only — never mutate tool result content.
|
|
144
|
+
// Gate on open work (pending/in_progress), not all-terminal lists.
|
|
145
|
+
pi.on("tool_result", async (event) => {
|
|
146
|
+
const isTodoTool = TODO_TOOL_NAMES.has(event.toolName);
|
|
147
|
+
if (
|
|
148
|
+
!isTodoTool &&
|
|
149
|
+
cadence.currentTurn - cadence.lastTodoToolUseTurn < REMINDER_INTERVAL
|
|
150
|
+
) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!isTodoTool && cadence.reminderInjectedThisCycle) return;
|
|
154
|
+
|
|
155
|
+
const hasOpenWork = isTodoTool ? false : hasOpenTodos(getTodos());
|
|
156
|
+
evaluateToolResult(cadence, event.toolName, hasOpenWork, cadenceConfig);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Transient injection for one LLM call — not persisted in the session store.
|
|
160
|
+
// Intent nudge (cold start / user-said-done) takes priority over idle cadence.
|
|
161
|
+
// Peer typings sometimes omit "context" from the overloaded `on` signature.
|
|
162
|
+
const onEvent = pi.on.bind(pi) as (event: string, handler: (...args: never[]) => unknown) => void;
|
|
163
|
+
onEvent("context", async (event: { messages: unknown[] }) => {
|
|
164
|
+
if (pendingIntentNudge) {
|
|
165
|
+
const text = pendingIntentNudge;
|
|
166
|
+
pendingIntentNudge = null;
|
|
167
|
+
return injectReminderMessage(event.messages, text);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!drainReminderForContext(cadence)) return;
|
|
171
|
+
|
|
172
|
+
const reminder = buildSystemReminder(getTodos());
|
|
173
|
+
if (!reminder) return;
|
|
174
|
+
|
|
175
|
+
return injectReminderMessage(event.messages, reminder);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Refresh from in-memory store — do NOT replayFromBranch here (branch is stale).
|
|
179
|
+
// Clear intent nudge once todo_write succeeds (model complied; avoid double-inject).
|
|
180
|
+
pi.on("tool_execution_end", async (event) => {
|
|
181
|
+
if (event.toolName !== TOOL_WRITE || event.isError) return;
|
|
182
|
+
pendingIntentNudge = null;
|
|
183
|
+
overlay?.update();
|
|
184
|
+
});
|
|
185
|
+
}
|