pi-jar 0.1.3 → 0.1.5

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/src/task-tool.ts CHANGED
@@ -1,53 +1,80 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
- import type { Todo } from "./tasks.ts";
5
- import { TODO_STATUSES, TodoStore, todoMark } from "./tasks.ts";
4
+ import type { Todo, TodoInput, TodoStatus } from "./tasks.ts";
5
+ import { TODO_STATUSES, TodoStore, todoMark, todoProgress, todoTotals } from "./tasks.ts";
6
6
 
7
- const ACTIONS = ["write", "list", "add", "start", "done", "open", "edit", "delete"] as const;
8
- const Action = Type.Unsafe<(typeof ACTIONS)[number]>({ type: "string", enum: ACTIONS as any });
9
- const Status = Type.Unsafe<(typeof TODO_STATUSES)[number]>({ type: "string", enum: TODO_STATUSES as any });
7
+ const ACTIONS = ["write", "list", "append", "remove", "update", "add", "start", "done", "open", "edit", "delete"] as const;
8
+ const Action = Type.Unsafe<(typeof ACTIONS)[number]>({ type: "string", enum: [...ACTIONS] });
9
+ const Status = Type.Unsafe<(typeof TODO_STATUSES)[number]>({ type: "string", enum: [...TODO_STATUSES] });
10
+ const Content = Type.String({ description: "Imperative task title, e.g. \"Run the tests\"." });
11
+ const ActiveForm = Type.Optional(Type.String({ description: "Present-continuous form shown while it runs, e.g. \"Running the tests\"." }));
10
12
 
11
13
  const Parameters = Type.Object({
12
14
  todos: Type.Optional(Type.Array(Type.Object({
13
- content: Type.String({ description: "Imperative task title, e.g. \"Run the tests\"." }),
15
+ id: Type.Optional(Type.String({ description: "Existing stable task id for legacy write; omitted ids match by title." })),
16
+ content: Content,
14
17
  status: Status,
15
- activeForm: Type.Optional(Type.String({ description: "Present-continuous form shown while it runs, e.g. \"Running the tests\"." }))
16
- }), { description: "The complete, updated task list. Replaces the current list." })),
18
+ activeForm: ActiveForm,
19
+ subtasks: Type.Optional(Type.Array(Type.Object({ id: Type.Optional(Type.String()), content: Content, status: Status, activeForm: ActiveForm }), {
20
+ description: "Optional steps of this task (one level deep). The task's status then follows its subtasks."
21
+ }))
22
+ }), { description: "For append: new tasks only. For update: exactly one updated task. With write (or no action): the complete list, replacing it." })),
17
23
  action: Type.Optional(Action),
18
- id: Type.Optional(Type.String({ description: "Task id for start/done/open/edit/delete." })),
19
- title: Type.Optional(Type.String({ description: "Concise actionable task title for add/edit." })),
24
+ id: Type.Optional(Type.String({ description: "Stable task id for update/remove/start/done/open/edit/delete. On a parent, start runs its first open subtask; done/open apply to all its subtasks." })),
25
+ title: Type.Optional(Type.String({ description: "Task title for append/add/edit/update." })),
26
+ status: Type.Optional(Status),
27
+ activeForm: ActiveForm,
28
+ parent: Type.Optional(Type.String({ description: "For append/add: id of a top-level task to add the new task under as a subtask." })),
20
29
  details: Type.Optional(Type.String({ description: "Optional short task context for a new task." }))
21
30
  });
22
31
 
23
32
  interface TaskToolDetails {
24
33
  action: string;
25
- /** Bounded preview only; the canonical list lives in TodoStore. */
34
+ /** Bounded preview (the full list in a subagent); the canonical list lives in TodoStore. */
26
35
  items: Todo[];
36
+ /** Leaf tasks: subtasks plus top-level tasks without subtasks. */
27
37
  total: number;
28
38
  done: number;
39
+ /** Every item, parents included. */
40
+ count?: number;
29
41
  current?: string;
30
42
  changed?: string;
43
+ /** Full stable id index, including items outside the bounded preview. */
44
+ ids?: { id: string; title: string; parentId?: string }[];
45
+ /** Optional deliberately small render surface for terse mutations such as start/done. */
46
+ display?: Todo[];
31
47
  truncated?: boolean;
48
+ /** Subtask counts of previewed parents when the preview cuts the list. */
49
+ progress?: { id: string; done: number; total: number }[];
32
50
  }
