@astralyn/pi 0.84.3 → 0.84.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli/project-trust.d.ts.map +1 -1
  3. package/dist/cli/project-trust.js +1 -4
  4. package/dist/cli/project-trust.js.map +1 -1
  5. package/dist/core/extensions/index.d.ts +1 -1
  6. package/dist/core/extensions/index.d.ts.map +1 -1
  7. package/dist/core/extensions/index.js.map +1 -1
  8. package/dist/core/extensions/types.d.ts +2 -13
  9. package/dist/core/extensions/types.d.ts.map +1 -1
  10. package/dist/core/extensions/types.js.map +1 -1
  11. package/dist/extensions/subagent/agents-command.d.ts +4 -0
  12. package/dist/extensions/subagent/agents-command.d.ts.map +1 -0
  13. package/dist/extensions/subagent/agents-command.js +230 -0
  14. package/dist/extensions/subagent/agents-command.js.map +1 -0
  15. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  16. package/dist/extensions/subagent/agents.js +2 -0
  17. package/dist/extensions/subagent/agents.js.map +1 -1
  18. package/dist/extensions/subagent/display-name.d.ts +6 -0
  19. package/dist/extensions/subagent/display-name.d.ts.map +1 -0
  20. package/dist/extensions/subagent/display-name.js +13 -0
  21. package/dist/extensions/subagent/display-name.js.map +1 -0
  22. package/dist/extensions/subagent/index.d.ts.map +1 -1
  23. package/dist/extensions/subagent/index.js +2 -127
  24. package/dist/extensions/subagent/index.js.map +1 -1
  25. package/dist/extensions/subagent/render.d.ts.map +1 -1
  26. package/dist/extensions/subagent/render.js +1 -10
  27. package/dist/extensions/subagent/render.js.map +1 -1
  28. package/dist/extensions/subagent/settings.d.ts +0 -1
  29. package/dist/extensions/subagent/settings.d.ts.map +1 -1
  30. package/dist/extensions/subagent/settings.js +3 -7
  31. package/dist/extensions/subagent/settings.js.map +1 -1
  32. package/dist/extensions/subagent/types.d.ts +2 -0
  33. package/dist/extensions/subagent/types.d.ts.map +1 -1
  34. package/dist/extensions/subagent/types.js.map +1 -1
  35. package/dist/extensions/subagent/ui/model-list.d.ts +67 -0
  36. package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -0
  37. package/dist/extensions/subagent/ui/model-list.js +242 -0
  38. package/dist/extensions/subagent/ui/model-list.js.map +1 -0
  39. package/dist/extensions/subagent/ui/profile-editor.d.ts +54 -0
  40. package/dist/extensions/subagent/ui/profile-editor.d.ts.map +1 -0
  41. package/dist/extensions/subagent/ui/profile-editor.js +154 -0
  42. package/dist/extensions/subagent/ui/profile-editor.js.map +1 -0
  43. package/dist/extensions/subagent/ui/profile-picker.d.ts +32 -0
  44. package/dist/extensions/subagent/ui/profile-picker.d.ts.map +1 -0
  45. package/dist/extensions/subagent/ui/profile-picker.js +149 -0
  46. package/dist/extensions/subagent/ui/profile-picker.js.map +1 -0
  47. package/dist/extensions/todo/constants.d.ts +6 -17
  48. package/dist/extensions/todo/constants.d.ts.map +1 -1
  49. package/dist/extensions/todo/constants.js +20 -31
  50. package/dist/extensions/todo/constants.js.map +1 -1
  51. package/dist/extensions/todo/index.d.ts.map +1 -1
  52. package/dist/extensions/todo/index.js +36 -57
  53. package/dist/extensions/todo/index.js.map +1 -1
  54. package/dist/extensions/todo/schema.d.ts +20 -59
  55. package/dist/extensions/todo/schema.d.ts.map +1 -1
  56. package/dist/extensions/todo/schema.js +23 -69
  57. package/dist/extensions/todo/schema.js.map +1 -1
  58. package/dist/extensions/todo/state.d.ts +23 -65
  59. package/dist/extensions/todo/state.d.ts.map +1 -1
  60. package/dist/extensions/todo/state.js +238 -1262
  61. package/dist/extensions/todo/state.js.map +1 -1
  62. package/dist/extensions/todo/view.d.ts +19 -10
  63. package/dist/extensions/todo/view.d.ts.map +1 -1
  64. package/dist/extensions/todo/view.js +411 -463
  65. package/dist/extensions/todo/view.js.map +1 -1
  66. package/dist/extensions/todo/widget.d.ts +15 -0
  67. package/dist/extensions/todo/widget.d.ts.map +1 -0
  68. package/dist/extensions/todo/widget.js +50 -0
  69. package/dist/extensions/todo/widget.js.map +1 -0
  70. package/dist/modes/interactive/components/extension-selector.d.ts +1 -2
  71. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  72. package/dist/modes/interactive/components/extension-selector.js +5 -11
  73. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  74. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  76. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  77. package/dist/modes/rpc/rpc-mode.js +1 -8
  78. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  79. package/docs/bundled/README.md +1 -1
  80. package/docs/bundled/extensions/subagent.md +19 -4
  81. package/docs/bundled/extensions/todo.md +84 -24
  82. package/docs/bundled/tool-presentation.md +1 -1
  83. package/npm-shrinkwrap.json +2 -2
  84. package/package.json +1 -1
  85. package/dist/extensions/subagent/picker.d.ts +0 -31
  86. package/dist/extensions/subagent/picker.d.ts.map +0 -1
  87. package/dist/extensions/subagent/picker.js +0 -109
  88. package/dist/extensions/subagent/picker.js.map +0 -1
  89. package/dist/extensions/todo/overlay.d.ts +0 -24
  90. package/dist/extensions/todo/overlay.d.ts.map +0 -1
  91. package/dist/extensions/todo/overlay.js +0 -135
  92. package/dist/extensions/todo/overlay.js.map +0 -1
@@ -1,209 +1,283 @@
1
- import { truncateToWidth } from "@earendil-works/pi-tui";
2
- import { LIST_DISPLAY_MAX_ITEMS, TODO_MAX_BATCH_ITEMS, TODO_MAX_BLOCKED_BY } from "./constants.js";
1
+ /**
2
+ * todo/view.ts — v2 presentation: the one-line above-editor widget renderer,
3
+ * tool call/group renderers, the /todos list formatter, and model-facing
4
+ * result text. Wire input (args, details, snapshots) is read defensively and
5
+ * bounded so partial or hostile input cannot grow output without limit.
6
+ */
7
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
+ import { TODO_MAX_BATCH_ITEMS, TODO_MAX_DESCRIPTION_LENGTH, TODO_MAX_ITEMS, TODO_MAX_SUBJECT_LENGTH, } from "./constants.js";
3
9
  import { TODO_DETAILS_SCHEMA_VERSION, } from "./schema.js";
