@astralyn/pi 0.84.2 → 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.
- package/CHANGELOG.md +41 -0
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +2 -3
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +1 -4
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +3 -5
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -25
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +29 -408
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction-controller.d.ts +89 -0
- package/dist/core/compaction-controller.d.ts.map +1 -0
- package/dist/core/compaction-controller.js +428 -0
- package/dist/core/compaction-controller.js.map +1 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.js +3 -3
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +2 -27
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/provider-attribution.js +2 -2
- package/dist/core/provider-attribution.js.map +1 -1
- package/dist/core/settings-manager.js +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/telemetry.d.ts +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +1 -1
- package/dist/core/telemetry.js.map +1 -1
- package/dist/extensions/deepwiki/index.d.ts.map +1 -1
- package/dist/extensions/deepwiki/index.js +23 -1
- package/dist/extensions/deepwiki/index.js.map +1 -1
- package/dist/extensions/deepwiki/render.d.ts +1 -1
- package/dist/extensions/deepwiki/render.d.ts.map +1 -1
- package/dist/extensions/deepwiki/render.js +4 -2
- package/dist/extensions/deepwiki/render.js.map +1 -1
- package/dist/extensions/question/index.d.ts.map +1 -1
- package/dist/extensions/question/index.js +0 -1
- package/dist/extensions/question/index.js.map +1 -1
- package/dist/extensions/subagent/agents-command.d.ts +4 -0
- package/dist/extensions/subagent/agents-command.d.ts.map +1 -0
- package/dist/extensions/subagent/agents-command.js +230 -0
- package/dist/extensions/subagent/agents-command.js.map +1 -0
- package/dist/extensions/subagent/agents.d.ts.map +1 -1
- package/dist/extensions/subagent/agents.js +2 -0
- package/dist/extensions/subagent/agents.js.map +1 -1
- package/dist/extensions/subagent/display-name.d.ts +6 -0
- package/dist/extensions/subagent/display-name.d.ts.map +1 -0
- package/dist/extensions/subagent/display-name.js +13 -0
- package/dist/extensions/subagent/display-name.js.map +1 -0
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +19 -126
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent/render.d.ts.map +1 -1
- package/dist/extensions/subagent/render.js +1 -10
- package/dist/extensions/subagent/render.js.map +1 -1
- package/dist/extensions/subagent/settings.d.ts +0 -1
- package/dist/extensions/subagent/settings.d.ts.map +1 -1
- package/dist/extensions/subagent/settings.js +3 -7
- package/dist/extensions/subagent/settings.js.map +1 -1
- package/dist/extensions/subagent/types.d.ts +2 -0
- package/dist/extensions/subagent/types.d.ts.map +1 -1
- package/dist/extensions/subagent/types.js.map +1 -1
- package/dist/extensions/subagent/ui/model-list.d.ts +67 -0
- package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/model-list.js +242 -0
- package/dist/extensions/subagent/ui/model-list.js.map +1 -0
- package/dist/extensions/subagent/ui/profile-editor.d.ts +54 -0
- package/dist/extensions/subagent/ui/profile-editor.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/profile-editor.js +154 -0
- package/dist/extensions/subagent/ui/profile-editor.js.map +1 -0
- package/dist/extensions/subagent/ui/profile-picker.d.ts +32 -0
- package/dist/extensions/subagent/ui/profile-picker.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/profile-picker.js +149 -0
- package/dist/extensions/subagent/ui/profile-picker.js.map +1 -0
- package/dist/extensions/todo/constants.d.ts +6 -17
- package/dist/extensions/todo/constants.d.ts.map +1 -1
- package/dist/extensions/todo/constants.js +20 -31
- package/dist/extensions/todo/constants.js.map +1 -1
- package/dist/extensions/todo/index.d.ts.map +1 -1
- package/dist/extensions/todo/index.js +36 -57
- package/dist/extensions/todo/index.js.map +1 -1
- package/dist/extensions/todo/schema.d.ts +20 -59
- package/dist/extensions/todo/schema.d.ts.map +1 -1
- package/dist/extensions/todo/schema.js +23 -69
- package/dist/extensions/todo/schema.js.map +1 -1
- package/dist/extensions/todo/state.d.ts +23 -65
- package/dist/extensions/todo/state.d.ts.map +1 -1
- package/dist/extensions/todo/state.js +238 -1262
- package/dist/extensions/todo/state.js.map +1 -1
- package/dist/extensions/todo/view.d.ts +19 -10
- package/dist/extensions/todo/view.d.ts.map +1 -1
- package/dist/extensions/todo/view.js +411 -463
- package/dist/extensions/todo/view.js.map +1 -1
- package/dist/extensions/todo/widget.d.ts +15 -0
- package/dist/extensions/todo/widget.d.ts.map +1 -0
- package/dist/extensions/todo/widget.js +50 -0
- package/dist/extensions/todo/widget.js.map +1 -0
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -4
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +6 -12
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-chrome.d.ts +28 -0
- package/dist/modes/interactive/components/tool-chrome.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chrome.js +81 -0
- package/dist/modes/interactive/components/tool-chrome.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +0 -3
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +11 -108
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +35 -52
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -8
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/bundled/README.md +1 -1
- package/docs/bundled/extensions/subagent.md +20 -5
- package/docs/bundled/extensions/todo.md +84 -24
- package/docs/bundled/tool-presentation.md +3 -3
- package/docs/extensions.md +6 -23
- package/docs/settings.md +1 -1
- package/docs/usage.md +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/extensions/subagent/picker.d.ts +0 -31
- package/dist/extensions/subagent/picker.d.ts.map +0 -1
- package/dist/extensions/subagent/picker.js +0 -109
- package/dist/extensions/subagent/picker.js.map +0 -1
- package/dist/extensions/todo/overlay.d.ts +0 -24
- package/dist/extensions/todo/overlay.d.ts.map +0 -1
- package/dist/extensions/todo/overlay.js +0 -135
- package/dist/extensions/todo/overlay.js.map +0 -1
- package/dist/utils/coalescing-terminal.d.ts +0 -132
- package/dist/utils/coalescing-terminal.d.ts.map +0 -1
- package/dist/utils/coalescing-terminal.js +0 -292
- package/dist/utils/coalescing-terminal.js.map +0 -1
|
@@ -1,209 +1,283 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
15
|
+
const STATUS_COLOR = {
|
|
12
16
|
pending: "dim",
|
|
13
17
|
in_progress: "warning",
|
|
14
18
|
completed: "success",
|
|
15
|
-
deleted: "muted",
|
|
16
19
|
};
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
30
|
-
counts.inProgress++;
|
|
31
|
-
else if (item.status === "completed")
|
|
40
|
+
else
|
|
32
41
|
counts.completed++;
|
|
33
42
|
}
|
|
34
43
|
return counts;
|
|
35
44
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
for
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
183
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
184
|
+
? value
|
|
185
|
+
: undefined;
|
|
122
186
|
}
|
|
123
187
|
catch {
|
|
124
|
-
return
|
|
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
|
-
|
|
191
|
+
/** Field read that never throws, even for exotic objects with throwing getters. */
|
|
192
|
+
function safeValue(record, key) {
|
|
140
193
|
try {
|
|
141
|
-
return
|
|
194
|
+
return record?.[key];
|
|
142
195
|
}
|
|
143
196
|
catch {
|
|
144
197
|
return undefined;
|
|
145
198
|
}
|
|
146
199
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
|
235
|
+
return undefined;
|
|
160
236
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
if (text)
|
|
171
|
-
strings.push(text);
|
|
253
|
+
ids.push(id);
|
|
172
254
|
}
|
|
173
|
-
return
|
|
255
|
+
return ids;
|
|
174
256
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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 =
|
|
307
|
+
const ids = readIds(safeValue(details.change, "ids"), true);
|
|
265
308
|
return ids?.length === count ? ids : undefined;
|
|
266
309
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
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
|
|
279
|
-
|
|
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
|
-
|
|
332
|
+
details.push(` ${theme.fg("dim", descriptionPreview(item.description))}`);
|
|
287
333
|
}
|
|
288
334
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
push(
|
|
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
|
-
|
|
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 =
|
|
324
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
351
|
-
|
|
388
|
+
function stateSubject(map, id) {
|
|
389
|
+
const item = map.get(id);
|
|
390
|
+
return item ? safeSubject(safeValue(item, "subject")) : "";
|
|
352
391
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
|
386
|
-
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
|
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
|