pi-long-task 0.3.5 → 0.3.6
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 +273 -69
- package/src/render.ts +121 -23
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -139,9 +139,9 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
|
|
|
139
139
|
overlayOptions: {
|
|
140
140
|
anchor: "right-center",
|
|
141
141
|
width: "34%",
|
|
142
|
-
minWidth:
|
|
143
|
-
maxHeight: "
|
|
144
|
-
margin:
|
|
142
|
+
minWidth: 36,
|
|
143
|
+
maxHeight: "100%",
|
|
144
|
+
margin: 0,
|
|
145
145
|
nonCapturing: true,
|
|
146
146
|
visible: (termWidth, termHeight) => termWidth >= 96 && termHeight >= 16,
|
|
147
147
|
},
|
|
@@ -193,24 +193,27 @@ function supportsTuiOverlay(ctx: UiContext): boolean {
|
|
|
193
193
|
function renderSidebarWidgetLines(update: CoordinatorProgressUpdate): string[] {
|
|
194
194
|
const progress = update.taskProgress;
|
|
195
195
|
const summary = progress?.summary;
|
|
196
|
-
const
|
|
197
|
-
const lines = [
|
|
196
|
+
const statusDetails = sidebarUpdateStateDetails(update);
|
|
197
|
+
const lines = ["Pi Long Task", `${statusDetails.icon} ${statusDetails.label} · ${update.message}`];
|
|
198
198
|
if (summary) {
|
|
199
199
|
lines.push(
|
|
200
|
-
`Tasks: ${summary.completedTasks}/${summary.totalTasks}
|
|
201
|
-
(summary.
|
|
202
|
-
(summary.
|
|
200
|
+
`Tasks: ${summary.completedTasks}/${summary.totalTasks} · ${summary.completedPercent}%` +
|
|
201
|
+
(summary.currentTasks ? ` · ${summary.currentTasks} active` : "") +
|
|
202
|
+
(summary.pendingTasks ? ` · ${summary.pendingTasks} queued` : "") +
|
|
203
|
+
(summary.failedTasks ? ` · ${summary.failedTasks} failed` : "") +
|
|
204
|
+
(summary.blockedTasks ? ` · ${summary.blockedTasks} blocked` : ""),
|
|
203
205
|
);
|
|
204
206
|
}
|
|
205
207
|
if (progress && progress.tasks.length > 0) {
|
|
206
208
|
const currentIndex = focusedTaskIndex(progress);
|
|
207
209
|
const currentTask = currentIndex >= 0 ? progress.tasks[currentIndex] : undefined;
|
|
208
210
|
if (currentTask) {
|
|
209
|
-
|
|
211
|
+
const details = taskStatusDetails(currentTask.status);
|
|
212
|
+
lines.push(`${details.label}: ${details.icon} TODO ${currentTask.taskId} — ${currentTask.title}`);
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
if (update.workerCostTotal > 0) {
|
|
213
|
-
lines.push(`
|
|
216
|
+
lines.push(`Spent: ${formatCost(update.workerCostTotal)}`);
|
|
214
217
|
}
|
|
215
218
|
return lines.map((line) => truncateToWidth(line, 96));
|
|
216
219
|
}
|
|
@@ -220,45 +223,76 @@ function renderSidebarOverlayLines(
|
|
|
220
223
|
theme: Theme,
|
|
221
224
|
width: number,
|
|
222
225
|
): 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
|
-
];
|
|
226
|
+
const safeWidth = Math.max(28, width);
|
|
227
|
+
const contentWidth = Math.max(8, safeWidth - 4);
|
|
228
|
+
const rows = renderSidebarRows(update, theme, contentWidth);
|
|
229
|
+
return rows.map((row) => sidebarPanelRow(row, contentWidth, theme));
|
|
231
230
|
}
|
|
232
231
|
|
|
233
|
-
function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme: Theme): string[] {
|
|
232
|
+
function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme: Theme, width: number): string[] {
|
|
234
233
|
if (!update) {
|
|
235
|
-
return [theme.fg("muted", "Preparing long-task sidebar...")];
|
|
234
|
+
return ["", sidebarHeading("Pi Long Task", theme), "", theme.fg("muted", "Preparing long-task sidebar...")];
|
|
236
235
|
}
|
|
237
236
|
|
|
238
237
|
const progress = update.taskProgress;
|
|
239
|
-
const rows = [
|
|
240
|
-
|
|
241
|
-
rows.push(
|
|
238
|
+
const rows = [""];
|
|
239
|
+
for (const line of wrapPlainText(sidebarHeadline(update, progress), width, 2)) {
|
|
240
|
+
rows.push(sidebarHeading(line, theme));
|
|
241
|
+
}
|
|
242
|
+
rows.push(renderSidebarStateLine(update, theme));
|
|
243
|
+
|
|
244
|
+
const message = normalizeMessageForSidebar(update.message, update);
|
|
245
|
+
if (message) {
|
|
246
|
+
rows.push(...wrapPlainText(message, width, 2).map((line) => theme.fg("dim", line)));
|
|
242
247
|
}
|
|
243
|
-
rows.push("", theme.fg("muted", truncateToWidth(update.message, 72)));
|
|
244
248
|
|
|
245
249
|
if (!progress || progress.tasks.length === 0) {
|
|
246
|
-
rows.push("", theme.fg("muted", "Waiting for TODO plan
|
|
250
|
+
rows.push("", sidebarHeading("Context", theme), theme.fg("muted", "Waiting for TODO plan"));
|
|
251
|
+
if (update.workerCostTotal > 0) {
|
|
252
|
+
rows.push(theme.fg("muted", `${formatCost(update.workerCostTotal)} spent`));
|
|
253
|
+
}
|
|
247
254
|
return rows;
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
const summary = progress.summary;
|
|
251
|
-
rows.push(
|
|
252
|
-
|
|
258
|
+
rows.push(
|
|
259
|
+
"",
|
|
260
|
+
sidebarHeading("Context", theme),
|
|
261
|
+
theme.fg(
|
|
262
|
+
"muted",
|
|
263
|
+
`${summary.completedTasks.toLocaleString()}/${summary.totalTasks.toLocaleString()} tasks complete`,
|
|
264
|
+
),
|
|
265
|
+
theme.fg("muted", `${summary.completedPercent}% complete`),
|
|
266
|
+
);
|
|
267
|
+
if (update.workerCostTotal > 0) {
|
|
268
|
+
rows.push(theme.fg("muted", `${formatCost(update.workerCostTotal)} spent`));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
rows.push(
|
|
272
|
+
"",
|
|
273
|
+
sidebarHeading("Progress", theme),
|
|
274
|
+
progressBarLine(progress, theme),
|
|
275
|
+
progressCountsLine(summary, theme),
|
|
276
|
+
progressStateLegend(progress, theme),
|
|
277
|
+
);
|
|
253
278
|
|
|
254
279
|
const currentIndex = focusedTaskIndex(progress);
|
|
255
|
-
|
|
256
|
-
|
|
280
|
+
const currentTask = currentIndex >= 0 ? progress.tasks[currentIndex] : undefined;
|
|
281
|
+
rows.push("", sidebarHeading("Current", theme));
|
|
282
|
+
if (currentTask) {
|
|
283
|
+
const details = taskStatusDetails(currentTask.status);
|
|
284
|
+
rows.push(
|
|
285
|
+
theme.fg(
|
|
286
|
+
details.color,
|
|
287
|
+
`${details.icon} TODO ${currentTask.taskId} ${theme.fg("dim", "·")} ${details.label}${currentTaskMeta(currentTask, theme)}`,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
rows.push(...wrapPlainText(currentTask.title, width, 2).map((line) => theme.fg("muted", line)));
|
|
257
291
|
} else {
|
|
258
292
|
rows.push(theme.fg("success", "No active task"));
|
|
259
293
|
}
|
|
260
294
|
|
|
261
|
-
rows.push("",
|
|
295
|
+
rows.push("", sidebarHeading("Task timeline", theme));
|
|
262
296
|
const taskIndexes = centeredTaskIndexes(progress.tasks.length, currentIndex, 9);
|
|
263
297
|
const first = taskIndexes[0] ?? 0;
|
|
264
298
|
const last = taskIndexes[taskIndexes.length - 1] ?? -1;
|
|
@@ -278,7 +312,7 @@ function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme:
|
|
|
278
312
|
|
|
279
313
|
const subtasks = update.subtasks ?? [];
|
|
280
314
|
if (subtasks.length > 0) {
|
|
281
|
-
rows.push("",
|
|
315
|
+
rows.push("", sidebarHeading("Current status", theme));
|
|
282
316
|
for (const subtask of subtasks.slice(0, 6)) {
|
|
283
317
|
rows.push(renderSubtaskRow(subtask, theme));
|
|
284
318
|
}
|
|
@@ -316,61 +350,181 @@ function renderTaskRow(
|
|
|
316
350
|
theme: Theme,
|
|
317
351
|
): string {
|
|
318
352
|
const details = taskStatusDetails(task.status);
|
|
319
|
-
const attempts = task.attempts > 0 && task.status !== "completed" ? ` · ${task.attempts}x` : "";
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
|
|
353
|
+
const attempts = task.attempts > 0 && task.status !== "completed" ? ` ${theme.fg("dim", "·")} ${task.attempts}x` : "";
|
|
354
|
+
const title = truncateToWidth(task.title, 72);
|
|
355
|
+
const focusMarker = focused ? theme.fg("accent", "›") : theme.fg("dim", " ");
|
|
356
|
+
const icon = theme.fg(details.color, details.icon);
|
|
357
|
+
const label = `TODO ${task.taskId}`;
|
|
358
|
+
const row = `${label} ${theme.fg("dim", "·")} ${details.label} ${theme.fg("dim", "·")} ${title}${attempts}`;
|
|
359
|
+
const styledRow = focused ? theme.fg(details.color, theme.bold(row)) : theme.fg(details.textColor, row);
|
|
360
|
+
return `${focusMarker} ${icon} ${styledRow}`;
|
|
323
361
|
}
|
|
324
362
|
|
|
325
363
|
function renderSubtaskRow(subtask: NonNullable<CoordinatorProgressUpdate["subtasks"]>[number], theme: Theme): string {
|
|
326
364
|
const details = progressItemStatusDetails(subtask.status);
|
|
327
|
-
return theme.fg(details.color, `${details.
|
|
365
|
+
return `${theme.fg(details.color, details.icon)} ${theme.fg(details.textColor, `${details.label} · ${subtask.text}`)}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function renderSidebarStateLine(update: CoordinatorProgressUpdate, theme: Theme): string {
|
|
369
|
+
const details = sidebarUpdateStateDetails(update);
|
|
370
|
+
const suffix = [
|
|
371
|
+
update.attempt && update.attempt > 1 ? `attempt ${update.attempt}` : undefined,
|
|
372
|
+
update.workerCostTotal > 0 ? `${formatCost(update.workerCostTotal)} spent` : undefined,
|
|
373
|
+
]
|
|
374
|
+
.filter(Boolean)
|
|
375
|
+
.join(` ${theme.fg("dim", "·")} `);
|
|
376
|
+
const meta = suffix ? ` ${theme.fg("dim", "·")} ${theme.fg("muted", suffix)}` : "";
|
|
377
|
+
return `${theme.fg(details.color, details.icon)} ${theme.fg(details.color, details.label)}${meta}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function currentTaskMeta(
|
|
381
|
+
task: NonNullable<CoordinatorProgressUpdate["taskProgress"]>["tasks"][number],
|
|
382
|
+
theme: Theme,
|
|
383
|
+
): string {
|
|
384
|
+
const attempts = task.attempts > 0 && task.status !== "completed" ? [`attempt ${task.attempts}`] : [];
|
|
385
|
+
if (task.lastReportedStatus && task.status !== "current") {
|
|
386
|
+
attempts.push(task.lastReportedStatus);
|
|
387
|
+
}
|
|
388
|
+
return attempts.length > 0 ? ` ${theme.fg("dim", "·")} ${theme.fg("muted", attempts.join(" · "))}` : "";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function progressMeterLine(progress: NonNullable<CoordinatorProgressUpdate["taskProgress"]>, theme: Theme): string {
|
|
392
|
+
const total = progress.tasks.length;
|
|
393
|
+
if (total === 0) {
|
|
394
|
+
return theme.fg("dim", "────────");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const maxSegments = 12;
|
|
398
|
+
const step = Math.max(1, Math.ceil(total / maxSegments));
|
|
399
|
+
const segments: string[] = [];
|
|
400
|
+
for (let index = 0; index < total; index += step) {
|
|
401
|
+
const slice = progress.tasks.slice(index, Math.min(total, index + step));
|
|
402
|
+
segments.push(progressMeterSegment(slice, theme));
|
|
403
|
+
}
|
|
404
|
+
return segments.join("");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function progressMeterSegment(
|
|
408
|
+
tasks: Array<NonNullable<CoordinatorProgressUpdate["taskProgress"]>["tasks"][number]>,
|
|
409
|
+
theme: Theme,
|
|
410
|
+
): string {
|
|
411
|
+
if (tasks.some((task) => task.status === "failed")) {
|
|
412
|
+
return theme.fg("error", "×");
|
|
413
|
+
}
|
|
414
|
+
if (tasks.some((task) => task.status === "blocked")) {
|
|
415
|
+
return theme.fg("warning", "!");
|
|
416
|
+
}
|
|
417
|
+
if (tasks.some((task) => task.status === "current")) {
|
|
418
|
+
return theme.fg("accent", "▢");
|
|
419
|
+
}
|
|
420
|
+
if (tasks.every((task) => task.status === "completed")) {
|
|
421
|
+
return theme.fg("success", "■");
|
|
422
|
+
}
|
|
423
|
+
if (tasks.some((task) => task.status === "completed")) {
|
|
424
|
+
return theme.fg("success", "▪");
|
|
425
|
+
}
|
|
426
|
+
return theme.fg("dim", "·");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function progressStateLegend(progress: NonNullable<CoordinatorProgressUpdate["taskProgress"]>, theme: Theme): string {
|
|
430
|
+
const statuses = new Set(progress.tasks.map((task) => task.status));
|
|
431
|
+
const items = [
|
|
432
|
+
statuses.has("completed") ? theme.fg("success", "■ done") : undefined,
|
|
433
|
+
statuses.has("current") ? theme.fg("accent", "▢ active") : undefined,
|
|
434
|
+
statuses.has("pending") ? theme.fg("dim", "· queued") : undefined,
|
|
435
|
+
statuses.has("failed") ? theme.fg("error", "× failed") : undefined,
|
|
436
|
+
statuses.has("blocked") ? theme.fg("warning", "! blocked") : undefined,
|
|
437
|
+
].filter(Boolean);
|
|
438
|
+
return theme.fg("muted", items.join(" · "));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function sidebarUpdateStateDetails(update: CoordinatorProgressUpdate): {
|
|
442
|
+
icon: string;
|
|
443
|
+
label: string;
|
|
444
|
+
color: "accent" | "success" | "warning" | "error" | "muted";
|
|
445
|
+
} {
|
|
446
|
+
if (update.status) {
|
|
447
|
+
switch (update.status) {
|
|
448
|
+
case "done":
|
|
449
|
+
return { icon: "✓", label: "done", color: "success" };
|
|
450
|
+
case "failed":
|
|
451
|
+
return { icon: "×", label: "failed", color: "error" };
|
|
452
|
+
case "blocked":
|
|
453
|
+
return { icon: "!", label: "blocked", color: "warning" };
|
|
454
|
+
case "partial":
|
|
455
|
+
return { icon: "!", label: "partial", color: "warning" };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
switch (update.phase) {
|
|
460
|
+
case "planning":
|
|
461
|
+
return { icon: "+", label: "Planning", color: "warning" };
|
|
462
|
+
case "planned":
|
|
463
|
+
return { icon: "✓", label: "Plan ready", color: "success" };
|
|
464
|
+
case "task_start":
|
|
465
|
+
return { icon: "▢", label: "Running task", color: "accent" };
|
|
466
|
+
case "worker_tool":
|
|
467
|
+
return { icon: "+", label: "Worker tool", color: "warning" };
|
|
468
|
+
case "task_done":
|
|
469
|
+
return { icon: "✓", label: "Task complete", color: "success" };
|
|
470
|
+
case "task_blocked":
|
|
471
|
+
return { icon: "!", label: "Task blocked", color: "warning" };
|
|
472
|
+
case "task_failed":
|
|
473
|
+
return { icon: "×", label: "Task failed", color: "error" };
|
|
474
|
+
case "complete":
|
|
475
|
+
return { icon: "✓", label: "Complete", color: "success" };
|
|
476
|
+
}
|
|
328
477
|
}
|
|
329
478
|
|
|
330
479
|
function taskStatusDetails(status: NonNullable<CoordinatorProgressUpdate["taskProgress"]>["tasks"][number]["status"]): {
|
|
331
480
|
icon: string;
|
|
332
|
-
|
|
481
|
+
label: string;
|
|
482
|
+
color: "accent" | "success" | "warning" | "error" | "dim" | "muted";
|
|
483
|
+
textColor: "accent" | "text" | "success" | "warning" | "error" | "dim" | "muted";
|
|
333
484
|
} {
|
|
334
485
|
switch (status) {
|
|
335
486
|
case "completed":
|
|
336
|
-
return { icon: "✓", color: "success" };
|
|
487
|
+
return { icon: "✓", label: "done", color: "success", textColor: "muted" };
|
|
337
488
|
case "current":
|
|
338
|
-
return { icon: "
|
|
489
|
+
return { icon: "▢", label: "active", color: "accent", textColor: "text" };
|
|
339
490
|
case "failed":
|
|
340
|
-
return { icon: "
|
|
491
|
+
return { icon: "×", label: "failed", color: "error", textColor: "error" };
|
|
341
492
|
case "blocked":
|
|
342
|
-
return { icon: "!", color: "warning" };
|
|
493
|
+
return { icon: "!", label: "blocked", color: "warning", textColor: "warning" };
|
|
343
494
|
case "pending":
|
|
344
|
-
return { icon: "○", color: "dim" };
|
|
495
|
+
return { icon: "○", label: "queued", color: "dim", textColor: "dim" };
|
|
345
496
|
}
|
|
346
497
|
}
|
|
347
498
|
|
|
348
499
|
function progressItemStatusDetails(status: NonNullable<CoordinatorProgressUpdate["subtasks"]>[number]["status"]): {
|
|
349
500
|
icon: string;
|
|
501
|
+
label: string;
|
|
350
502
|
color: "success" | "warning" | "error" | "dim" | "muted";
|
|
503
|
+
textColor: "success" | "warning" | "error" | "dim" | "muted";
|
|
351
504
|
} {
|
|
352
505
|
switch (status) {
|
|
353
506
|
case "done":
|
|
354
|
-
return { icon: "✓", color: "success" };
|
|
507
|
+
return { icon: "✓", label: "done", color: "success", textColor: "muted" };
|
|
355
508
|
case "in_progress":
|
|
356
|
-
return { icon: "
|
|
509
|
+
return { icon: "+", label: "active", color: "warning", textColor: "warning" };
|
|
357
510
|
case "failed":
|
|
358
|
-
return { icon: "
|
|
511
|
+
return { icon: "×", label: "failed", color: "error", textColor: "error" };
|
|
359
512
|
case "blocked":
|
|
360
|
-
return { icon: "!", color: "warning" };
|
|
513
|
+
return { icon: "!", label: "blocked", color: "warning", textColor: "warning" };
|
|
361
514
|
case "empty":
|
|
362
|
-
return { icon: "○", color: "dim" };
|
|
515
|
+
return { icon: "○", label: "queued", color: "dim", textColor: "dim" };
|
|
363
516
|
}
|
|
364
517
|
}
|
|
365
518
|
|
|
366
|
-
function progressBarLine(
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
519
|
+
function progressBarLine(progress: NonNullable<CoordinatorProgressUpdate["taskProgress"]>, theme: Theme): string {
|
|
520
|
+
const summary = progress.summary;
|
|
521
|
+
return `${theme.fg("muted", "Tasks")} ${theme.fg(
|
|
522
|
+
"success",
|
|
523
|
+
`${summary.completedTasks}/${summary.totalTasks}`,
|
|
524
|
+
)} ${theme.fg("dim", "·")} ${theme.fg("muted", `${summary.completedPercent}% complete`)} ${theme.fg(
|
|
371
525
|
"dim",
|
|
372
|
-
"
|
|
373
|
-
)}
|
|
526
|
+
"·",
|
|
527
|
+
)} ${progressMeterLine(progress, theme)}`;
|
|
374
528
|
}
|
|
375
529
|
|
|
376
530
|
function progressCountsLine(
|
|
@@ -378,30 +532,80 @@ function progressCountsLine(
|
|
|
378
532
|
theme: Theme,
|
|
379
533
|
): string {
|
|
380
534
|
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,
|
|
535
|
+
theme.fg("success", `✓ ${summary.completedTasks} done`),
|
|
536
|
+
summary.currentTasks ? theme.fg("accent", `▢ ${summary.currentTasks} active`) : undefined,
|
|
537
|
+
summary.pendingTasks ? theme.fg("dim", `○ ${summary.pendingTasks} queued`) : undefined,
|
|
538
|
+
summary.failedTasks ? theme.fg("error", `× ${summary.failedTasks} failed`) : undefined,
|
|
539
|
+
summary.blockedTasks ? theme.fg("warning", `! ${summary.blockedTasks} blocked`) : undefined,
|
|
386
540
|
]
|
|
387
541
|
.filter(Boolean)
|
|
388
542
|
.join(" · ");
|
|
389
543
|
}
|
|
390
544
|
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
545
|
+
function sidebarHeading(text: string, theme: Theme): string {
|
|
546
|
+
return theme.fg("toolTitle", theme.bold(text));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function sidebarHeadline(
|
|
550
|
+
update: CoordinatorProgressUpdate,
|
|
551
|
+
progress: CoordinatorProgressUpdate["taskProgress"],
|
|
552
|
+
): string {
|
|
553
|
+
if (update.taskId && update.title) {
|
|
554
|
+
return `TODO ${update.taskId} — ${update.title}`;
|
|
555
|
+
}
|
|
556
|
+
const currentTask = progress?.currentTask ?? progress?.nextTask;
|
|
557
|
+
if (currentTask) {
|
|
558
|
+
return `TODO ${currentTask.taskId} — ${currentTask.title}`;
|
|
559
|
+
}
|
|
560
|
+
return "Pi Long Task";
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function normalizeMessageForSidebar(updateMessage: string, update: CoordinatorProgressUpdate): string | undefined {
|
|
564
|
+
const message = updateMessage.trim();
|
|
565
|
+
if (!message) {
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
const title = update.taskId && update.title ? `TODO ${update.taskId} — ${update.title}` : undefined;
|
|
569
|
+
return title && message.includes(title) && message.length <= title.length + 16 ? undefined : message;
|
|
397
570
|
}
|
|
398
571
|
|
|
399
|
-
function
|
|
400
|
-
|
|
572
|
+
function wrapPlainText(text: string, width: number, limit?: number): string[] {
|
|
573
|
+
const safeWidth = Math.max(8, width);
|
|
574
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
575
|
+
if (words.length === 0) {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const lines: string[] = [];
|
|
580
|
+
let line = "";
|
|
581
|
+
for (const word of words) {
|
|
582
|
+
const next = line ? `${line} ${word}` : word;
|
|
583
|
+
if (next.length <= safeWidth) {
|
|
584
|
+
line = next;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (line) {
|
|
588
|
+
lines.push(line);
|
|
589
|
+
}
|
|
590
|
+
line = word.length > safeWidth ? truncateToWidth(word, safeWidth) : word;
|
|
591
|
+
if (limit && lines.length >= limit) {
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (line && (!limit || lines.length < limit)) {
|
|
596
|
+
lines.push(line);
|
|
597
|
+
}
|
|
598
|
+
if (limit && lines.length > limit) {
|
|
599
|
+
lines.length = limit;
|
|
600
|
+
}
|
|
601
|
+
if (limit && words.join(" ").length > lines.join(" ").length && lines.length > 0) {
|
|
602
|
+
lines[lines.length - 1] = truncateToWidth(`${lines[lines.length - 1]} …`, safeWidth);
|
|
603
|
+
}
|
|
604
|
+
return lines;
|
|
401
605
|
}
|
|
402
606
|
|
|
403
|
-
function
|
|
404
|
-
return
|
|
607
|
+
function sidebarPanelRow(text: string, width: number, theme: Theme): string {
|
|
608
|
+
return `${theme.fg("borderMuted", "│")} ${truncateToWidth(text, width, "…", true)}`;
|
|
405
609
|
}
|
|
406
610
|
|
|
407
611
|
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 {
|