4
- import { unresolvedDependencyIds } from "./state.js";
5
10
  export const STATUS_MARK = {
6
11
  pending: "[ ]",
7
12
  in_progress: "[>]",
8
13
  completed: "[x]",
9
- deleted: "[-]",
10
14
  };
11
- export const STATUS_COLOR = {
15
+ const STATUS_COLOR = {
12
16
  pending: "dim",
13
17
  in_progress: "warning",
14
18
  completed: "success",
15
- deleted: "muted",
16
19
  };
17
- const OVERLAY_BODY_MAX_ITEMS = 10;
18
- function visibleTodos(state) {
19
- return state.items.filter((item) => item.status !== "deleted");
20
- }
21
- export function hasVisibleOverlayItems(state, hiddenCompleted) {
22
- return visibleTodos(state).some((item) => item.status !== "completed" || !hiddenCompleted.has(item.id));
23
- }
24
- function todoCounts(items) {
25
- const counts = { total: items.length, pending: 0, inProgress: 0, completed: 0 };
26
- for (const item of items) {
27
- if (item.status === "pending")
20
+ const STATUS_RANK = {
21
+ in_progress: 0,
22
+ pending: 1,
23
+ completed: 2,
24
+ };
25
+ // --- One-line widget --------------------------------------------------------
26
+ const WIDGET_HEADER_SEPARATOR = " · ";
27
+ const WIDGET_ITEM_SEPARATOR = " ";
28
+ /** True while any open task exists; the widget is registered only in that case. */
29
+ export function hasOpenTodos(state) {
30
+ return state.items.some((item) => item.status === "pending" || item.status === "in_progress");
31
+ }
32
+ /** Status counts across the whole list. */
33
+ function widgetCounts(state) {
34
+ const counts = { active: 0, pending: 0, completed: 0 };
35
+ for (const item of state.items) {
36
+ if (item.status === "in_progress")
37
+ counts.active++;
38
+ else if (item.status === "pending")
28
39
  counts.pending++;
29
- else if (item.status === "in_progress")
30
- counts.inProgress++;
31
- else if (item.status === "completed")
40
+ else
32
41
  counts.completed++;
33
42
  }
34
43
  return counts;
35
44
  }
36
- export function renderOverlayLines(state, theme, width, hiddenCompleted) {
37
- const allVisible = visibleTodos(state);
38
- const visible = allVisible.filter((item) => item.status !== "completed" || !hiddenCompleted.has(item.id));
39
- if (!visible.length)
45
+ function widgetSegment(item, subject, theme) {
46
+ return `${theme.fg(STATUS_COLOR[item.status], STATUS_MARK[item.status])} ${theme.fg("accent", `#${item.id}`)} ${theme.fg("text", subject)}`;
47
+ }
48
+ function widgetLine(header, segments, overflow) {
49
+ const body = [...segments, ...(overflow ? [overflow] : [])].join(WIDGET_ITEM_SEPARATOR);
50
+ return body ? `${header}${WIDGET_HEADER_SEPARATOR}${body}` : header;
51
+ }
52
+ /** Long overflow with counts (zero parts omitted), e.g. `+4 more (4 completed)`. */
53
+ function widgetOverflowLong(remaining, counts) {
54
+ const parts = [
55
+ counts.active > 0 ? `${counts.active} active` : "",
56
+ counts.pending > 0 ? `${counts.pending} pending` : "",
57
+ counts.completed > 0 ? `${counts.completed} completed` : "",
58
+ ].filter(Boolean);
59
+ return `+${remaining} more${parts.length ? ` (${parts.join(", ")})` : ""}`;
60
+ }
61
+ /** One-line widget: `Todos 2/6 · [>] #4 s [ ] #5 s +N more (…)`; completed tasks are hidden but counted in the overflow. */
62
+ export function renderWidgetLine(state, theme, width) {
63
+ const safeWidth = Math.max(1, width);
64
+ const items = state.items;
65
+ const candidates = items
66
+ .filter((item) => item.status !== "completed")
67
+ .sort((first, second) => STATUS_RANK[first.status] - STATUS_RANK[second.status] || first.id - second.id);
68
+ if (candidates.length === 0)
40
69
  return [];
41
- const counts = todoCounts(allVisible);
42
- const hasActive = visible.some((item) => item.status === "pending" || item.status === "in_progress");
43
- const heading = `${theme.fg(hasActive ? "accent" : "dim", "Todos")} ${theme.fg("dim", `(${counts.completed}/${counts.total})`)}`;
44
- const lines = [truncateToWidth(heading, width, "...")];
45
- const showIds = visible.some((item) => item.blockedBy?.length);
46
- const body = chooseOverlayItems(visible, state, OVERLAY_BODY_MAX_ITEMS);
47
- for (const item of body.items) {
48
- lines.push(truncateToWidth(` ${formatOverlayItem(item, state, theme, showIds)}`, width, "..."));
49
- }
50
- if (body.hidden.length > 0) {
51
- lines.push(truncateToWidth(theme.fg("dim", ` ${formatHiddenSummary(body.hidden)}`), width, "..."));
70
+ const total = items.length;
71
+ const completed = items.filter((item) => item.status === "completed").length;
72
+ const header = `${theme.fg("accent", "Todos")} ${theme.fg("dim", `${completed}/${total}`)}`;
73
+ const counts = widgetCounts(state);
74
+ const shown = [];
75
+ let overflow = "";
76
+ // Best overflow for a trial segment set: the long counts form when it fits,
77
+ // the short `+N more` form otherwise, undefined when even that overflows.
78
+ const bestOverflow = (trial) => {
79
+ const remaining = total - trial.length;
80
+ const segments = trial.map((entry) => widgetSegment(entry.item, entry.subject, theme));
81
+ const activeShown = trial.some((entry) => entry.item.status === "in_progress") ? 1 : 0;
82
+ const hidden = {
83
+ active: counts.active - activeShown,
84
+ pending: counts.pending - (trial.length - activeShown),
85
+ completed: counts.completed,
86
+ };
87
+ if (remaining <= 0)
88
+ return visibleWidth(widgetLine(header, segments, "")) <= safeWidth ? "" : undefined;
89
+ const long = theme.fg("dim", widgetOverflowLong(remaining, hidden));
90
+ if (visibleWidth(widgetLine(header, segments, long)) <= safeWidth)
91
+ return long;
92
+ const short = theme.fg("dim", `+${remaining} more`);
93
+ return visibleWidth(widgetLine(header, segments, short)) <= safeWidth ? short : undefined;
94
+ };
95
+ // Extreme narrow widths: the best summary that fits, then the bare header.
96
+ const fallback = () => {
97
+ const best = bestOverflow([]);
98
+ if (best !== undefined)
99
+ return [widgetLine(header, [], best)];
100
+ if (visibleWidth(header) <= safeWidth)
101
+ return [header];
102
+ return [truncateToWidth(header, safeWidth, "…")];
103
+ };
104
+ // Segments are added whole, active first then pending by id, while the
105
+ // line keeps fitting; only the active subject may be truncated.
106
+ for (const candidate of candidates) {
107
+ const best = bestOverflow([...shown, { item: candidate, subject: candidate.subject }]);
108
+ if (best !== undefined) {
109
+ shown.push({ item: candidate, subject: candidate.subject });
110
+ overflow = best;
111
+ continue;
112
+ }
113
+ if (candidate.status !== "in_progress")
114
+ break;
115
+ const remaining = total - shown.length - 1;
116
+ const suffix = remaining > 0 ? `${WIDGET_ITEM_SEPARATOR}+${remaining} more` : "";
117
+ const prefix = `${header}${WIDGET_HEADER_SEPARATOR}${widgetSegment(candidate, "", theme)}${suffix}`;
118
+ const available = safeWidth - visibleWidth(prefix);
119
+ if (available < 1)
120
+ return fallback();
121
+ shown.push({ item: candidate, subject: truncateToWidth(candidate.subject, available, "…") });
122
+ overflow = bestOverflow(shown) ?? "";
123
+ }
124
+ if (shown.length === 0)
125
+ return fallback();
126
+ const segments = shown.map((entry) => widgetSegment(entry.item, entry.subject, theme));
127
+ const line = widgetLine(header, segments, overflow);
128
+ // Safety net: the visible width always stays within the terminal width.
129
+ return [visibleWidth(line) <= safeWidth ? line : truncateToWidth(line, safeWidth, "…")];
130
+ }
131
+ // --- Full list (used by /todos and the model-facing list result) ------------
132
+ /** `Todos: X in progress, Y pending, Z completed` header plus two-line tasks. */
133
+ function formatTodoList(state) {
134
+ if (state.items.length === 0)
135
+ return "No todos.";
136
+ const counts = widgetCounts(state);
137
+ const lines = [`Todos: ${counts.active} in progress, ${counts.pending} pending, ${counts.completed} completed`];
138
+ const items = [...state.items].sort((first, second) => STATUS_RANK[first.status] - STATUS_RANK[second.status] || first.id - second.id);
139
+ for (const item of items) {
140
+ lines.push(`${STATUS_MARK[item.status]} #${item.id} ${item.subject}`);
141
+ lines.push(` ${item.description}`);
52
142
  }
53
- lines.push("");
54
- return lines;
55
- }
56
- function byIdAsc(first, second) {
57
- return first.id - second.id;
58
- }
59
- function overlayPriority(state, item) {
60
- if (item.status === "in_progress")
61
- return 0;
62
- if (item.status === "pending" && unresolvedDependencyIds(state, item).length === 0)
63
- return 1;
64
- if (item.status === "pending")
65
- return 2;
66
- return 3; // Completed items here are recent; older ones are hidden by TodoOverlay.
67
- }
68
- function chooseOverlayItems(items, state, maxBody) {
69
- const prioritized = [...items].sort((first, second) => {
70
- const priorityDelta = overlayPriority(state, first) - overlayPriority(state, second);
71
- return priorityDelta || byIdAsc(first, second);
72
- });
73
- if (prioritized.length <= maxBody)
74
- return { items: prioritized, hidden: [] };
75
- const shown = prioritized.slice(0, Math.max(1, maxBody - 1));
76
- return { items: shown, hidden: prioritized.slice(shown.length) };
77
- }
78
- function formatHiddenSummary(items) {
79
- const counts = todoCounts(items);
80
- const parts = [];
81
- if (counts.inProgress)
82
- parts.push(`${counts.inProgress} in progress`);
83
- if (counts.pending)
84
- parts.push(`${counts.pending} pending`);
85
- if (counts.completed)
86
- parts.push(`${counts.completed} completed`);
87
- return `+${items.length} more${parts.length ? ` (${parts.join(", ")})` : ""}`;
143
+ return lines.join("\n");
88
144
  }
89
- function formatOverlayItem(item, state, theme, showId) {
90
- let text = theme.fg(STATUS_COLOR[item.status], STATUS_MARK[item.status]);
91
- if (showId)
92
- text += ` ${theme.fg("accent", `#${item.id}`)}`;
93
- const subject = item.status === "completed" ? theme.strikethrough(theme.fg("dim", item.subject)) : theme.fg("text", item.subject);
94
- text += ` ${subject}`;
95
- if (item.status === "in_progress" && item.activeForm)
96
- text += ` ${theme.fg("dim", `(${item.activeForm})`)}`;
97
- const unresolved = unresolvedDependencyIds(state, item);
98
- if (unresolved.length) {
99
- text += ` ${theme.fg("dim", `blocked by ${unresolved.map((id) => `#${id}`).join(",")}`)}`;
100
- if (item.status === "pending" || item.status === "in_progress")
101
- text += ` ${theme.fg("warning", "(deps incomplete)")}`;
145
+ /** /todos command output: full list, subject line plus indented description per task. */
146
+ export const formatCommandList = formatTodoList;
147
+ /** Model-facing text for one completed todo call; bounded by list and input limits. */
148
+ export function formatTodoContent(change, state) {
149
+ switch (change.kind) {
150
+ case "create": {
151
+ const parts = change.ids.map((id) => {
152
+ const item = state.items.find((entry) => entry.id === id);
153
+ return item ? `#${item.id}: ${item.subject}` : `#${id}`;
154
+ });
155
+ const noun = change.ids.length === 1 ? "task" : "tasks";
156
+ return `Created ${change.ids.length} ${noun}${parts.length ? `: ${parts.join("; ")}` : ""}`;
157
+ }
158
+ case "update": {
159
+ const item = state.items.find((entry) => entry.id === change.id);
160
+ const subject = item ? `: ${item.subject}` : "";
161
+ const transition = change.from === change.to ? "" : ` (${change.from} -> ${change.to})`;
162
+ const demoted = change.demotedId !== undefined ? `; demoted #${change.demotedId} to pending` : "";
163
+ return `Updated #${change.id}${transition}${subject}${demoted}`;
164
+ }
165
+ case "list":
166
+ return formatTodoList(state);
167
+ case "delete": {
168
+ const noun = change.removed.length === 1 ? "task" : "tasks";
169
+ const parts = change.removed.map((entry) => `#${entry.id}: ${entry.subject}`);
170
+ return `Deleted ${change.removed.length} ${noun}${parts.length ? `: ${parts.join("; ")}` : ""}`;
171
+ }
102
172
  }
103
- return text;
104
- }
105
- const EDGE_PARAMS = ["blockedBy", "addBlockedBy", "removeBlockedBy", "addBlocks", "removeBlocks"];
106
- function callText(value) {
107
- return typeof value === "string" ? value.trim() : "";
108
173
  }
109
- function callSubject(value) {
110
- return callText(value).replace(/\s+/g, " ");
111
- }
112
- function callId(value) {
113
- return typeof value === "number" && Number.isInteger(value) ? value : undefined;
114
- }
115
- function callIds(value) {
116
- if (!Array.isArray(value))
117
- return [];
118
- const ids = [];
119
- let count;
174
+ const CALL_ITEMS_MAX = TODO_MAX_BATCH_ITEMS;
175
+ const CALL_SUBJECT_PREVIEW_COUNT = 2;
176
+ const CALL_SUBJECT_PREVIEW_WIDTH = 72;
177
+ const CALL_DESCRIPTION_PREVIEW_LENGTH = 120;
178
+ const GROUP_FAILURE_MAX_LENGTH = 120;
179
+ const TODO_ACTIONS = new Set(["create", "update", "list", "delete"]);
180
+ /** Record-like read; proxies that throw on inspection are treated as absent. */
181
+ function safeRecord(value) {
120
182
  try {
121
- count = Math.min(value.length, TODO_MAX_BLOCKED_BY);
183
+ return value !== null && typeof value === "object" && !Array.isArray(value)
184
+ ? value
185
+ : undefined;
122
186
  }
123
187
  catch {
124
- return ids;
125
- }
126
- for (let index = 0; index < count; index++) {
127
- let entry;
128
- try {
129
- entry = value[index];
130
- }
131
- catch {
132
- continue;
133
- }
134
- if (typeof entry === "number" && Number.isSafeInteger(entry) && entry >= 1)
135
- ids.push(entry);
188
+ return undefined;
136
189
  }
137
- return ids;
138
190
  }
139
- function callArrayLength(value) {
191
+ /** Field read that never throws, even for exotic objects with throwing getters. */
192
+ function safeValue(record, key) {
140
193
  try {
141
- return Array.isArray(value) ? value.length : undefined;
194
+ return record?.[key];
142
195
  }
143
196
  catch {
144
197
  return undefined;
145
198
  }
146
199
  }
147
- const CALL_BATCH_SUBJECT_PREVIEW_COUNT = 2;
148
- const CALL_SUBJECT_PREVIEW_WIDTH = 72;
149
- const CALL_DESCRIPTION_PREVIEW_LENGTH = 120;
150
- function callStrings(value) {
151
- if (!Array.isArray(value))
152
- return [];
153
- const strings = [];
154
- let count;
200
+ function safeString(value) {
201
+ return typeof value === "string" ? value : undefined;
202
+ }
203
+ /** Trimmed, whitespace-collapsed, length-clipped text (empty when absent). */
204
+ function clipText(value, max) {
205
+ return safeString(value)?.trim().replace(/\s+/g, " ").slice(0, max) ?? "";
206
+ }
207
+ const safeSubject = (value) => clipText(value, TODO_MAX_SUBJECT_LENGTH);
208
+ const safeDescription = (value) => clipText(value, TODO_MAX_DESCRIPTION_LENGTH);
209
+ function safeId(value) {
210
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
211
+ }
212
+ function safeStatus(value) {
213
+ const status = safeString(value);
214
+ if (status === "pending" || status === "in_progress" || status === "completed")
215
+ return status;
216
+ return undefined;
217
+ }
218
+ function readArray(value, limit = CALL_ITEMS_MAX) {
155
219
  try {
156
- count = Math.min(value.length, TODO_MAX_BLOCKED_BY);
220
+ if (!Array.isArray(value))
221
+ return undefined;
222
+ const length = value.length;
223
+ const values = [];
224
+ for (let index = 0; index < Math.min(length, limit); index++) {
225
+ try {
226
+ values.push(value[index]);
227
+ }
228
+ catch {
229
+ values.push(undefined);
230
+ }
231
+ }
232
+ return { values, length };
157
233
  }
158
234
  catch {
159
- return strings;
235
+ return undefined;
160
236
  }
161
- for (let index = 0; index < count; index++) {
162
- let entry;
163
- try {
164
- entry = value[index];
165
- }
166
- catch {
237
+ }
238
+ /** Id lists: lenient for delete args (invalid entries drop), strict for result changes (any invalid entry rejects). */
239
+ function readIds(value, strict) {
240
+ const array = readArray(value);
241
+ if (strict && (!array || array.length === 0 || array.length > CALL_ITEMS_MAX))
242
+ return undefined;
243
+ if (!array)
244
+ return strict ? undefined : [];
245
+ const ids = [];
246
+ for (const entry of array.values) {
247
+ const id = safeId(entry);
248
+ if (id === undefined) {
249
+ if (strict)
250
+ return undefined;
167
251
  continue;
168
252
  }
169
- const text = callText(entry);
170
- if (text)
171
- strings.push(text);
253
+ ids.push(id);
172
254
  }
173
- return strings;
255
+ return ids;
174
256
  }
175
- function callBatchItems(value) {
176
- if (!Array.isArray(value))
177
- return [];
178
- let count;
179
- try {
180
- count = Math.min(value.length, TODO_MAX_BATCH_ITEMS);
181
- }
182
- catch {
183
- return [];
184
- }
185
- const items = [];
186
- for (let index = 0; index < count; index++) {
187
- let valueAtIndex;
188
- try {
189
- valueAtIndex = value[index];
190
- }
191
- catch {
192
- valueAtIndex = undefined;
193
- }
194
- const item = safeRecord(valueAtIndex);
195
- items.push({
196
- subject: item ? callSubject(safeValue(item, "subject")) : "",
197
- description: item ? callText(safeValue(item, "description")) : "",
198
- blockedBy: item ? callIds(safeValue(item, "blockedBy")) : [],
199
- blockedByKeys: item ? callStrings(safeValue(item, "blockedByKeys")) : [],
257
+ /** Create args: sparse or invalid entries drop; the batch caps at the maximum. */
258
+ function batchItems(value) {
259
+ const result = [];
260
+ for (const raw of readArray(value)?.values ?? []) {
261
+ const item = safeRecord(raw);
262
+ if (!item)
263
+ continue;
264
+ result.push({
265
+ subject: safeSubject(safeValue(item, "subject")),
266
+ description: safeDescription(safeValue(item, "description")),
200
267
  });
201
268
  }
202
- return items;
269
+ return result;
270
+ }
271
+ function descriptionPreview(value) {
272
+ const text = value.replace(/\s+/g, " ");
273
+ return text.length > CALL_DESCRIPTION_PREVIEW_LENGTH
274
+ ? `${text.slice(0, CALL_DESCRIPTION_PREVIEW_LENGTH - 1)}…`
275
+ : text;
203
276
  }
277
+ /** At most two subjects, each truncated to the preview width, plus `+N more`. */
204
278
  function formatSubjectPreview(subjects, total) {
205
279
  const shown = subjects
206
- .slice(0, CALL_BATCH_SUBJECT_PREVIEW_COUNT)
280
+ .slice(0, CALL_SUBJECT_PREVIEW_COUNT)
207
281
  .filter(Boolean)
208
282
  .map((subject) => truncateToWidth(subject, CALL_SUBJECT_PREVIEW_WIDTH, "…"));
209
283
  if (!shown.length)
@@ -211,340 +285,214 @@ function formatSubjectPreview(subjects, total) {
211
285
  const hidden = Math.max(0, total - shown.length);
212
286
  return `${shown.join(", ")}${hidden ? `, +${hidden} more` : ""}`;
213
287
  }
214
- function callBatchSubjectPreview(items, total) {
215
- return formatSubjectPreview(items.map((item) => item.subject), total);
216
- }
217
- function callDescriptionPreview(value) {
218
- const text = value.replace(/\s+/g, " ");
219
- return text.length > CALL_DESCRIPTION_PREVIEW_LENGTH
220
- ? `${text.slice(0, CALL_DESCRIPTION_PREVIEW_LENGTH - 1)}…`
221
- : text;
222
- }
223
- function callNonNegativeInteger(value) {
224
- return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
225
- }
226
- function isTodoStatus(value) {
227
- return Object.hasOwn(STATUS_MARK, value);
228
- }
229
- function formatCallHeadline(args, theme) {
230
- const action = callText(args?.action);
231
- const displayAction = action === "create_many" ? "create" : action;
232
- const parts = [theme.fg("toolTitle", theme.bold(displayAction ? `todo ${displayAction}` : "todo"))];
233
- if (action === "create_many") {
234
- const items = callBatchItems(args?.items);
235
- const count = callArrayLength(args?.items);
236
- if (count !== undefined) {
237
- parts.push(theme.fg("dim", `${count} ${count === 1 ? "task" : "tasks"}`));
238
- const preview = callBatchSubjectPreview(items, count);
239
- if (preview) {
240
- parts.push(theme.fg("dim", "·"));
241
- parts.push(theme.fg("text", preview));
242
- }
243
- }
244
- }
245
- const id = callId(args?.id);
246
- if (id !== undefined)
247
- parts.push(theme.fg("accent", `#${id}`));
248
- const status = callText(args?.status);
249
- if (status && isTodoStatus(status))
250
- parts.push(theme.fg(STATUS_COLOR[status], status));
251
- const subject = callSubject(args?.subject);
252
- if (subject)
253
- parts.push(theme.fg("text", subject));
254
- // Dependency-only updates would otherwise render as a bare `todo update #3`.
255
- const hasEdgeChange = EDGE_PARAMS.some((key) => callIds(args?.[key]).length > 0);
256
- if (!status && !subject && hasEdgeChange)
257
- parts.push(theme.fg("dim", "dependencies"));
258
- return parts.join(" ");
288
+ /** v2 details guard shared by the call and group renderers. */
289
+ function todoDetails(value) {
290
+ const details = safeRecord(value);
291
+ if (!details || safeValue(details, "schemaVersion") !== TODO_DETAILS_SCHEMA_VERSION)
292
+ return undefined;
293
+ const change = safeRecord(safeValue(details, "change"));
294
+ const state = safeRecord(safeValue(details, "state"));
295
+ if (!change || !state)
296
+ return undefined;
297
+ const kind = safeString(safeValue(change, "kind"));
298
+ if (!kind || !TODO_ACTIONS.has(kind))
299
+ return undefined;
300
+ return { change, state };
259
301
  }
260
- function batchResultIds(result, count) {
261
- const details = groupDetails(result?.details);
262
- if (!details || details.action !== "create_many")
302
+ /** Result ids for an expanded create, only when they match the shown items. */
303
+ function createResultIds(result, count) {
304
+ const details = todoDetails(result?.details);
305
+ if (!details || safeString(safeValue(details.change, "kind")) !== "create")
263
306
  return undefined;
264
- const ids = groupIds(safeValue(details.operation, "ids"));
307
+ const ids = readIds(safeValue(details.change, "ids"), true);
265
308
  return ids?.length === count ? ids : undefined;
266
309
  }
267
- function formatCallDetails(args, theme, result) {
268
- const lines = [];
269
- const push = (label, value) => {
270
- if (value)
271
- lines.push(theme.fg("dim", `${label}: ${value}`));
272
- };
273
- const action = callText(args?.action);
274
- if (action === "create_many") {
275
- const items = callBatchItems(args?.items);
276
- const ids = batchResultIds(result, items.length);
310
+ /** Headline plus the parameters the result never echoes as detail lines. */
311
+ function formatCallParts(args, theme, result) {
312
+ const action = clipText(args?.action, TODO_MAX_SUBJECT_LENGTH);
313
+ const verb = TODO_ACTIONS.has(action) ? action : undefined;
314
+ const headline = [theme.fg("toolTitle", theme.bold(verb ? `todo ${verb}` : "todo"))];
315
+ const details = [];
316
+ if (verb === "create") {
317
+ const items = batchItems(args?.items);
318
+ const rawCount = readArray(args?.items)?.length;
319
+ if (rawCount !== undefined) {
320
+ const count = Math.min(rawCount, CALL_ITEMS_MAX);
321
+ headline.push(theme.fg("dim", `${count} ${count === 1 ? "task" : "tasks"}`));
322
+ const subjects = items.map((item) => item.subject);
323
+ const preview = formatSubjectPreview(subjects, count);
324
+ if (preview)
325
+ headline.push(theme.fg("dim", "·"), theme.fg("text", preview));
326
+ }
327
+ const ids = createResultIds(result, items.length);
277
328
  for (const [index, item] of items.entries()) {
278
- const dependencies = [...item.blockedBy.map((id) => `#${id}`), ...item.blockedByKeys];
279
- let line = theme.fg("accent", ids ? `#${ids[index]}` : `${index + 1}.`);
280
- if (item.subject)
281
- line += ` ${theme.fg("text", item.subject)}`;
282
- if (dependencies.length)
283
- line += ` ${theme.fg("dim", `· blocked by ${dependencies.join(", ")}`)}`;
284
- lines.push(line);
329
+ const marker = ids ? theme.fg("accent", `#${ids[index]}`) : theme.fg("accent", `${index + 1}.`);
330
+ details.push(item.subject ? `${marker} ${theme.fg("text", item.subject)}` : marker);
285
331
  if (item.description)
286
- lines.push(` ${theme.fg("dim", callDescriptionPreview(item.description))}`);
332
+ details.push(` ${theme.fg("dim", descriptionPreview(item.description))}`);
287
333
  }
288
334
  }
289
- push("description", callText(args?.description));
290
- push("activeForm", callText(args?.activeForm));
291
- push("owner", callText(args?.owner));
292
- for (const key of EDGE_PARAMS) {
293
- const ids = callIds(args?.[key]);
294
- if (ids.length)
295
- push(key, ids.map((id) => `#${id}`).join(","));
335
+ else if (verb === "update") {
336
+ const id = safeId(args?.id);
337
+ if (id !== undefined)
338
+ headline.push(theme.fg("accent", `#${id}`));
339
+ const status = safeStatus(args?.status);
340
+ if (status)
341
+ headline.push(theme.fg(STATUS_COLOR[status], status));
342
+ const subject = safeSubject(args?.subject);
343
+ if (subject)
344
+ headline.push(theme.fg("text", truncateToWidth(subject, CALL_SUBJECT_PREVIEW_WIDTH, "…")));
345
+ const description = safeDescription(args?.description);
346
+ if (description)
347
+ details.push(` ${theme.fg("dim", descriptionPreview(description))}`);
348
+ }
349
+ else if (verb === "delete") {
350
+ const ids = readIds(args?.ids, false) ?? [];
351
+ if (ids.length) {
352
+ const rendered = theme.fg("accent", ids.map((id) => `#${id}`).join(", "));
353
+ headline.push(rendered);
354
+ details.push(rendered);
355
+ }
296
356
  }
297
- if (args?.includeDeleted === true)
298
- push("includeDeleted", "true");
299
- const limit = callId(args?.limit);
300
- if (limit !== undefined)
301
- push("limit", String(limit));
302
- const afterId = callId(args?.afterId);
303
- if (afterId !== undefined)
304
- push("afterId", String(afterId));
305
- push("query", callText(args?.query));
306
- if (args?.unblockedOnly === true)
307
- push("unblockedOnly", "true");
308
- if (args?.confirm === true)
309
- push("confirm", "true");
310
- const expectedCount = callNonNegativeInteger(args?.expectedCount);
311
- if (expectedCount !== undefined)
312
- push("expectedCount", String(expectedCount));
313
- const metadata = args?.metadata;
314
- if (metadata && typeof metadata === "object")
315
- push("metadata", Object.keys(metadata).join(","));
316
- return lines;
357
+ return { headline: headline.join(" "), details };
317
358
  }
318
- /**
319
- * One-line call summary when collapsed; the same headline plus the parameters
320
- * the result never echoes (description, activeForm, dependencies) when expanded.
321
- */
359
+ /** One-line call summary when collapsed; headline plus details when expanded. */
322
360
  export function formatTodoCall(args, theme, expanded, result) {
323
- const headline = formatCallHeadline(args, theme);
324
- if (!expanded)
325
- return headline;
326
- return [headline, ...formatCallDetails(args, theme, result)].join("\n");
327
- }
328
- const GROUP_FAILURE_MAX_LENGTH = 120;
329
- const GROUP_MAX_IDS = 20;
330
- const GROUP_MAX_SNAPSHOT_ITEMS = 10_000;
331
- const TODO_ACTIONS = new Set(["create", "create_many", "update", "list", "get", "delete", "clear"]);
332
- function safeRecord(value) {
333
- try {
334
- return value !== null && typeof value === "object" && !Array.isArray(value)
335
- ? value
336
- : undefined;
337
- }
338
- catch {
339
- return undefined;
340
- }
361
+ const { headline, details } = formatCallParts(args, theme, result);
362
+ return !expanded || details.length === 0 ? headline : [headline, ...details].join("\n");
341
363
  }
342
- function safeValue(record, key) {
343
- try {
344
- return record[key];
345
- }
346
- catch {
364
+ /** Id -> item index plus status counts over the v2 snapshot (one defensive walk). */
365
+ function stateIndex(state) {
366
+ const array = readArray(safeValue(state, "items"), TODO_MAX_ITEMS);
367
+ if (!array || array.length > TODO_MAX_ITEMS)
347
368
  return undefined;
369
+ const byId = new Map();
370
+ const counts = { inProgress: 0, pending: 0, completed: 0 };
371
+ for (const raw of array.values) {
372
+ const item = safeRecord(raw);
373
+ if (!item)
374
+ continue;
375
+ const id = safeId(safeValue(item, "id"));
376
+ if (id !== undefined && !byId.has(id))
377
+ byId.set(id, item);
378
+ const status = safeStatus(safeValue(item, "status"));
379
+ if (status === "in_progress")
380
+ counts.inProgress++;
381
+ else if (status === "pending")
382
+ counts.pending++;
383
+ else if (status === "completed")
384
+ counts.completed++;
348
385
  }
386
+ return { byId, counts };
349
387
  }
350
- function safeString(value) {
351
- return typeof value === "string" ? value : undefined;
388
+ function stateSubject(map, id) {
389
+ const item = map.get(id);
390
+ return item ? safeSubject(safeValue(item, "subject")) : "";
352
391
  }
353
- function groupDetails(value) {
354
- const details = safeRecord(value);
355
- if (!details || safeValue(details, "schemaVersion") !== TODO_DETAILS_SCHEMA_VERSION)
356
- return undefined;
357
- const action = safeString(safeValue(details, "action"));
358
- if (!action || !TODO_ACTIONS.has(action))
392
+ /** Deleted entries: one invalid or empty entry rejects the whole list. */
393
+ function removedEntries(value) {
394
+ const array = readArray(value);
395
+ if (!array || array.length === 0 || array.length > CALL_ITEMS_MAX)
359
396
  return undefined;
360
- const operation = safeRecord(safeValue(details, "operation"));
361
- if (!operation || safeValue(operation, "kind") !== action)
362
- return undefined;
363
- return { action, operation, items: safeValue(details, "items") };
364
- }
365
- function groupPositiveInteger(value) {
366
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
367
- }
368
- function groupIds(value) {
369
- try {
370
- if (!Array.isArray(value) || value.length === 0 || value.length > GROUP_MAX_IDS)
397
+ const removed = [];
398
+ for (const raw of array.values) {
399
+ const id = safeId(safeValue(safeRecord(raw), "id"));
400
+ const subject = safeSubject(safeValue(safeRecord(raw), "subject"));
401
+ if (id === undefined || !subject)
371
402
  return undefined;
372
- const ids = [];
373
- for (const valueAtIndex of value) {
374
- const id = groupPositiveInteger(valueAtIndex);
375
- if (id === undefined)
376
- return undefined;
377
- ids.push(id);
378
- }
379
- return ids;
380
- }
381
- catch {
382
- return undefined;
403
+ removed.push({ id, subject });
383
404
  }
405
+ return removed;
384
406
  }
385
- function groupNonNegativeInteger(value) {
386
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
387
- }
388
- function formatGroupStatusCounts(value, resultCount) {
389
- const count = groupNonNegativeInteger(resultCount);
390
- if (count === undefined)
391
- return undefined;
392
- if (count === 0)
393
- return "no tasks";
394
- const rawCounts = safeRecord(value);
395
- if (!rawCounts)
407
+ function formatTodoGroupSuccess(args, theme, result) {
408
+ const parsed = todoDetails(result.details);
409
+ if (!parsed)
396
410
  return undefined;
397
- const parts = [];
398
- let total = 0;
399
- for (const status of ["pending", "in_progress", "completed", "deleted"]) {
400
- const statusCount = groupNonNegativeInteger(safeValue(rawCounts, status));
401
- if (statusCount === undefined)
402
- continue;
403
- total += statusCount;
404
- if (statusCount > 0)
405
- parts.push(`${statusCount} ${status}`);
406
- }
407
- return total === count && parts.length ? parts.join(", ") : undefined;
408
- }
409
- function groupItem(value, id) {
410
- try {
411
- if (!Array.isArray(value) || value.length > GROUP_MAX_SNAPSHOT_ITEMS)
411
+ const { change, state } = parsed;
412
+ const verb = (word) => theme.fg("toolTitle", `todo ${word}`);
413
+ const withPreview = (word, ids, subjects) => {
414
+ const text = formatSubjectPreview(subjects, subjects.length);
415
+ const consecutive = ids.length > 1 && ids.every((id, index) => id === ids[0] + index);
416
+ const rendered = consecutive ? `#${ids[0]}–#${ids.at(-1)}` : ids.map((id) => `#${id}`).join(", ");
417
+ const base = `${verb(word)} ${theme.fg("accent", rendered)}`;
418
+ return text ? `${base}${theme.fg("dim", " · ")}${theme.fg("text", text)}` : base;
419
+ };
420
+ const kind = safeString(safeValue(change, "kind"));
421
+ if (kind === "create") {
422
+ const ids = readIds(safeValue(change, "ids"), true);
423
+ const index = stateIndex(state);
424
+ if (!ids || !index)
412
425
  return undefined;
413
- for (const rawItem of value) {
414
- const item = safeRecord(rawItem);
415
- if (item && safeValue(item, "id") === id)
416
- return item;
417
- }
426
+ const subjects = ids.map((id) => stateSubject(index.byId, id));
427
+ return withPreview("created", ids, subjects);
418
428
  }
419
- catch {
420
- return undefined;
429
+ if (kind === "update") {
430
+ const id = safeId(safeValue(change, "id"));
431
+ const index = stateIndex(state);
432
+ if (id === undefined || !index)
433
+ return undefined;
434
+ const to = safeStatus(safeValue(change, "to"));
435
+ const demotedId = safeId(safeValue(change, "demotedId"));
436
+ const parts = [verb("updated"), theme.fg("accent", `#${id}`)];
437
+ if (to)
438
+ parts.push(theme.fg(STATUS_COLOR[to], to));
439
+ const subject = stateSubject(index.byId, id) || safeSubject(args?.subject) || "";
440
+ if (subject)
441
+ parts.push(theme.fg("text", truncateToWidth(subject, CALL_SUBJECT_PREVIEW_WIDTH, "…")));
442
+ if (demotedId !== undefined)
443
+ parts.push(theme.fg("dim", `· demoted #${demotedId}`));
444
+ return parts.join(" ");
445
+ }
446
+ if (kind === "list") {
447
+ const counts = stateIndex(state)?.counts;
448
+ if (!counts)
449
+ return undefined;
450
+ const parts = [];
451
+ if (counts.inProgress > 0)
452
+ parts.push(`${counts.inProgress} in progress`);
453
+ if (counts.pending > 0)
454
+ parts.push(`${counts.pending} pending`);
455
+ if (counts.completed > 0)
456
+ parts.push(`${counts.completed} completed`);
457
+ return `${verb("list")}: ${theme.fg("dim", parts.length ? parts.join(", ") : "no tasks")}`;
458
+ }
459
+ if (kind === "delete") {
460
+ const removed = removedEntries(safeValue(change, "removed"));
461
+ if (!removed)
462
+ return undefined;
463
+ const subjects = removed.map((entry) => entry.subject);
464
+ return withPreview("deleted", removed.map((entry) => entry.id), subjects);
421
465
  }
422
466
  return undefined;
423
467
  }
424
- function groupItemStatus(value, id) {
425
- const item = groupItem(value, id);
426
- if (!item)
427
- return undefined;
428
- const status = safeString(safeValue(item, "status"));
429
- return status && isTodoStatus(status) ? status : undefined;
430
- }
431
- function groupItemSubject(value, id) {
432
- const item = groupItem(value, id);
433
- if (!item)
434
- return undefined;
435
- const subject = callSubject(safeValue(item, "subject"));
436
- return subject || undefined;
437
- }
438
- function formatGroupIds(ids) {
439
- const consecutive = ids.length > 1 && ids.every((id, index) => id === ids[0] + index);
440
- return consecutive ? `#${ids[0]}–#${ids.at(-1)}` : ids.map((id) => `#${id}`).join(", ");
441
- }
442
- function formatGroupFailure(result) {
443
- try {
444
- for (const block of result?.content.slice(0, 8) ?? []) {
445
- if (block.type !== "text" || typeof block.text !== "string")
446
- continue;
447
- const reason = block.text
448
- .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
449
- .replace(/\s+/g, " ")
450
- .trim();
451
- if (!reason)
452
- continue;
453
- return reason.length > GROUP_FAILURE_MAX_LENGTH ? `${reason.slice(0, GROUP_FAILURE_MAX_LENGTH - 1)}…` : reason;
454
- }
455
- }
456
- catch {
457
- // Tool results can come from historical or third-party sources.
458
- }
459
- return "tool failed";
460
- }
461
- function formatTodoGroupSuccess(args, result) {
462
- const details = groupDetails(result.details);
463
- if (!details)
464
- return undefined;
465
- switch (details.action) {
466
- case "create": {
467
- const ids = groupIds(safeValue(details.operation, "ids"));
468
- if (!ids || ids.length !== 1)
469
- return undefined;
470
- const subject = groupItemSubject(details.items, ids[0]) ?? callSubject(args?.subject);
471
- const preview = subject ? truncateToWidth(subject, CALL_SUBJECT_PREVIEW_WIDTH, "…") : "";
472
- return `todo created #${ids[0]}${preview ? ` ${preview}` : ""}`;
473
- }
474
- case "create_many": {
475
- const ids = groupIds(safeValue(details.operation, "ids"));
476
- if (!ids)
477
- return undefined;
478
- const callItems = callBatchItems(args?.items);
479
- const subjects = ids.map((id, index) => groupItemSubject(details.items, id) ?? callItems[index]?.subject ?? "");
480
- const preview = formatSubjectPreview(subjects, ids.length);
481
- return `todo created ${formatGroupIds(ids)}${preview ? ` · ${preview}` : ""}`;
482
- }
483
- case "update": {
484
- const id = groupPositiveInteger(safeValue(details.operation, "id"));
485
- const status = safeString(safeValue(details.operation, "status"));
486
- return id !== undefined && status && isTodoStatus(status) ? `todo updated #${id} ${status}` : undefined;
487
- }
488
- case "list": {
489
- const counts = formatGroupStatusCounts(safeValue(details.operation, "statusCounts"), safeValue(details.operation, "resultCount"));
490
- return counts === undefined ? undefined : `todo list: ${counts}`;
491
- }
492
- case "get": {
493
- const id = groupPositiveInteger(safeValue(details.operation, "id"));
494
- const status = id === undefined ? undefined : groupItemStatus(details.items, id);
495
- return status ? `todo get #${id} ${status}` : undefined;
496
- }
497
- case "delete": {
498
- const id = groupPositiveInteger(safeValue(details.operation, "id"));
499
- if (id === undefined)
500
- return undefined;
501
- const subject = groupItemSubject(details.items, id);
502
- const preview = subject ? truncateToWidth(subject, CALL_SUBJECT_PREVIEW_WIDTH, "…") : "";
503
- return `todo deleted #${id}${preview ? ` ${preview}` : ""}`;
504
- }
505
- case "clear": {
506
- const count = callNonNegativeInteger(safeValue(details.operation, "count"));
507
- return count === undefined ? undefined : `todo cleared ${count} ${count === 1 ? "task" : "tasks"}`;
508
- }
509
- }
510
- }
511
468
  /** Compact result-aware summary used only by collapsed todo tool groups. */
512
469
  export function formatTodoGroupCall(args, theme, context) {
513
470
  if (context.isError) {
514
- return `${formatTodoCall(args, theme, false)} ${theme.fg("error", `failed: ${formatGroupFailure(context.result)}`)}`;
471
+ let reason = "tool failed";
472
+ try {
473
+ for (const block of context.result?.content.slice(0, 8) ?? []) {
474
+ if (block.type !== "text" || typeof block.text !== "string")
475
+ continue;
476
+ const text = block.text
477
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
478
+ .replace(/\s+/g, " ")
479
+ .trim();
480
+ if (!text)
481
+ continue;
482
+ reason = text.length > GROUP_FAILURE_MAX_LENGTH ? `${text.slice(0, GROUP_FAILURE_MAX_LENGTH - 1)}…` : text;
483
+ break;
484
+ }
485
+ }
486
+ catch {
487
+ // Tool results can come from historical or third-party sources.
488
+ }
489
+ return `${formatTodoCall(args, theme, false)} ${theme.fg("error", `failed: ${reason}`)}`;
515
490
  }
516
491
  if (!context.isPartial && context.result) {
517
- const summary = formatTodoGroupSuccess(args, context.result);
492
+ const summary = formatTodoGroupSuccess(args, theme, context.result);
518
493
  if (summary)
519
- return theme.fg("toolTitle", summary);
494
+ return summary;
520
495
  }
521
496
  return formatTodoCall(args, theme, false);
522
497
  }
523
- export function formatCommandList(state) {
524
- const visible = visibleTodos(state);
525
- if (!visible.length)
526
- return "No todos yet.";
527
- const lines = [];
528
- let shown = 0;
529
- for (const status of ["in_progress", "pending", "completed"]) {
530
- const group = visible.filter((item) => item.status === status).sort(byIdAsc);
531
- if (!group.length || shown >= LIST_DISPLAY_MAX_ITEMS)
532
- continue;
533
- lines.push(status);
534
- for (const item of group) {
535
- if (shown >= LIST_DISPLAY_MAX_ITEMS)
536
- break;
537
- const active = item.status === "in_progress" && item.activeForm ? ` (${item.activeForm})` : "";
538
- const owner = item.owner ? ` @${item.owner}` : "";
539
- const unresolved = unresolvedDependencyIds(state, item);
540
- const deps = unresolved.length ? ` blocked by ${unresolved.map((id) => `#${id}`).join(",")}` : "";
541
- lines.push(` ${STATUS_MARK[item.status]} #${item.id} ${item.subject}${active}${owner}${deps}`);
542
- shown++;
543
- }
544
- }
545
- if (visible.length > shown) {
546
- lines.push(`… and ${visible.length - shown} more; use todo list with limit and afterId to page.`);
547
- }
548
- return lines.join("\n");
549
- }
550
498
  //# sourceMappingURL=view.js.map