pi-long-task 0.3.5 → 0.3.7
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/package.json +1 -1
- package/src/index.ts +331 -118
- package/src/render.ts +121 -23
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
3
|
-
import { truncateToWidth, type Component, type
|
|
3
|
+
import { truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
4
4
|
|
|
5
5
|
import { runCoordinator, type CoordinatorProgressUpdate, type CoordinatorResult } from "./coordinator.ts";
|
|
6
6
|
import { longTaskInputTransform } from "./input_router.ts";
|
|
@@ -76,6 +76,9 @@ function toolDetails(result: CoordinatorResult) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const LONG_TASK_WIDGET_KEY = "pi-long-task-sidebar";
|
|
79
|
+
const SIDEBAR_WIDGET_RESERVED_INPUT_ROWS = 8;
|
|
80
|
+
const SIDEBAR_WIDGET_MIN_ROWS = 4;
|
|
81
|
+
const SIDEBAR_WIDGET_MAX_ROWS = 24;
|
|
79
82
|
|
|
80
83
|
type UiContext = ExtensionContext;
|
|
81
84
|
|
|
@@ -86,10 +89,12 @@ export interface LongTaskSidebarController {
|
|
|
86
89
|
|
|
87
90
|
class PiLongTaskSidebarComponent implements Component {
|
|
88
91
|
private readonly theme: Theme;
|
|
92
|
+
private readonly maxRows: (() => number | undefined) | undefined;
|
|
89
93
|
private update: CoordinatorProgressUpdate | undefined;
|
|
90
94
|
|
|
91
|
-
constructor(theme: Theme) {
|
|
95
|
+
constructor(theme: Theme, maxRows?: () => number | undefined) {
|
|
92
96
|
this.theme = theme;
|
|
97
|
+
this.maxRows = maxRows;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
setUpdate(update: CoordinatorProgressUpdate): void {
|
|
@@ -98,7 +103,8 @@ class PiLongTaskSidebarComponent implements Component {
|
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
render(width: number): string[] {
|
|
101
|
-
|
|
106
|
+
const lines = renderSidebarOverlayLines(this.update, this.theme, width);
|
|
107
|
+
return limitSidebarPanelLines(lines, this.theme, width, this.maxRows?.());
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
invalidate(): void {
|
|
@@ -112,48 +118,25 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
|
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
let latestUpdate: CoordinatorProgressUpdate | undefined;
|
|
115
|
-
let
|
|
116
|
-
let
|
|
117
|
-
let overlayDone: ((result: undefined) => void) | undefined;
|
|
118
|
-
let overlayHandle: OverlayHandle | undefined;
|
|
121
|
+
let sidebarComponent: PiLongTaskSidebarComponent | undefined;
|
|
122
|
+
let sidebarTui: TUI | undefined;
|
|
119
123
|
let closed = false;
|
|
120
124
|
|
|
121
|
-
ctx
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
overlayDone = done;
|
|
128
|
-
overlayComponent = new PiLongTaskSidebarComponent(theme);
|
|
125
|
+
if (supportsTuiWidget(ctx)) {
|
|
126
|
+
ctx.ui.setWidget(
|
|
127
|
+
LONG_TASK_WIDGET_KEY,
|
|
128
|
+
(tui, theme) => {
|
|
129
|
+
sidebarTui = tui;
|
|
130
|
+
sidebarComponent = new PiLongTaskSidebarComponent(theme, () => sidebarWidgetLineLimit(tui));
|
|
129
131
|
if (latestUpdate) {
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
if (closed) {
|
|
133
|
-
done(undefined);
|
|
132
|
+
sidebarComponent.setUpdate(latestUpdate);
|
|
134
133
|
}
|
|
135
|
-
return
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
overlay: true,
|
|
139
|
-
overlayOptions: {
|
|
140
|
-
anchor: "right-center",
|
|
141
|
-
width: "34%",
|
|
142
|
-
minWidth: 32,
|
|
143
|
-
maxHeight: "85%",
|
|
144
|
-
margin: 1,
|
|
145
|
-
nonCapturing: true,
|
|
146
|
-
visible: (termWidth, termHeight) => termWidth >= 96 && termHeight >= 16,
|
|
147
|
-
},
|
|
148
|
-
onHandle: (handle) => {
|
|
149
|
-
overlayHandle = handle;
|
|
150
|
-
handle.unfocus();
|
|
151
|
-
},
|
|
134
|
+
return sidebarComponent;
|
|
152
135
|
},
|
|
136
|
+
{ placement: "aboveEditor" },
|
|
153
137
|
);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
138
|
+
} else {
|
|
139
|
+
ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, ["Pi Long Task: preparing sidebar..."], { placement: "aboveEditor" });
|
|
157
140
|
}
|
|
158
141
|
|
|
159
142
|
return {
|
|
@@ -162,9 +145,11 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
|
|
|
162
145
|
return;
|
|
163
146
|
}
|
|
164
147
|
latestUpdate = update;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
148
|
+
sidebarComponent?.setUpdate(update);
|
|
149
|
+
sidebarTui?.requestRender();
|
|
150
|
+
if (!sidebarComponent) {
|
|
151
|
+
ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, renderSidebarWidgetLines(update), { placement: "aboveEditor" });
|
|
152
|
+
}
|
|
168
153
|
},
|
|
169
154
|
close(): void {
|
|
170
155
|
if (closed) {
|
|
@@ -172,45 +157,48 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
|
|
|
172
157
|
}
|
|
173
158
|
closed = true;
|
|
174
159
|
ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, undefined);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} else {
|
|
178
|
-
overlayHandle?.hide();
|
|
179
|
-
}
|
|
180
|
-
overlayComponent = undefined;
|
|
181
|
-
overlayTui = undefined;
|
|
182
|
-
overlayDone = undefined;
|
|
183
|
-
overlayHandle = undefined;
|
|
160
|
+
sidebarComponent = undefined;
|
|
161
|
+
sidebarTui = undefined;
|
|
184
162
|
},
|
|
185
163
|
};
|
|
186
164
|
}
|
|
187
165
|
|
|
188
|
-
function
|
|
166
|
+
function supportsTuiWidget(ctx: UiContext): boolean {
|
|
189
167
|
const mode = (ctx as UiContext & { mode?: string }).mode;
|
|
190
168
|
return mode === "tui" || mode === undefined;
|
|
191
169
|
}
|
|
192
170
|
|
|
171
|
+
function sidebarWidgetLineLimit(tui: TUI): number {
|
|
172
|
+
const terminalRows = tui.terminal.rows;
|
|
173
|
+
const rows = Number.isFinite(terminalRows) ? Math.max(0, Math.floor(terminalRows)) : 24;
|
|
174
|
+
const availableRows = rows - SIDEBAR_WIDGET_RESERVED_INPUT_ROWS;
|
|
175
|
+
return Math.max(SIDEBAR_WIDGET_MIN_ROWS, Math.min(SIDEBAR_WIDGET_MAX_ROWS, availableRows));
|
|
176
|
+
}
|
|
177
|
+
|
|
193
178
|
function renderSidebarWidgetLines(update: CoordinatorProgressUpdate): string[] {
|
|
194
179
|
const progress = update.taskProgress;
|
|
195
180
|
const summary = progress?.summary;
|
|
196
|
-
const
|
|
197
|
-
const lines = [
|
|
181
|
+
const statusDetails = sidebarUpdateStateDetails(update);
|
|
182
|
+
const lines = ["Pi Long Task", `${statusDetails.icon} ${statusDetails.label} · ${update.message}`];
|
|
198
183
|
if (summary) {
|
|
199
184
|
lines.push(
|
|
200
|
-
`Tasks: ${summary.completedTasks}/${summary.totalTasks}
|
|
201
|
-
(summary.
|
|
202
|
-
(summary.
|
|
185
|
+
`Tasks: ${summary.completedTasks}/${summary.totalTasks} · ${summary.completedPercent}%` +
|
|
186
|
+
(summary.currentTasks ? ` · ${summary.currentTasks} active` : "") +
|
|
187
|
+
(summary.pendingTasks ? ` · ${summary.pendingTasks} queued` : "") +
|
|
188
|
+
(summary.failedTasks ? ` · ${summary.failedTasks} failed` : "") +
|
|
189
|
+
(summary.blockedTasks ? ` · ${summary.blockedTasks} blocked` : ""),
|
|
203
190
|
);
|
|
204
191
|
}
|
|
205
192
|
if (progress && progress.tasks.length > 0) {
|
|
206
193
|
const currentIndex = focusedTaskIndex(progress);
|
|
207
194
|
const currentTask = currentIndex >= 0 ? progress.tasks[currentIndex] : undefined;
|
|
208
195
|
if (currentTask) {
|
|
209
|
-
|
|
196
|
+
const details = taskStatusDetails(currentTask.status);
|
|
197
|
+
lines.push(`${details.label}: ${details.icon} TODO ${currentTask.taskId} — ${currentTask.title}`);
|
|
210
198
|
}
|
|
211
199
|
}
|
|
212
200
|
if (update.workerCostTotal > 0) {
|
|
213
|
-
lines.push(`
|
|
201
|
+
lines.push(`Spent: ${formatCost(update.workerCostTotal)}`);
|
|
214
202
|
}
|
|
215
203
|
return lines.map((line) => truncateToWidth(line, 96));
|
|
216
204
|
}
|
|
@@ -220,45 +208,100 @@ function renderSidebarOverlayLines(
|
|
|
220
208
|
theme: Theme,
|
|
221
209
|
width: number,
|
|
222
210
|
): string[] {
|
|
223
|
-
const safeWidth = Math.max(
|
|
224
|
-
const
|
|
225
|
-
const rows = renderSidebarRows(update, theme);
|
|
226
|
-
return
|
|
227
|
-
sidebarBorder("Pi Long Task", safeWidth, theme),
|
|
228
|
-
...rows.map((row) => sidebarRow(row, innerWidth, theme)),
|
|
229
|
-
theme.fg("borderMuted", `└${"─".repeat(innerWidth)}┘`),
|
|
230
|
-
];
|
|
211
|
+
const safeWidth = Math.max(28, width);
|
|
212
|
+
const contentWidth = Math.max(8, safeWidth - 4);
|
|
213
|
+
const rows = renderSidebarRows(update, theme, contentWidth);
|
|
214
|
+
return rows.map((row) => sidebarPanelRow(row, contentWidth, theme));
|
|
231
215
|
}
|
|
232
216
|
|
|
233
|
-
function
|
|
217
|
+
function limitSidebarPanelLines(lines: string[], theme: Theme, width: number, maxRows: number | undefined): string[] {
|
|
218
|
+
if (maxRows === undefined || !Number.isFinite(maxRows)) {
|
|
219
|
+
return lines;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const limit = Math.max(0, Math.floor(maxRows));
|
|
223
|
+
if (lines.length <= limit) {
|
|
224
|
+
return lines;
|
|
225
|
+
}
|
|
226
|
+
if (limit === 0) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const contentWidth = Math.max(8, Math.max(28, width) - 4);
|
|
231
|
+
if (limit === 1) {
|
|
232
|
+
return [sidebarPanelRow(theme.fg("dim", "…"), contentWidth, theme)];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const visibleLines = lines.slice(0, limit - 1);
|
|
236
|
+
const omitted = lines.length - visibleLines.length;
|
|
237
|
+
visibleLines.push(sidebarPanelRow(theme.fg("dim", `… ${omitted} more`), contentWidth, theme));
|
|
238
|
+
return visibleLines;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme: Theme, width: number): string[] {
|
|
234
242
|
if (!update) {
|
|
235
|
-
return [theme.fg("muted", "Preparing long-task sidebar...")];
|
|
243
|
+
return ["", sidebarHeading("Pi Long Task", theme), "", theme.fg("muted", "Preparing long-task sidebar...")];
|
|
236
244
|
}
|
|
237
245
|
|
|
238
246
|
const progress = update.taskProgress;
|
|
239
|
-
const rows = [
|
|
240
|
-
|
|
241
|
-
rows.push(
|
|
247
|
+
const rows = [""];
|
|
248
|
+
for (const line of wrapPlainText(sidebarHeadline(update, progress), width, 2)) {
|
|
249
|
+
rows.push(sidebarHeading(line, theme));
|
|
250
|
+
}
|
|
251
|
+
rows.push(renderSidebarStateLine(update, theme));
|
|
252
|
+
|
|
253
|
+
const message = normalizeMessageForSidebar(update.message, update);
|
|
254
|
+
if (message) {
|
|
255
|
+
rows.push(...wrapPlainText(message, width, 2).map((line) => theme.fg("dim", line)));
|
|
242
256
|
}
|
|
243
|
-
rows.push("", theme.fg("muted", truncateToWidth(update.message, 72)));
|
|
244
257
|
|
|
245
258
|
if (!progress || progress.tasks.length === 0) {
|
|
246
|
-
rows.push("", theme.fg("muted", "Waiting for TODO plan
|
|
259
|
+
rows.push("", sidebarHeading("Context", theme), theme.fg("muted", "Waiting for TODO plan"));
|
|
260
|
+
if (update.workerCostTotal > 0) {
|
|
261
|
+
rows.push(theme.fg("muted", `${formatCost(update.workerCostTotal)} spent`));
|
|
262
|
+
}
|
|
247
263
|
return rows;
|
|
248
264
|
}
|
|
249
265
|
|
|
250
266
|
const summary = progress.summary;
|
|
251
|
-
rows.push(
|
|
252
|
-
|
|
267
|
+
rows.push(
|
|
268
|
+
"",
|
|
269
|
+
sidebarHeading("Context", theme),
|
|
270
|
+
theme.fg(
|
|
271
|
+
"muted",
|
|
272
|
+
`${summary.completedTasks.toLocaleString()}/${summary.totalTasks.toLocaleString()} tasks complete`,
|
|
273
|
+
),
|
|
274
|
+
theme.fg("muted", `${summary.completedPercent}% complete`),
|
|
275
|
+
);
|
|
276
|
+
if (update.workerCostTotal > 0) {
|
|
277
|
+
rows.push(theme.fg("muted", `${formatCost(update.workerCostTotal)} spent`));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
rows.push(
|
|
281
|
+
"",
|
|
282
|
+
sidebarHeading("Progress", theme),
|
|
283
|
+
progressBarLine(progress, theme),
|
|
284
|
+
progressCountsLine(summary, theme),
|
|
285
|
+
progressStateLegend(progress, theme),
|
|
286
|
+
);
|
|
253
287
|
|
|
254
288
|
const currentIndex = focusedTaskIndex(progress);
|
|
255
|
-
|
|
256
|
-
|
|
289
|
+
const currentTask = currentIndex >= 0 ? progress.tasks[currentIndex] : undefined;
|
|
290
|
+
rows.push("", sidebarHeading("Current", theme));
|
|
291
|
+
if (currentTask) {
|
|
292
|
+
const details = taskStatusDetails(currentTask.status);
|
|
293
|
+
rows.push(
|
|
294
|
+
theme.fg(
|
|
295
|
+
details.color,
|
|
296
|
+
`${details.icon} TODO ${currentTask.taskId} ${theme.fg("dim", "·")} ${details.label}${currentTaskMeta(currentTask, theme)}`,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
rows.push(...wrapPlainText(currentTask.title, width, 2).map((line) => theme.fg("muted", line)));
|
|
257
300
|
} else {
|
|
258
301
|
rows.push(theme.fg("success", "No active task"));
|
|
259
302
|
}
|
|
260
303
|
|
|
261
|
-
rows.push("",
|
|
304
|
+
rows.push("", sidebarHeading("Task timeline", theme));
|
|
262
305
|
const taskIndexes = centeredTaskIndexes(progress.tasks.length, currentIndex, 9);
|
|
263
306
|
const first = taskIndexes[0] ?? 0;
|
|
264
307
|
const last = taskIndexes[taskIndexes.length - 1] ?? -1;
|
|
@@ -278,7 +321,7 @@ function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme:
|
|
|
278
321
|
|
|
279
322
|
const subtasks = update.subtasks ?? [];
|
|
280
323
|
if (subtasks.length > 0) {
|
|
281
|
-
rows.push("",
|
|
324
|
+
rows.push("", sidebarHeading("Current status", theme));
|
|
282
325
|
for (const subtask of subtasks.slice(0, 6)) {
|
|
283
326
|
rows.push(renderSubtaskRow(subtask, theme));
|
|
284
327
|
}
|
|
@@ -316,61 +359,181 @@ function renderTaskRow(
|
|
|
316
359
|
theme: Theme,
|
|
317
360
|
): string {
|
|
318
361
|
const details = taskStatusDetails(task.status);
|
|
319
|
-
const attempts = task.attempts > 0 && task.status !== "completed" ? ` · ${task.attempts}x` : "";
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
|
|
362
|
+
const attempts = task.attempts > 0 && task.status !== "completed" ? ` ${theme.fg("dim", "·")} ${task.attempts}x` : "";
|
|
363
|
+
const title = truncateToWidth(task.title, 72);
|
|
364
|
+
const focusMarker = focused ? theme.fg("accent", "›") : theme.fg("dim", " ");
|
|
365
|
+
const icon = theme.fg(details.color, details.icon);
|
|
366
|
+
const label = `TODO ${task.taskId}`;
|
|
367
|
+
const row = `${label} ${theme.fg("dim", "·")} ${details.label} ${theme.fg("dim", "·")} ${title}${attempts}`;
|
|
368
|
+
const styledRow = focused ? theme.fg(details.color, theme.bold(row)) : theme.fg(details.textColor, row);
|
|
369
|
+
return `${focusMarker} ${icon} ${styledRow}`;
|
|
323
370
|
}
|
|
324
371
|
|
|
325
372
|
function renderSubtaskRow(subtask: NonNullable<CoordinatorProgressUpdate["subtasks"]>[number], theme: Theme): string {
|
|
326
373
|
const details = progressItemStatusDetails(subtask.status);
|
|
327
|
-
return theme.fg(details.color, `${details.
|
|
374
|
+
return `${theme.fg(details.color, details.icon)} ${theme.fg(details.textColor, `${details.label} · ${subtask.text}`)}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function renderSidebarStateLine(update: CoordinatorProgressUpdate, theme: Theme): string {
|
|
378
|
+
const details = sidebarUpdateStateDetails(update);
|
|
379
|
+
const suffix = [
|
|
380
|
+
update.attempt && update.attempt > 1 ? `attempt ${update.attempt}` : undefined,
|
|
381
|
+
update.workerCostTotal > 0 ? `${formatCost(update.workerCostTotal)} spent` : undefined,
|
|
382
|
+
]
|
|
383
|
+
.filter(Boolean)
|
|
384
|
+
.join(` ${theme.fg("dim", "·")} `);
|
|
385
|
+
const meta = suffix ? ` ${theme.fg("dim", "·")} ${theme.fg("muted", suffix)}` : "";
|
|
386
|
+
return `${theme.fg(details.color, details.icon)} ${theme.fg(details.color, details.label)}${meta}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function currentTaskMeta(
|
|
390
|
+
task: NonNullable<CoordinatorProgressUpdate["taskProgress"]>["tasks"][number],
|
|
391
|
+
theme: Theme,
|
|
392
|
+
): string {
|
|
393
|
+
const attempts = task.attempts > 0 && task.status !== "completed" ? [`attempt ${task.attempts}`] : [];
|
|
394
|
+
if (task.lastReportedStatus && task.status !== "current") {
|
|
395
|
+
attempts.push(task.lastReportedStatus);
|
|
396
|
+
}
|
|
397
|
+
return attempts.length > 0 ? ` ${theme.fg("dim", "·")} ${theme.fg("muted", attempts.join(" · "))}` : "";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function progressMeterLine(progress: NonNullable<CoordinatorProgressUpdate["taskProgress"]>, theme: Theme): string {
|
|
401
|
+
const total = progress.tasks.length;
|
|
402
|
+
if (total === 0) {
|
|
403
|
+
return theme.fg("dim", "────────");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const maxSegments = 12;
|
|
407
|
+
const step = Math.max(1, Math.ceil(total / maxSegments));
|
|
408
|
+
const segments: string[] = [];
|
|
409
|
+
for (let index = 0; index < total; index += step) {
|
|
410
|
+
const slice = progress.tasks.slice(index, Math.min(total, index + step));
|
|
411
|
+
segments.push(progressMeterSegment(slice, theme));
|
|
412
|
+
}
|
|
413
|
+
return segments.join("");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function progressMeterSegment(
|
|
417
|
+
tasks: Array<NonNullable<CoordinatorProgressUpdate["taskProgress"]>["tasks"][number]>,
|
|
418
|
+
theme: Theme,
|
|
419
|
+
): string {
|
|
420
|
+
if (tasks.some((task) => task.status === "failed")) {
|
|
421
|
+
return theme.fg("error", "×");
|
|
422
|
+
}
|
|
423
|
+
if (tasks.some((task) => task.status === "blocked")) {
|
|
424
|
+
return theme.fg("warning", "!");
|
|
425
|
+
}
|
|
426
|
+
if (tasks.some((task) => task.status === "current")) {
|
|
427
|
+
return theme.fg("accent", "▢");
|
|
428
|
+
}
|
|
429
|
+
if (tasks.every((task) => task.status === "completed")) {
|
|
430
|
+
return theme.fg("success", "■");
|
|
431
|
+
}
|
|
432
|
+
if (tasks.some((task) => task.status === "completed")) {
|
|
433
|
+
return theme.fg("success", "▪");
|
|
434
|
+
}
|
|
435
|
+
return theme.fg("dim", "·");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function progressStateLegend(progress: NonNullable<CoordinatorProgressUpdate["taskProgress"]>, theme: Theme): string {
|
|
439
|
+
const statuses = new Set(progress.tasks.map((task) => task.status));
|
|
440
|
+
const items = [
|
|
441
|
+
statuses.has("completed") ? theme.fg("success", "■ done") : undefined,
|
|
442
|
+
statuses.has("current") ? theme.fg("accent", "▢ active") : undefined,
|
|
443
|
+
statuses.has("pending") ? theme.fg("dim", "· queued") : undefined,
|
|
444
|
+
statuses.has("failed") ? theme.fg("error", "× failed") : undefined,
|
|
445
|
+
statuses.has("blocked") ? theme.fg("warning", "! blocked") : undefined,
|
|
446
|
+
].filter(Boolean);
|
|
447
|
+
return theme.fg("muted", items.join(" · "));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function sidebarUpdateStateDetails(update: CoordinatorProgressUpdate): {
|
|
451
|
+
icon: string;
|
|
452
|
+
label: string;
|
|
453
|
+
color: "accent" | "success" | "warning" | "error" | "muted";
|
|
454
|
+
} {
|
|
455
|
+
if (update.status) {
|
|
456
|
+
switch (update.status) {
|
|
457
|
+
case "done":
|
|
458
|
+
return { icon: "✓", label: "done", color: "success" };
|
|
459
|
+
case "failed":
|
|
460
|
+
return { icon: "×", label: "failed", color: "error" };
|
|
461
|
+
case "blocked":
|
|
462
|
+
return { icon: "!", label: "blocked", color: "warning" };
|
|
463
|
+
case "partial":
|
|
464
|
+
return { icon: "!", label: "partial", color: "warning" };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
switch (update.phase) {
|
|
469
|
+
case "planning":
|
|
470
|
+
return { icon: "+", label: "Planning", color: "warning" };
|
|
471
|
+
case "planned":
|
|
472
|
+
return { icon: "✓", label: "Plan ready", color: "success" };
|
|
473
|
+
case "task_start":
|
|
474
|
+
return { icon: "▢", label: "Running task", color: "accent" };
|
|
475
|
+
case "worker_tool":
|
|
476
|
+
return { icon: "+", label: "Worker tool", color: "warning" };
|
|
477
|
+
case "task_done":
|
|
478
|
+
return { icon: "✓", label: "Task complete", color: "success" };
|
|
479
|
+
case "task_blocked":
|
|
480
|
+
return { icon: "!", label: "Task blocked", color: "warning" };
|
|
481
|
+
case "task_failed":
|
|
482
|
+
return { icon: "×", label: "Task failed", color: "error" };
|
|
483
|
+
case "complete":
|
|
484
|
+
return { icon: "✓", label: "Complete", color: "success" };
|
|
485
|
+
}
|
|
328
486
|
}
|
|
329
487
|
|
|
330
488
|
function taskStatusDetails(status: NonNullable<CoordinatorProgressUpdate["taskProgress"]>["tasks"][number]["status"]): {
|
|
331
489
|
icon: string;
|
|
332
|
-
|
|
490
|
+
label: string;
|
|
491
|
+
color: "accent" | "success" | "warning" | "error" | "dim" | "muted";
|
|
492
|
+
textColor: "accent" | "text" | "success" | "warning" | "error" | "dim" | "muted";
|
|
333
493
|
} {
|
|
334
494
|
switch (status) {
|
|
335
495
|
case "completed":
|
|
336
|
-
return { icon: "✓", color: "success" };
|
|
496
|
+
return { icon: "✓", label: "done", color: "success", textColor: "muted" };
|
|
337
497
|
case "current":
|
|
338
|
-
return { icon: "
|
|
498
|
+
return { icon: "▢", label: "active", color: "accent", textColor: "text" };
|
|
339
499
|
case "failed":
|
|
340
|
-
return { icon: "
|
|
500
|
+
return { icon: "×", label: "failed", color: "error", textColor: "error" };
|
|
341
501
|
case "blocked":
|
|
342
|
-
return { icon: "!", color: "warning" };
|
|
502
|
+
return { icon: "!", label: "blocked", color: "warning", textColor: "warning" };
|
|
343
503
|
case "pending":
|
|
344
|
-
return { icon: "○", color: "dim" };
|
|
504
|
+
return { icon: "○", label: "queued", color: "dim", textColor: "dim" };
|
|
345
505
|
}
|
|
346
506
|
}
|
|
347
507
|
|
|
348
508
|
function progressItemStatusDetails(status: NonNullable<CoordinatorProgressUpdate["subtasks"]>[number]["status"]): {
|
|
349
509
|
icon: string;
|
|
510
|
+
label: string;
|
|
350
511
|
color: "success" | "warning" | "error" | "dim" | "muted";
|
|
512
|
+
textColor: "success" | "warning" | "error" | "dim" | "muted";
|
|
351
513
|
} {
|
|
352
514
|
switch (status) {
|
|
353
515
|
case "done":
|
|
354
|
-
return { icon: "✓", color: "success" };
|
|
516
|
+
return { icon: "✓", label: "done", color: "success", textColor: "muted" };
|
|
355
517
|
case "in_progress":
|
|
356
|
-
return { icon: "
|
|
518
|
+
return { icon: "+", label: "active", color: "warning", textColor: "warning" };
|
|
357
519
|
case "failed":
|
|
358
|
-
return { icon: "
|
|
520
|
+
return { icon: "×", label: "failed", color: "error", textColor: "error" };
|
|
359
521
|
case "blocked":
|
|
360
|
-
return { icon: "!", color: "warning" };
|
|
522
|
+
return { icon: "!", label: "blocked", color: "warning", textColor: "warning" };
|
|
361
523
|
case "empty":
|
|
362
|
-
return { icon: "○", color: "dim" };
|
|
524
|
+
return { icon: "○", label: "queued", color: "dim", textColor: "dim" };
|
|
363
525
|
}
|
|
364
526
|
}
|
|
365
527
|
|
|
366
|
-
function progressBarLine(
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
528
|
+
function progressBarLine(progress: NonNullable<CoordinatorProgressUpdate["taskProgress"]>, theme: Theme): string {
|
|
529
|
+
const summary = progress.summary;
|
|
530
|
+
return `${theme.fg("muted", "Tasks")} ${theme.fg(
|
|
531
|
+
"success",
|
|
532
|
+
`${summary.completedTasks}/${summary.totalTasks}`,
|
|
533
|
+
)} ${theme.fg("dim", "·")} ${theme.fg("muted", `${summary.completedPercent}% complete`)} ${theme.fg(
|
|
371
534
|
"dim",
|
|
372
|
-
"
|
|
373
|
-
)}
|
|
535
|
+
"·",
|
|
536
|
+
)} ${progressMeterLine(progress, theme)}`;
|
|
374
537
|
}
|
|
375
538
|
|
|
376
539
|
function progressCountsLine(
|
|
@@ -378,30 +541,80 @@ function progressCountsLine(
|
|
|
378
541
|
theme: Theme,
|
|
379
542
|
): string {
|
|
380
543
|
return [
|
|
381
|
-
theme.fg("success", `✓ ${summary.completedTasks}`),
|
|
382
|
-
summary.currentTasks ? theme.fg("
|
|
383
|
-
summary.pendingTasks ? theme.fg("dim", `○ ${summary.pendingTasks}`) : undefined,
|
|
384
|
-
summary.failedTasks ? theme.fg("error",
|
|
385
|
-
summary.blockedTasks ? theme.fg("warning", `! ${summary.blockedTasks}`) : undefined,
|
|
544
|
+
theme.fg("success", `✓ ${summary.completedTasks} done`),
|
|
545
|
+
summary.currentTasks ? theme.fg("accent", `▢ ${summary.currentTasks} active`) : undefined,
|
|
546
|
+
summary.pendingTasks ? theme.fg("dim", `○ ${summary.pendingTasks} queued`) : undefined,
|
|
547
|
+
summary.failedTasks ? theme.fg("error", `× ${summary.failedTasks} failed`) : undefined,
|
|
548
|
+
summary.blockedTasks ? theme.fg("warning", `! ${summary.blockedTasks} blocked`) : undefined,
|
|
386
549
|
]
|
|
387
550
|
.filter(Boolean)
|
|
388
551
|
.join(" · ");
|
|
389
552
|
}
|
|
390
553
|
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
554
|
+
function sidebarHeading(text: string, theme: Theme): string {
|
|
555
|
+
return theme.fg("toolTitle", theme.bold(text));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function sidebarHeadline(
|
|
559
|
+
update: CoordinatorProgressUpdate,
|
|
560
|
+
progress: CoordinatorProgressUpdate["taskProgress"],
|
|
561
|
+
): string {
|
|
562
|
+
if (update.taskId && update.title) {
|
|
563
|
+
return `TODO ${update.taskId} — ${update.title}`;
|
|
564
|
+
}
|
|
565
|
+
const currentTask = progress?.currentTask ?? progress?.nextTask;
|
|
566
|
+
if (currentTask) {
|
|
567
|
+
return `TODO ${currentTask.taskId} — ${currentTask.title}`;
|
|
568
|
+
}
|
|
569
|
+
return "Pi Long Task";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function normalizeMessageForSidebar(updateMessage: string, update: CoordinatorProgressUpdate): string | undefined {
|
|
573
|
+
const message = updateMessage.trim();
|
|
574
|
+
if (!message) {
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
const title = update.taskId && update.title ? `TODO ${update.taskId} — ${update.title}` : undefined;
|
|
578
|
+
return title && message.includes(title) && message.length <= title.length + 16 ? undefined : message;
|
|
397
579
|
}
|
|
398
580
|
|
|
399
|
-
function
|
|
400
|
-
|
|
581
|
+
function wrapPlainText(text: string, width: number, limit?: number): string[] {
|
|
582
|
+
const safeWidth = Math.max(8, width);
|
|
583
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
584
|
+
if (words.length === 0) {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const lines: string[] = [];
|
|
589
|
+
let line = "";
|
|
590
|
+
for (const word of words) {
|
|
591
|
+
const next = line ? `${line} ${word}` : word;
|
|
592
|
+
if (next.length <= safeWidth) {
|
|
593
|
+
line = next;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (line) {
|
|
597
|
+
lines.push(line);
|
|
598
|
+
}
|
|
599
|
+
line = word.length > safeWidth ? truncateToWidth(word, safeWidth) : word;
|
|
600
|
+
if (limit && lines.length >= limit) {
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (line && (!limit || lines.length < limit)) {
|
|
605
|
+
lines.push(line);
|
|
606
|
+
}
|
|
607
|
+
if (limit && lines.length > limit) {
|
|
608
|
+
lines.length = limit;
|
|
609
|
+
}
|
|
610
|
+
if (limit && words.join(" ").length > lines.join(" ").length && lines.length > 0) {
|
|
611
|
+
lines[lines.length - 1] = truncateToWidth(`${lines[lines.length - 1]} …`, safeWidth);
|
|
612
|
+
}
|
|
613
|
+
return lines;
|
|
401
614
|
}
|
|
402
615
|
|
|
403
|
-
function
|
|
404
|
-
return
|
|
616
|
+
function sidebarPanelRow(text: string, width: number, theme: Theme): string {
|
|
617
|
+
return `${theme.fg("borderMuted", "│")} ${truncateToWidth(text, width, "…", true)}`;
|
|
405
618
|
}
|
|
406
619
|
|
|
407
620
|
function formatCost(value: number): string {
|
package/src/render.ts
CHANGED
|
@@ -111,22 +111,36 @@ function renderLongTaskProgress(details: Record<string, unknown> | undefined, fa
|
|
|
111
111
|
const toolName = stringValue(details?.toolName);
|
|
112
112
|
const prefix = phase === "worker_tool" && toolName ? `worker ${toolName}` : phase || "progress";
|
|
113
113
|
const currentTask = progressTaskDetails(details?.currentTask);
|
|
114
|
+
const progress = taskProgressModel(details?.taskProgress);
|
|
115
|
+
|
|
114
116
|
if (!currentTask) {
|
|
115
|
-
return `${theme.fg("
|
|
117
|
+
return `${theme.fg("warning", "+")} ${theme.fg("warning", `${progressPhaseLabel(phase)}:`)} ${message}`;
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
const taskLabel = `TODO ${currentTask.taskId} — ${currentTask.title}`;
|
|
121
|
+
const status = progressItemStatusDetails(currentTask.status);
|
|
122
|
+
const activitySuffix =
|
|
123
|
+
phase === "worker_tool" && toolName ? ` ${theme.fg("dim", "·")} ${theme.fg("muted", `worker ${toolName}`)}` : "";
|
|
124
|
+
const attempt = numberValue(details?.attempt);
|
|
125
|
+
const attemptSuffix =
|
|
126
|
+
attempt && attempt > 1 ? ` ${theme.fg("dim", "·")} ${theme.fg("muted", `attempt ${attempt}`)}` : "";
|
|
119
127
|
const lines = [
|
|
120
|
-
`${
|
|
128
|
+
`${theme.fg("warning", "+")} ${theme.fg("warning", `${progressPhaseLabel(phase)}:`)} ${theme.fg(
|
|
129
|
+
status.textColor,
|
|
130
|
+
taskLabel,
|
|
131
|
+
)}${activitySuffix}${attemptSuffix}`,
|
|
121
132
|
];
|
|
133
|
+
|
|
134
|
+
if (progress) {
|
|
135
|
+
lines.push(renderTaskProgressStrip(progress, theme));
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
if (message && !message.includes(taskLabel)) {
|
|
123
|
-
lines.push(` ${theme.fg("dim", message)}`);
|
|
139
|
+
lines.push(` ${theme.fg("dim", "⚙")} ${theme.fg("dim", `${prefix} · ${message}`)}`);
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
for (const subtask of progressSubtaskDetails(details?.subtasks)) {
|
|
127
|
-
lines.push(
|
|
128
|
-
` ${progressBubble(subtask.status, theme)} ${theme.fg(progressTextColor(subtask.status), subtask.text)}`,
|
|
129
|
-
);
|
|
143
|
+
lines.push(renderProgressSubtaskLine(subtask, theme));
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
return lines.join("\n");
|
|
@@ -215,36 +229,120 @@ function progressSubtaskDetails(value: unknown): ProgressSubtaskRenderDetails[]
|
|
|
215
229
|
});
|
|
216
230
|
}
|
|
217
231
|
|
|
232
|
+
function progressPhaseLabel(phase: string): string {
|
|
233
|
+
switch (phase) {
|
|
234
|
+
case "planning":
|
|
235
|
+
case "planned":
|
|
236
|
+
return "Thought";
|
|
237
|
+
case "task_start":
|
|
238
|
+
case "worker_tool":
|
|
239
|
+
return "Build";
|
|
240
|
+
case "task_done":
|
|
241
|
+
return "Done";
|
|
242
|
+
case "task_failed":
|
|
243
|
+
return "Failed";
|
|
244
|
+
case "task_blocked":
|
|
245
|
+
return "Blocked";
|
|
246
|
+
case "complete":
|
|
247
|
+
return "Complete";
|
|
248
|
+
default:
|
|
249
|
+
return "Progress";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderTaskProgressStrip(progress: TaskProgressModel, theme: Theme): string {
|
|
254
|
+
const summary = progress.summary;
|
|
255
|
+
const track = taskProgressTrack(progress, theme);
|
|
256
|
+
const counts = [
|
|
257
|
+
theme.fg("muted", `${summary.completedTasks}/${summary.totalTasks}`),
|
|
258
|
+
theme.fg("muted", `${summary.completedPercent}%`),
|
|
259
|
+
summary.failedTasks ? theme.fg("error", `${summary.failedTasks} failed`) : undefined,
|
|
260
|
+
summary.blockedTasks ? theme.fg("warning", `${summary.blockedTasks} blocked`) : undefined,
|
|
261
|
+
summary.currentTasks ? theme.fg("warning", `${summary.currentTasks} active`) : undefined,
|
|
262
|
+
summary.pendingTasks ? theme.fg("dim", `${summary.pendingTasks} queued`) : undefined,
|
|
263
|
+
]
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
.join(` ${theme.fg("dim", "·")} `);
|
|
266
|
+
return ` ${track} ${counts}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function taskProgressTrack(progress: TaskProgressModel, theme: Theme): string {
|
|
270
|
+
const taskIndexes = focusedTaskIndexes(progress);
|
|
271
|
+
const first = taskIndexes[0] ?? 0;
|
|
272
|
+
const last = taskIndexes[taskIndexes.length - 1] ?? -1;
|
|
273
|
+
const prefix = first > 0 ? theme.fg("dim", "… ") : "";
|
|
274
|
+
const suffix = last >= 0 && last < progress.tasks.length - 1 ? theme.fg("dim", " …") : "";
|
|
275
|
+
return `${prefix}${taskIndexes.map((index) => taskProgressTaskGlyph(progress.tasks[index], theme)).join(" ")}${suffix}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function focusedTaskIndexes(progress: TaskProgressModel): number[] {
|
|
279
|
+
const total = progress.tasks.length;
|
|
280
|
+
if (total <= 0) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
const limit = Math.min(total, 8);
|
|
284
|
+
const focus = Math.max(0, Math.min(total - 1, progress.currentIndex ?? progress.nextIndex ?? 0));
|
|
285
|
+
const start = Math.max(0, Math.min(total - limit, focus - Math.floor(limit / 2)));
|
|
286
|
+
return Array.from({ length: limit }, (_value, index) => start + index);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function taskProgressTaskGlyph(task: TaskProgressModel["tasks"][number] | undefined, theme: Theme): string {
|
|
290
|
+
if (!task) {
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
const details = taskStatusDetails(task.status);
|
|
294
|
+
return theme.fg(details.color, details.icon);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function renderProgressSubtaskLine(subtask: ProgressSubtaskRenderDetails, theme: Theme): string {
|
|
298
|
+
const details = progressItemStatusDetails(subtask.status);
|
|
299
|
+
return ` ${theme.fg(details.color, details.icon)} ${theme.fg(details.textColor, `${details.label} · ${subtask.text}`)}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
218
302
|
function progressItemStatus(value: unknown): ProgressItemStatus | undefined {
|
|
219
303
|
return value === "empty" || value === "in_progress" || value === "done" || value === "failed" || value === "blocked"
|
|
220
304
|
? value
|
|
221
305
|
: undefined;
|
|
222
306
|
}
|
|
223
307
|
|
|
224
|
-
function
|
|
308
|
+
function taskStatusDetails(status: TaskProgressModel["tasks"][number]["status"]): {
|
|
309
|
+
icon: string;
|
|
310
|
+
label: string;
|
|
311
|
+
color: "accent" | "success" | "warning" | "dim" | "error";
|
|
312
|
+
textColor: "accent" | "text" | "success" | "warning" | "dim" | "error";
|
|
313
|
+
} {
|
|
225
314
|
switch (status) {
|
|
226
|
-
case "
|
|
227
|
-
return
|
|
315
|
+
case "completed":
|
|
316
|
+
return { icon: "✓", label: "done", color: "success", textColor: "dim" };
|
|
317
|
+
case "current":
|
|
318
|
+
return { icon: "▢", label: "active", color: "accent", textColor: "text" };
|
|
228
319
|
case "failed":
|
|
229
|
-
return
|
|
320
|
+
return { icon: "×", label: "failed", color: "error", textColor: "error" };
|
|
230
321
|
case "blocked":
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
return
|
|
322
|
+
return { icon: "!", label: "blocked", color: "warning", textColor: "warning" };
|
|
323
|
+
case "pending":
|
|
324
|
+
return { icon: "○", label: "queued", color: "dim", textColor: "dim" };
|
|
234
325
|
}
|
|
235
326
|
}
|
|
236
327
|
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
328
|
+
function progressItemStatusDetails(status: ProgressItemStatus): {
|
|
329
|
+
icon: string;
|
|
330
|
+
label: string;
|
|
331
|
+
color: "accent" | "success" | "warning" | "dim" | "error";
|
|
332
|
+
textColor: "accent" | "success" | "warning" | "dim" | "error";
|
|
333
|
+
} {
|
|
334
|
+
switch (status) {
|
|
335
|
+
case "done":
|
|
336
|
+
return { icon: "✓", label: "done", color: "success", textColor: "dim" };
|
|
337
|
+
case "in_progress":
|
|
338
|
+
return { icon: "+", label: "active", color: "warning", textColor: "warning" };
|
|
339
|
+
case "failed":
|
|
340
|
+
return { icon: "×", label: "failed", color: "error", textColor: "error" };
|
|
341
|
+
case "blocked":
|
|
342
|
+
return { icon: "!", label: "blocked", color: "warning", textColor: "warning" };
|
|
343
|
+
case "empty":
|
|
344
|
+
return { icon: "○", label: "queued", color: "dim", textColor: "dim" };
|
|
246
345
|
}
|
|
247
|
-
return "dim";
|
|
248
346
|
}
|
|
249
347
|
|
|
250
348
|
function longTaskDetails(details: Record<string, unknown> | undefined): CoordinatorToolRenderDetails | undefined {
|