@nguyenquangthai/pi-todo 0.2.2 → 0.2.3
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 +6 -0
- package/package.json +62 -62
- 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/reminder-cadence.ts
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure cadence logic for system-reminder injection (ported from tintinweb/pi-tasks).
|
|
3
|
-
*
|
|
4
|
-
* tool_result tracks cadence only — never mutates tool output.
|
|
5
|
-
* context drains the pending reminder into a transient user message for one LLM call.
|
|
6
|
-
*
|
|
7
|
-
* Reminder body is state-aware (edxeth/meh pi-tasks pattern): list open todos only,
|
|
8
|
-
* and call out the in_progress item so the model updates completed status.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { formatPlainTodoLine } from "./format.js";
|
|
12
|
-
import type { TodoItem } from "./types.js";
|
|
13
|
-
import { hasOpenTodos, isOpenTodo } from "./validate.js";
|
|
14
|
-
|
|
15
|
-
export interface CadenceState {
|
|
16
|
-
currentTurn: number;
|
|
17
|
-
lastTodoToolUseTurn: number;
|
|
18
|
-
reminderInjectedThisCycle: boolean;
|
|
19
|
-
reminderDue: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface CadenceConfig {
|
|
23
|
-
/** Turns without a todo-tool call before a reminder is considered due. */
|
|
24
|
-
reminderInterval: number;
|
|
25
|
-
/** Tool names that count as todo usage and reset cadence. */
|
|
26
|
-
todoToolNames: ReadonlySet<string>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function createCadenceState(): CadenceState {
|
|
30
|
-
return {
|
|
31
|
-
currentTurn: 0,
|
|
32
|
-
lastTodoToolUseTurn: 0,
|
|
33
|
-
reminderInjectedThisCycle: false,
|
|
34
|
-
reminderDue: false,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function resetCadenceState(state: CadenceState): void {
|
|
39
|
-
state.currentTurn = 0;
|
|
40
|
-
state.lastTodoToolUseTurn = 0;
|
|
41
|
-
state.reminderInjectedThisCycle = false;
|
|
42
|
-
state.reminderDue = false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function onTurnStart(state: CadenceState): void {
|
|
46
|
-
state.currentTurn++;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Decide cadence change from a tool_result. Mutates state; returns whether
|
|
51
|
-
* the reminder should be queued for the next context event.
|
|
52
|
-
*
|
|
53
|
-
* `hasOpenWork` must reflect pending/in_progress only — all-terminal lists
|
|
54
|
-
* should not re-arm reminders (OpenCode: done means overlay gone).
|
|
55
|
-
*/
|
|
56
|
-
export function evaluateToolResult(
|
|
57
|
-
state: CadenceState,
|
|
58
|
-
toolName: string,
|
|
59
|
-
hasOpenWork: boolean,
|
|
60
|
-
config: CadenceConfig,
|
|
61
|
-
): { markDue: boolean } {
|
|
62
|
-
if (config.todoToolNames.has(toolName)) {
|
|
63
|
-
state.lastTodoToolUseTurn = state.currentTurn;
|
|
64
|
-
state.reminderInjectedThisCycle = false;
|
|
65
|
-
state.reminderDue = false;
|
|
66
|
-
return { markDue: false };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (state.currentTurn - state.lastTodoToolUseTurn < config.reminderInterval) {
|
|
70
|
-
return { markDue: false };
|
|
71
|
-
}
|
|
72
|
-
if (state.reminderInjectedThisCycle) return { markDue: false };
|
|
73
|
-
if (!hasOpenWork) return { markDue: false };
|
|
74
|
-
|
|
75
|
-
state.reminderDue = true;
|
|
76
|
-
return { markDue: true };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Drain pending reminder when `context` fires. */
|
|
80
|
-
export function drainReminderForContext(state: CadenceState): boolean {
|
|
81
|
-
if (!state.reminderDue) return false;
|
|
82
|
-
state.reminderDue = false;
|
|
83
|
-
state.reminderInjectedThisCycle = true;
|
|
84
|
-
state.lastTodoToolUseTurn = state.currentTurn;
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Default: 4 turns without
|
|
89
|
-
export const REMINDER_INTERVAL = 4;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Build a transient system-reminder from the live open-todo snapshot.
|
|
93
|
-
* Returns null when there is nothing open (caller must not inject).
|
|
94
|
-
*/
|
|
95
|
-
export function buildSystemReminder(todos: readonly TodoItem[]): string | null {
|
|
96
|
-
if (!hasOpenTodos(todos)) return null;
|
|
97
|
-
|
|
98
|
-
const open = todos.filter(isOpenTodo);
|
|
99
|
-
const inProgress = open.filter((t) => t.status === "in_progress");
|
|
100
|
-
const lines = open.map(formatPlainTodoLine);
|
|
101
|
-
|
|
102
|
-
const focus =
|
|
103
|
-
inProgress.length > 0
|
|
104
|
-
? `Active item still in_progress: "${inProgress[0].content}". If that work is finished, call
|
|
105
|
-
: `Open items are still pending with none in_progress. If you are about to work, call
|
|
106
|
-
|
|
107
|
-
return `<system-reminder>
|
|
108
|
-
The todo tools haven't been used recently, and open work remains:
|
|
109
|
-
|
|
110
|
-
${lines.map((l) => `- ${l}`).join("\n")}
|
|
111
|
-
|
|
112
|
-
${focus}
|
|
113
|
-
|
|
114
|
-
Only act if relevant to the current work. This is a gentle reminder — ignore if not applicable. NEVER mention this reminder to the user.
|
|
115
|
-
</system-reminder>`;
|
|
116
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pure cadence logic for system-reminder injection (ported from tintinweb/pi-tasks).
|
|
3
|
+
*
|
|
4
|
+
* tool_result tracks cadence only — never mutates tool output.
|
|
5
|
+
* context drains the pending reminder into a transient user message for one LLM call.
|
|
6
|
+
*
|
|
7
|
+
* Reminder body is state-aware (edxeth/meh pi-tasks pattern): list open todos only,
|
|
8
|
+
* and call out the in_progress item so the model updates completed status.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { formatPlainTodoLine } from "./format.js";
|
|
12
|
+
import type { TodoItem } from "./types.js";
|
|
13
|
+
import { hasOpenTodos, isOpenTodo } from "./validate.js";
|
|
14
|
+
|
|
15
|
+
export interface CadenceState {
|
|
16
|
+
currentTurn: number;
|
|
17
|
+
lastTodoToolUseTurn: number;
|
|
18
|
+
reminderInjectedThisCycle: boolean;
|
|
19
|
+
reminderDue: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CadenceConfig {
|
|
23
|
+
/** Turns without a todo-tool call before a reminder is considered due. */
|
|
24
|
+
reminderInterval: number;
|
|
25
|
+
/** Tool names that count as todo usage and reset cadence. */
|
|
26
|
+
todoToolNames: ReadonlySet<string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createCadenceState(): CadenceState {
|
|
30
|
+
return {
|
|
31
|
+
currentTurn: 0,
|
|
32
|
+
lastTodoToolUseTurn: 0,
|
|
33
|
+
reminderInjectedThisCycle: false,
|
|
34
|
+
reminderDue: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resetCadenceState(state: CadenceState): void {
|
|
39
|
+
state.currentTurn = 0;
|
|
40
|
+
state.lastTodoToolUseTurn = 0;
|
|
41
|
+
state.reminderInjectedThisCycle = false;
|
|
42
|
+
state.reminderDue = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function onTurnStart(state: CadenceState): void {
|
|
46
|
+
state.currentTurn++;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Decide cadence change from a tool_result. Mutates state; returns whether
|
|
51
|
+
* the reminder should be queued for the next context event.
|
|
52
|
+
*
|
|
53
|
+
* `hasOpenWork` must reflect pending/in_progress only — all-terminal lists
|
|
54
|
+
* should not re-arm reminders (OpenCode: done means overlay gone).
|
|
55
|
+
*/
|
|
56
|
+
export function evaluateToolResult(
|
|
57
|
+
state: CadenceState,
|
|
58
|
+
toolName: string,
|
|
59
|
+
hasOpenWork: boolean,
|
|
60
|
+
config: CadenceConfig,
|
|
61
|
+
): { markDue: boolean } {
|
|
62
|
+
if (config.todoToolNames.has(toolName)) {
|
|
63
|
+
state.lastTodoToolUseTurn = state.currentTurn;
|
|
64
|
+
state.reminderInjectedThisCycle = false;
|
|
65
|
+
state.reminderDue = false;
|
|
66
|
+
return { markDue: false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (state.currentTurn - state.lastTodoToolUseTurn < config.reminderInterval) {
|
|
70
|
+
return { markDue: false };
|
|
71
|
+
}
|
|
72
|
+
if (state.reminderInjectedThisCycle) return { markDue: false };
|
|
73
|
+
if (!hasOpenWork) return { markDue: false };
|
|
74
|
+
|
|
75
|
+
state.reminderDue = true;
|
|
76
|
+
return { markDue: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Drain pending reminder when `context` fires. */
|
|
80
|
+
export function drainReminderForContext(state: CadenceState): boolean {
|
|
81
|
+
if (!state.reminderDue) return false;
|
|
82
|
+
state.reminderDue = false;
|
|
83
|
+
state.reminderInjectedThisCycle = true;
|
|
84
|
+
state.lastTodoToolUseTurn = state.currentTurn;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Default: 4 turns without todo_write/todo_read while open work remains. */
|
|
89
|
+
export const REMINDER_INTERVAL = 4;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build a transient system-reminder from the live open-todo snapshot.
|
|
93
|
+
* Returns null when there is nothing open (caller must not inject).
|
|
94
|
+
*/
|
|
95
|
+
export function buildSystemReminder(todos: readonly TodoItem[]): string | null {
|
|
96
|
+
if (!hasOpenTodos(todos)) return null;
|
|
97
|
+
|
|
98
|
+
const open = todos.filter(isOpenTodo);
|
|
99
|
+
const inProgress = open.filter((t) => t.status === "in_progress");
|
|
100
|
+
const lines = open.map(formatPlainTodoLine);
|
|
101
|
+
|
|
102
|
+
const focus =
|
|
103
|
+
inProgress.length > 0
|
|
104
|
+
? `Active item still in_progress: "${inProgress[0].content}". If that work is finished, call todo_write immediately with it marked completed (full replace), then set the next pending item to in_progress (exactly one). Do not leave a stale [•] after finishing a step.`
|
|
105
|
+
: `Open items are still pending with none in_progress. If you are about to work, call todo_write and mark exactly one item in_progress before continuing.`;
|
|
106
|
+
|
|
107
|
+
return `<system-reminder>
|
|
108
|
+
The todo tools haven't been used recently, and open work remains:
|
|
109
|
+
|
|
110
|
+
${lines.map((l) => `- ${l}`).join("\n")}
|
|
111
|
+
|
|
112
|
+
${focus}
|
|
113
|
+
|
|
114
|
+
Only act if relevant to the current work. This is a gentle reminder — ignore if not applicable. NEVER mention this reminder to the user.
|
|
115
|
+
</system-reminder>`;
|
|
116
|
+
}
|
package/src/replay.ts
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
import type { TodoItem, TodoWriteDetails } from "./types.js";
|
|
2
|
-
import { TODO_STATE_ENTRY_TYPE, TOOL_WRITE } from "./types.js";
|
|
3
|
-
import { TERMINAL_STATUSES, TODO_PRIORITIES, TODO_STATUSES } from "./types.js";
|
|
4
|
-
|
|
5
|
-
type BranchEntry = {
|
|
6
|
-
type?: string;
|
|
7
|
-
customType?: string;
|
|
8
|
-
data?: unknown;
|
|
9
|
-
message?: unknown;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
-
return !!value && typeof value === "object";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function isTodoItem(value: unknown): value is TodoItem {
|
|
17
|
-
if (!isRecord(value)) return false;
|
|
18
|
-
return (
|
|
19
|
-
typeof value.content === "string" &&
|
|
20
|
-
typeof value.status === "string" &&
|
|
21
|
-
(TODO_STATUSES as readonly string[]).includes(value.status) &&
|
|
22
|
-
typeof value.priority === "string" &&
|
|
23
|
-
(TODO_PRIORITIES as readonly string[]).includes(value.priority)
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isTodoList(value: unknown): value is TodoItem[] {
|
|
28
|
-
return Array.isArray(value) && value.every(isTodoItem);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isWriteDetails(value: unknown): value is TodoWriteDetails {
|
|
32
|
-
return isRecord(value) && isTodoList(value.todos);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Last-write-wins over the current branch:
|
|
37
|
-
* - custom `pi-todo.state` entries
|
|
38
|
-
* - `
|
|
39
|
-
*/
|
|
40
|
-
export function replayFromBranch(ctx: {
|
|
41
|
-
sessionManager: { getBranch(): Iterable<unknown> };
|
|
42
|
-
}): TodoItem[] {
|
|
43
|
-
let todos: TodoItem[] = [];
|
|
44
|
-
|
|
45
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
46
|
-
const e = entry as BranchEntry;
|
|
47
|
-
|
|
48
|
-
if (e.type === "custom" && e.customType === TODO_STATE_ENTRY_TYPE) {
|
|
49
|
-
if (isRecord(e.data) && isTodoList(e.data.todos)) {
|
|
50
|
-
todos = e.data.todos.map((t) => ({ ...t }));
|
|
51
|
-
}
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (e.type !== "message" || !isRecord(e.message)) continue;
|
|
56
|
-
const msg = e.message;
|
|
57
|
-
if (msg.role !== "toolResult" || msg.toolName !== TOOL_WRITE) continue;
|
|
58
|
-
if (!isWriteDetails(msg.details)) continue;
|
|
59
|
-
// Skip error envelopes — they did not commit
|
|
60
|
-
if (msg.details.error) continue;
|
|
61
|
-
todos = msg.details.todos.map((t) => ({ ...t }));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return todos;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function isTerminalList(todos: readonly TodoItem[]): boolean {
|
|
68
|
-
return todos.length === 0 || todos.every((t) => TERMINAL_STATUSES.has(t.status));
|
|
69
|
-
}
|
|
1
|
+
import type { TodoItem, TodoWriteDetails } from "./types.js";
|
|
2
|
+
import { TODO_STATE_ENTRY_TYPE, TOOL_WRITE } from "./types.js";
|
|
3
|
+
import { TERMINAL_STATUSES, TODO_PRIORITIES, TODO_STATUSES } from "./types.js";
|
|
4
|
+
|
|
5
|
+
type BranchEntry = {
|
|
6
|
+
type?: string;
|
|
7
|
+
customType?: string;
|
|
8
|
+
data?: unknown;
|
|
9
|
+
message?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return !!value && typeof value === "object";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isTodoItem(value: unknown): value is TodoItem {
|
|
17
|
+
if (!isRecord(value)) return false;
|
|
18
|
+
return (
|
|
19
|
+
typeof value.content === "string" &&
|
|
20
|
+
typeof value.status === "string" &&
|
|
21
|
+
(TODO_STATUSES as readonly string[]).includes(value.status) &&
|
|
22
|
+
typeof value.priority === "string" &&
|
|
23
|
+
(TODO_PRIORITIES as readonly string[]).includes(value.priority)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isTodoList(value: unknown): value is TodoItem[] {
|
|
28
|
+
return Array.isArray(value) && value.every(isTodoItem);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isWriteDetails(value: unknown): value is TodoWriteDetails {
|
|
32
|
+
return isRecord(value) && isTodoList(value.todos);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Last-write-wins over the current branch:
|
|
37
|
+
* - custom `pi-todo.state` entries
|
|
38
|
+
* - `todo_write` toolResult details
|
|
39
|
+
*/
|
|
40
|
+
export function replayFromBranch(ctx: {
|
|
41
|
+
sessionManager: { getBranch(): Iterable<unknown> };
|
|
42
|
+
}): TodoItem[] {
|
|
43
|
+
let todos: TodoItem[] = [];
|
|
44
|
+
|
|
45
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
46
|
+
const e = entry as BranchEntry;
|
|
47
|
+
|
|
48
|
+
if (e.type === "custom" && e.customType === TODO_STATE_ENTRY_TYPE) {
|
|
49
|
+
if (isRecord(e.data) && isTodoList(e.data.todos)) {
|
|
50
|
+
todos = e.data.todos.map((t) => ({ ...t }));
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (e.type !== "message" || !isRecord(e.message)) continue;
|
|
56
|
+
const msg = e.message;
|
|
57
|
+
if (msg.role !== "toolResult" || msg.toolName !== TOOL_WRITE) continue;
|
|
58
|
+
if (!isWriteDetails(msg.details)) continue;
|
|
59
|
+
// Skip error envelopes — they did not commit
|
|
60
|
+
if (msg.details.error) continue;
|
|
61
|
+
todos = msg.details.todos.map((t) => ({ ...t }));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return todos;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isTerminalList(todos: readonly TodoItem[]): boolean {
|
|
68
|
+
return todos.length === 0 || todos.every((t) => TERMINAL_STATUSES.has(t.status));
|
|
69
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import type { TodoItem } from "./types.js";
|
|
2
|
-
|
|
3
|
-
let currentTodos: TodoItem[] = [];
|
|
4
|
-
/** Serializes
|
|
5
|
-
let storeChain: Promise<unknown> = Promise.resolve();
|
|
6
|
-
|
|
7
|
-
export function getTodos(): TodoItem[] {
|
|
8
|
-
return currentTodos.map((t) => ({ ...t }));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function setTodos(todos: readonly TodoItem[]): void {
|
|
12
|
-
currentTodos = todos.map((t) => ({ ...t }));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function clearTodos(): void {
|
|
16
|
-
currentTodos = [];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Run store-touching work in a single-file queue so parallel tool batches
|
|
21
|
-
* (
|
|
22
|
-
*/
|
|
23
|
-
export function withStoreLock<T>(fn: () => T | Promise<T>): Promise<T> {
|
|
24
|
-
const run = storeChain.then(() => fn());
|
|
25
|
-
storeChain = run.then(
|
|
26
|
-
() => undefined,
|
|
27
|
-
() => undefined,
|
|
28
|
-
);
|
|
29
|
-
return run;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Test helper — reset module state between tests. */
|
|
33
|
-
export function __resetStore(): void {
|
|
34
|
-
currentTodos = [];
|
|
35
|
-
storeChain = Promise.resolve();
|
|
36
|
-
}
|
|
1
|
+
import type { TodoItem } from "./types.js";
|
|
2
|
+
|
|
3
|
+
let currentTodos: TodoItem[] = [];
|
|
4
|
+
/** Serializes todo_write/todo_read across parallel tool batches. */
|
|
5
|
+
let storeChain: Promise<unknown> = Promise.resolve();
|
|
6
|
+
|
|
7
|
+
export function getTodos(): TodoItem[] {
|
|
8
|
+
return currentTodos.map((t) => ({ ...t }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function setTodos(todos: readonly TodoItem[]): void {
|
|
12
|
+
currentTodos = todos.map((t) => ({ ...t }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function clearTodos(): void {
|
|
16
|
+
currentTodos = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run store-touching work in a single-file queue so parallel tool batches
|
|
21
|
+
* (todo_write + todo_read in the same turn) cannot race.
|
|
22
|
+
*/
|
|
23
|
+
export function withStoreLock<T>(fn: () => T | Promise<T>): Promise<T> {
|
|
24
|
+
const run = storeChain.then(() => fn());
|
|
25
|
+
storeChain = run.then(
|
|
26
|
+
() => undefined,
|
|
27
|
+
() => undefined,
|
|
28
|
+
);
|
|
29
|
+
return run;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Test helper — reset module state between tests. */
|
|
33
|
+
export function __resetStore(): void {
|
|
34
|
+
currentTodos = [];
|
|
35
|
+
storeChain = Promise.resolve();
|
|
36
|
+
}
|
package/src/tools/todoread.ts
CHANGED
|
@@ -37,13 +37,18 @@ export function registerTodoReadTool(pi: ExtensionAPI): void {
|
|
|
37
37
|
},
|
|
38
38
|
|
|
39
39
|
renderCall(_args, theme) {
|
|
40
|
-
return new Text(theme.fg("toolTitle", theme.bold("
|
|
40
|
+
return new Text(theme.fg("toolTitle", theme.bold("todo_read")), 0, 0);
|
|
41
41
|
},
|
|
42
42
|
|
|
43
43
|
renderResult(result, _opts, theme) {
|
|
44
44
|
const details = result.details as { todos?: unknown[] } | undefined;
|
|
45
|
-
const
|
|
46
|
-
|
|
45
|
+
const todos = details?.todos;
|
|
46
|
+
if (!Array.isArray(todos) || todos.length === 0) {
|
|
47
|
+
return new Text(theme.fg("dim", "0 items"), 0, 0);
|
|
48
|
+
}
|
|
49
|
+
const total = todos.length;
|
|
50
|
+
const open = Array.isArray(todos) ? todos.filter((t: any) => t.status === "pending" || t.status === "in_progress").length : 0;
|
|
51
|
+
return new Text(theme.fg("muted", `${open} open / ${total} total`), 0, 0);
|
|
47
52
|
},
|
|
48
53
|
});
|
|
49
54
|
}
|
package/src/tools/todowrite.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function registerTodoWriteTool(
|
|
|
69
69
|
renderCall(args, theme) {
|
|
70
70
|
const n = Array.isArray(args.todos) ? args.todos.length : 0;
|
|
71
71
|
return new Text(
|
|
72
|
-
theme.fg("toolTitle", theme.bold("
|
|
72
|
+
theme.fg("toolTitle", theme.bold("todo_write ")) + theme.fg("accent", `${n} item(s)`),
|
|
73
73
|
0,
|
|
74
74
|
0,
|
|
75
75
|
);
|
|
@@ -84,6 +84,11 @@ export function registerTodoWriteTool(
|
|
|
84
84
|
return new Text(theme.fg("dim", "No change"), 0, 0);
|
|
85
85
|
}
|
|
86
86
|
const text = result.content[0];
|
|
87
|
+
if (!details?.error && !details?.unchanged) {
|
|
88
|
+
const open = details?.todos ? details.todos.filter((t: any) => t.status === "pending" || t.status === "in_progress").length : 0;
|
|
89
|
+
const total = details?.todos?.length ?? 0;
|
|
90
|
+
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", `${open} open / ${total} total`), 0, 0);
|
|
91
|
+
}
|
|
87
92
|
const msg = text?.type === "text" ? text.text.split("\n")[0] ?? "Updated" : "Updated";
|
|
88
93
|
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", msg), 0, 0);
|
|
89
94
|
},
|
package/src/types.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
export const TODO_STATUSES = ["pending", "in_progress", "completed", "cancelled"] as const;
|
|
2
|
-
export type TodoStatus = (typeof TODO_STATUSES)[number];
|
|
3
|
-
|
|
4
|
-
export const TODO_PRIORITIES = ["high", "medium", "low"] as const;
|
|
5
|
-
export type TodoPriority = (typeof TODO_PRIORITIES)[number];
|
|
6
|
-
|
|
7
|
-
export const TERMINAL_STATUSES: ReadonlySet<TodoStatus> = new Set(["completed", "cancelled"]);
|
|
8
|
-
|
|
9
|
-
export interface TodoItem {
|
|
10
|
-
content: string;
|
|
11
|
-
status: TodoStatus;
|
|
12
|
-
priority: TodoPriority;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface TodoWriteDetails {
|
|
16
|
-
todos: TodoItem[];
|
|
17
|
-
error?: string;
|
|
18
|
-
unchanged?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const TODO_STATE_ENTRY_TYPE = "pi-todo.state";
|
|
22
|
-
export const TOOL_WRITE = "
|
|
23
|
-
export const TOOL_READ = "
|
|
24
|
-
export const WIDGET_KEY = "pi-todo";
|
|
25
|
-
export const MAX_OVERLAY_LINES = 12;
|
|
26
|
-
/** Max characters per todo content after sanitize (context/tool safety). */
|
|
27
|
-
export const MAX_CONTENT_LENGTH = 500;
|
|
28
|
-
/** Max todo lines echoed in todowrite/todoread text (full list still in details/JSON). */
|
|
29
|
-
export const MAX_RESULT_LINES = 40;
|
|
1
|
+
export const TODO_STATUSES = ["pending", "in_progress", "completed", "cancelled"] as const;
|
|
2
|
+
export type TodoStatus = (typeof TODO_STATUSES)[number];
|
|
3
|
+
|
|
4
|
+
export const TODO_PRIORITIES = ["high", "medium", "low"] as const;
|
|
5
|
+
export type TodoPriority = (typeof TODO_PRIORITIES)[number];
|
|
6
|
+
|
|
7
|
+
export const TERMINAL_STATUSES: ReadonlySet<TodoStatus> = new Set(["completed", "cancelled"]);
|
|
8
|
+
|
|
9
|
+
export interface TodoItem {
|
|
10
|
+
content: string;
|
|
11
|
+
status: TodoStatus;
|
|
12
|
+
priority: TodoPriority;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TodoWriteDetails {
|
|
16
|
+
todos: TodoItem[];
|
|
17
|
+
error?: string;
|
|
18
|
+
unchanged?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const TODO_STATE_ENTRY_TYPE = "pi-todo.state";
|
|
22
|
+
export const TOOL_WRITE = "todo_write";
|
|
23
|
+
export const TOOL_READ = "todo_read";
|
|
24
|
+
export const WIDGET_KEY = "pi-todo";
|
|
25
|
+
export const MAX_OVERLAY_LINES = 12;
|
|
26
|
+
/** Max characters per todo content after sanitize (context/tool safety). */
|
|
27
|
+
export const MAX_CONTENT_LENGTH = 500;
|
|
28
|
+
/** Max todo lines echoed in todowrite/todoread text (full list still in details/JSON). */
|
|
29
|
+
export const MAX_RESULT_LINES = 40;
|