@firstpick/pi-extension-todo-progress 0.1.2 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +5 -5
  2. package/index.ts +33 -56
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -4,12 +4,12 @@ Auto todo/progress tracking for multi-goal prompts.
4
4
 
5
5
  ## What it does
6
6
 
7
- - Auto-creates todos when prompts appear multi-step.
8
- - Keeps a progress widget visible until completion.
7
+ - Instructs the agent to create concise, agent-authored todos for multi-step work.
8
+ - Tracks checklist markers from assistant messages instead of copying raw user prompt lines.
9
9
  - Uses explicit status markers: `[ ]` not started, `[-]` partial, `[x]` done.
10
+ - Clears the widget automatically when all items are complete.
10
11
  - Shows up to 5 rows.
11
- - Supports mouse wheel scrolling in custom UI.
12
- - Supports hiding completed list.
12
+ - Supports hiding the current list manually.
13
13
 
14
14
  ## Install
15
15
 
@@ -27,7 +27,7 @@ None.
27
27
 
28
28
  ## Shortcuts
29
29
 
30
- - `Ctrl+Alt+X` — hide completed list.
30
+ - `Ctrl+Alt+X` — hide current list.
31
31
  - `Ctrl+Alt+J` / `Ctrl+Alt+K` — scroll todo list down/up.
32
32
 
33
33
  ## Tools
package/index.ts CHANGED
@@ -1,111 +1,88 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  type TodoStatus = "todo" | "partial" | "done";
4
4
  type TodoItem = { text: string; status: TodoStatus };
5
- type TodoState = { visible: boolean; items: TodoItem[]; offset: number; doneDismissHint: boolean };
5
+ type TodoState = { visible: boolean; items: TodoItem[]; offset: number };
6
6
 
7
7
  const KEY = "todo-progress";
8
8
  const MAX_ROWS = 5;
9
9
 
