@pi9/todo 0.1.0 → 0.3.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 +21 -82
- package/media/todo-plan.png +0 -0
- package/package.json +3 -2
- package/src/format.ts +64 -27
- package/src/persistence.ts +30 -59
- package/src/reminder-cadence.ts +59 -0
- package/src/reminder.ts +24 -0
- package/src/renderer.ts +11 -67
- package/src/schema.ts +17 -21
- package/src/settings.ts +85 -66
- package/src/state.ts +163 -69
- package/src/tool.ts +100 -34
- package/src/types.ts +46 -22
- package/src/visibility.ts +1 -5
- package/src/widget-component.ts +17 -19
- package/src/widget-layout.ts +43 -74
- package/src/widget.ts +73 -28
package/src/widget.ts
CHANGED
|
@@ -1,57 +1,102 @@
|
|
|
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 {
|
|
4
|
+
import type { TodoSettings } from "./settings.js";
|
|
5
|
+
import { isTerminalTodo, type TodoState } from "./types.js";
|
|
5
6
|
import { TodoWidgetComponent } from "./widget-component.js";
|
|
6
7
|
|
|
7
|
-
export type
|
|
8
|
+
export type TodoWidgetSettings = Partial<Pick<
|
|
9
|
+
TodoSettings,
|
|
10
|
+
"widgetPlacement" | "maxVisibleTasks" | "fallbackGlyphs"
|
|
11
|
+
>>;
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
fallbackGlyphs?: boolean;
|
|
13
|
+
type WidgetComponentFactory = (tui: TUI, theme: Theme) => Component & { dispose?(): void };
|
|
14
|
+
type TodoWidgetUI = {
|
|
15
|
+
notify?: (message: string, level?: "info" | "warning" | "error") => void;
|
|
16
|
+
setWidget?: {
|
|
17
|
+
(id: string, content: string[] | undefined, options?: { placement?: "belowEditor" | "aboveEditor" }): void;
|
|
18
|
+
(id: string, content: WidgetComponentFactory | undefined, options?: { placement?: "belowEditor" | "aboveEditor" }): void;
|
|
19
|
+
};
|
|
17
20
|
};
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
const TERMINAL_WIDGET_DELAY_MS = 5_000;
|
|
23
|
+
type TodoWidgetLifecycle = {
|
|
24
|
+
hadOpenTasks: boolean;
|
|
25
|
+
terminalClearTimer?: ReturnType<typeof setTimeout>;
|
|
26
|
+
};
|
|
27
|
+
const widgetLifecycles = new WeakMap<TodoWidgetUI, TodoWidgetLifecycle>();
|
|
20
28
|
|
|
21
29
|
export type TodoWidgetContext = {
|
|
22
30
|
hasUI?: boolean;
|
|
23
|
-
ui?:
|
|
24
|
-
notify?: (message: string, level?: "info" | "warning" | "error") => void;
|
|
25
|
-
setWidget?: {
|
|
26
|
-
(id: string, content: string[] | undefined, options?: { placement?: "belowEditor" | "aboveEditor" }): void;
|
|
27
|
-
(id: string, content: WidgetComponentFactory | undefined, options?: { placement?: "belowEditor" | "aboveEditor" }): void;
|
|
28
|
-
};
|
|
29
|
-
};
|
|
31
|
+
ui?: TodoWidgetUI;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
/** Update (or clear) the persistent todo widget without requiring the host UI at runtime. */
|
|
33
35
|
export function updateTodoWidget(ctx: TodoWidgetContext | undefined, state: TodoState | undefined, settings: TodoWidgetSettings = {}): void {
|
|
34
36
|
if (!ctx?.hasUI || !ctx.ui?.setWidget) return;
|
|
37
|
+
const ui = ctx.ui;
|
|
38
|
+
const lifecycle = widgetLifecycles.get(ui) ?? { hadOpenTasks: false };
|
|
39
|
+
|
|
35
40
|
try {
|
|
36
41
|
const placement = settings.widgetPlacement ?? "aboveEditor";
|
|
37
42
|
if (placement === "off") {
|
|
38
|
-
|
|
43
|
+
cancelTerminalClear(lifecycle);
|
|
44
|
+
widgetLifecycles.delete(ui);
|
|
45
|
+
ui.setWidget!("todo", undefined);
|
|
39
46
|
return;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
const hasTasks = state?.phases.some((phase) => phase.tasks.length > 0) ?? false;
|
|
50
|
+
const hasOpenTasks = state?.phases.some((phase) => phase.tasks.some((task) => !isTerminalTodo(task))) ?? false;
|
|
51
|
+
const showFinalState = hasTasks && !hasOpenTasks
|
|
52
|
+
&& (lifecycle.hadOpenTasks || lifecycle.terminalClearTimer !== undefined);
|
|
53
|
+
const visibleState = hasOpenTasks || showFinalState ? state : undefined;
|
|
54
|
+
const factory: WidgetComponentFactory | undefined = visibleState
|
|
55
|
+
? (tui, theme) => new TodoWidgetComponent(visibleState, theme, {
|
|
47
56
|
maxVisible: settings.maxVisibleTasks,
|
|
48
57
|
fallbackGlyphs: settings.fallbackGlyphs,
|
|
58
|
+
blankLineBelow: placement === "aboveEditor",
|
|
49
59
|
}, tui)
|
|
50
60
|
: undefined;
|
|
51
|
-
|
|
61
|
+
ui.setWidget!("todo", factory, { placement });
|
|
62
|
+
|
|
63
|
+
if (hasOpenTasks) {
|
|
64
|
+
cancelTerminalClear(lifecycle);
|
|
65
|
+
lifecycle.hadOpenTasks = true;
|
|
66
|
+
widgetLifecycles.set(ui, lifecycle);
|
|
67
|
+
} else if (showFinalState) {
|
|
68
|
+
lifecycle.hadOpenTasks = false;
|
|
69
|
+
if (!lifecycle.terminalClearTimer) {
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
widgetLifecycles.delete(ui);
|
|
72
|
+
clearWidget(ui);
|
|
73
|
+
}, TERMINAL_WIDGET_DELAY_MS);
|
|
74
|
+
timer.unref?.();
|
|
75
|
+
lifecycle.terminalClearTimer = timer;
|
|
76
|
+
}
|
|
77
|
+
widgetLifecycles.set(ui, lifecycle);
|
|
78
|
+
} else {
|
|
79
|
+
cancelTerminalClear(lifecycle);
|
|
80
|
+
widgetLifecycles.delete(ui);
|
|
81
|
+
}
|
|
52
82
|
} catch (error) {
|
|
53
|
-
|
|
54
|
-
ctx.ui.notify?.(`Todo widget update failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
55
|
-
} catch { }
|
|
83
|
+
notifyWidgetError(ui, error);
|
|
56
84
|
}
|
|
57
85
|
}
|
|
86
|
+
|
|
87
|
+
function cancelTerminalClear(lifecycle: TodoWidgetLifecycle): void {
|
|
88
|
+
if (lifecycle.terminalClearTimer) clearTimeout(lifecycle.terminalClearTimer);
|
|
89
|
+
delete lifecycle.terminalClearTimer;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function clearWidget(ui: TodoWidgetUI): void {
|
|
93
|
+
try {
|
|
94
|
+
ui.setWidget?.("todo", undefined);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
notifyWidgetError(ui, error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function notifyWidgetError(ui: TodoWidgetUI, error: unknown): void {
|
|
101
|
+
ui.notify?.(`Todo widget update failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
102
|
+
}
|