@justjoshn/pi-pstack 0.15.36-pi.12 → 0.15.36-pi.13

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/todo.ts CHANGED
@@ -8,18 +8,21 @@ export interface TodoItem {
8
8
  id: string;
9
9
  content: string;
10
10
  status: TodoStatus;
11
+ dependencies?: string[];
11
12
  }
12
13
 
13
14
  export interface TodoInput {
14
15
  id: string;
15
16
  content?: string;
16
17
  status?: TodoStatus;
18
+ dependencies?: string[];
17
19
  }
18
20
 
19
21
  interface TodoDetails {
20
22
  todos: TodoItem[];
21
23
  }
22
24
 
25
+ const TODO_ENTRY = 'pstack/todos';
23
26
  const WIDGET_KEY = 'pstack-todos';
24
27
  const MARKS: Record<TodoStatus, string> = { pending: '[ ]', in_progress: '[>]', completed: '[x]', cancelled: '[-]' };
25
28
  const GLYPHS: Record<TodoStatus, string> = { pending: '○', in_progress: '▶', completed: '✓', cancelled: '✗' };
@@ -43,7 +46,8 @@ function complete(item: TodoInput, existing: TodoItem | undefined): TodoItem {
43
46
  const content = item.content ?? existing?.content;
44
47
  const status = item.status ?? existing?.status;
45
48
  if (content === undefined || status === undefined) throw new Error(`Todo "${item.id}" is new, so it needs both content and status.`);
46
- return { id: item.id, content, status };
49
+ const dependencies = item.dependencies ?? existing?.dependencies;
50
+ return { id: item.id, content, status, ...(dependencies === undefined ? {} : { dependencies }) };
47
51
  }
48
52
 
49
53
  /** TodoWrite: merge updates by id and appends new ids; otherwise the call replaces the list. */
@@ -68,18 +72,25 @@ export function formatTodos(todos: readonly TodoItem[]): string {
68
72
  const done = todos.filter((item) => item.status === 'completed').length;
69
73
  const active = todos.filter((item) => item.status === 'in_progress').length;
70
74
  const header = `Todos (${done}/${todos.length} completed${active > 0 ? `, ${active} in progress` : ''}):`;
71
- return [header, ...todos.map((item) => `${MARKS[item.status]} ${item.id}: ${item.content}`)].join('\n');
75
+ return [header, ...todos.map((item) => `${MARKS[item.status]} ${item.id}: ${item.content}${item.dependencies === undefined || item.dependencies.length === 0 ? '' : ` (depends on ${item.dependencies.join(', ')})`}`)].join('\n');
72
76
  }
73
77
 
74
78
  interface BranchEntry {
75
79
  type: string;
80
+ customType?: string;
81
+ data?: unknown;
76
82
  message?: { role?: string; toolName?: string; isError?: boolean; details?: unknown };
77
83
  }
78
84
 
79
- /** The list as of the active branch: the details of the last successful TodoWrite on it. A failed call stores `details: {}`. */
85
+ /** The list as of the active branch, from a plan import or the last successful TodoWrite. */
80
86
  export function todosFromBranch(entries: readonly BranchEntry[]): TodoItem[] {
81
87
  for (let index = entries.length - 1; index >= 0; index -= 1) {
82
- const message = entries[index]?.message;
88
+ const entry = entries[index];
89
+ if (entry?.type === 'custom' && entry.customType === TODO_ENTRY) {
90
+ const todos = (entry.data as Partial<TodoDetails> | undefined)?.todos;
91
+ if (Array.isArray(todos)) return todos;
92
+ }
93
+ const message = entry?.message;
83
94
  const todos = (message?.details as Partial<TodoDetails> | undefined)?.todos;
84
95
  if (entries[index]?.type === 'message' && message?.role === 'toolResult' && message.toolName === 'TodoWrite' && message.isError !== true && Array.isArray(todos)) return todos;
85
96
  }
@@ -116,7 +127,7 @@ const TODO_DESCRIPTION = [
116
127
  'Prefer creating the first todo as in_progress, and start on it with tool calls in the same batch as the write. Batch todo updates with your other tool calls.',
117
128
  ].join('\n');
118
129
 
119
- export function registerTodoWrite(pi: ExtensionAPI): { section: () => string | undefined } {
130
+ export function registerTodoWrite(pi: ExtensionAPI, options: { onWrite?: (todos: readonly TodoItem[]) => void } = {}) {
120
131
  let todos: TodoItem[] = [];
121
132
  const show = (ctx: ExtensionContext) => {
122
133
  if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, widgetLines(todos));
@@ -139,9 +150,21 @@ export function registerTodoWrite(pi: ExtensionAPI): { section: () => string | u
139
150
  annotations: { readOnlyHint: true, openWorldHint: false },
140
151
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
141
152
  todos = applyTodoWrite(todos, params.todos, params.merge);
153
+ try {
154
+ options.onWrite?.(todos);
155
+ } catch (error) {
156
+ ctx.ui.notify(`The todo list was updated, but its plan file could not be synchronized: ${error instanceof Error ? error.message : String(error)}`, 'warning');
157
+ }
142
158
  show(ctx);
143
159
  return { content: [{ type: 'text', text: formatTodos(todos) }], details: { todos } };
144
160
  },
145
161
  });
146
- return { section: () => todoSection(todos) };
162
+ return {
163
+ section: () => todoSection(todos),
164
+ replace: (items: readonly TodoItem[], ctx: ExtensionContext) => {
165
+ todos = applyTodoWrite([], items, false);
166
+ pi.appendEntry(TODO_ENTRY, { todos });
167
+ show(ctx);
168
+ },
169
+ };
147
170
  }