@firstpick/pi-extension-todo-progress 0.1.5 → 0.1.7

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 +7 -5
  2. package/index.ts +84 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,9 @@ Auto todo/progress tracking for multi-goal prompts.
6
6
 
7
7
  - Instructs the agent to create concise, agent-authored todos for multi-step work.
8
8
  - Tracks checklist markers from assistant messages instead of copying raw user prompt lines.
9
- - Uses explicit status markers: `[ ]` not started, `[-]` partial, `[x]` done.
9
+ - Instructs the agent to emit markdown checklist lines exactly like `- [ ] item`, `- [-] item`, or `- [x] item`.
10
+ - Also accepts bare markers like `[ ] item` as a fallback for robustness.
11
+ - Strips matched checklist lines from assistant messages after mirroring them into the widget, keeping the widget as the canonical todo view.
10
12
  - Clears the widget automatically when all items are complete.
11
13
  - Shows up to 5 rows.
12
14
  - Supports hiding the current list manually.
@@ -37,10 +39,10 @@ None.
37
39
  ## Example view
38
40
 
39
41
  ```text
40
- Todo
41
- - [x] Inspect package structure
42
- - [-] Update README examples
43
- - [ ] Run readiness checks
42
+ Todo 1/3 done, 1 partial
43
+ [x] Inspect package structure
44
+ [-] Update README examples
45
+ [ ] Run readiness checks
44
46
  ```
45
47
 
46
48
  For multi-step requests, Pi keeps a compact progress widget visible and updates it as work moves from planned to in-progress to done.
package/index.ts CHANGED
@@ -6,6 +6,8 @@ type TodoState = { visible: boolean; items: TodoItem[]; offset: number };
6
6
 
7
7
  const KEY = "todo-progress";
8
8
  const MAX_ROWS = 5;
9
+ const MAX_ITEMS = 12;
10
+ const TODO_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[( |x|X|-)\]\s+(.+)$/;
9
11
 
