@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/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 { DEFAULT_TODO_UI_SETTINGS, loadTodoUiSettings, type TodoUiSettings } from "./settings.js";
17
+ import { DEFAULT_TODO_SETTINGS, loadTodoSettings, type TodoSettings } from "./settings.js";
9
18
  import { createTodoState, todoAddressKey, transitionTodoState } from "./state.js";
10
- import { TODO_STATUSES, type TodoAction, type TodoAddress, type TodoState, type TodoToolDetails } from "./types.js";
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, 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): Map<string, TodoAddress> {
19
- return new Map(state.phases.flatMap((phase) => phase.tasks.map((task) => {
20
- const address = { phase: phase.name, task: task.name };
21
- return [todoAddressKey(address.phase, address.task), address];
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
- const after = taskStatuses(next);
28
- const addresses = taskAddresses(next);
29
- return [...after.keys()].filter((key) => before.get(key) !== after.get(key)).map((key) => addresses.get(key)!);
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: TodoUiSettings = { ...DEFAULT_TODO_UI_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 loadTodoUiSettings(ctx);
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 concise phased plan that reflects both intended work and current execution progress.",
175
+ "Maintain a phased task plan.",
111
176
  "Actions:",
112
- " set: Replace the entire plan. All tasks reset to pending. Use only for a new plan or full re-plan.",
113
- " add: Append tasks or phases without touching existing work.",
114
- " transition: Update task statuses (" + TODO_STATUSES.join(", ") + ") by exact phase and task name.",
115
- " view: Return the plan, optionally filtered to one phase.",
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 with the todo tool; keep statuses current as you go",
182
+ promptSnippet: "Track multi-step work in a phased task plan",
118
183
  promptGuidelines: [
119
- "Use todo for non-trivial work with three or more distinct steps; skip todo for simple tasks.",
120
- "Transition todo statuses immediately and honestly—mark tasks completed only when fully done, not merely attempted, and cancel abandoned tasks rather than leaving them pending.",
121
- "Todo tasks in_progress must all belong to a single phase; finish or cancel a phase's active tasks before starting the next.",
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 as TodoAction);
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
- ? [...taskAddresses(next).values()]
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 { TODO_ACTIONS, type TodoActionName } from "./types.js";
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 {
@@ -1,4 +1,4 @@
1
- import { wrapTextWithAnsi, type Component, type TUI } from "@earendil-works/pi-tui";
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: TodoWidgetLayoutOptions = {},
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
- return renderTodoWidgetLines(this.state, this.theme, safeWidth, {
33
- ...this.options,
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
- }).flatMap(line => wrapTextWithAnsi(line, safeWidth));
40
+ });
41
+ if (blankLineBelow && lines.length > 0) lines.push("");
42
+ return lines;
36
43
  }
37
44
 
38
45
  dispose(): void {
@@ -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 = Array.isArray(state?.phases) ? state.phases : [];
29
- if (phases.length === 0 || !phases.some(phase => phase.tasks.length > 0)) return [];
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(summary(phases), theme), safeWidth)];
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}. ${phaseSummary(phase)}`;
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 selectPhase(phases: TodoPhase[]): number {
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 TodoWidgetPlacement = "belowEditor" | "aboveEditor" | "off";
8
-
9
- /**
10
- * Deliberately structural so this unit does not depend on the settings persistence layer.
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 hasVisibleTasks = state?.phases.some(phase => phase.tasks.some(task =>
36
+ const visibleState = state?.phases.some(phase => phase.tasks.some(task =>
43
37
  task.status === "pending" || task.status === "in_progress",
44
- )) ?? false;
45
- const factory: WidgetComponentFactory | undefined = hasVisibleTasks
46
- ? (tui, theme) => new TodoWidgetComponent(state!, theme, {
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
- try {
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
  }