@xynogen/pix-todo 0.1.14 → 0.1.15
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/README.md +2 -2
- package/package.json +2 -1
- package/src/todo.test.ts +76 -19
- package/src/todo.ts +37 -77
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@ Registers the `todo` tool, which gives the agent a persistent task checklist tha
|
|
|
8
8
|
|
|
9
9
|
## Auto-collapse
|
|
10
10
|
|
|
11
|
-
The checklist card auto-collapses after a configurable delay (default 10 seconds,
|
|
11
|
+
The checklist card uses the shared `@xynogen/pix-data/collapse` state machine and auto-collapses after a configurable delay (default 10 seconds) to a row such as `✓ todo #2 release prep · 1/2 done`. Expanding an elapsed card restores the immutable checklist snapshot for that result, with its colored status glyphs, without restarting the timer. Failed actions keep their exact diagnostic instead of rendering a checklist. The delay and per-tool toggle are read from `~/.pi/agent/pix.json`:
|
|
12
12
|
|
|
13
13
|
```jsonc
|
|
14
14
|
{
|
|
15
15
|
"collapse": {
|
|
16
16
|
"enabled": true,
|
|
17
|
-
"
|
|
17
|
+
"delaySec": 10,
|
|
18
18
|
"tools": { "todo": true }
|
|
19
19
|
}
|
|
20
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-todo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Pi tool — durable execution checklist (todo)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@xynogen/pix-data": "^0.3.2",
|
|
37
38
|
"@xynogen/pix-pretty": "^1.7.20",
|
|
38
39
|
"typebox": "^1.1.38"
|
|
39
40
|
},
|
package/src/todo.test.ts
CHANGED
|
@@ -33,6 +33,7 @@ function makeHost(
|
|
|
33
33
|
params: Record<string, unknown>,
|
|
34
34
|
) => Promise<{
|
|
35
35
|
content: Array<{ type: string; text: string }>;
|
|
36
|
+
details?: unknown;
|
|
36
37
|
isError?: boolean;
|
|
37
38
|
}>)
|
|
38
39
|
| null = null;
|
|
@@ -124,6 +125,7 @@ async function run(
|
|
|
124
125
|
params: Record<string, unknown>,
|
|
125
126
|
) => Promise<{
|
|
126
127
|
content: Array<{ type: string; text: string }>;
|
|
128
|
+
details?: unknown;
|
|
127
129
|
isError?: boolean;
|
|
128
130
|
}>,
|
|
129
131
|
params: Record<string, unknown>,
|
|
@@ -788,33 +790,88 @@ describe("todo card layout", () => {
|
|
|
788
790
|
const call = host.renderCall({ action: "list" }, tagTheme, {});
|
|
789
791
|
expect(call.render(80).join("\n")).toBe("");
|
|
790
792
|
});
|
|
791
|
-
});
|
|
792
793
|
|
|
793
|
-
|
|
794
|
-
// The card snapshots `todos` on first render; later execute() mutations must
|
|
795
|
-
// NOT bleed into an already-rendered card. Guards the invariant the inline
|
|
796
|
-
// comment defends.
|
|
797
|
-
test("a rendered card keeps its state after todos mutate", async () => {
|
|
794
|
+
test("expanded mode restores a collapsed checklist", async () => {
|
|
798
795
|
const host = makeHost();
|
|
799
796
|
registerTodo(host.pi);
|
|
800
797
|
await host.emit("session_start", {}, { sessionManager: host.sessionManager });
|
|
801
|
-
await run(host.execute, { action: "set", items: "alpha\nbravo" });
|
|
798
|
+
const result = await run(host.execute, { action: "set", items: "alpha\nbravo" });
|
|
799
|
+
const rendered = host
|
|
800
|
+
.render(result, { expanded: true }, tagTheme, {
|
|
801
|
+
state: { collapsed: true },
|
|
802
|
+
invalidate: () => {},
|
|
803
|
+
})
|
|
804
|
+
.render(80)
|
|
805
|
+
.join("\n");
|
|
802
806
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
807
|
+
expect(rendered).toContain("1. alpha");
|
|
808
|
+
expect(rendered).toContain("[muted]○[/]");
|
|
809
|
+
expect(rendered).not.toContain("[success]✓[/] [toolTitle]<b>todo</b>[/]");
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test("failed todo actions render their exact error", async () => {
|
|
813
|
+
const host = makeHost();
|
|
814
|
+
registerTodo(host.pi);
|
|
815
|
+
await host.emit("session_start", {}, { sessionManager: host.sessionManager });
|
|
816
|
+
const result = await run(host.execute, { action: "update", id: 99, status: "done" });
|
|
817
|
+
const rendered = host
|
|
818
|
+
.render(result, { expanded: false }, tagTheme, {
|
|
819
|
+
state: { collapsed: true },
|
|
820
|
+
invalidate: () => {},
|
|
821
|
+
})
|
|
822
|
+
.render(80)
|
|
823
|
+
.join("\n")
|
|
824
|
+
.trimEnd();
|
|
825
|
+
|
|
826
|
+
expect(rendered).toBe("No todo with id 99.");
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test("a collapsed result is exactly one shared-style line", async () => {
|
|
830
|
+
const host = makeHost();
|
|
831
|
+
registerTodo(host.pi);
|
|
832
|
+
await host.emit("session_start", {}, { sessionManager: host.sessionManager });
|
|
833
|
+
await run(host.execute, { action: "set", items: "foundation\ntodo renderer" });
|
|
834
|
+
const result = await run(host.execute, { action: "update", id: 1, status: "done" });
|
|
835
|
+
const activeResult = await run(host.execute, {
|
|
836
|
+
action: "update",
|
|
837
|
+
id: 2,
|
|
838
|
+
status: "in_progress",
|
|
839
|
+
});
|
|
840
|
+
const lines = host
|
|
841
|
+
.render(activeResult, { expanded: false }, tagTheme, {
|
|
842
|
+
state: { collapsed: true },
|
|
843
|
+
invalidate: () => {},
|
|
844
|
+
})
|
|
845
|
+
.render(120);
|
|
846
|
+
|
|
847
|
+
expect(result.details).toBeDefined();
|
|
848
|
+
expect(lines).toHaveLength(1);
|
|
849
|
+
expect(lines[0]?.trimEnd()).toBe(
|
|
850
|
+
"[success]✓[/] [toolTitle]<b>todo</b>[/] [muted]#2 todo renderer[/] [dim]·[/] [dim]1/2 done[/]",
|
|
851
|
+
);
|
|
852
|
+
});
|
|
853
|
+
});
|
|
809
854
|
|
|
810
|
-
|
|
855
|
+
describe("renderResult snapshot isolation", () => {
|
|
856
|
+
test("successful results carry immutable snapshots", async () => {
|
|
857
|
+
const host = makeHost();
|
|
858
|
+
registerTodo(host.pi);
|
|
859
|
+
await host.emit("session_start", {}, { sessionManager: host.sessionManager });
|
|
860
|
+
const original = await run(host.execute, { action: "set", items: "alpha\nbravo" });
|
|
811
861
|
await run(host.execute, { action: "set", items: "changed" });
|
|
812
862
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
863
|
+
const details = original.details as { snapshot: TodoItem[] };
|
|
864
|
+
expect(details.snapshot.map((item) => item.text)).toEqual(["alpha", "bravo"]);
|
|
865
|
+
const rendered = host
|
|
866
|
+
.render(original, { expanded: true }, tagTheme, {
|
|
867
|
+
state: {},
|
|
868
|
+
invalidate: () => {},
|
|
869
|
+
})
|
|
870
|
+
.render(80)
|
|
871
|
+
.join("\n");
|
|
872
|
+
expect(rendered).toContain("1. alpha");
|
|
873
|
+
expect(rendered).toContain("2. bravo");
|
|
874
|
+
expect(rendered).not.toContain("changed");
|
|
818
875
|
});
|
|
819
876
|
});
|
|
820
877
|
|
package/src/todo.ts
CHANGED
|
@@ -9,67 +9,14 @@
|
|
|
9
9
|
* checklist is seeded by the model via the tool's `set` action.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
14
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
13
|
import { Text } from "@earendil-works/pi-tui";
|
|
14
|
+
import { type CollapseState, tickCollapse } from "@xynogen/pix-data/collapse";
|
|
16
15
|
import { formatCollapsedToolRow } from "@xynogen/pix-pretty/utils";
|
|
17
16
|
import { Type } from "typebox";
|
|
18
17
|
|
|
19
18
|
import { once } from "./once.ts";
|
|
20
19
|
|
|
21
|
-
// ── Collapse config from ~/.pi/agent/pix.json ────────────────────────────────
|
|
22
|
-
|
|
23
|
-
interface CollapseConf {
|
|
24
|
-
enabled: boolean;
|
|
25
|
-
delaySec: number;
|
|
26
|
-
tools: Record<string, boolean | undefined>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const DEFAULT_COLLAPSE: CollapseConf = {
|
|
30
|
-
enabled: true,
|
|
31
|
-
delaySec: 10,
|
|
32
|
-
tools: {},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function readCollapseConfig(): CollapseConf {
|
|
36
|
-
try {
|
|
37
|
-
const home = process.env.HOME ?? "";
|
|
38
|
-
if (!home) return DEFAULT_COLLAPSE;
|
|
39
|
-
const p = join(home, ".pi/agent", "pix.json");
|
|
40
|
-
if (!existsSync(p)) return DEFAULT_COLLAPSE;
|
|
41
|
-
const raw = JSON.parse(readFileSync(p, "utf-8")) as Record<string, unknown>;
|
|
42
|
-
const c = raw?.collapse as Record<string, unknown> | undefined;
|
|
43
|
-
if (!c || typeof c !== "object") return DEFAULT_COLLAPSE;
|
|
44
|
-
return {
|
|
45
|
-
enabled: typeof c.enabled === "boolean" ? c.enabled : true,
|
|
46
|
-
delaySec: typeof c.delaySec === "number" && c.delaySec > 0 ? c.delaySec : 10,
|
|
47
|
-
tools:
|
|
48
|
-
c.tools && typeof c.tools === "object"
|
|
49
|
-
? (c.tools as Record<string, boolean | undefined>)
|
|
50
|
-
: {},
|
|
51
|
-
};
|
|
52
|
-
} catch {
|
|
53
|
-
return DEFAULT_COLLAPSE;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let collapseConf: CollapseConf | null = null;
|
|
58
|
-
function getCollapseConfig(): CollapseConf {
|
|
59
|
-
if (!collapseConf) collapseConf = readCollapseConfig();
|
|
60
|
-
return collapseConf;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function shouldCollapseTodo(): boolean {
|
|
64
|
-
const c = getCollapseConfig();
|
|
65
|
-
const perTool = c.tools.todo;
|
|
66
|
-
return typeof perTool === "boolean" ? perTool : c.enabled;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function collapseDelayMs(): number {
|
|
70
|
-
return getCollapseConfig().delaySec * 1000;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
20
|
export type TodoStatus = "pending" | "in_progress" | "done" | "blocked";
|
|
74
21
|
|
|
75
22
|
export interface TodoItem {
|
|
@@ -78,6 +25,15 @@ export interface TodoItem {
|
|
|
78
25
|
status: TodoStatus;
|
|
79
26
|
}
|
|
80
27
|
|
|
28
|
+
type TodoAction = "list" | "set" | "add" | "update" | "clear";
|
|
29
|
+
|
|
30
|
+
interface TodoResultDetails {
|
|
31
|
+
_type: "todoResult";
|
|
32
|
+
action: TodoAction;
|
|
33
|
+
outcome: "success" | "error";
|
|
34
|
+
snapshot: TodoItem[];
|
|
35
|
+
}
|
|
36
|
+
|
|
81
37
|
const TODO_GLYPH: Record<TodoStatus, string> = {
|
|
82
38
|
pending: "○",
|
|
83
39
|
in_progress: "◐",
|
|
@@ -112,7 +68,7 @@ export function renderTodoSummaryLine(items: TodoItem[], theme: TodoTheme): stri
|
|
|
112
68
|
: done === items.length
|
|
113
69
|
? "complete"
|
|
114
70
|
: "checklist";
|
|
115
|
-
return formatCollapsedToolRow(theme, "todo", target, meta);
|
|
71
|
+
return formatCollapsedToolRow(theme, "todo", target, meta, "success");
|
|
116
72
|
}
|
|
117
73
|
|
|
118
74
|
/** Colored checklist for the TUI: glyphs tinted by status, active row bold. */
|
|
@@ -218,37 +174,41 @@ export default function registerTodo(pi: ExtensionAPI): void {
|
|
|
218
174
|
renderCall() {
|
|
219
175
|
return new Text("", 0, 0);
|
|
220
176
|
},
|
|
221
|
-
renderResult(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (!state.snapshot) state.snapshot = todos.map((t) => ({ ...t }));
|
|
230
|
-
// Start the collapse timer once per row; invalidate() triggers rerender.
|
|
231
|
-
// Config-driven: reads from ~/.pi/agent/pix.json collapse section.
|
|
232
|
-
if (shouldCollapseTodo() && !state.collapsed && !state.timer) {
|
|
233
|
-
state.timer = setTimeout(() => {
|
|
234
|
-
state.collapsed = true;
|
|
235
|
-
context.invalidate();
|
|
236
|
-
}, collapseDelayMs());
|
|
177
|
+
renderResult(result, options, theme, context) {
|
|
178
|
+
const details = result.details as TodoResultDetails | undefined;
|
|
179
|
+
const resultText = result.content
|
|
180
|
+
.filter((part) => part.type === "text")
|
|
181
|
+
.map((part) => part.text)
|
|
182
|
+
.join("\n");
|
|
183
|
+
if (context.isError || details?.outcome === "error" || !details) {
|
|
184
|
+
return new Text(resultText, 0, 0);
|
|
237
185
|
}
|
|
238
|
-
|
|
239
|
-
|
|
186
|
+
|
|
187
|
+
const collapsed = tickCollapse(
|
|
188
|
+
"todo",
|
|
189
|
+
context.state as CollapseState,
|
|
190
|
+
context.invalidate,
|
|
191
|
+
options.expanded,
|
|
192
|
+
);
|
|
193
|
+
const render = collapsed ? renderTodoSummaryLine : renderTodoLines;
|
|
194
|
+
return new Text(render(details.snapshot, theme as TodoTheme), 0, 0);
|
|
240
195
|
},
|
|
241
196
|
|
|
242
197
|
async execute(_id, params) {
|
|
243
|
-
|
|
244
|
-
|
|
198
|
+
const action = params.action as TodoAction;
|
|
199
|
+
const details = (outcome: TodoResultDetails["outcome"]): TodoResultDetails => ({
|
|
200
|
+
_type: "todoResult",
|
|
201
|
+
action,
|
|
202
|
+
outcome,
|
|
203
|
+
snapshot: todos.map((item) => ({ ...item })),
|
|
204
|
+
});
|
|
245
205
|
const ok = (text: string) => ({
|
|
246
206
|
content: [{ type: "text" as const, text }],
|
|
247
|
-
details:
|
|
207
|
+
details: details("success"),
|
|
248
208
|
});
|
|
249
209
|
const fail = (text: string) => ({
|
|
250
210
|
content: [{ type: "text" as const, text }],
|
|
251
|
-
details:
|
|
211
|
+
details: details("error"),
|
|
252
212
|
isError: true,
|
|
253
213
|
});
|
|
254
214
|
switch (params.action) {
|