10
12
  function statusLabel(status: TodoStatus): string {
11
13
  if (status === "done") return "[x]";
@@ -17,13 +19,13 @@ function clear(ctx: ExtensionContext, s: TodoState) {
17
19
  s.visible = false;
18
20
  s.items = [];
19
21
  s.offset = 0;
20
- if (ctx.hasUI) ctx.ui.setWidget(KEY, []);
22
+ if (ctx.hasUI) ctx.ui.setWidget(KEY, undefined);
21
23
  }
22
24
 
23
25
  function render(ctx: ExtensionContext, s: TodoState) {
24
26
  if (!ctx.hasUI) return;
25
27
  if (!s.visible || s.items.length === 0) {
26
- ctx.ui.setWidget(KEY, []);
28
+ ctx.ui.setWidget(KEY, undefined);
27
29
  return;
28
30
  }
29
31
 
@@ -41,14 +43,65 @@ function render(ctx: ExtensionContext, s: TodoState) {
41
43
  ctx.ui.setWidget(KEY, lines);
42
44
  }
43
45
 
46
+ function parseTodoLine(line: string): TodoItem | undefined {
47
+ const match = TODO_LINE_REGEX.exec(line);
48
+ if (!match) return undefined;
49
+
50
+ const mark = (match[1] || " ").toLowerCase();
51
+ const label = (match[2] || "").trim().replace(/\s+/g, " ");
52
+ if (!label) return undefined;
53
+
54
+ return {
55
+ status: mark === "x" ? "done" : mark === "-" ? "partial" : "todo",
56
+ text: label,
57
+ };
58
+ }
59
+
60
+ function extractChecklist(text: string): TodoItem[] {
61
+ const checklist: TodoItem[] = [];
62
+ let inFence = false;
63
+
64
+ for (const line of text.split(/\r?\n/)) {
65
+ if (/^\s*```/.test(line)) {
66
+ inFence = !inFence;
67
+ continue;
68
+ }
69
+ if (inFence) continue;
70
+
71
+ const item = parseTodoLine(line);
72
+ if (item) checklist.push(item);
73
+ }
74
+
75
+ return checklist;
76
+ }
77
+
78
+ function stripChecklistLines(text: string): string {
79
+ let inFence = false;
80
+ const kept: string[] = [];
81
+
82
+ for (const line of text.split(/\r?\n/)) {
83
+ if (/^\s*```/.test(line)) {
84
+ inFence = !inFence;
85
+ kept.push(line);
86
+ continue;
87
+ }
88
+
89
+ if (!inFence && parseTodoLine(line)) continue;
90
+ kept.push(line);
91
+ }
92
+
93
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
94
+ }
95
+
44
96
  export default function todoProgress(pi: ExtensionAPI) {
45
97
  const state: TodoState = { visible: false, items: [], offset: 0 };
46
98
 
47
- pi.on("before_agent_start", async (event) => {
99
+ pi.on("before_agent_start", async (event, ctx) => {
100
+ clear(ctx, state);
48
101
  return {
49
102
  systemPrompt:
50
103
  event.systemPrompt +
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.",
104
+ "\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. Emit todo updates as markdown checklist lines exactly like `- [ ] item`, `- [-] item`, or `- [x] item`; do not use raw user prompt lines as todos. Update checklist markers as work changes. Todo checklists are live-turn progress only: still emit `[x]` updates when possible, but the extension will close the widget deterministically when the agent turn ends.",
52
105
  };
53
106
  });
54
107
 
@@ -60,24 +113,40 @@ export default function todoProgress(pi: ExtensionAPI) {
60
113
  pi.on("message_end", async (event, ctx) => {
61
114
  if (event.message.role !== "assistant") return;
62
115
 
63
- const text = event.message.content.filter((c: any) => c.type === "text").map((c: any) => c.text).join("\n");
64
-
65
- const checklistRegex = /^\s*(?:[-*]|\d+[.)])\s*\[( |x|X|-)\]\s+(.+)$/gm;
66
- const checklist: Array<TodoItem> = [];
67
- for (const match of text.matchAll(checklistRegex)) {
68
- const mark = (match[1] || " ").toLowerCase();
69
- const label = (match[2] || "").trim().replace(/\s+/g, " ");
70
- const status: TodoStatus = mark === "x" ? "done" : mark === "-" ? "partial" : "todo";
71
- if (label) checklist.push({ status, text: label });
72
- }
116
+ const textParts = event.message.content.filter((c: any) => c.type === "text");
117
+ const checklist = textParts.flatMap((c: any) => extractChecklist(c.text));
73
118
 
74
119
  if (checklist.length === 0) return;
75
120
 
76
- state.items = checklist.slice(0, 12);
121
+ state.items = checklist.slice(0, MAX_ITEMS);
77
122
  state.offset = Math.min(state.offset, Math.max(0, state.items.length - MAX_ROWS));
78
123
  state.visible = true;
79
124
  render(ctx, state);
125
+
126
+ return {
127
+ message: {
128
+ ...event.message,
129
+ content: event.message.content.map((c: any) => (c.type === "text" ? { ...c, text: stripChecklistLines(c.text) } : c)),
130
+ },
131
+ };
132
+ });
133
+
134
+ pi.on("agent_end", async (_event, ctx) => {
135
+ // The widget represents live progress for the active turn, not persistent task state.
136
+ // Do not leave stale partial/todo items visible if the model forgets a final update.
137
+ clear(ctx, state);
138
+ });
139
+
140
+ pi.on("session_shutdown", async (_event, ctx) => clear(ctx, state));
141
+ pi.on("session_before_switch", async (_event, ctx) => {
142
+ clear(ctx, state);
143
+ return undefined;
144
+ });
145
+ pi.on("session_before_fork", async (_event, ctx) => {
146
+ clear(ctx, state);
147
+ return undefined;
80
148
  });
149
+ pi.on("session_tree", async (_event, ctx) => clear(ctx, state));
81
150
 
82
151
  pi.registerShortcut("ctrl+alt+x", {
83
152
  description: "Dismiss completed todo widget",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-extension-todo-progress",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Aggressive automatic todo progress widget for multi-goal prompts in Pi.",
5
5
  "license": "MIT",
6
6
  "keywords": [