pi-long-task 0.3.15 → 0.3.16

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  Notable changes to Pi Long Task are recorded here. This project follows semantic versioning.
4
4
 
5
+ ## 0.3.16 - 2026-08-20
6
+
7
+ ### Changed
8
+
9
+ - Make the sidebar's "Active status" follow live worker commentary and tool activity instead of repeating a generic coordinator message.
10
+ - Show active bash commands and read, edit, and write paths, including tool completion or failure state.
11
+ - Preserve the latest worker activity across unrelated cost updates while keeping the compact fallback layout unchanged.
12
+
5
13
  ## 0.3.15 - 2026-08-20
6
14
 
7
15
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-long-task",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "type": "module",
5
5
  "description": "Pi coding agent extension that breaks large coding requests into tracked TODOs and runs them in isolated AI worker sessions. A long-running task runner and subagent orchestrator for Pi, with a live TUI progress sidebar, retries, goal loops, and optional per-task git commits.",
6
6
  "keywords": [
@@ -96,6 +96,7 @@ export interface CoordinatorProgressUpdate {
96
96
  commitSkipped?: string;
97
97
  toolName?: string;
98
98
  workerEventType?: string;
99
+ activeStatus?: string;
99
100
  isError?: boolean;
100
101
  totalTasks?: number;
101
102
  workerCostTotal: number;
@@ -215,6 +216,9 @@ interface RuntimeOptions {
215
216
  now: () => Date;
216
217
  onProgress?: CoordinatorProgressHandler;
217
218
  workerCostState: WorkerCostState;
219
+ workerActivityByWorker: Map<string, string>;
220
+ workerTextByWorker: Map<string, string>;
221
+ workerTextPublishedLengthByWorker: Map<string, number>;
218
222
  plannerDiagnostics: PlannerDiagnostic[];
219
223
  }
220
224
 
@@ -261,6 +265,12 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
261
265
  }
262
266
 
263
267
  const attempt = (previousAttempts.get(nextTask.taskId)?.length ?? 0) + 1;
268
+ const initialActivity =
269
+ nextTask.statusItems.find((item) => !item.done)?.text ?? `Starting TODO ${nextTask.taskId}`;
270
+ const worker = workerKey(nextTask.taskId, attempt);
271
+ runtime.workerActivityByWorker.set(worker, initialActivity);
272
+ runtime.workerTextByWorker.delete(worker);
273
+ runtime.workerTextPublishedLengthByWorker.delete(worker);
264
274
  emitProgress(
265
275
  runtime,
266
276
  `Running TODO ${nextTask.taskId} — ${nextTask.title}${attempt > 1 ? ` (attempt ${attempt})` : ""}...`,
@@ -269,6 +279,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
269
279
  taskId: nextTask.taskId,
270
280
  title: nextTask.title,
271
281
  attempt,
282
+ activeStatus: initialActivity,
272
283
  ...currentTaskProgress(nextTask, "in_progress"),
273
284
  taskProgress: buildTaskProgressModel({
274
285
  tasks: tasksBeforeAttempt,
@@ -792,6 +803,9 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
792
803
  now: options.now ?? (() => new Date()),
793
804
  onProgress: options.onProgress,
794
805
  workerCostState: createWorkerCostState(),
806
+ workerActivityByWorker: new Map(),
807
+ workerTextByWorker: new Map(),
808
+ workerTextPublishedLengthByWorker: new Map(),
795
809
  plannerDiagnostics: [],
796
810
  };
797
811
  }
@@ -950,11 +964,74 @@ function emitWorkerEventProgress(
950
964
  task: Pick<Task, "taskId" | "title" | "statusItems">,
951
965
  attempts: readonly TaskAttemptSummary[],
952
966
  attempt: number,
953
- event: { type: string; toolName?: string; isError?: boolean; usageCostTotal?: number; usageCostKey?: string },
967
+ event: {
968
+ type: string;
969
+ toolName?: string;
970
+ activity?: string;
971
+ textDelta?: string;
972
+ isError?: boolean;
973
+ usageCostTotal?: number;
974
+ usageCostKey?: string;
975
+ },
954
976
  ): void {
955
- if (event.usageCostTotal !== undefined) {
956
- const changed = recordLiveWorkerCost(runtime.workerCostState, workerKey(task.taskId, attempt), event);
957
- if (changed) {
977
+ const worker = workerKey(task.taskId, attempt);
978
+ let activeStatus = runtime.workerActivityByWorker.get(worker);
979
+
980
+ if (event.type === "message_update" && event.textDelta) {
981
+ const workerText = `${runtime.workerTextByWorker.get(worker) ?? ""}${event.textDelta}`;
982
+ runtime.workerTextByWorker.set(worker, workerText);
983
+ const streamedStatus = activeStatusFromWorkerText(workerText);
984
+ const publishedLength = runtime.workerTextPublishedLengthByWorker.get(worker) ?? 0;
985
+ const publishBoundary = /[\n.!?:]\s*$/.test(event.textDelta) || streamedStatus.length - publishedLength >= 48;
986
+ if (streamedStatus && publishBoundary) {
987
+ activeStatus = streamedStatus;
988
+ runtime.workerActivityByWorker.set(worker, activeStatus);
989
+ runtime.workerTextPublishedLengthByWorker.set(worker, streamedStatus.length);
990
+ emitProgress(runtime, activeStatus, {
991
+ phase: "worker_tool",
992
+ taskId: task.taskId,
993
+ title: task.title,
994
+ attempt,
995
+ status: "in_progress",
996
+ workerEventType: event.type,
997
+ activeStatus,
998
+ ...currentTaskProgress(task, "in_progress"),
999
+ taskProgress: buildTaskProgressModel({ tasks, attempts, currentTaskId: task.taskId }),
1000
+ });
1001
+ }
1002
+ return;
1003
+ }
1004
+
1005
+ if (event.type === "message_end") {
1006
+ runtime.workerTextByWorker.delete(worker);
1007
+ runtime.workerTextPublishedLengthByWorker.delete(worker);
1008
+ }
1009
+
1010
+ if (event.activity) {
1011
+ activeStatus = event.activity;
1012
+ runtime.workerActivityByWorker.set(worker, activeStatus);
1013
+ }
1014
+
1015
+ const costChanged =
1016
+ event.usageCostTotal !== undefined && recordLiveWorkerCost(runtime.workerCostState, worker, event);
1017
+
1018
+ if (event.type === "message_end" && event.activity) {
1019
+ emitProgress(runtime, event.activity, {
1020
+ phase: "worker_tool",
1021
+ taskId: task.taskId,
1022
+ title: task.title,
1023
+ attempt,
1024
+ status: "in_progress",
1025
+ workerEventType: event.type,
1026
+ activeStatus,
1027
+ ...currentTaskProgress(task, "in_progress"),
1028
+ taskProgress: buildTaskProgressModel({ tasks, attempts, currentTaskId: task.taskId }),
1029
+ });
1030
+ return;
1031
+ }
1032
+
1033
+ if (!event.toolName || (event.type !== "tool_execution_start" && event.type !== "tool_execution_end")) {
1034
+ if (costChanged) {
958
1035
  emitProgress(
959
1036
  runtime,
960
1037
  `TODO ${task.taskId}: worker cost updated to ${formatCost(runtime.workerCostState.total)}.`,
@@ -965,17 +1042,25 @@ function emitWorkerEventProgress(
965
1042
  attempt,
966
1043
  status: "in_progress",
967
1044
  workerEventType: event.type,
1045
+ activeStatus,
968
1046
  ...currentTaskProgress(task, "in_progress"),
969
1047
  taskProgress: buildTaskProgressModel({ tasks, attempts, currentTaskId: task.taskId }),
970
1048
  },
971
1049
  );
972
1050
  }
973
- }
974
-
975
- if (!event.toolName || (event.type !== "tool_execution_start" && event.type !== "tool_execution_end")) {
976
1051
  return;
977
1052
  }
1053
+
978
1054
  const action = event.type === "tool_execution_start" ? "started" : event.isError ? "failed" : "finished";
1055
+ if (event.type === "tool_execution_end") {
1056
+ const previousActivity = runtime.workerActivityByWorker.get(worker) ?? `Running ${event.toolName}`;
1057
+ activeStatus = event.isError ? `Failed: ${previousActivity}` : `Finished: ${previousActivity}`;
1058
+ runtime.workerActivityByWorker.set(worker, activeStatus);
1059
+ } else if (!activeStatus) {
1060
+ activeStatus = `Running ${event.toolName}`;
1061
+ runtime.workerActivityByWorker.set(worker, activeStatus);
1062
+ }
1063
+
979
1064
  const update: Omit<CoordinatorProgressUpdate, "message" | "runId" | "todoPath" | "resultPath" | "workerCostTotal"> = {
980
1065
  phase: "worker_tool",
981
1066
  taskId: task.taskId,
@@ -984,6 +1069,7 @@ function emitWorkerEventProgress(
984
1069
  status: action,
985
1070
  toolName: event.toolName,
986
1071
  workerEventType: event.type,
1072
+ activeStatus,
987
1073
  isError: event.isError,
988
1074
  ...currentTaskProgress(task, "in_progress"),
989
1075
  taskProgress: buildTaskProgressModel({ tasks, attempts, currentTaskId: task.taskId }),
@@ -991,7 +1077,12 @@ function emitWorkerEventProgress(
991
1077
  if (event.isError) {
992
1078
  update.status = "failed";
993
1079
  }
994
- emitProgress(runtime, `TODO ${task.taskId}: worker tool ${event.toolName} ${action}.`, update);
1080
+ emitProgress(runtime, activeStatus, update);
1081
+ }
1082
+
1083
+ function activeStatusFromWorkerText(text: string): string {
1084
+ const taskResultIndex = text.indexOf("TASK_RESULT:");
1085
+ return (taskResultIndex >= 0 ? text.slice(0, taskResultIndex) : text).replace(/\s+/g, " ").trim();
995
1086
  }
996
1087
 
997
1088
  function emitTaskOutcomeProgress(
package/src/index.ts CHANGED
@@ -277,7 +277,7 @@ function renderSidebarWidgetLines(update: CoordinatorProgressUpdate): string[] {
277
277
  const statusDetails = sidebarUpdateStateDetails(update);
278
278
  const lines = [
279
279
  "Pi Long Task",
280
- ...wrapPlainText(`${statusDetails.icon} ${statusDetails.label} · ${update.message}`, 96),
280
+ `${statusDetails.icon} ${statusDetails.label} · ${update.activeStatus ?? update.message}`,
281
281
  ];
282
282
  if (summary) {
283
283
  lines.push(
@@ -293,9 +293,7 @@ function renderSidebarWidgetLines(update: CoordinatorProgressUpdate): string[] {
293
293
  const currentTask = currentIndex >= 0 ? progress.tasks[currentIndex] : undefined;
294
294
  if (currentTask) {
295
295
  const details = taskStatusDetails(currentTask.status);
296
- lines.push(
297
- ...wrapPlainText(`${details.label}: ${details.icon} TODO ${currentTask.taskId} — ${currentTask.title}`, 96),
298
- );
296
+ lines.push(`${details.label}: ${details.icon} TODO ${currentTask.taskId} — ${currentTask.title}`);
299
297
  }
300
298
  }
301
299
  if (update.workerCostTotal > 0) {
@@ -397,10 +395,10 @@ function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme:
397
395
  rows.push(theme.fg("success", "No active task"));
398
396
  }
399
397
 
400
- const message = normalizeMessageForSidebar(update.message, update);
401
- if (currentTask && message) {
398
+ const activeStatus = update.activeStatus ?? normalizeMessageForSidebar(update.message, update);
399
+ if (currentTask && activeStatus) {
402
400
  rows.push("", sidebarHeading("Active status", theme));
403
- rows.push(...wrapPlainText(message, width, 6).map((line) => theme.fg("dim", line)));
401
+ rows.push(...wrapPlainText(activeStatus, width, 6).map((line) => theme.fg("accent", line)));
404
402
  }
405
403
 
406
404
  rows.push("", sidebarHeading("Task timeline", theme));
@@ -275,6 +275,7 @@ export interface CapturedWorkerEvent {
275
275
  type: string;
276
276
  textDelta?: string;
277
277
  toolName?: string;
278
+ activity?: string;
278
279
  isError?: boolean;
279
280
  note?: string;
280
281
  usageCostTotal?: number;
@@ -887,18 +888,25 @@ function summarizeWorkerEvent(event: unknown): CapturedWorkerEvent | undefined {
887
888
  }
888
889
 
889
890
  if (event.type.startsWith("tool_execution_")) {
891
+ const toolName = typeof event.toolName === "string" ? event.toolName : undefined;
890
892
  return {
891
893
  type: event.type,
892
- toolName: typeof event.toolName === "string" ? event.toolName : undefined,
894
+ toolName,
895
+ activity:
896
+ event.type === "tool_execution_start" && toolName ? workerToolActivity(toolName, event.args) : undefined,
893
897
  isError: typeof event.isError === "boolean" ? event.isError : undefined,
894
898
  };
895
899
  }
896
900
 
897
901
  if (event.type === "message_end") {
898
902
  const usageCostTotal = workerUsageCostFromEvent(event);
899
- return usageCostTotal === undefined
900
- ? { type: event.type }
901
- : { type: event.type, usageCostTotal, usageCostKey: workerUsageCostKeyFromEvent(event) };
903
+ const activity = workerAssistantActivity(assistantMessageText(event.message));
904
+ return {
905
+ type: event.type,
906
+ activity: activity || undefined,
907
+ usageCostTotal,
908
+ usageCostKey: usageCostTotal === undefined ? undefined : workerUsageCostKeyFromEvent(event),
909
+ };
902
910
  }
903
911
 
904
912
  if (
@@ -915,6 +923,36 @@ function summarizeWorkerEvent(event: unknown): CapturedWorkerEvent | undefined {
915
923
  return undefined;
916
924
  }
917
925
 
926
+ function workerToolActivity(toolName: string, argsValue: unknown): string {
927
+ const args = isRecord(argsValue) ? argsValue : undefined;
928
+ const path = args && typeof args.path === "string" ? args.path : "";
929
+
930
+ switch (toolName) {
931
+ case "bash": {
932
+ const command = args && typeof args.command === "string" ? oneLine(args.command) : "";
933
+ return command ? `$ ${command}` : "Running bash";
934
+ }
935
+ case "read":
936
+ return path ? `Reading ${path}` : "Reading a file";
937
+ case "edit":
938
+ return path ? `Editing ${path}` : "Editing a file";
939
+ case "write":
940
+ return path ? `Writing ${path}` : "Writing a file";
941
+ default:
942
+ return `Running ${toolName}`;
943
+ }
944
+ }
945
+
946
+ function workerAssistantActivity(text: string): string {
947
+ const taskResultIndex = text.indexOf("TASK_RESULT:");
948
+ const activity = oneLine(taskResultIndex >= 0 ? text.slice(0, taskResultIndex) : text);
949
+ return activity || (taskResultIndex >= 0 ? "Reporting task results" : "");
950
+ }
951
+
952
+ function oneLine(value: string): string {
953
+ return value.replace(/\s+/g, " ").trim();
954
+ }
955
+
918
956
  export function workerUsageCostFromEvent(event: unknown): number | undefined {
919
957
  if (!isRecord(event)) {
920
958
  return undefined;