@xynogen/pix-todo 0.1.12 → 0.1.14
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/package.json +2 -1
- package/src/todo.test.ts +58 -5
- package/src/todo.ts +28 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-todo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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-pretty": "^1.7.20",
|
|
37
38
|
"typebox": "^1.1.38"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
package/src/todo.test.ts
CHANGED
|
@@ -26,6 +26,7 @@ function makeHost(
|
|
|
26
26
|
data?: unknown;
|
|
27
27
|
}> = [],
|
|
28
28
|
) {
|
|
29
|
+
let capturedParameters: unknown;
|
|
29
30
|
let capturedExecute:
|
|
30
31
|
| ((
|
|
31
32
|
id: string,
|
|
@@ -46,14 +47,21 @@ function makeHost(
|
|
|
46
47
|
context: unknown,
|
|
47
48
|
) => { render(width: number): string[] })
|
|
48
49
|
| null = null;
|
|
50
|
+
let capturedRenderCall:
|
|
51
|
+
| ((args: unknown, theme: unknown, context: unknown) => { render(width: number): string[] })
|
|
52
|
+
| null = null;
|
|
49
53
|
|
|
50
54
|
const pi = {
|
|
51
55
|
registerTool(def: {
|
|
52
56
|
name: string;
|
|
57
|
+
parameters: unknown;
|
|
53
58
|
execute: typeof capturedExecute;
|
|
59
|
+
renderCall?: typeof capturedRenderCall;
|
|
54
60
|
renderResult?: typeof capturedRender;
|
|
55
61
|
}) {
|
|
62
|
+
capturedParameters = def.parameters;
|
|
56
63
|
capturedExecute = def.execute;
|
|
64
|
+
if (def.renderCall) capturedRenderCall = def.renderCall;
|
|
57
65
|
if (def.renderResult) capturedRender = def.renderResult;
|
|
58
66
|
},
|
|
59
67
|
appendEntry(type: string, data: unknown) {
|
|
@@ -82,10 +90,18 @@ function makeHost(
|
|
|
82
90
|
return {
|
|
83
91
|
pi,
|
|
84
92
|
sessionManager,
|
|
93
|
+
get parameters() {
|
|
94
|
+
if (!capturedParameters) throw new Error("parameters not captured");
|
|
95
|
+
return capturedParameters;
|
|
96
|
+
},
|
|
85
97
|
get execute() {
|
|
86
98
|
if (!capturedExecute) throw new Error("execute not captured");
|
|
87
99
|
return capturedExecute;
|
|
88
100
|
},
|
|
101
|
+
get renderCall() {
|
|
102
|
+
if (!capturedRenderCall) throw new Error("renderCall not captured");
|
|
103
|
+
return capturedRenderCall;
|
|
104
|
+
},
|
|
89
105
|
get render() {
|
|
90
106
|
if (!capturedRender) throw new Error("render not captured");
|
|
91
107
|
return capturedRender;
|
|
@@ -119,6 +135,30 @@ function text(result: { content: Array<{ type: string; text: string }> }) {
|
|
|
119
135
|
return result.content.map((c) => c.text).join("\n");
|
|
120
136
|
}
|
|
121
137
|
|
|
138
|
+
// ─── Tool schema ────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
test("todo exposes action and status as guided string enums", () => {
|
|
141
|
+
const host = makeHost();
|
|
142
|
+
registerTodo(host.pi);
|
|
143
|
+
const schema = host.parameters as {
|
|
144
|
+
properties: {
|
|
145
|
+
action: { type?: string; enum?: string[]; description?: string };
|
|
146
|
+
status: { type?: string; enum?: string[]; description?: string };
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
const action = schema.properties.action;
|
|
150
|
+
const status = schema.properties.status;
|
|
151
|
+
|
|
152
|
+
expect(action.type).toBe("string");
|
|
153
|
+
expect(action.enum).toEqual(["list", "set", "add", "update", "clear"]);
|
|
154
|
+
expect(action.description).toContain('"list" shows items');
|
|
155
|
+
expect(action.description).toContain('"update" changes one item by id');
|
|
156
|
+
expect(status?.type).toBe("string");
|
|
157
|
+
expect(status?.enum).toEqual(["pending", "in_progress", "done", "blocked"]);
|
|
158
|
+
expect(status?.description).toContain('"pending" = not started');
|
|
159
|
+
expect(status?.description).toContain('"blocked" = cannot proceed');
|
|
160
|
+
});
|
|
161
|
+
|
|
122
162
|
// ─── parseItems (via set/add) ───────────────────────────────────────────────
|
|
123
163
|
|
|
124
164
|
describe("todo actions", () => {
|
|
@@ -741,6 +781,15 @@ describe("renderTodoLines (colored TUI render)", () => {
|
|
|
741
781
|
});
|
|
742
782
|
});
|
|
743
783
|
|
|
784
|
+
describe("todo card layout", () => {
|
|
785
|
+
test("keeps the call row empty so the collapsed card is one line", () => {
|
|
786
|
+
const host = makeHost();
|
|
787
|
+
registerTodo(host.pi);
|
|
788
|
+
const call = host.renderCall({ action: "list" }, tagTheme, {});
|
|
789
|
+
expect(call.render(80).join("\n")).toBe("");
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
744
793
|
describe("renderResult snapshot isolation", () => {
|
|
745
794
|
// The card snapshots `todos` on first render; later execute() mutations must
|
|
746
795
|
// NOT bleed into an already-rendered card. Guards the invariant the inline
|
|
@@ -770,15 +819,19 @@ describe("renderResult snapshot isolation", () => {
|
|
|
770
819
|
});
|
|
771
820
|
|
|
772
821
|
describe("renderTodoSummaryLine (collapsed one-liner)", () => {
|
|
773
|
-
test("empty list renders
|
|
774
|
-
expect(renderTodoSummaryLine([], tagTheme)).toBe(
|
|
822
|
+
test("empty list renders a compact tool row", () => {
|
|
823
|
+
expect(renderTodoSummaryLine([], tagTheme)).toBe(
|
|
824
|
+
"[success]✓[/] [toolTitle]<b>todo</b>[/] [muted]empty[/]",
|
|
825
|
+
);
|
|
775
826
|
});
|
|
776
827
|
|
|
777
|
-
test("renders
|
|
828
|
+
test("renders active work and progress in one row", () => {
|
|
778
829
|
const items: TodoItem[] = [
|
|
779
830
|
{ id: 1, text: "a", status: "done" },
|
|
780
|
-
{ id: 2, text: "b", status: "
|
|
831
|
+
{ id: 2, text: "b", status: "in_progress" },
|
|
781
832
|
];
|
|
782
|
-
expect(renderTodoSummaryLine(items, tagTheme)).toBe(
|
|
833
|
+
expect(renderTodoSummaryLine(items, tagTheme)).toBe(
|
|
834
|
+
"[success]✓[/] [toolTitle]<b>todo</b>[/] [muted]#2 b[/] [dim]·[/] [dim]1/2 done[/]",
|
|
835
|
+
);
|
|
783
836
|
});
|
|
784
837
|
});
|
package/src/todo.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Text } from "@earendil-works/pi-tui";
|
|
16
|
+
import { formatCollapsedToolRow } from "@xynogen/pix-pretty/utils";
|
|
16
17
|
import { Type } from "typebox";
|
|
17
18
|
|
|
18
19
|
import { once } from "./once.ts";
|
|
@@ -99,9 +100,19 @@ export type TodoTheme = {
|
|
|
99
100
|
|
|
100
101
|
/** One-line dim summary used once a card has collapsed. */
|
|
101
102
|
export function renderTodoSummaryLine(items: TodoItem[], theme: TodoTheme): string {
|
|
102
|
-
if (!items.length) return theme
|
|
103
|
+
if (!items.length) return formatCollapsedToolRow(theme, "todo", "empty");
|
|
103
104
|
const done = items.filter((t) => t.status === "done").length;
|
|
104
|
-
|
|
105
|
+
const active = items.find((t) => t.status === "in_progress");
|
|
106
|
+
const blocked = items.filter((t) => t.status === "blocked").length;
|
|
107
|
+
const meta = [`${done}/${items.length} done`, blocked > 0 ? `${blocked} blocked` : ""]
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(" · ");
|
|
110
|
+
const target = active
|
|
111
|
+
? `#${active.id} ${active.text}`
|
|
112
|
+
: done === items.length
|
|
113
|
+
? "complete"
|
|
114
|
+
: "checklist";
|
|
115
|
+
return formatCollapsedToolRow(theme, "todo", target, meta);
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
/** Colored checklist for the TUI: glyphs tinted by status, active row bold. */
|
|
@@ -178,16 +189,11 @@ export default function registerTodo(pi: ExtensionAPI): void {
|
|
|
178
189
|
"Call `todo(action:'list')` to recover your place after long runs or context compaction.",
|
|
179
190
|
],
|
|
180
191
|
parameters: Type.Object({
|
|
181
|
-
action: Type.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
Type.Literal("update"),
|
|
187
|
-
Type.Literal("clear"),
|
|
188
|
-
],
|
|
189
|
-
{ description: "Operation to perform" },
|
|
190
|
-
),
|
|
192
|
+
action: Type.Enum(["list", "set", "add", "update", "clear"] as const, {
|
|
193
|
+
type: "string",
|
|
194
|
+
description:
|
|
195
|
+
'Required operation: "list" shows items; "set" replaces all from items; "add" appends items; "update" changes one item by id; "clear" removes all.',
|
|
196
|
+
}),
|
|
191
197
|
items: Type.Optional(
|
|
192
198
|
Type.String({
|
|
193
199
|
description: "For set/add: newline-separated or numbered list of todo texts.",
|
|
@@ -195,15 +201,11 @@ export default function registerTodo(pi: ExtensionAPI): void {
|
|
|
195
201
|
),
|
|
196
202
|
id: Type.Optional(Type.Number({ description: "For update: target todo id." })),
|
|
197
203
|
status: Type.Optional(
|
|
198
|
-
Type.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
Type.Literal("blocked"),
|
|
204
|
-
],
|
|
205
|
-
{ description: "For update: new status." },
|
|
206
|
-
),
|
|
204
|
+
Type.Enum(["pending", "in_progress", "done", "blocked"] as const, {
|
|
205
|
+
type: "string",
|
|
206
|
+
description:
|
|
207
|
+
'For update: "pending" = not started; "in_progress" = active; "done" = finished; "blocked" = cannot proceed.',
|
|
208
|
+
}),
|
|
207
209
|
),
|
|
208
210
|
text: Type.Optional(
|
|
209
211
|
Type.String({
|
|
@@ -211,6 +213,11 @@ export default function registerTodo(pi: ExtensionAPI): void {
|
|
|
211
213
|
}),
|
|
212
214
|
),
|
|
213
215
|
}),
|
|
216
|
+
// The result already owns the checklist and its collapsed `✓ todo …` row.
|
|
217
|
+
// Keeping the call renderer empty prevents a duplicate standalone header.
|
|
218
|
+
renderCall() {
|
|
219
|
+
return new Text("", 0, 0);
|
|
220
|
+
},
|
|
214
221
|
renderResult(_result, _options, theme, context) {
|
|
215
222
|
// Snapshot this row's todos once (live `todos` mutate across calls;
|
|
216
223
|
// a card should keep the state it was created with).
|