33
51
 
52
+ type TodoParam = { id?: string; content: string; status: TodoStatus; activeForm?: string; subtasks?: readonly TodoParam[] };
53
+ /** Deeper nesting is passed through so the store rejects it with a clear message. */
54
+ const toInput = (item: TodoParam): TodoInput => ({
55
+ ...(item.id !== undefined ? { id: item.id } : {}),
56
+ title: item.content, status: item.status,
57
+ ...(item.activeForm !== undefined ? { activeForm: item.activeForm } : {}),
58
+ ...(item.subtasks !== undefined ? { subtasks: item.subtasks.map(toInput) } : {})
59
+ });
60
+
34
61
  const stats = (items: readonly Todo[]) => {
35
- let done = 0;
36
- let current: Todo | undefined;
37
- for (const item of items) {
38
- if (item.done) done++;
39
- if (!current && item.status === "in_progress") current = item;
40
- }
41
- return { total: items.length, done, current };
62
+ const parents = new Set<string>();
63
+ for (const item of items) if (item.parentId) parents.add(item.parentId);
64
+ const current = items.find((item) => item.status === "in_progress" && !parents.has(item.id));
65
+ const parent = current?.parentId ? items.find((item) => item.id === current.parentId) : undefined;
66
+ return { ...todoTotals(items), current: current && (parent ? parent.title + " › " : "") + current.title };
42
67
  };
43
68
  const summary = (items: readonly Todo[]) => {
44
69
  const value = stats(items);
45
- return `${value.done}/${value.total} done` + (value.current ? ` · now: ${value.current.title}` : "");
70
+ return `${value.done}/${value.total} done` + (value.current ? ` · now: ${value.current}` : "");
46
71
  };
47
72
 
48
- const lines = (items: readonly Todo[]) => items.length
49
- ? items.map((item) => `${todoMark(item)} [${item.status}] ${item.title} (${item.id})`).join("\n")
50
- : "No tracked tasks.";
73
+ const line = (items: readonly Todo[], item: Todo) => {
74
+ const progress = todoProgress(items, item.id);
75
+ return `${item.parentId ? " " : ""}${todoMark(item)} [${item.status}] ${item.title}${progress.total ? ` (${progress.done}/${progress.total} done)` : ""} (${item.id})`;
76
+ };
77
+ const lines = (items: readonly Todo[]) => items.length ? items.map((item) => line(items, item)).join("\n") : "No tracked tasks.";
51
78
 
52
79
  const REMINDER = "Keep using jar_todo to track progress: mark the next task in_progress before starting it and completed as soon as it is verified.";
53
80
 
