@agnishc/edb-todo 0.8.1 → 0.10.3
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/CHANGELOG.md +50 -0
- package/README.md +151 -18
- package/package.json +1 -1
- package/src/auto-clear.ts +87 -0
- package/src/component.ts +314 -69
- package/src/config.ts +25 -0
- package/src/file-store.ts +408 -0
- package/src/index.ts +584 -82
- package/src/process-tracker.ts +146 -0
- package/src/prompt.ts +19 -17
- package/src/schemas.ts +55 -24
- package/src/state.ts +251 -72
- package/src/types.ts +18 -2
package/src/component.ts
CHANGED
|
@@ -1,22 +1,71 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
Container,
|
|
4
|
+
matchesKey,
|
|
5
|
+
type SettingItem,
|
|
6
|
+
SettingsList,
|
|
7
|
+
Spacer,
|
|
8
|
+
Text,
|
|
9
|
+
truncateToWidth,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
|
+
import type { TodoConfig } from "./config.js";
|
|
12
|
+
import { saveTodoConfig } from "./config.js";
|
|
13
|
+
import type { FileTaskStore } from "./file-store.js";
|
|
14
|
+
import { priorityColor, priorityLabel, renderTaskListResult } from "./state.js";
|
|
15
|
+
import type { Task, TaskDetails } from "./types.js";
|
|
16
|
+
import { PRIORITY_ORDER } from "./types.js";
|
|
4
17
|
|
|
5
18
|
// ── /todos interactive viewer ──────────────────────────────────────────────────
|
|
6
19
|
|
|
7
20
|
export class TodoViewComponent {
|
|
21
|
+
private cursorIndex: number = 0;
|
|
22
|
+
private showCompleted: boolean = true;
|
|
8
23
|
private cachedWidth?: number;
|
|
9
24
|
private cachedLines?: string[];
|
|
25
|
+
private flatTasks: Task[] = [];
|
|
10
26
|
|
|
11
27
|
constructor(
|
|
12
28
|
private readonly tasks: Task[],
|
|
13
29
|
private readonly theme: any,
|
|
14
30
|
private readonly onClose: () => void,
|
|
15
|
-
) {
|
|
31
|
+
) {
|
|
32
|
+
this.rebuildFlatTasks();
|
|
33
|
+
if (this.flatTasks.length > 0) {
|
|
34
|
+
this.cursorIndex = Math.min(this.cursorIndex, this.flatTasks.length - 1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
16
37
|
|
|
17
38
|
handleInput(data: string): void {
|
|
18
39
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
19
40
|
this.onClose();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
44
|
+
if (this.cursorIndex > 0) this.cursorIndex--;
|
|
45
|
+
this.invalidate();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
49
|
+
if (this.cursorIndex < this.flatTasks.length - 1) this.cursorIndex++;
|
|
50
|
+
this.invalidate();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (data === "c") {
|
|
54
|
+
this.showCompleted = !this.showCompleted;
|
|
55
|
+
this.rebuildFlatTasks();
|
|
56
|
+
this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.flatTasks.length - 1));
|
|
57
|
+
this.invalidate();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (matchesKey(data, "home") || data === "g") {
|
|
61
|
+
this.cursorIndex = 0;
|
|
62
|
+
this.invalidate();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (matchesKey(data, "end") || data === "G") {
|
|
66
|
+
this.cursorIndex = Math.max(0, this.flatTasks.length - 1);
|
|
67
|
+
this.invalidate();
|
|
68
|
+
return;
|
|
20
69
|
}
|
|
21
70
|
}
|
|
22
71
|
|
|
@@ -31,7 +80,9 @@ export class TodoViewComponent {
|
|
|
31
80
|
const titleText = " Tasks ";
|
|
32
81
|
const sideLen = Math.max(0, width - titleText.length - 3);
|
|
33
82
|
const headerLine =
|
|
34
|
-
th.fg("borderMuted", "─".repeat(3)) +
|
|
83
|
+
th.fg("borderMuted", "─".repeat(3)) +
|
|
84
|
+
th.fg("accent", th.bold(titleText)) +
|
|
85
|
+
th.fg("borderMuted", "─".repeat(sideLen));
|
|
35
86
|
lines.push(truncateToWidth(headerLine, width));
|
|
36
87
|
lines.push("");
|
|
37
88
|
|
|
@@ -41,43 +92,72 @@ export class TodoViewComponent {
|
|
|
41
92
|
// ── Progress bar ──
|
|
42
93
|
const completedCount = this.tasks.filter((t) => t.status === "completed").length;
|
|
43
94
|
const total = this.tasks.length;
|
|
44
|
-
const barWidth = Math.min(
|
|
95
|
+
const barWidth = Math.min(20, width - 22);
|
|
45
96
|
const filled = total > 0 ? Math.round((completedCount / total) * barWidth) : 0;
|
|
46
97
|
const empty = barWidth - filled;
|
|
47
98
|
const bar = `[${th.fg("success", "█".repeat(filled))}${th.fg("dim", "░".repeat(empty))}]`;
|
|
48
|
-
|
|
99
|
+
const pct = total > 0 ? Math.round((completedCount / total) * 100) : 0;
|
|
100
|
+
lines.push(
|
|
101
|
+
truncateToWidth(
|
|
102
|
+
` ${bar} ${th.fg("muted", `${completedCount}/${total}`)} ${th.fg("dim", `(${pct}%)`)}`,
|
|
103
|
+
width,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
49
106
|
lines.push("");
|
|
50
107
|
|
|
51
|
-
// ──
|
|
108
|
+
// ── Build sections ──
|
|
52
109
|
const inProgress = this.tasks.filter((t) => t.status === "in_progress");
|
|
110
|
+
const pending = this.tasks
|
|
111
|
+
.filter((t) => t.status === "pending")
|
|
112
|
+
.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
|
|
113
|
+
const done = this.tasks.filter((t) => t.status === "completed");
|
|
114
|
+
|
|
115
|
+
let flatIdx = 0;
|
|
116
|
+
|
|
53
117
|
if (inProgress.length > 0) {
|
|
54
|
-
lines.push(
|
|
55
|
-
|
|
118
|
+
lines.push(
|
|
119
|
+
truncateToWidth(
|
|
120
|
+
` ${th.fg("accent", th.bold("In Progress"))} ${th.fg("dim", `(${inProgress.length})`)}`,
|
|
121
|
+
width,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
for (const t of inProgress) {
|
|
125
|
+
lines.push(...this.renderTask(t, width, flatIdx));
|
|
126
|
+
flatIdx++;
|
|
127
|
+
}
|
|
56
128
|
lines.push("");
|
|
57
129
|
}
|
|
58
130
|
|
|
59
|
-
// ── Pending (sorted by priority) ──
|
|
60
|
-
const pending = this.tasks
|
|
61
|
-
.filter((t) => t.status === "pending")
|
|
62
|
-
.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
|
|
63
131
|
if (pending.length > 0) {
|
|
64
|
-
lines.push(
|
|
65
|
-
|
|
132
|
+
lines.push(
|
|
133
|
+
truncateToWidth(` ${th.fg("muted", th.bold("Pending"))} ${th.fg("dim", `(${pending.length})`)}`, width),
|
|
134
|
+
);
|
|
135
|
+
for (const t of pending) {
|
|
136
|
+
lines.push(...this.renderTask(t, width, flatIdx));
|
|
137
|
+
flatIdx++;
|
|
138
|
+
}
|
|
66
139
|
lines.push("");
|
|
67
140
|
}
|
|
68
141
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
for (const t of done)
|
|
142
|
+
if (done.length > 0 && this.showCompleted) {
|
|
143
|
+
lines.push(
|
|
144
|
+
truncateToWidth(` ${th.fg("dim", th.bold("Completed"))} ${th.fg("dim", `(${done.length})`)}`, width),
|
|
145
|
+
);
|
|
146
|
+
for (const t of done) {
|
|
147
|
+
lines.push(...this.renderTask(t, width, flatIdx));
|
|
148
|
+
flatIdx++;
|
|
149
|
+
}
|
|
150
|
+
lines.push("");
|
|
151
|
+
} else if (done.length > 0 && !this.showCompleted) {
|
|
152
|
+
lines.push(truncateToWidth(` ${th.fg("dim", `${done.length} completed — press c to show`)}`, width));
|
|
74
153
|
lines.push("");
|
|
75
154
|
}
|
|
76
155
|
}
|
|
77
156
|
|
|
78
157
|
// ── Footer ──
|
|
79
158
|
lines.push(truncateToWidth(th.fg("borderMuted", "─".repeat(width)), width));
|
|
80
|
-
|
|
159
|
+
const keys = ["↑↓ navigate", "c toggle completed", "esc close"];
|
|
160
|
+
lines.push(truncateToWidth(` ${th.fg("dim", keys.join(" • "))}`, width));
|
|
81
161
|
lines.push("");
|
|
82
162
|
|
|
83
163
|
this.cachedWidth = width;
|
|
@@ -85,72 +165,237 @@ export class TodoViewComponent {
|
|
|
85
165
|
return lines;
|
|
86
166
|
}
|
|
87
167
|
|
|
88
|
-
private renderTask(task: Task, width: number): string[] {
|
|
168
|
+
private renderTask(task: Task, width: number, flatIdx: number): string[] {
|
|
89
169
|
const th = this.theme;
|
|
170
|
+
const isFocused = flatIdx === this.cursorIndex;
|
|
90
171
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
172
|
+
let icon: string;
|
|
173
|
+
if (task.status === "completed") {
|
|
174
|
+
icon = th.fg("success", "✓");
|
|
175
|
+
} else if (task.status === "in_progress") {
|
|
176
|
+
icon = th.fg("accent", "●");
|
|
177
|
+
} else {
|
|
178
|
+
icon = th.fg("dim", "○");
|
|
179
|
+
}
|
|
97
180
|
|
|
98
|
-
const
|
|
99
|
-
const pLabel = th.fg(
|
|
181
|
+
const pColor = priorityColor(task.priority);
|
|
182
|
+
const pLabel = th.fg(pColor, priorityLabel(task.priority));
|
|
100
183
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
184
|
+
let contentText: string;
|
|
185
|
+
if (task.status === "completed") {
|
|
186
|
+
contentText = th.fg("dim", th.strikethrough(task.content));
|
|
187
|
+
} else if (task.status === "in_progress") {
|
|
188
|
+
contentText = th.fg("text", th.bold(task.content));
|
|
189
|
+
} else {
|
|
190
|
+
contentText = th.fg("muted", task.content);
|
|
191
|
+
}
|
|
107
192
|
|
|
108
193
|
const idHint = th.fg("dim", ` [${task.id}]`);
|
|
194
|
+
const cursor = isFocused ? th.fg("accent", "❯") : " ";
|
|
109
195
|
|
|
110
|
-
|
|
196
|
+
// Dependency hint
|
|
197
|
+
let depHint = "";
|
|
198
|
+
if (task.blockedBy.length > 0) {
|
|
199
|
+
const openBlockers = task.blockedBy.filter((_bid) => {
|
|
200
|
+
// We only have the flat list, use basic check
|
|
201
|
+
return true; // shown for visibility
|
|
202
|
+
});
|
|
203
|
+
if (openBlockers.length > 0) {
|
|
204
|
+
depHint = th.fg("dim", ` ← blocked by ${openBlockers.map((id) => `#${id}`).join(", ")}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (task.blocks.length > 0) {
|
|
208
|
+
depHint += th.fg("dim", ` → blocks ${task.blocks.map((id) => `#${id}`).join(", ")}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return [truncateToWidth(` ${cursor} ${icon} ${pLabel} ${contentText}${idHint}${depHint}`, width)];
|
|
111
212
|
}
|
|
112
213
|
|
|
113
|
-
|
|
214
|
+
private rebuildFlatTasks(): void {
|
|
215
|
+
const inProgress = this.tasks.filter((t) => t.status === "in_progress");
|
|
216
|
+
const pending = this.tasks
|
|
217
|
+
.filter((t) => t.status === "pending")
|
|
218
|
+
.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
|
|
219
|
+
const done = this.showCompleted ? this.tasks.filter((t) => t.status === "completed") : [];
|
|
220
|
+
this.flatTasks = [...inProgress, ...pending, ...done];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
invalidate(): void {
|
|
224
|
+
this.cachedWidth = undefined;
|
|
225
|
+
this.cachedLines = undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Static tool result renderer ────────────────────────────────────────────
|
|
114
229
|
|
|
115
230
|
static renderTaskResult(details: TaskDetails | undefined, expanded: boolean, theme: any): any {
|
|
116
|
-
|
|
117
|
-
|
|
231
|
+
return renderTaskListResult(details?.tasks ?? [], expanded, theme);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Settings panel ──────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
export async function openTodoSettings(ui: any, cfg: TodoConfig, cwd: string, clearDelayTurns: number): Promise<void> {
|
|
238
|
+
await ui.custom((_tui: any, theme: any, _kb: any, done: (r: undefined) => void) => {
|
|
239
|
+
const items: SettingItem[] = [
|
|
240
|
+
{
|
|
241
|
+
id: "taskScope",
|
|
242
|
+
label: "Task storage",
|
|
243
|
+
description:
|
|
244
|
+
"memory: tasks live only in memory, lost when session ends. " +
|
|
245
|
+
"session: persisted per session (tasks-<sessionId>.json), survives resume. " +
|
|
246
|
+
"project: shared across all sessions (tasks.json). " +
|
|
247
|
+
"Takes effect on next session start.",
|
|
248
|
+
currentValue: cfg.taskScope ?? "session",
|
|
249
|
+
values: ["memory", "session", "project"],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "autoClearCompleted",
|
|
253
|
+
label: "Auto-clear completed tasks",
|
|
254
|
+
description:
|
|
255
|
+
"never: completed tasks stay visible until manually cleared. " +
|
|
256
|
+
"on_list_complete: cleared automatically after all tasks are done. " +
|
|
257
|
+
"on_task_complete: each task cleared shortly after it completes. " +
|
|
258
|
+
`Clearing lags ~${clearDelayTurns} turns.`,
|
|
259
|
+
currentValue: cfg.autoClearCompleted ?? "on_list_complete",
|
|
260
|
+
values: ["never", "on_list_complete", "on_task_complete"],
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
const list = new SettingsList(
|
|
265
|
+
items,
|
|
266
|
+
10,
|
|
267
|
+
getSettingsListTheme(),
|
|
268
|
+
(id, newValue) => {
|
|
269
|
+
if (id === "taskScope") {
|
|
270
|
+
cfg.taskScope = newValue as TodoConfig["taskScope"];
|
|
271
|
+
saveTodoConfig(cwd, cfg);
|
|
272
|
+
}
|
|
273
|
+
if (id === "autoClearCompleted") {
|
|
274
|
+
cfg.autoClearCompleted = newValue as TodoConfig["autoClearCompleted"];
|
|
275
|
+
saveTodoConfig(cwd, cfg);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
() => done(undefined),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
class SettingsPanel extends Container {
|
|
282
|
+
handleInput(data: string) {
|
|
283
|
+
list.handleInput(data);
|
|
284
|
+
}
|
|
118
285
|
}
|
|
119
286
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
287
|
+
const root = new SettingsPanel();
|
|
288
|
+
root.addChild(new Text(theme.bold(theme.fg("accent", "⚙ Todo Settings")), 0, 0));
|
|
289
|
+
root.addChild(new Spacer(1));
|
|
290
|
+
root.addChild(list);
|
|
291
|
+
return root;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── /todos detailed task viewer (select-based) ─────────────────────────────────
|
|
123
296
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
297
|
+
export async function openTodosMenu(
|
|
298
|
+
ui: any,
|
|
299
|
+
store: FileTaskStore,
|
|
300
|
+
cfg: TodoConfig,
|
|
301
|
+
cwd: string,
|
|
302
|
+
onTaskUpdate: (taskId: string, status?: string) => void,
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
const AUTO_CLEAR_DELAY = 4;
|
|
132
305
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
t.status === "completed"
|
|
137
|
-
? theme.fg("dim", theme.strikethrough(t.content))
|
|
138
|
-
: t.status === "in_progress"
|
|
139
|
-
? theme.fg("text", theme.bold(t.content))
|
|
140
|
-
: theme.fg("muted", t.content);
|
|
306
|
+
const mainMenu = async (): Promise<void> => {
|
|
307
|
+
const tasks = store.list();
|
|
308
|
+
const completedCount = tasks.filter((t) => t.status === "completed").length;
|
|
141
309
|
|
|
142
|
-
|
|
310
|
+
const choices: string[] = [`View all tasks (${tasks.length})`];
|
|
311
|
+
if (completedCount > 0) choices.push(`Clear completed (${completedCount})`);
|
|
312
|
+
if (tasks.length > 0) choices.push(`Clear all (${tasks.length})`);
|
|
313
|
+
choices.push("⚙ Settings");
|
|
314
|
+
|
|
315
|
+
const choice = await ui.select("Tasks", choices);
|
|
316
|
+
if (!choice) return;
|
|
317
|
+
|
|
318
|
+
if (choice.startsWith("View")) {
|
|
319
|
+
return viewTasks();
|
|
320
|
+
} else if (choice.startsWith("Clear completed")) {
|
|
321
|
+
store.clearCompleted();
|
|
322
|
+
store.deleteFileIfEmpty();
|
|
323
|
+
onTaskUpdate("", undefined);
|
|
324
|
+
return mainMenu();
|
|
325
|
+
} else if (choice.startsWith("Clear all")) {
|
|
326
|
+
store.clearAll();
|
|
327
|
+
store.deleteFileIfEmpty();
|
|
328
|
+
onTaskUpdate("", undefined);
|
|
329
|
+
return mainMenu();
|
|
330
|
+
} else if (choice.startsWith("⚙")) {
|
|
331
|
+
await openTodoSettings(ui, cfg, cwd, AUTO_CLEAR_DELAY);
|
|
332
|
+
return mainMenu();
|
|
143
333
|
}
|
|
334
|
+
};
|
|
144
335
|
|
|
145
|
-
|
|
146
|
-
|
|
336
|
+
const viewTasks = async (): Promise<void> => {
|
|
337
|
+
const tasks = store.list();
|
|
338
|
+
if (tasks.length === 0) {
|
|
339
|
+
await ui.select("No tasks", ["← Back"]);
|
|
340
|
+
return mainMenu();
|
|
147
341
|
}
|
|
148
342
|
|
|
149
|
-
|
|
150
|
-
|
|
343
|
+
const icon = (status: string) => {
|
|
344
|
+
if (status === "completed") return "✔";
|
|
345
|
+
if (status === "in_progress") return "◼";
|
|
346
|
+
return "◻";
|
|
347
|
+
};
|
|
151
348
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
349
|
+
const choices = tasks.map((t) => `${icon(t.status)} #${t.id} [${t.status}] ${t.content}`);
|
|
350
|
+
choices.push("← Back");
|
|
351
|
+
|
|
352
|
+
const selected = await ui.select("Tasks", choices);
|
|
353
|
+
if (!selected || selected === "← Back") return mainMenu();
|
|
354
|
+
|
|
355
|
+
const match = selected.match(/#([a-z0-9]+)/);
|
|
356
|
+
if (match) return viewTaskDetail(match[1]);
|
|
357
|
+
return viewTasks();
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const viewTaskDetail = async (taskId: string): Promise<void> => {
|
|
361
|
+
const task = store.get(taskId);
|
|
362
|
+
if (!task) return viewTasks();
|
|
363
|
+
|
|
364
|
+
const actions: string[] = [];
|
|
365
|
+
if (task.status === "pending") actions.push("▸ Start (in_progress)");
|
|
366
|
+
if (task.status === "in_progress") actions.push("✓ Complete");
|
|
367
|
+
actions.push("✗ Delete");
|
|
368
|
+
actions.push("← Back");
|
|
369
|
+
|
|
370
|
+
const deps: string[] = [];
|
|
371
|
+
if (task.blockedBy.length > 0) deps.push(`Blocked by: ${task.blockedBy.map((id) => `#${id}`).join(", ")}`);
|
|
372
|
+
if (task.blocks.length > 0) deps.push(`Blocks: ${task.blocks.map((id) => `#${id}`).join(", ")}`);
|
|
373
|
+
|
|
374
|
+
const detailLines = [
|
|
375
|
+
`#${task.id} [${task.status}] ${task.content}`,
|
|
376
|
+
task.description ? `\n${task.description}` : "",
|
|
377
|
+
deps.length > 0 ? `\n${deps.join(" | ")}` : "",
|
|
378
|
+
]
|
|
379
|
+
.filter(Boolean)
|
|
380
|
+
.join("");
|
|
381
|
+
|
|
382
|
+
const action = await ui.select(detailLines, actions);
|
|
383
|
+
|
|
384
|
+
if (action === "▸ Start (in_progress)") {
|
|
385
|
+
store.update(taskId, { status: "in_progress" });
|
|
386
|
+
onTaskUpdate(taskId, "in_progress");
|
|
387
|
+
return viewTasks();
|
|
388
|
+
} else if (action === "✓ Complete") {
|
|
389
|
+
store.update(taskId, { status: "completed" });
|
|
390
|
+
onTaskUpdate(taskId, "completed");
|
|
391
|
+
return viewTasks();
|
|
392
|
+
} else if (action === "✗ Delete") {
|
|
393
|
+
store.update(taskId, { status: "deleted" });
|
|
394
|
+
onTaskUpdate(taskId, "deleted");
|
|
395
|
+
return viewTasks();
|
|
396
|
+
}
|
|
397
|
+
return viewTasks();
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
await mainMenu();
|
|
156
401
|
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// <cwd>/.pi/tasks-config.json — persists extension settings across sessions
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export interface TodoConfig {
|
|
7
|
+
/** Where tasks are stored. Default: "session" */
|
|
8
|
+
taskScope?: "memory" | "session" | "project";
|
|
9
|
+
/** Auto-clear completed tasks. Default: "on_list_complete" */
|
|
10
|
+
autoClearCompleted?: "never" | "on_list_complete" | "on_task_complete";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function loadTodoConfig(cwd: string): TodoConfig {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(join(cwd, ".pi", "tasks-config.json"), "utf-8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveTodoConfig(cwd: string, config: TodoConfig): void {
|
|
22
|
+
const path = join(cwd, ".pi", "tasks-config.json");
|
|
23
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
24
|
+
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
25
|
+
}
|