@agnishc/edb-todo 0.8.2 → 0.10.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.
- package/CHANGELOG.md +29 -0
- package/README.md +145 -33
- package/package.json +1 -1
- package/src/auto-clear.ts +87 -0
- package/src/component.ts +194 -57
- package/src/config.ts +25 -0
- package/src/file-store.ts +408 -0
- package/src/index.ts +554 -108
- package/src/process-tracker.ts +146 -0
- package/src/prompt.ts +15 -11
- package/src/schemas.ts +52 -27
- package/src/state.ts +224 -97
- package/src/types.ts +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.4] - 2026-05-15
|
|
4
|
+
|
|
5
|
+
## [0.10.3] - 2026-05-15
|
|
6
|
+
|
|
7
|
+
## [0.9.0] - 2026-05-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `todo_create` tool — create individual tasks with `content`, `description`, `priority`, `activeForm`, and `metadata`
|
|
11
|
+
- `todo_get` tool — retrieve full task details: description, dependencies (`blocks`/`blockedBy`), metadata
|
|
12
|
+
- `todo_update` tool — update individual tasks (status, content, description, priority, owner); add dependency edges (`addBlocks`/`addBlockedBy`); `status: "deleted"` permanently removes the task
|
|
13
|
+
- **File-backed storage** — tasks now persist to disk with file locking and atomic writes
|
|
14
|
+
- `memory`: in-memory only (lost on session end)
|
|
15
|
+
- `session` *(default)*: per-session file at `<cwd>/.pi/tasks/tasks-<sessionId>.json`
|
|
16
|
+
- `project`: shared across all sessions at `<cwd>/.pi/tasks/tasks.json`
|
|
17
|
+
- **Dependency management** — bidirectional `blocks`/`blockedBy` edges with cycle detection and warnings
|
|
18
|
+
- **Auto-clear completed tasks** — configurable via settings: `never` / `on_list_complete` *(default)* / `on_task_complete`; turn-based delay so completions linger briefly before disappearing
|
|
19
|
+
- **Settings panel** — `/todos → ⚙ Settings` opens a native TUI settings panel (taskScope + autoClearCompleted); saved to `<cwd>/.pi/tasks-config.json`
|
|
20
|
+
- **System-reminder injection** — periodic `<system-reminder>` nudges appended to non-task tool results after `REMINDER_INTERVAL` turns of inactivity, encouraging the model to keep tasks up to date
|
|
21
|
+
- **Enhanced widget** — animated star spinner (✳✴✵…) for in-progress tasks, elapsed time display (e.g. `42s`, `2m 5s`), blocked-by hints inline
|
|
22
|
+
- `PI_TODO` environment variable override: `off` (memory only), named list (`~/.pi/tasks/<name>.json`), or absolute/relative path
|
|
23
|
+
- `/todos` command now shows a select-based menu with View / Clear completed / Clear all / Settings
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Widget placement changed from status bar to **above editor** (persistent, always visible)
|
|
27
|
+
- Session state no longer reconstructed from tool-result branch entries — file-backed store is the source of truth
|
|
28
|
+
- `todo_write` now merges `blocks`, `blockedBy`, and `metadata` from existing tasks when a task ID is reused (non-destructive for dependency edges)
|
|
29
|
+
- System prompt injection now includes task IDs and blocked-by info
|
|
30
|
+
- `priorityLabel` now correctly outputs `High`/`Medium`/`Low`
|
|
31
|
+
|
|
3
32
|
## [0.8.2] - 2026-05-11
|
|
4
33
|
|
|
5
34
|
### Added
|
package/README.md
CHANGED
|
@@ -2,54 +2,166 @@
|
|
|
2
2
|
|
|
3
3
|
A Pi CLI extension that gives the agent a structured task list to prevent **goal drift** — the tendency for agents to lose track of the original plan as context grows and tool calls accumulate.
|
|
4
4
|
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **6 LLM tools** — `TaskCreate`, `TaskList`, `TaskGet`, `TaskUpdate`, `TaskOutput`, `TaskStop` — matching pi-tasks behavior
|
|
8
|
+
- **Persistent widget** — live task list above the editor with animated spinner (✳✽), elapsed time, and blocked-by hints
|
|
9
|
+
- **File-backed storage** — memory / session / project scope with file locking and atomic writes
|
|
10
|
+
- **Dependency management** — bidirectional `blocks` / `blockedBy` edges with cycle detection
|
|
11
|
+
- **Auto-clear completed tasks** — configurable: never / on_list_complete / on_task_complete
|
|
12
|
+
- **System-reminder injection** — periodic nudges when task tools haven't been used recently
|
|
13
|
+
- **Settings panel** — `/todos → ⚙ Settings` (task storage + auto-clear, saved to `tasks-config.json`)
|
|
14
|
+
- **Priority system** — high / medium / low with color coding (Red / Yellow / Dim)
|
|
15
|
+
|
|
5
16
|
## How it works
|
|
6
17
|
|
|
7
|
-
1. The agent
|
|
8
|
-
2. Before every agent turn, active tasks are injected into the system prompt
|
|
9
|
-
3. A live widget above the editor shows
|
|
10
|
-
4.
|
|
11
|
-
5. Completed tasks remain visible
|
|
18
|
+
1. The agent uses `TaskCreate` to plan multi-step work as a structured task list
|
|
19
|
+
2. Before every agent turn, active tasks are injected into the system prompt
|
|
20
|
+
3. A live widget above the editor shows tasks with status icons and elapsed time for active tasks
|
|
21
|
+
4. Tasks persist to disk per-session (or project-wide) and survive session resume
|
|
22
|
+
5. Completed tasks remain visible until auto-cleared or manually removed
|
|
12
23
|
|
|
13
24
|
## Tools
|
|
14
25
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
|
26
|
+
### `TaskCreate`
|
|
27
|
+
|
|
28
|
+
Create a structured task. Used proactively for complex multi-step work.
|
|
29
|
+
|
|
30
|
+
| Parameter | Type | Required | Description |
|
|
31
|
+
|-----------|------|----------|-------------|
|
|
32
|
+
| `content` | string | ✓ | Brief actionable title in imperative form |
|
|
33
|
+
| `description` | string | | Detailed context and acceptance criteria |
|
|
34
|
+
| `priority` | `high` \| `medium` \| `low` | | Default: `medium` |
|
|
35
|
+
| `activeForm` | string | | Spinner text when in_progress (e.g., "Running tests") |
|
|
36
|
+
| `metadata` | object | | Arbitrary key-value pairs |
|
|
37
|
+
|
|
38
|
+
### `TaskList`
|
|
39
|
+
|
|
40
|
+
List all tasks sorted by status (pending first, then in_progress, then completed) and ID.
|
|
41
|
+
|
|
42
|
+
Returns each task's id, content, status, priority, and open blockedBy entries.
|
|
43
|
+
|
|
44
|
+
### `TaskGet`
|
|
45
|
+
|
|
46
|
+
Get full details for a specific task by ID — including description, dependencies, and metadata.
|
|
47
|
+
|
|
48
|
+
| Parameter | Type | Description |
|
|
49
|
+
|-----------|------|-------------|
|
|
50
|
+
| `id` | string | The task ID |
|
|
20
51
|
|
|
21
|
-
|
|
52
|
+
### `TaskUpdate`
|
|
22
53
|
|
|
23
|
-
|
|
24
|
-
|--------|------|---------|
|
|
25
|
-
| `pending` | `○` | Not started |
|
|
26
|
-
| `in_progress` | `●` | Actively working — only one at a time |
|
|
27
|
-
| `completed` | `✓` | Done — remains in the list |
|
|
54
|
+
Update task fields, status, and dependencies.
|
|
28
55
|
|
|
29
|
-
|
|
56
|
+
| Parameter | Type | Description |
|
|
57
|
+
|-----------|------|-------------|
|
|
58
|
+
| `id` | string | Task ID (required) |
|
|
59
|
+
| `status` | `pending` \| `in_progress` \| `completed` \| `deleted` | New status (`deleted` permanently removes) |
|
|
60
|
+
| `content` | string | New title |
|
|
61
|
+
| `description` | string | New description |
|
|
62
|
+
| `priority` | `high` \| `medium` \| `low` | New priority |
|
|
63
|
+
| `activeForm` | string | Spinner text |
|
|
64
|
+
| `owner` | string | Agent/owner name |
|
|
65
|
+
| `metadata` | object | Shallow merge (set key to `null` to delete) |
|
|
66
|
+
| `addBlocks` | string[] | Task IDs this task blocks |
|
|
67
|
+
| `addBlockedBy` | string[] | Task IDs that block this task |
|
|
30
68
|
|
|
31
|
-
|
|
32
|
-
|----------|-------|-------|
|
|
33
|
-
| `high` | High | Red |
|
|
34
|
-
| `medium` | Medium | Yellow |
|
|
35
|
-
| `low` | Low | Dim |
|
|
69
|
+
Setting `status: "deleted"` permanently removes the task and cleans up all dependency edges.
|
|
36
70
|
|
|
37
|
-
|
|
71
|
+
Dependencies are bidirectional — `addBlocks: ["t2"]` on task `t1` also adds `blockedBy: ["t1"]` to task `t2`.
|
|
72
|
+
|
|
73
|
+
### `TaskOutput`
|
|
74
|
+
|
|
75
|
+
Retrieve output from a running or completed background task process.
|
|
76
|
+
|
|
77
|
+
| Parameter | Type | Default | Description |
|
|
78
|
+
|-----------|------|---------|-------------|
|
|
79
|
+
| `task_id` | string | — | Task ID (required) |
|
|
80
|
+
| `block` | boolean | `true` | Wait for completion |
|
|
81
|
+
| `timeout` | number | `30000` | Max wait time in ms (max 600000) |
|
|
82
|
+
|
|
83
|
+
### `TaskStop`
|
|
84
|
+
|
|
85
|
+
Stop a running background task process. Sends SIGTERM, waits 5 seconds, then SIGKILL. Marks the task as completed.
|
|
86
|
+
|
|
87
|
+
| Parameter | Type | Description |
|
|
88
|
+
|-----------|------|-------------|
|
|
89
|
+
| `task_id` | string | Task ID to stop |
|
|
90
|
+
|
|
91
|
+
## Task lifecycle
|
|
38
92
|
|
|
39
93
|
```
|
|
40
|
-
|
|
94
|
+
pending → in_progress → completed
|
|
95
|
+
→ deleted (permanently removed)
|
|
41
96
|
```
|
|
42
97
|
|
|
43
|
-
|
|
98
|
+
## Dependency management
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Task t2 cannot start until t1 is completed
|
|
102
|
+
TaskUpdate { id: "t2", addBlockedBy: ["t1"] }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Edges are bidirectional. The widget and `TaskList` show open blockers inline (`› blocked by #t1`). Cycles and self-dependencies produce warnings but are stored.
|
|
106
|
+
|
|
107
|
+
## Task storage
|
|
108
|
+
|
|
109
|
+
Configured via `/todos → ⚙ Settings` or the `PI_TODO` environment variable:
|
|
110
|
+
|
|
111
|
+
| Mode | File | Behaviour |
|
|
112
|
+
|------|------|-----------|
|
|
113
|
+
| `memory` | *(none)* | In-memory only — tasks lost when session ends |
|
|
114
|
+
| `session` *(default)* | `<cwd>/.pi/tasks/tasks-<sessionId>.json` | Per-session, survives resume |
|
|
115
|
+
| `project` | `<cwd>/.pi/tasks/tasks.json` | Shared across all sessions in the project |
|
|
116
|
+
|
|
117
|
+
Settings are saved to `<cwd>/.pi/tasks-config.json`.
|
|
118
|
+
|
|
119
|
+
### Environment variable override
|
|
120
|
+
|
|
121
|
+
| Variable | Value | Behaviour |
|
|
122
|
+
|----------|-------|-----------|
|
|
123
|
+
| `PI_TODO` | `off` | In-memory only (CI/automation) |
|
|
124
|
+
| `PI_TODO` | `sprint-1` | Named shared list at `~/.pi/tasks/sprint-1.json` |
|
|
125
|
+
| `PI_TODO` | `/abs/path.json` | Explicit absolute file path |
|
|
126
|
+
|
|
127
|
+
## Auto-clear completed tasks
|
|
128
|
+
|
|
129
|
+
| Mode | Behaviour |
|
|
130
|
+
|------|-----------|
|
|
131
|
+
| `never` | Completed tasks stay visible until manually cleared |
|
|
132
|
+
| `on_list_complete` *(default)* | Cleared after all tasks complete and a few idle turns pass |
|
|
133
|
+
| `on_task_complete` | Each task cleared individually a few turns after completion |
|
|
134
|
+
|
|
135
|
+
## Widget
|
|
136
|
+
|
|
137
|
+
Persistent task list rendered above the editor:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
● 4 tasks (1 done, 1 in progress, 2 open)
|
|
141
|
+
✔ Design the API
|
|
142
|
+
✳ Implementing auth… (42s)
|
|
143
|
+
◻ Write tests › blocked by #t2
|
|
144
|
+
◻ Update docs
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Icon | Meaning |
|
|
148
|
+
|------|---------|
|
|
149
|
+
| `✔` | Completed (strikethrough + dim) |
|
|
150
|
+
| `◼` | In-progress |
|
|
151
|
+
| `✳`/`✽` | Animated spinner — actively executing (shows elapsed time) |
|
|
152
|
+
| `◻` | Pending |
|
|
153
|
+
|
|
154
|
+
## `/todos` command
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
/todos — open the interactive task manager
|
|
158
|
+
```
|
|
44
159
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
| `G` / `End` | Jump to last task |
|
|
51
|
-
| `c` | Toggle completed tasks visibility |
|
|
52
|
-
| `Esc` / `Ctrl+C` | Close viewer |
|
|
160
|
+
Menu options:
|
|
161
|
+
- **View all tasks** — select a task to start / complete / delete it
|
|
162
|
+
- **Clear completed** — remove all completed tasks
|
|
163
|
+
- **Clear all** — remove all tasks
|
|
164
|
+
- **⚙ Settings** — configure task storage and auto-clear
|
|
53
165
|
|
|
54
166
|
## Install
|
|
55
167
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto-clear.ts — Turn-based auto-clearing of completed tasks.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - "on_task_complete": each completed task gets its own countdown, deleted individually
|
|
6
|
+
* - "on_list_complete": countdown starts when ALL tasks are completed, cleared as a batch
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FileTaskStore } from "./file-store.js";
|
|
10
|
+
|
|
11
|
+
export type AutoClearMode = "never" | "on_list_complete" | "on_task_complete";
|
|
12
|
+
|
|
13
|
+
export class AutoClearManager {
|
|
14
|
+
/** Per-task: turn when task was marked completed ("on_task_complete" mode). */
|
|
15
|
+
private completedAtTurn = new Map<string, number>();
|
|
16
|
+
/** Turn when ALL tasks became completed ("on_list_complete" mode). */
|
|
17
|
+
private allCompletedAtTurn: number | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
public getStore: () => FileTaskStore,
|
|
21
|
+
private getMode: () => AutoClearMode,
|
|
22
|
+
/** How many turns completed tasks linger before auto-clearing. */
|
|
23
|
+
private clearDelayTurns = 4,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/** Record a task completion. Call after updating status. */
|
|
27
|
+
trackCompletion(taskId: string, currentTurn: number): void {
|
|
28
|
+
const mode = this.getMode();
|
|
29
|
+
if (mode === "never") return;
|
|
30
|
+
|
|
31
|
+
if (mode === "on_task_complete") {
|
|
32
|
+
this.completedAtTurn.set(taskId, currentTurn);
|
|
33
|
+
} else if (mode === "on_list_complete") {
|
|
34
|
+
this.checkAllCompleted(currentTurn);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private checkAllCompleted(currentTurn: number): void {
|
|
39
|
+
const tasks = this.getStore().list();
|
|
40
|
+
if (tasks.length > 0 && tasks.every((t) => t.status === "completed")) {
|
|
41
|
+
if (this.allCompletedAtTurn === null) this.allCompletedAtTurn = currentTurn;
|
|
42
|
+
} else {
|
|
43
|
+
this.allCompletedAtTurn = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Reset batch countdown (e.g., when a new task is created). */
|
|
48
|
+
resetBatchCountdown(): void {
|
|
49
|
+
this.allCompletedAtTurn = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Reset all tracking state (e.g., on new session). */
|
|
53
|
+
reset(): void {
|
|
54
|
+
this.completedAtTurn.clear();
|
|
55
|
+
this.allCompletedAtTurn = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Called on each turn start. Deletes tasks whose linger period has expired.
|
|
60
|
+
* Returns true if any tasks were cleared.
|
|
61
|
+
*/
|
|
62
|
+
onTurnStart(currentTurn: number): boolean {
|
|
63
|
+
const mode = this.getMode();
|
|
64
|
+
let cleared = false;
|
|
65
|
+
|
|
66
|
+
if (mode === "on_task_complete") {
|
|
67
|
+
for (const [taskId, turn] of this.completedAtTurn) {
|
|
68
|
+
const task = this.getStore().get(taskId);
|
|
69
|
+
if (!task || task.status !== "completed") {
|
|
70
|
+
this.completedAtTurn.delete(taskId);
|
|
71
|
+
} else if (currentTurn - turn >= this.clearDelayTurns) {
|
|
72
|
+
this.getStore().delete(taskId);
|
|
73
|
+
this.completedAtTurn.delete(taskId);
|
|
74
|
+
cleared = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else if (mode === "on_list_complete" && this.allCompletedAtTurn !== null) {
|
|
78
|
+
if (currentTurn - this.allCompletedAtTurn >= this.clearDelayTurns) {
|
|
79
|
+
this.getStore().clearCompleted();
|
|
80
|
+
this.allCompletedAtTurn = null;
|
|
81
|
+
cleared = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return cleared;
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/component.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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";
|
|
5
17
|
|
|
6
18
|
// ── /todos interactive viewer ──────────────────────────────────────────────────
|
|
7
19
|
|
|
@@ -28,19 +40,16 @@ export class TodoViewComponent {
|
|
|
28
40
|
this.onClose();
|
|
29
41
|
return;
|
|
30
42
|
}
|
|
31
|
-
|
|
32
43
|
if (matchesKey(data, "up") || data === "k") {
|
|
33
44
|
if (this.cursorIndex > 0) this.cursorIndex--;
|
|
34
45
|
this.invalidate();
|
|
35
46
|
return;
|
|
36
47
|
}
|
|
37
|
-
|
|
38
48
|
if (matchesKey(data, "down") || data === "j") {
|
|
39
49
|
if (this.cursorIndex < this.flatTasks.length - 1) this.cursorIndex++;
|
|
40
50
|
this.invalidate();
|
|
41
51
|
return;
|
|
42
52
|
}
|
|
43
|
-
|
|
44
53
|
if (data === "c") {
|
|
45
54
|
this.showCompleted = !this.showCompleted;
|
|
46
55
|
this.rebuildFlatTasks();
|
|
@@ -48,13 +57,11 @@ export class TodoViewComponent {
|
|
|
48
57
|
this.invalidate();
|
|
49
58
|
return;
|
|
50
59
|
}
|
|
51
|
-
|
|
52
60
|
if (matchesKey(data, "home") || data === "g") {
|
|
53
61
|
this.cursorIndex = 0;
|
|
54
62
|
this.invalidate();
|
|
55
63
|
return;
|
|
56
64
|
}
|
|
57
|
-
|
|
58
65
|
if (matchesKey(data, "end") || data === "G") {
|
|
59
66
|
this.cursorIndex = Math.max(0, this.flatTasks.length - 1);
|
|
60
67
|
this.invalidate();
|
|
@@ -162,7 +169,6 @@ export class TodoViewComponent {
|
|
|
162
169
|
const th = this.theme;
|
|
163
170
|
const isFocused = flatIdx === this.cursorIndex;
|
|
164
171
|
|
|
165
|
-
// ── Status icon ──
|
|
166
172
|
let icon: string;
|
|
167
173
|
if (task.status === "completed") {
|
|
168
174
|
icon = th.fg("success", "✓");
|
|
@@ -172,11 +178,9 @@ export class TodoViewComponent {
|
|
|
172
178
|
icon = th.fg("dim", "○");
|
|
173
179
|
}
|
|
174
180
|
|
|
175
|
-
// ── Priority badge ──
|
|
176
181
|
const pColor = priorityColor(task.priority);
|
|
177
182
|
const pLabel = th.fg(pColor, priorityLabel(task.priority));
|
|
178
183
|
|
|
179
|
-
// ── Content ──
|
|
180
184
|
let contentText: string;
|
|
181
185
|
if (task.status === "completed") {
|
|
182
186
|
contentText = th.fg("dim", th.strikethrough(task.content));
|
|
@@ -186,13 +190,25 @@ export class TodoViewComponent {
|
|
|
186
190
|
contentText = th.fg("muted", task.content);
|
|
187
191
|
}
|
|
188
192
|
|
|
189
|
-
// ── ID hint ──
|
|
190
193
|
const idHint = th.fg("dim", ` [${task.id}]`);
|
|
191
|
-
|
|
192
|
-
// ── Cursor indicator ──
|
|
193
194
|
const cursor = isFocused ? th.fg("accent", "❯") : " ";
|
|
194
195
|
|
|
195
|
-
|
|
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)];
|
|
196
212
|
}
|
|
197
213
|
|
|
198
214
|
private rebuildFlatTasks(): void {
|
|
@@ -201,7 +217,6 @@ export class TodoViewComponent {
|
|
|
201
217
|
.filter((t) => t.status === "pending")
|
|
202
218
|
.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
|
|
203
219
|
const done = this.showCompleted ? this.tasks.filter((t) => t.status === "completed") : [];
|
|
204
|
-
|
|
205
220
|
this.flatTasks = [...inProgress, ...pending, ...done];
|
|
206
221
|
}
|
|
207
222
|
|
|
@@ -210,55 +225,177 @@ export class TodoViewComponent {
|
|
|
210
225
|
this.cachedLines = undefined;
|
|
211
226
|
}
|
|
212
227
|
|
|
213
|
-
// ──
|
|
228
|
+
// ── Static tool result renderer ────────────────────────────────────────────
|
|
214
229
|
|
|
215
230
|
static renderTaskResult(details: TaskDetails | undefined, expanded: boolean, theme: any): any {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
);
|
|
219
280
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const total = list.length;
|
|
224
|
-
|
|
225
|
-
// ── Summary line ──
|
|
226
|
-
const parts: string[] = [];
|
|
227
|
-
if (inProgCount > 0) parts.push(theme.fg("accent", `● ${inProgCount} active`));
|
|
228
|
-
parts.push(theme.fg("success", `✓ ${doneCount}/${total} done`));
|
|
229
|
-
let output = parts.join(" ");
|
|
230
|
-
|
|
231
|
-
// ── Task lines ──
|
|
232
|
-
const display = expanded ? list : list.slice(0, 5);
|
|
233
|
-
for (const t of display) {
|
|
234
|
-
let icon: string;
|
|
235
|
-
if (t.status === "completed") {
|
|
236
|
-
icon = theme.fg("success", "✓");
|
|
237
|
-
} else if (t.status === "in_progress") {
|
|
238
|
-
icon = theme.fg("accent", "●");
|
|
239
|
-
} else {
|
|
240
|
-
icon = theme.fg("dim", "○");
|
|
281
|
+
class SettingsPanel extends Container {
|
|
282
|
+
handleInput(data: string) {
|
|
283
|
+
list.handleInput(data);
|
|
241
284
|
}
|
|
285
|
+
}
|
|
242
286
|
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
}
|
|
245
294
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
295
|
+
// ── /todos detailed task viewer (select-based) ─────────────────────────────────
|
|
296
|
+
|
|
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;
|
|
305
|
+
|
|
306
|
+
const mainMenu = async (): Promise<void> => {
|
|
307
|
+
const tasks = store.list();
|
|
308
|
+
const completedCount = tasks.filter((t) => t.status === "completed").length;
|
|
309
|
+
|
|
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();
|
|
333
|
+
}
|
|
334
|
+
};
|
|
254
335
|
|
|
255
|
-
|
|
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();
|
|
256
341
|
}
|
|
257
342
|
|
|
258
|
-
|
|
259
|
-
|
|
343
|
+
const icon = (status: string) => {
|
|
344
|
+
if (status === "completed") return "✔";
|
|
345
|
+
if (status === "in_progress") return "◼";
|
|
346
|
+
return "◻";
|
|
347
|
+
};
|
|
348
|
+
|
|
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();
|
|
260
396
|
}
|
|
397
|
+
return viewTasks();
|
|
398
|
+
};
|
|
261
399
|
|
|
262
|
-
|
|
263
|
-
}
|
|
400
|
+
await mainMenu();
|
|
264
401
|
}
|