@@ -60,74 +87,138 @@ export function registerTaskTool(
60
87
  pi.registerTool({
61
88
  name: "jar_todo",
62
89
  label: "tasks",
63
- description: "Maintain pi-jar's session task list, shown live to the user. Send `todos` with the complete updated list (each with content, status pending|in_progress|completed, and activeForm). Single-task actions (start, done, open, add, edit, delete, list) are also available.",
64
- promptSnippet: "Track multi-step work with jar_todo: write the full list, keep exactly one task in_progress, complete tasks as they finish.",
90
+ description: "Maintain session tasks: write establishes a fresh complete task plan, update changes one existing task by stable id, append adds genuinely new work, and remove deletes one task tree. start/done/open/edit remain concise status operations. Stable IDs are returned for every task.",
91
+ promptSnippet: "Track multi-step work with jar_todo: write the task plan first, update/remove existing tasks by stable id, append only when new work arrives mid-run, and keep one leaf in_progress.",
65
92
  promptGuidelines: [
66
93
  "Use jar_todo proactively, without waiting for the user, for any task with three or more distinct steps, when the user gives several tasks, or right after receiving new instructions. Skip it for single trivial requests and pure questions.",
67
- "Prefer jar_todo with `todos` (the full updated list). Each item has `content` (imperative, e.g. \"Run tests\"), `status`, and `activeForm` (present continuous, e.g. \"Running tests\"), which the user sees while it runs.",
68
- "Keep exactly one task in_progress at a time. Mark a task in_progress before you start it and completed immediately after it is verified; do not batch completions.",
94
+ "Prefer write when establishing the task plan for a new request. If the existing checklist is fully completed, start the next request with a fresh write rather than appending onto old completed work.",
95
+ "Use update with a stable id to revise one existing task and remove to delete one task tree. Use append mainly when the user steers or adds genuinely new requirements while an active checklist is already in progress; do not use append as the default way to create a new plan.",
96
+ "write with todos is the complete current task plan and removes omitted tasks. Each item has content, status, and optional activeForm/subtasks.",
97
+ "Break a large task into `subtasks` (same shape, one level deep). A parent's status follows its subtasks: it is completed when all of them are. Progress counts leaf tasks (subtasks and tasks without subtasks).",
98
+ "Keep exactly one leaf task in_progress at a time. Mark a task in_progress before you start it and completed immediately after it is verified; do not batch completions.",
69
99
  "Only mark a task completed when it is fully done. If tests fail, work is partial or you are blocked, keep it in_progress and add a task for what must be resolved. Remove tasks that are no longer relevant."
70
100
  ],
71
101
  parameters: Parameters,
72
102
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
73
103
  const current = store();
74
- const action = params.todos ? "write" : params.action ?? "list";
104
+ const action = params.action ?? (params.todos !== undefined ? "write" : "list");
75
105
  if (!current) {
76
106
  return { content: [{ type: "text", text: "Task tracking is unavailable before a Pi session starts." }], details: { action, items: [], total: 0, done: 0 } satisfies TaskToolDetails };
77
107
  }
78
108
  let changedId: string | undefined;
79
109
  let error: string | undefined;
80
- switch (action) {
110
+ let inputs: TodoInput[] | undefined;
111
+ if (params.todos !== undefined) {
112
+ try {
113
+ if (!Array.isArray(params.todos)) throw new Error();
114
+ inputs = params.todos.map(toInput);
115
+ } catch { error = "todos must be an array of task objects with array subtasks."; }
116
+ if (!["write", "append", "update"].includes(action)) error = "todos may only be used with write, append, or update; no tasks were changed.";
117
+ }
118
+ if (!error) switch (action) {
81
119
  case "list":
82
120
  break;
83
121
  case "write":
84
- if (!params.todos) { error = "write needs `todos`: the complete updated list."; break; }
85
- error = current.write(params.todos.map((item) => ({ title: item.content, status: item.status, ...(item.activeForm ? { activeForm: item.activeForm } : {}) })));
122
+ if (!inputs) { error = "write needs `todos`: the complete updated list."; break; }
123
+ error = current.write(inputs);
124
+ break;
125
+ case "append": {
126
+ const before = new Set(current.all().map((item) => item.id));
127
+ if (inputs && params.title !== undefined) { error = "append accepts either todos or title, not both."; break; }
128
+ error = current.appendTodos(inputs ?? [{ title: params.title ?? "", status: params.status ?? "pending", activeForm: params.activeForm, details: params.details }], params.parent);
129
+ if (!error) changedId = current.all().find((item) => !before.has(item.id))?.id;
86
130
  break;
131
+ }
132
+ case "update": {
133
+ if (!params.id) { error = "update needs a valid existing task id."; break; }
134
+ if (inputs && (inputs.length !== 1 || params.title !== undefined || params.status !== undefined || params.activeForm !== undefined)) { error = "update accepts exactly one todos item OR title/status/activeForm fields."; break; }
135
+ const replacement = inputs?.[0] ?? {
136
+ ...(params.title !== undefined ? { title: params.title } : {}),
137
+ ...(params.status !== undefined ? { status: params.status } : {}),
138
+ ...(params.activeForm !== undefined ? { activeForm: params.activeForm } : {})
139
+ };
140
+ error = current.update(params.id, replacement);
141
+ if (!error) changedId = params.id;
142
+ break;
143
+ }
87
144
  case "add": {
88
- const item = params.title?.trim() ? current.add(params.title, params.details) : undefined;
89
- if (!item) error = "add needs a short title.";
145
+ const parent = params.parent?.trim() || undefined;
146
+ const title = params.title ?? "";
147
+ error = current.addError(title, parent);
148
+ const item = error ? undefined : current.add(title, params.details, parent);
149
+ if (!error && !item) error = "Could not save the task.";
90
150
  changedId = item?.id;
91
151
  break;
92
152
  }
93
153
  case "start":
94
154
  case "done":
95
- case "open":
96
- if (!params.id || !current.setStatus(params.id, action === "start" ? "in_progress" : action === "done" ? "completed" : "pending")) error = `${action} needs a valid id from jar_todo list.`;
97
- else changedId = params.id;
155
+ case "open": {
156
+ const item = params.id ? current.get(params.id) : undefined;
157
+ const progress = item ? todoProgress(current.all(), item.id) : { done: 0, total: 0 };
158
+ if (!item) error = `${action} needs a valid id from jar_todo list.`;
159
+ else if (action === "start" && progress.total && progress.done === progress.total) error = `every subtask of ${item.id} is completed; open it or add a subtask first.`;
160
+ else if (!current.setStatus(item.id, action === "start" ? "in_progress" : action === "done" ? "completed" : "pending")) error = "Could not save the task.";
161
+ else changedId = action === "start" ? (current.current()?.id ?? item.id) : item.id;
98
162
  break;
163
+ }
99
164
  case "edit":
100
165
  if (!params.id || !params.title?.trim() || !current.edit(params.id, params.title)) error = "edit needs a valid id and a title.";
101
166
  else changedId = params.id;
102
167
  break;
168
+ case "remove":
103
169
  case "delete":
104
- if (!params.id || !current.delete(params.id)) error = "delete needs a valid id from jar_todo list.";
170
+ if (!params.id || !current.delete(params.id)) error = `${action} needs a valid id from jar_todo list.`;
105
171
  else changedId = params.id;
106
172
  break;
173
+ default: error = "Unknown task action; no tasks were changed.";
107
174
  }
108
175
  if (action !== "list" && !error) changed(ctx);
109
176
  const items = current.all();
110
177
  const state = stats(items);
178
+ // The changed task in context: its whole parent group, subtasks indented.
111
179
  const changedItem = changedId ? items.find((item) => item.id === changedId) : undefined;
180
+ const root = changedItem?.parentId ?? changedItem?.id;
181
+ const group = root ? items.filter((item) => item.id === root || item.parentId === root) : [];
182
+ const mutationItems = action === "write" || action === "append" ? items : group;
183
+ const mutationPreview = mutationItems.slice(0, 8);
184
+ const terseMutation = action === "start" || action === "done";
185
+ const terseItem = terseMutation && changedId ? items.find((item) => item.id === changedId) : undefined;
186
+ // Keep task prose bounded while still returning every addressable id in canonical order.
187
+ const idIndex = mutationItems.length > 8
188
+ ? "\nTask IDs (list order): " + items.map((item, index) => `${index + 1}=${item.id}`).join(", ") + "\nUse list for all task titles."
189
+ : "";
112
190
  const message = error
113
191
  ? `Could not ${action} tasks: ${error} (${summary(items)}).`
114
192
  : action === "list"
115
193
  ? `Tasks (${summary(items)}):\n${lines(items)}`
116
- : `Task list updated (${summary(items)}).${changedItem ? `\n${todoMark(changedItem)} [${changedItem.status}] ${changedItem.title} (${changedItem.id})` : ""}\n${REMINDER}`;
117
- const preview = items.slice(0, 8);
194
+ : action === "start" && terseItem
195
+ ? `Started task:\n${line(items, terseItem)}`
196
+ : action === "done" && terseItem
197
+ ? `Completed task:\n${line(items, terseItem)}`
198
+ : `Task list updated (${summary(items)}).${mutationPreview.length ? "\n" + mutationPreview.map((item) => line(items, item)).join("\n") : ""}${idIndex}\n${REMINDER}`;
199
+ // A subagent's parent mirrors the checklist from these details, so children send it whole.
200
+ const preview = process.env.PI_JAR_CHILD ? items : items.slice(0, 8);
201
+ const truncated = preview.length < items.length;
202
+ const progress = truncated ? preview.flatMap((item) => {
203
+ const value = todoProgress(items, item.id);
204
+ return value.total ? [{ id: item.id, ...value }] : [];
205
+ }) : [];
118
206
  return {
119
207
  content: [{ type: "text", text: message }],
120
208
  details: {
121
- action, items: preview, total: state.total, done: state.done,
122
- ...(state.current ? { current: state.current.title } : {}),
123
- ...(preview.length < items.length ? { truncated: true } : {}),
209
+ action, items: preview, total: state.total, done: state.done, count: items.length,
210
+ ids: items.map(({ id, title, parentId }) => ({ id, title, ...(parentId ? { parentId } : {}) })),
211
+ ...(terseItem ? { display: [{ ...terseItem }] } : {}),
212
+ ...(state.current ? { current: state.current } : {}),
213
+ ...(truncated ? { truncated: true } : {}),
214
+ ...(progress.length ? { progress } : {}),
124
215
  ...(changedId ? { changed: changedId } : {})
125
216
  } satisfies TaskToolDetails
126
217
  };
127
218
  },
128
219
  renderCall(args, theme) {
129
220
  let text = theme.fg("toolTitle", theme.bold("tasks"));
130
- if (args.todos) text += " " + theme.fg("accent", `update · ${args.todos.length} item${args.todos.length === 1 ? "" : "s"}`);
221
+ if (args.todos) text += " " + theme.fg("accent", `${args.action ?? "write"} · ${args.todos.length} item${args.todos.length === 1 ? "" : "s"}`);
131
222
  else {
132
223
  text += " " + theme.fg("accent", args.action ?? "list");
133
224
  if (args.title) text += " " + theme.fg("muted", args.title);
@@ -138,22 +229,33 @@ export function registerTaskTool(
138
229
  if (isPartial) return new Text(theme.fg("warning", "Updating tasks…"), 0, 0);
139
230
  const details = result.details as TaskToolDetails | undefined;
140
231
  const items = details?.items ?? [];
141
- const total = details?.total ?? items.length;
142
- const done = details?.done ?? items.filter((item) => item.done).length;
143
- if (!total) return new Text(theme.fg("dim", "No tracked tasks."), 0, 0);
144
- const shown = expanded ? items : items.slice(0, 8);
145
- let text = theme.fg("dim", `${done}/${total} done${details?.current ? " · now: " + details.current : ""}`);
146
- for (const item of shown) text += "\n" + todoRow(item, (color, value) => theme.fg(color, value), (value) => theme.bold(value));
147
- if (details?.truncated || shown.length < total) text += "\n" + theme.fg("dim", ` … +${Math.max(0, total - shown.length)} more · /jar tasks`);
232
+ const displayItems = details?.display ?? items;
233
+ // Older results predate `count`, when `total` counted every item.
234
+ const count = details?.count ?? details?.total ?? items.length;
235
+ const totals = details?.total === undefined ? todoTotals(items) : { done: details.done, total: details.total };
236
+ if (!count) return new Text(theme.fg("dim", "No tracked tasks."), 0, 0);
237
+ const known = new Map((details?.progress ?? []).map((value) => [value.id, value]));
238
+ const shown = details?.display ? displayItems : expanded ? items : items.slice(0, 8);
239
+ let text = details?.display ? "" : theme.fg("dim", `${totals.done}/${totals.total} done${details?.current ? " · now: " + details.current : ""}`);
240
+ for (const item of shown) {
241
+ const row = todoRow(item, (color, value) => theme.fg(color, value), (value) => theme.bold(value), known.get(item.id) ?? todoProgress(items, item.id));
242
+ text += (text ? "\n" : "") + row;
243
+ }
244
+ if (!details?.display && (details?.truncated || shown.length < count)) text += "\n" + theme.fg("dim", ` … +${Math.max(0, count - shown.length)} more · /jar tasks`);
148
245
  return new Text(text, 0, 0);
149
246
  }
150
247
  });
151
248
  }
152
249
 
153
250
  type TodoColor = "accent" | "muted" | "dim" | "success";
154
- /** One checklist row: completed rows are struck through, the running one is bold. */
155
- export function todoRow(item: Todo, fg: (color: TodoColor, text: string) => string, bold: (text: string) => string = (text) => text): string {
156
- if (item.status === "completed") return fg("success", " ✔ ") + fg("dim", "\x1b[9m" + item.title + "\x1b[29m");
157
- if (item.status === "in_progress") return fg("accent", " ◼ ") + fg("accent", bold(item.title));
158
- return fg("muted", " ☐ " + item.title);
251
+ /**
252
+ * One checklist row: completed rows are struck through, the running one is bold. Subtasks are
253
+ * indented; pass a parent's subtask `progress` to show its `(done/total)`.
254
+ */
255
+ export function todoRow(item: Todo, fg: (color: TodoColor, text: string) => string, bold: (text: string) => string = (text) => text, progress?: { done: number; total: number }): string {
256
+ const indent = item.parentId ? " " : " ";
257
+ const suffix = progress?.total ? fg("dim", ` (${progress.done}/${progress.total})`) : "";
258
+ if (item.status === "completed") return fg("success", indent + "✔ ") + fg("dim", "\x1b[9m" + item.title + "\x1b[29m") + suffix;
259
+ if (item.status === "in_progress") return fg("accent", indent + "◼ ") + fg("accent", bold(item.title)) + suffix;
260
+ return fg("muted", indent + "☐ " + item.title) + suffix;
159
261
  }
package/src/tasks-ui.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { promptChoice, promptText, todoView, type TodoFilter } from "./dialogs.ts";
3
- import { TodoStore } from "./tasks.ts";
3
+ import { TodoStore, todoProgress } from "./tasks.ts";
4
4
 
5
5
  const help = "Usage: /jar tasks [list|add TITLE|done ID|open ID|edit ID TITLE|delete ID]";
6
6
 
@@ -11,7 +11,15 @@ export async function manageTasks(args: string, ctx: ExtensionContext, store: To
11
11
  };
12
12
  const list = () => {
13
13
  const items = store.all();
14
- notify(items.length ? items.map((item) => `${item.done ? "[x]" : "[ ]"} ${item.title} (${item.id})`).join("\n") : "pi-jar to-do: empty (separate from other task managers)");
14
+ notify(items.length ? items.map((item) => {
15
+ const progress = todoProgress(items, item.id);
16
+ return `${item.parentId ? " " : ""}${item.done ? "[x]" : "[ ]"} ${item.title}${progress.total ? ` (${progress.done}/${progress.total})` : ""} (${item.id})`;
17
+ }).join("\n") : "pi-jar to-do: empty (separate from other task managers)");
18
+ };
19
+ /** Deleting a parent also deletes its subtasks, so say so before confirming. */
20
+ const deleteLabel = (id: string, title: string) => {
21
+ const count = todoProgress(store.all(), id).total;
22
+ return count ? `${title} (and ${count} subtask${count === 1 ? "" : "s"})` : title;
15
23
  };
16
24
  if (!args && ctx.hasUI && ctx.mode === "tui") {
17
25
  const filter: { value: TodoFilter } = { value: "all" };
@@ -29,7 +37,7 @@ export async function manageTasks(args: string, ctx: ExtensionContext, store: To
29
37
  if (title) success = store.edit(action.id, title);
30
38
  } else if (action.id && action.kind === "delete") {
31
39
  const item = store.get(action.id);
32
- const choice = item && await promptChoice(ctx, "Delete to-do?", item.title, ["Delete item", "Keep item"], 1);
40
+ const choice = item && await promptChoice(ctx, "Delete to-do?", deleteLabel(item.id, item.title), ["Delete item", "Keep item"], 1);
33
41
  if (choice === 0) success = store.delete(action.id);
34
42
  }
35
43
  if (success) changed();
@@ -47,8 +55,10 @@ export async function manageTasks(args: string, ctx: ExtensionContext, store: To
47
55
  }
48
56
  case "done":
49
57
  case "open": {
58
+ // On a parent, done completes and open reopens every subtask.
50
59
  const item = id && store.get(id);
51
- if (item && item.done !== (verb === "done") && store.toggle(id)) { changed(); notify("Updated pi-jar to-do"); }
60
+ const already = item && (verb === "done" ? item.done : item.status === "pending");
61
+ if (item && !already && store.setStatus(id, verb === "done" ? "completed" : "pending")) { changed(); notify("Updated pi-jar to-do"); }
52
62
  else notify("To-do not found or already in that state", "error");
53
63
  return;
54
64
  }
@@ -61,7 +71,7 @@ export async function manageTasks(args: string, ctx: ExtensionContext, store: To
61
71
  case "delete": {
62
72
  if (!id || !store.get(id)) { notify("To-do not found", "error"); return; }
63
73
  if (ctx.hasUI && ctx.mode === "tui") {
64
- const choice = await promptChoice(ctx, "Delete pi-jar to-do?", store.get(id)!.title, ["Delete item", "Keep item"], 1);
74
+ const choice = await promptChoice(ctx, "Delete pi-jar to-do?", deleteLabel(id, store.get(id)!.title), ["Delete item", "Keep item"], 1);
65
75
  if (choice !== 0) return;
66
76
  }
67
77
  // Headless deletion is explicit and requires the full ID; TUI deletion asks for confirmation.