10
- function splitGoals(text: string): string[] {
11
- const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
12
- const bullets = lines.filter((l) => /^(-|\*|\d+[.)])\s+/.test(l)).map((l) => l.replace(/^(-|\*|\d+[.)])\s+/, ""));
13
- if (bullets.length >= 2) return bullets;
14
- const parts = text.split(/\band then\b|\bthen\b|,\s*and\s+/gi).map((s) => s.trim()).filter((s) => s.length > 8);
15
- return parts.slice(0, 12);
16
- }
17
-
18
- function normalize(text: string): string {
19
- return text.toLowerCase().replace(/[`*_~]/g, "").replace(/\s+/g, " ").trim();
20
- }
21
-
22
10
  function statusLabel(status: TodoStatus): string {
23
11
  if (status === "done") return "[x]";
24
12
  if (status === "partial") return "[-]";
25
13
  return "[ ]";
26
14
  }
27
15
 
16
+ function clear(ctx: ExtensionContext, s: TodoState) {
17
+ s.visible = false;
18
+ s.items = [];
19
+ s.offset = 0;
20
+ if (ctx.hasUI) ctx.ui.setWidget(KEY, []);
21
+ }
22
+
28
23
  function render(ctx: ExtensionContext, s: TodoState) {
29
- if (!ctx.hasUI || !s.visible || s.items.length === 0) return;
24
+ if (!ctx.hasUI) return;
25
+ if (!s.visible || s.items.length === 0) {
26
+ ctx.ui.setWidget(KEY, []);
27
+ return;
28
+ }
29
+
30
30
  const done = s.items.filter((i) => i.status === "done").length;
31
+ if (done === s.items.length) {
32
+ clear(ctx, s);
33
+ return;
34
+ }
35
+
31
36
  const partial = s.items.filter((i) => i.status === "partial").length;
32
37
  const top = s.items.slice(s.offset, s.offset + MAX_ROWS);
33
38
  const lines = [ctx.ui.theme.fg("accent", `Todo ${done}/${s.items.length} done${partial ? `, ${partial} partial` : ""}`)];
34
39
  for (const item of top) lines.push(`${statusLabel(item.status)} ${item.text}`);
35
40
  if (s.items.length > MAX_ROWS) lines.push(ctx.ui.theme.fg("dim", `Scroll ${s.offset + 1}-${Math.min(s.offset + MAX_ROWS, s.items.length)} of ${s.items.length}`));
36
- if (done === s.items.length && s.doneDismissHint) lines.push(ctx.ui.theme.fg("success", "Done — Ctrl+Alt+X to dismiss"));
37
41
  ctx.ui.setWidget(KEY, lines);
38
42
  }
39
43
 
40
44
  export default function todoProgress(pi: ExtensionAPI) {
41
- const state: TodoState = { visible: true, items: [], offset: 0, doneDismissHint: true };
45
+ const state: TodoState = { visible: false, items: [], offset: 0 };
42
46
 
43
47
  pi.on("before_agent_start", async (event) => {
44
48
  return {
45
49
  systemPrompt:
46
50
  event.systemPrompt +
47
- "\n\n[TODO PROGRESS POLICY] Aggressively use a todo list for multi-goal requests. Prefer creating/updating todos once too much rather than once too little. Use explicit statuses: [ ] not started, [-] partial/in progress, [x] complete. Update checklist markers immediately when work status changes, including partial progress.",
51
+ "\n\n[TODO PROGRESS POLICY] For multi-step work, create a concise agent-authored checklist with 2-6 short items. Do not copy raw user-prompt lines as todos; rewrite them into clear action items. Use explicit markers: [ ] not started, [-] partial/in progress, [x] complete. Update checklist markers as work changes. Before your final answer, close the todo list by marking every remaining item [x] or by explicitly stating no todo list is needed for the completed single-step task.",
48
52
  };
49
53
  });
50
54
 
51
55
  pi.on("input", async (event, ctx) => {
52
- if (event.text.startsWith("/")) return { action: "continue" as const };
53
- const goals = splitGoals(event.text);
54
- if (goals.length >= 2) {
55
- state.items = goals.map((g) => ({ text: g, status: "todo" }));
56
- state.offset = 0;
57
- state.visible = true;
58
- render(ctx, state);
59
- }
56
+ if (!event.text.startsWith("/")) clear(ctx, state);
60
57
  return { action: "continue" as const };
61
58
  });
62
59
 
63
60
  pi.on("message_end", async (event, ctx) => {
64
- if (!state.items.length || event.message.role !== "assistant") return;
61
+ if (event.message.role !== "assistant") return;
65
62
 
66
63
  const text = event.message.content.filter((c: any) => c.type === "text").map((c: any) => c.text).join("\n");
67
- const normalizedText = normalize(text);
68
64
 
69
65
  const checklistRegex = /^\s*(?:[-*]|\d+[.)])\s*\[( |x|X|-)\]\s+(.+)$/gm;
70
- const checklist: Array<{ status: TodoStatus; text: string }> = [];
66
+ const checklist: Array<TodoItem> = [];
71
67
  for (const match of text.matchAll(checklistRegex)) {
72
68
  const mark = (match[1] || " ").toLowerCase();
73
- const label = normalize(match[2] || "");
69
+ const label = (match[2] || "").trim().replace(/\s+/g, " ");
74
70
  const status: TodoStatus = mark === "x" ? "done" : mark === "-" ? "partial" : "todo";
75
- checklist.push({ status, text: label });
71
+ if (label) checklist.push({ status, text: label });
76
72
  }
77
73
 
78
- for (const item of state.items) {
79
- const itemNorm = normalize(item.text);
80
- const exact = checklist.find((c) => c.text === itemNorm);
81
- const fuzzy = checklist.find((c) => c.text.includes(itemNorm) || itemNorm.includes(c.text));
82
- const candidate = exact ?? fuzzy;
83
-
84
- if (candidate) {
85
- item.status = candidate.status;
86
- continue;
87
- }
88
-
89
- // Fallback heuristic for free-form summaries without explicit checklist markers.
90
- if (item.status !== "done" && normalizedText.includes(itemNorm.slice(0, 24))) {
91
- if (/\b(done|completed|finished)\b/.test(normalizedText)) item.status = "done";
92
- else if (/\b(partial|partly|in progress|started|wip)\b/.test(normalizedText)) item.status = "partial";
93
- }
94
- }
74
+ if (checklist.length === 0) return;
95
75
 
76
+ state.items = checklist.slice(0, 12);
77
+ state.offset = Math.min(state.offset, Math.max(0, state.items.length - MAX_ROWS));
78
+ state.visible = true;
96
79
  render(ctx, state);
97
80
  });
98
81
 
99
82
  pi.registerShortcut("ctrl+alt+x", {
100
83
  description: "Dismiss completed todo widget",
101
84
  handler: async (ctx) => {
102
- const done = state.items.length > 0 && state.items.every((i) => i.status === "done");
103
- if (!done) {
104
- ctx.ui.notify("Todo not complete yet", "warning");
105
- return;
106
- }
107
- state.visible = false;
108
- ctx.ui.setWidget(KEY, []);
85
+ clear(ctx, state);
109
86
  ctx.ui.notify("Todo widget dismissed", "info");
110
87
  },
111
88
  });
@@ -113,5 +90,5 @@ export default function todoProgress(pi: ExtensionAPI) {
113
90
  pi.registerShortcut("ctrl+alt+j", { description: "Todo scroll down", handler: async (ctx) => { state.offset = Math.min(Math.max(0, state.items.length - MAX_ROWS), state.offset + 1); render(ctx, state); } });
114
91
  pi.registerShortcut("ctrl+alt+k", { description: "Todo scroll up", handler: async (ctx) => { state.offset = Math.max(0, state.offset - 1); render(ctx, state); } });
115
92
 
116
- pi.on("session_start", async (_event, ctx) => render(ctx, state));
93
+ pi.on("session_start", async (_event, ctx) => clear(ctx, state));
117
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-extension-todo-progress",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Aggressive automatic todo progress widget for multi-goal prompts in Pi.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -16,8 +16,8 @@
16
16
  ]
17
17
  },
18
18
  "peerDependencies": {
19
- "@mariozechner/pi-coding-agent": "*",
20
- "@mariozechner/pi-tui": "*"
19
+ "@earendil-works/pi-coding-agent": "*",
20
+ "@earendil-works/pi-tui": "*"
21
21
  },
22
22
  "files": [
23
23
  "index.ts",