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/README.md +57 -15
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/WORKFLOWS.md +32 -7
- package/extensions/index.ts +127 -48
- package/package.json +1 -1
- package/src/activity-view.ts +324 -44
- package/src/advisor.ts +9 -3
- package/src/ask-tool.ts +12 -7
- package/src/changes.ts +35 -11
- package/src/child-baselines.ts +42 -0
- package/src/compact-tools.ts +58 -24
- package/src/composer.ts +39 -12
- package/src/delegate-worktree.ts +360 -0
- package/src/delegate.ts +1137 -129
- package/src/democracy.ts +192 -0
- package/src/dialogs.ts +9 -5
- package/src/diff-view.ts +13 -1
- package/src/discussion.ts +153 -0
- package/src/flame.ts +18 -5
- package/src/footer.ts +38 -19
- package/src/goal-loop.ts +8 -1
- package/src/history-ui.ts +19 -12
- package/src/history.ts +3 -1
- package/src/model-roles.ts +120 -36
- package/src/panel.ts +5 -1
- package/src/prompt-search.ts +16 -8
- package/src/roles-ui.ts +18 -10
- package/src/row-cache.ts +26 -0
- package/src/session-ui.ts +9 -4
- package/src/settings-ui.ts +46 -24
- package/src/settings.ts +9 -1
- package/src/shells.ts +2 -1
- package/src/side-model.ts +45 -25
- package/src/split-view.ts +29 -12
- package/src/status.ts +2 -1
- package/src/task-tool.ts +156 -54
- package/src/tasks-ui.ts +15 -5
- package/src/tasks.ts +224 -38
- package/src/welcome.ts +26 -13
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
|
|
9
|
-
const Status = Type.Unsafe<(typeof TODO_STATUSES)[number]>({ type: "string", enum: TODO_STATUSES
|
|
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
|
-
|
|
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:
|
|
16
|
-
|
|
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: "
|
|
19
|
-
title: Type.Optional(Type.String({ description: "
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
70
|
+
return `${value.done}/${value.total} done` + (value.current ? ` · now: ${value.current}` : "");
|
|
46
71
|
};
|
|
47
72
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
: "
|
|
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
|
|
64
|
-
promptSnippet: "Track multi-step work with jar_todo: write the
|
|
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
|
|
68
|
-
"
|
|
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" :
|
|
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
|
-
|
|
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 (!
|
|
85
|
-
error = current.write(
|
|
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
|
|
89
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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 =
|
|
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
|
-
:
|
|
117
|
-
|
|
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
|
-
...(
|
|
123
|
-
...(
|
|
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",
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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) =>
|
|
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
|
-
|
|
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.
|