@pi9/todo 0.1.0 → 0.2.0
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 +11 -70
- package/package.json +2 -2
- package/src/format.ts +51 -17
- package/src/persistence.ts +30 -59
- package/src/reminder-cadence.ts +59 -0
- package/src/reminder.ts +24 -0
- package/src/renderer.ts +9 -55
- package/src/schema.ts +6 -16
- package/src/settings.ts +85 -66
- package/src/state.ts +132 -58
- package/src/tool.ts +100 -33
- package/src/types.ts +29 -22
- package/src/visibility.ts +1 -5
- package/src/widget-component.ts +13 -6
- package/src/widget-layout.ts +11 -52
- package/src/widget.ts +11 -18
package/src/tool.ts
CHANGED
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Container, type Component } from "@earendil-works/pi-tui";
|
|
3
|
-
import { formatTodoSummary } from "./format.js";
|
|
3
|
+
import { formatTodoCompactionContext, formatTodoSummary } from "./format.js";
|
|
4
4
|
import { restoreTodoState } from "./persistence.js";
|
|
5
|
+
import {
|
|
6
|
+
beginReminderAgentRun,
|
|
7
|
+
consumeDueReminder,
|
|
8
|
+
createReminderCadenceState,
|
|
9
|
+
noteReminderTurn,
|
|
10
|
+
noteTodoInteraction,
|
|
11
|
+
type ReminderCadenceConfig,
|
|
12
|
+
} from "./reminder-cadence.js";
|
|
13
|
+
import { formatTodoReminder } from "./reminder.js";
|
|
5
14
|
import { renderResult as renderTodoResult } from "./renderer.js";
|
|
6
15
|
import { TodoToolFrame, type TodoToolFrameContent, type TodoToolFrameTheme } from "./tool-frame.js";
|
|
7
16
|
import { TodoParamsSchema } from "./schema.js";
|
|
8
|
-
import {
|
|
17
|
+
import { DEFAULT_TODO_SETTINGS, loadTodoSettings, type TodoSettings } from "./settings.js";
|
|
9
18
|
import { createTodoState, todoAddressKey, transitionTodoState } from "./state.js";
|
|
10
|
-
import
|
|
19
|
+
import type { TodoAddress, TodoState, TodoStatus, TodoToolDetails } from "./types.js";
|
|
11
20
|
import { shouldRenderTodoAction } from "./visibility.js";
|
|
12
21
|
import { updateTodoWidget } from "./widget.js";
|
|
13
22
|
|
|
14
|
-
function taskStatuses(state: TodoState): Map<string,
|
|
23
|
+
function taskStatuses(state: TodoState): Map<string, TodoStatus> {
|
|
15
24
|
return new Map(state.phases.flatMap((phase) => phase.tasks.map((task) => [todoAddressKey(phase.name, task.name), task.status])));
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
function taskAddresses(state: TodoState):
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
function taskAddresses(state: TodoState): TodoAddress[] {
|
|
28
|
+
return state.phases.flatMap((phase) => phase.tasks.map((task) => ({
|
|
29
|
+
phase: phase.name,
|
|
30
|
+
task: task.name,
|
|
22
31
|
})));
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
function changedTasks(previous: TodoState, next: TodoState): TodoAddress[] {
|
|
26
35
|
const before = taskStatuses(previous);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function completedTasks(previous: TodoState, next: TodoState): TodoAddress[] {
|
|
33
|
-
const before = taskStatuses(previous);
|
|
34
|
-
return next.phases.flatMap((phase) => phase.tasks
|
|
35
|
-
.filter((task) => task.status === "completed" && before.has(todoAddressKey(phase.name, task.name)) && before.get(todoAddressKey(phase.name, task.name)) !== "completed")
|
|
36
|
-
.map((task) => ({ phase: phase.name, task: task.name })));
|
|
36
|
+
return next.phases.flatMap((phase) => phase.tasks.flatMap((task) =>
|
|
37
|
+
before.get(todoAddressKey(phase.name, task.name)) === task.status
|
|
38
|
+
? []
|
|
39
|
+
: [{ phase: phase.name, task: task.name }],
|
|
40
|
+
));
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
function createTodoFrame(
|
|
@@ -58,6 +62,15 @@ type TodoRenderInput = {
|
|
|
58
62
|
type TodoRenderTheme = Parameters<typeof renderTodoResult>[2];
|
|
59
63
|
type TrackedSetRenderer = { toolCallId: string; invalidate?: () => void };
|
|
60
64
|
|
|
65
|
+
function reminderConfig(settings: TodoSettings): ReminderCadenceConfig {
|
|
66
|
+
return {
|
|
67
|
+
minTurns: settings.reminderMinTurns,
|
|
68
|
+
maxTurns: settings.reminderMaxTurns,
|
|
69
|
+
outputTokens: settings.reminderOutputTokens,
|
|
70
|
+
maxPerRun: settings.reminderMaxPerRun,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
/** Expanded content for the one set result that is allowed to follow in-memory state. */
|
|
62
75
|
class LiveSetResult implements Component {
|
|
63
76
|
constructor(
|
|
@@ -81,7 +94,10 @@ class LiveSetResult implements Component {
|
|
|
81
94
|
|
|
82
95
|
export function registerTodoTool(pi: ExtensionAPI): void {
|
|
83
96
|
let state = createTodoState();
|
|
84
|
-
let settings:
|
|
97
|
+
let settings: TodoSettings = { ...DEFAULT_TODO_SETTINGS };
|
|
98
|
+
let reminderCadence = createReminderCadenceState();
|
|
99
|
+
let pendingCompactionContext: string | undefined;
|
|
100
|
+
let interactedWithTodoThisTurn = false;
|
|
85
101
|
let queue: Promise<void> = Promise.resolve();
|
|
86
102
|
let latestSetRenderer: TrackedSetRenderer | undefined;
|
|
87
103
|
|
|
@@ -95,30 +111,81 @@ export function registerTodoTool(pi: ExtensionAPI): void {
|
|
|
95
111
|
updateTodoWidget(ctx, state, settings);
|
|
96
112
|
};
|
|
97
113
|
|
|
114
|
+
const resetReminderTracking = (): void => {
|
|
115
|
+
reminderCadence = createReminderCadenceState();
|
|
116
|
+
interactedWithTodoThisTurn = false;
|
|
117
|
+
};
|
|
118
|
+
|
|
98
119
|
pi.on("session_start", async (_event, ctx) => {
|
|
99
|
-
const loaded = await
|
|
120
|
+
const loaded = await loadTodoSettings(ctx);
|
|
100
121
|
settings = loaded.settings;
|
|
101
122
|
if (loaded.warning) ctx.ui.notify(loaded.warning, "warning");
|
|
102
123
|
restore(ctx);
|
|
124
|
+
pendingCompactionContext = undefined;
|
|
125
|
+
resetReminderTracking();
|
|
126
|
+
});
|
|
127
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
128
|
+
restore(ctx);
|
|
129
|
+
pendingCompactionContext = undefined;
|
|
130
|
+
resetReminderTracking();
|
|
131
|
+
});
|
|
132
|
+
pi.on("session_compact", () => {
|
|
133
|
+
pendingCompactionContext = formatTodoCompactionContext(state);
|
|
134
|
+
if (pendingCompactionContext) reminderCadence = noteTodoInteraction(reminderCadence);
|
|
135
|
+
});
|
|
136
|
+
pi.on("before_agent_start", () => {
|
|
137
|
+
reminderCadence = beginReminderAgentRun(reminderCadence);
|
|
138
|
+
});
|
|
139
|
+
pi.on("turn_end", (event) => {
|
|
140
|
+
reminderCadence = interactedWithTodoThisTurn
|
|
141
|
+
? noteTodoInteraction(reminderCadence)
|
|
142
|
+
: noteReminderTurn(
|
|
143
|
+
reminderCadence,
|
|
144
|
+
event.message.role === "assistant" ? event.message.usage?.output ?? 0 : 0,
|
|
145
|
+
);
|
|
146
|
+
interactedWithTodoThisTurn = false;
|
|
147
|
+
});
|
|
148
|
+
pi.on("context", (event) => {
|
|
149
|
+
if (pendingCompactionContext) {
|
|
150
|
+
const content = pendingCompactionContext;
|
|
151
|
+
pendingCompactionContext = undefined;
|
|
152
|
+
return {
|
|
153
|
+
messages: [...event.messages, { role: "user", content, timestamp: Date.now() }],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!settings.dynamicReminders) return;
|
|
158
|
+
|
|
159
|
+
const reminder = formatTodoReminder(state);
|
|
160
|
+
if (!reminder) return;
|
|
161
|
+
|
|
162
|
+
const consumed = consumeDueReminder(reminderCadence, reminderConfig(settings));
|
|
163
|
+
if (!consumed.due) return;
|
|
164
|
+
|
|
165
|
+
reminderCadence = consumed.state;
|
|
166
|
+
return {
|
|
167
|
+
messages: [...event.messages, { role: "user", content: reminder, timestamp: Date.now() }],
|
|
168
|
+
};
|
|
103
169
|
});
|
|
104
|
-
pi.on("session_tree", (_event, ctx) => restore(ctx));
|
|
105
170
|
|
|
106
171
|
pi.registerTool({
|
|
107
172
|
name: "todo",
|
|
108
173
|
label: "Todo",
|
|
109
174
|
description: [
|
|
110
|
-
"Maintain a
|
|
175
|
+
"Maintain a phased task plan.",
|
|
111
176
|
"Actions:",
|
|
112
|
-
" set: Replace the
|
|
113
|
-
" add:
|
|
114
|
-
" transition:
|
|
115
|
-
" view: Return the plan
|
|
177
|
+
" set(phases): Replace the plan; supplied tasks start pending.",
|
|
178
|
+
" add(phases): Add phases or tasks; preserve existing tasks and statuses.",
|
|
179
|
+
" transition(transitions): Set statuses by exact phase and task names.",
|
|
180
|
+
" view(phase?): Return the full plan or one exact phase.",
|
|
116
181
|
].join("\n"),
|
|
117
|
-
promptSnippet: "Track multi-step work
|
|
182
|
+
promptSnippet: "Track multi-step work in a phased task plan",
|
|
118
183
|
promptGuidelines: [
|
|
119
|
-
"Use todo for
|
|
120
|
-
"Transition todo
|
|
121
|
-
"
|
|
184
|
+
"Use todo for work with 3+ distinct steps; skip it for 1–2 steps.",
|
|
185
|
+
"Transition todo tasks immediately when work starts or ends; do not defer updates until the end.",
|
|
186
|
+
"Complete todo tasks only after the work is done and verified; cancel abandoned or obsolete tasks.",
|
|
187
|
+
"Add material new work to todo as new tasks rather than expanding existing task scope.",
|
|
188
|
+
"Keep `in_progress` todo tasks in one phase; complete or cancel them before starting another phase.",
|
|
122
189
|
],
|
|
123
190
|
parameters: TodoParamsSchema,
|
|
124
191
|
renderShell: "self",
|
|
@@ -126,22 +193,22 @@ export function registerTodoTool(pi: ExtensionAPI): void {
|
|
|
126
193
|
execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
127
194
|
const run = queue.then(() => {
|
|
128
195
|
const previous = state;
|
|
129
|
-
const next = transitionTodoState(previous, params
|
|
196
|
+
const next = transitionTodoState(previous, params);
|
|
130
197
|
const details: TodoToolDetails = {
|
|
131
198
|
action: params.action,
|
|
132
199
|
state: next,
|
|
133
200
|
changedTasks: params.action === "view"
|
|
134
201
|
? []
|
|
135
202
|
: params.action === "set"
|
|
136
|
-
?
|
|
203
|
+
? taskAddresses(next)
|
|
137
204
|
: changedTasks(previous, next),
|
|
138
|
-
completedTasks: params.action === "transition" ? completedTasks(previous, next) : [],
|
|
139
205
|
};
|
|
140
206
|
if (params.action !== "view") {
|
|
141
207
|
state = next;
|
|
142
208
|
invalidateLatestSetRenderer();
|
|
143
209
|
}
|
|
144
210
|
updateTodoWidget(ctx, state, settings);
|
|
211
|
+
interactedWithTodoThisTurn = true;
|
|
145
212
|
return {
|
|
146
213
|
content: [{ type: "text" as const, text: formatTodoSummary(next) }],
|
|
147
214
|
details,
|
package/src/types.ts
CHANGED
|
@@ -1,63 +1,70 @@
|
|
|
1
1
|
export const TODO_STATUSES = ["pending", "in_progress", "completed", "cancelled"] as const;
|
|
2
2
|
export type TodoStatus = typeof TODO_STATUSES[number];
|
|
3
3
|
|
|
4
|
+
export function isTodoStatus(value: unknown): value is TodoStatus {
|
|
5
|
+
return typeof value === "string" && (TODO_STATUSES as readonly string[]).includes(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
export const TODO_ACTIONS = ["set", "add", "transition", "view"] as const;
|
|
5
9
|
export type TodoActionName = typeof TODO_ACTIONS[number];
|
|
6
10
|
|
|
11
|
+
export function isTodoActionName(value: unknown): value is TodoActionName {
|
|
12
|
+
return typeof value === "string" && (TODO_ACTIONS as readonly string[]).includes(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export type Todo = {
|
|
8
|
-
name: string;
|
|
9
|
-
status: TodoStatus;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly status: TodoStatus;
|
|
10
18
|
};
|
|
11
19
|
|
|
12
20
|
export type TodoPhase = {
|
|
13
|
-
name: string;
|
|
14
|
-
tasks: Todo[];
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly tasks: readonly Todo[];
|
|
15
23
|
};
|
|
16
24
|
|
|
17
25
|
export type TodoState = {
|
|
18
|
-
phases: TodoPhase[];
|
|
26
|
+
readonly phases: readonly TodoPhase[];
|
|
19
27
|
};
|
|
20
28
|
|
|
21
29
|
export type TodoAddress = {
|
|
22
|
-
phase: string;
|
|
23
|
-
task: string;
|
|
30
|
+
readonly phase: string;
|
|
31
|
+
readonly task: string;
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
/** Persisted with a successful tool result so session state can be restored. */
|
|
27
35
|
export type TodoToolDetails = {
|
|
28
|
-
action: TodoActionName;
|
|
29
|
-
state: TodoState;
|
|
30
|
-
changedTasks: TodoAddress[];
|
|
31
|
-
completedTasks: TodoAddress[];
|
|
36
|
+
readonly action: TodoActionName;
|
|
37
|
+
readonly state: TodoState;
|
|
38
|
+
readonly changedTasks: readonly TodoAddress[];
|
|
32
39
|
};
|
|
33
40
|
|
|
34
41
|
export type TodoPhaseInput = {
|
|
35
|
-
name: string;
|
|
36
|
-
tasks: string[];
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly tasks: readonly string[];
|
|
37
44
|
};
|
|
38
45
|
|
|
39
46
|
export type TodoTransitionInput = TodoAddress & {
|
|
40
|
-
status: TodoStatus;
|
|
47
|
+
readonly status: TodoStatus;
|
|
41
48
|
};
|
|
42
49
|
|
|
43
50
|
export type SetTodoAction = {
|
|
44
|
-
action: "set";
|
|
45
|
-
phases: TodoPhaseInput[];
|
|
51
|
+
readonly action: "set";
|
|
52
|
+
readonly phases: readonly TodoPhaseInput[];
|
|
46
53
|
};
|
|
47
54
|
|
|
48
55
|
export type AddTodoAction = {
|
|
49
|
-
action: "add";
|
|
50
|
-
phases: TodoPhaseInput[];
|
|
56
|
+
readonly action: "add";
|
|
57
|
+
readonly phases: readonly TodoPhaseInput[];
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
export type TransitionTodoAction = {
|
|
54
|
-
action: "transition";
|
|
55
|
-
transitions: TodoTransitionInput[];
|
|
61
|
+
readonly action: "transition";
|
|
62
|
+
readonly transitions: readonly TodoTransitionInput[];
|
|
56
63
|
};
|
|
57
64
|
|
|
58
65
|
export type ViewTodoAction = {
|
|
59
|
-
action: "view";
|
|
60
|
-
phase?: string;
|
|
66
|
+
readonly action: "view";
|
|
67
|
+
readonly phase?: string;
|
|
61
68
|
};
|
|
62
69
|
|
|
63
70
|
export type TodoAction = SetTodoAction | AddTodoAction | TransitionTodoAction | ViewTodoAction;
|
package/src/visibility.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { TodoToolVisibility } from "./settings.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
function isTodoActionName(action: unknown): action is TodoActionName {
|
|
5
|
-
return typeof action === "string" && (TODO_ACTIONS as readonly string[]).includes(action);
|
|
6
|
-
}
|
|
2
|
+
import { isTodoActionName } from "./types.js";
|
|
7
3
|
|
|
8
4
|
/** Returns whether a successful todo action should be shown in tool output. */
|
|
9
5
|
export function shouldRenderTodoAction(action: unknown, visibility: TodoToolVisibility): boolean {
|
package/src/widget-component.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
4
|
import type { TodoState } from "./types.js";
|
|
@@ -9,6 +9,10 @@ const DROPLET_DURATIONS_MS = [220, 110, 80, 100, 420] as const;
|
|
|
9
9
|
const PI_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
10
10
|
const PI_SPINNER_INTERVAL_MS = 80;
|
|
11
11
|
|
|
12
|
+
type TodoWidgetComponentOptions = TodoWidgetLayoutOptions & {
|
|
13
|
+
blankLineBelow?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
12
16
|
/** A width-aware component for Pi's persistent widget area. */
|
|
13
17
|
export class TodoWidgetComponent implements Component {
|
|
14
18
|
private frameIndex = 0;
|
|
@@ -17,7 +21,7 @@ export class TodoWidgetComponent implements Component {
|
|
|
17
21
|
constructor(
|
|
18
22
|
private readonly state: TodoState,
|
|
19
23
|
private readonly theme: Theme | undefined,
|
|
20
|
-
private readonly options:
|
|
24
|
+
private readonly options: TodoWidgetComponentOptions = {},
|
|
21
25
|
private readonly tui?: Pick<TUI, "requestRender">,
|
|
22
26
|
) {
|
|
23
27
|
if (tui && state.phases.some(phase => phase.tasks.some(task => task.status === "in_progress"))) {
|
|
@@ -25,14 +29,17 @@ export class TodoWidgetComponent implements Component {
|
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
invalidate(): void {
|
|
32
|
+
invalidate(): void {}
|
|
29
33
|
|
|
30
34
|
render(width: number): string[] {
|
|
31
35
|
const safeWidth = Math.max(1, Math.floor(width) || 1);
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
const { blankLineBelow, ...layoutOptions } = this.options;
|
|
37
|
+
const lines = renderTodoWidgetLines(this.state, this.theme, safeWidth, {
|
|
38
|
+
...layoutOptions,
|
|
34
39
|
activeMarker: this.frames[this.frameIndex],
|
|
35
|
-
})
|
|
40
|
+
});
|
|
41
|
+
if (blankLineBelow && lines.length > 0) lines.push("");
|
|
42
|
+
return lines;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
dispose(): void {
|
package/src/widget-layout.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
|
+
import { formatTodoProgress, todoTasks } from "./format.js";
|
|
4
5
|
import { todoGlyph } from "./glyphs.js";
|
|
6
|
+
import { currentTodoPhaseIndex } from "./state.js";
|
|
5
7
|
import type { Todo, TodoPhase, TodoState } from "./types.js";
|
|
6
8
|
|
|
7
9
|
export type TodoWidgetLayoutOptions = {
|
|
@@ -14,10 +16,7 @@ type ThemeLike = Partial<Pick<Theme, "bold" | "fg" | "strikethrough">>;
|
|
|
14
16
|
|
|
15
17
|
type DisplayTask = Todo & { taskIndex: number };
|
|
16
18
|
|
|
17
|
-
/**
|
|
18
|
-
* Produces compact widget rows. Every returned row is constrained to the supplied display width;
|
|
19
|
-
* the component may subsequently wrap a row when a host chooses a narrower cell.
|
|
20
|
-
*/
|
|
19
|
+
/** Produces compact widget rows constrained to the supplied display width. */
|
|
21
20
|
export function renderTodoWidgetLines(
|
|
22
21
|
state: TodoState | undefined,
|
|
23
22
|
theme: ThemeLike | undefined,
|
|
@@ -25,15 +24,13 @@ export function renderTodoWidgetLines(
|
|
|
25
24
|
options: TodoWidgetLayoutOptions = {},
|
|
26
25
|
): string[] {
|
|
27
26
|
const safeWidth = Math.max(1, Math.floor(width) || 1);
|
|
28
|
-
const phases =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const selectedPhaseIndex = selectPhase(phases);
|
|
27
|
+
const phases = state?.phases ?? [];
|
|
28
|
+
const selectedPhaseIndex = currentTodoPhaseIndex(phases);
|
|
32
29
|
if (selectedPhaseIndex < 0) return [];
|
|
33
30
|
const selectedPhase = phases[selectedPhaseIndex];
|
|
34
31
|
const maxVisible = boundedMaxVisible(options.maxVisible);
|
|
35
32
|
const selectedTasks = visibleTasks(selectedPhase.tasks, maxVisible);
|
|
36
|
-
const lines: string[] = [fit(toolTitle(
|
|
33
|
+
const lines: string[] = [fit(toolTitle(formatTodoProgress("Todos", todoTasks(state)), theme), safeWidth)];
|
|
37
34
|
|
|
38
35
|
for (let phaseIndex = 0; phaseIndex < phases.length; phaseIndex++) {
|
|
39
36
|
const phase = phases[phaseIndex];
|
|
@@ -58,55 +55,23 @@ export function renderTodoWidgetLines(
|
|
|
58
55
|
return lines;
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
/** Alias kept for callers that prefer a builder-style name. */
|
|
62
|
-
export const buildTodoWidgetLines = renderTodoWidgetLines;
|
|
63
|
-
|
|
64
58
|
function boundedMaxVisible(value: number | undefined): number {
|
|
65
|
-
if (!Number.isFinite(value)) return 5;
|
|
66
|
-
return Math.max(1, Math.floor(value
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function summary(phases: TodoPhase[]): string {
|
|
70
|
-
const tasks = phases.flatMap(phase => phase.tasks);
|
|
71
|
-
const active = tasks.filter(isActive).length;
|
|
72
|
-
const pending = tasks.filter(task => task.status === "pending").length;
|
|
73
|
-
const completed = tasks.filter(task => task.status === "completed").length;
|
|
74
|
-
const cancelled = tasks.filter(task => task.status === "cancelled").length;
|
|
75
|
-
return [
|
|
76
|
-
"Todos",
|
|
77
|
-
...(active ? [`${active} active`] : []),
|
|
78
|
-
...(pending ? [`${pending} pending`] : []),
|
|
79
|
-
...(completed ? [`${completed} completed`] : []),
|
|
80
|
-
...(cancelled ? [`${cancelled} cancelled`] : []),
|
|
81
|
-
].join(" · ");
|
|
59
|
+
if (value === undefined || !Number.isFinite(value)) return 5;
|
|
60
|
+
return Math.max(1, Math.floor(value));
|
|
82
61
|
}
|
|
83
62
|
|
|
84
63
|
function phaseTitle(phase: TodoPhase, phaseIndex: number, selected: boolean, theme: ThemeLike | undefined): string {
|
|
85
|
-
const title = `${phaseIndex + 1}. ${
|
|
64
|
+
const title = `${phaseIndex + 1}. ${formatTodoProgress(phase.name, phase.tasks)}`;
|
|
86
65
|
if (selected) return toolTitle(` ${title}`, theme);
|
|
87
66
|
return theme?.fg ? theme.fg("dim", ` ${title}`) : ` ${title}`;
|
|
88
67
|
}
|
|
89
68
|
|
|
90
|
-
function phaseSummary(phase: TodoPhase): string {
|
|
91
|
-
const active = phase.tasks.filter(isActive).length;
|
|
92
|
-
const pending = phase.tasks.filter(task => task.status === "pending").length;
|
|
93
|
-
const completed = phase.tasks.filter(task => task.status === "completed").length;
|
|
94
|
-
const cancelled = phase.tasks.filter(task => task.status === "cancelled").length;
|
|
95
|
-
return [
|
|
96
|
-
phase.name,
|
|
97
|
-
...(active ? [`${active} active`] : []),
|
|
98
|
-
...(pending ? [`${pending} pending`] : []),
|
|
99
|
-
...(completed ? [`${completed} completed`] : []),
|
|
100
|
-
...(cancelled ? [`${cancelled} cancelled`] : []),
|
|
101
|
-
].join(" · ");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
69
|
function toolTitle(text: string, theme: ThemeLike | undefined): string {
|
|
105
70
|
const bold = theme?.bold ? theme.bold(text) : text;
|
|
106
71
|
return theme?.fg ? theme.fg("toolTitle", bold) : bold;
|
|
107
72
|
}
|
|
108
73
|
|
|
109
|
-
function visibleTasks(tasks: Todo[], maxVisible: number): DisplayTask[] {
|
|
74
|
+
function visibleTasks(tasks: readonly Todo[], maxVisible: number): DisplayTask[] {
|
|
110
75
|
const ordered = tasks
|
|
111
76
|
.map((task, taskIndex) => ({ ...task, taskIndex }))
|
|
112
77
|
.filter(task => !isTerminal(task))
|
|
@@ -132,13 +97,7 @@ function taskPriority(task: Todo): number {
|
|
|
132
97
|
return 2;
|
|
133
98
|
}
|
|
134
99
|
|
|
135
|
-
function
|
|
136
|
-
const active = phases.findIndex(phase => phase.tasks.some(isActive));
|
|
137
|
-
if (active >= 0) return active;
|
|
138
|
-
return phases.findIndex(phase => phase.tasks.some(task => task.status === "pending"));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function terminalTaskSummary(tasks: Todo[]): string | undefined {
|
|
100
|
+
function terminalTaskSummary(tasks: readonly Todo[]): string | undefined {
|
|
142
101
|
const completed = tasks.filter(task => task.status === "completed").length;
|
|
143
102
|
const cancelled = tasks.filter(task => task.status === "cancelled").length;
|
|
144
103
|
const parts = [
|
package/src/widget.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
|
+
import type { TodoSettings } from "./settings.js";
|
|
4
5
|
import type { TodoState } from "./types.js";
|
|
5
6
|
import { TodoWidgetComponent } from "./widget-component.js";
|
|
6
7
|
|
|
7
|
-
export type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* The widget-prefixed forms allow a future settings module to expose namespaced UI settings.
|
|
12
|
-
*/
|
|
13
|
-
export type TodoWidgetSettings = {
|
|
14
|
-
widgetPlacement?: TodoWidgetPlacement;
|
|
15
|
-
maxVisibleTasks?: number;
|
|
16
|
-
fallbackGlyphs?: boolean;
|
|
17
|
-
};
|
|
8
|
+
export type TodoWidgetSettings = Partial<Pick<
|
|
9
|
+
TodoSettings,
|
|
10
|
+
"widgetPlacement" | "maxVisibleTasks" | "fallbackGlyphs"
|
|
11
|
+
>>;
|
|
18
12
|
|
|
19
13
|
type WidgetComponentFactory = (tui: TUI, theme: Theme) => Component & { dispose?(): void };
|
|
20
14
|
|
|
@@ -39,19 +33,18 @@ export function updateTodoWidget(ctx: TodoWidgetContext | undefined, state: Todo
|
|
|
39
33
|
return;
|
|
40
34
|
}
|
|
41
35
|
|
|
42
|
-
const
|
|
36
|
+
const visibleState = state?.phases.some(phase => phase.tasks.some(task =>
|
|
43
37
|
task.status === "pending" || task.status === "in_progress",
|
|
44
|
-
))
|
|
45
|
-
const factory: WidgetComponentFactory | undefined =
|
|
46
|
-
? (tui, theme) => new TodoWidgetComponent(
|
|
38
|
+
)) ? state : undefined;
|
|
39
|
+
const factory: WidgetComponentFactory | undefined = visibleState
|
|
40
|
+
? (tui, theme) => new TodoWidgetComponent(visibleState, theme, {
|
|
47
41
|
maxVisible: settings.maxVisibleTasks,
|
|
48
42
|
fallbackGlyphs: settings.fallbackGlyphs,
|
|
43
|
+
blankLineBelow: placement === "aboveEditor",
|
|
49
44
|
}, tui)
|
|
50
45
|
: undefined;
|
|
51
46
|
ctx.ui.setWidget("todo", factory, { placement });
|
|
52
47
|
} catch (error) {
|
|
53
|
-
|
|
54
|
-
ctx.ui.notify?.(`Todo widget update failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
55
|
-
} catch { }
|
|
48
|
+
ctx.ui.notify?.(`Todo widget update failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
56
49
|
}
|
|
57
50
|
}
|