pi-long-task 0.3.15 → 0.3.17

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,21 @@
2
2
 
3
3
  Notable changes to Pi Long Task are recorded here. This project follows semantic versioning.
4
4
 
5
+ ## 0.3.17 - 2026-08-26
6
+
7
+ ### Fixed
8
+
9
+ - Prevent repeated `Finished:` and `Failed:` prefixes from accumulating in the sidebar's active status across tool events.
10
+ - Reset the active status at every tool start so a completed tool's status cannot leak into the next tool.
11
+
12
+ ## 0.3.16 - 2026-08-20
13
+
14
+ ### Changed
15
+
16
+ - Make the sidebar's "Active status" follow live worker commentary and tool activity instead of repeating a generic coordinator message.
17
+ - Show active bash commands and read, edit, and write paths, including tool completion or failure state.
18
+ - Preserve the latest worker activity across unrelated cost updates while keeping the compact fallback layout unchanged.
19
+
5
20
  ## 0.3.15 - 2026-08-20
6
21
 
7
22
  ### 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.17",
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,29 @@ 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 = stripToolOutcomePrefix(
1057
+ runtime.workerActivityByWorker.get(worker) ?? `Running ${event.toolName}`,
1058
+ );
1059
+ activeStatus = event.isError ? `Failed: ${previousActivity}` : `Finished: ${previousActivity}`;
1060
+ runtime.workerActivityByWorker.set(worker, activeStatus);
1061
+ } else {
1062
+ // Every tool start begins a new activity. Keeping the previous tool's completed
1063
+ // status here causes each later tool end to prepend another "Finished:" label.
1064
+ activeStatus = event.activity ?? `Running ${event.toolName}`;
1065
+ runtime.workerActivityByWorker.set(worker, activeStatus);
1066
+ }
1067
+
979
1068
  const update: Omit<CoordinatorProgressUpdate, "message" | "runId" | "todoPath" | "resultPath" | "workerCostTotal"> = {
980
1069
  phase: "worker_tool",
981
1070
  taskId: task.taskId,
@@ -984,6 +1073,7 @@ function emitWorkerEventProgress(
984
1073
  status: action,
985
1074
  toolName: event.toolName,
986
1075
  workerEventType: event.type,
1076
+ activeStatus,
987
1077
  isError: event.isError,
988
1078
  ...currentTaskProgress(task, "in_progress"),
989
1079
  taskProgress: buildTaskProgressModel({ tasks, attempts, currentTaskId: task.taskId }),
@@ -991,7 +1081,16 @@ function emitWorkerEventProgress(
991
1081
  if (event.isError) {
992
1082
  update.status = "failed";
993
1083
  }
994
- emitProgress(runtime, `TODO ${task.taskId}: worker tool ${event.toolName} ${action}.`, update);
1084
+ emitProgress(runtime, activeStatus, update);
1085
+ }
1086
+
1087
+ function stripToolOutcomePrefix(activity: string): string {
1088
+ return activity.replace(/^(?:(?:Finished|Failed):\s*)+/i, "");
1089
+ }
1090
+
1091
+ function activeStatusFromWorkerText(text: string): string {
1092
+ const taskResultIndex = text.indexOf("TASK_RESULT:");
1093
+ return (taskResultIndex >= 0 ? text.slice(0, taskResultIndex) : text).replace(/\s+/g, " ").trim();
995
1094
  }
996
1095
 
997
1096
  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;