@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-todo",
3
- "version": "0.1.12",
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 muted placeholder", () => {
774
- expect(renderTodoSummaryLine([], tagTheme)).toBe("[muted](no todos)[/]");
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 single dim done/total line with check", () => {
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: "pending" },
831
+ { id: 2, text: "b", status: "in_progress" },
781
832
  ];
782
- expect(renderTodoSummaryLine(items, tagTheme)).toBe("[muted]Todos 1/2 done ✓[/]");
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.fg("muted", "(no todos)");
103
+ if (!items.length) return formatCollapsedToolRow(theme, "todo", "empty");
103
104
  const done = items.filter((t) => t.status === "done").length;
104
- return theme.fg("muted", `Todos ${done}/${items.length} done ✓`);
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.Union(
182
- [
183
- Type.Literal("list"),
184
- Type.Literal("set"),
185
- Type.Literal("add"),
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.Union(
199
- [
200
- Type.Literal("pending"),
201
- Type.Literal("in_progress"),
202
- Type.Literal("done"),
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).