pi-long-task 0.3.14 → 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,21 @@
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
+
13
+ ## 0.3.15 - 2026-08-20
14
+
15
+ ### Changed
16
+
17
+ - Show the full active task status in a dedicated "Active status" section below the active task in the TUI sidebar, wrapping instead of truncating long status messages.
18
+ - Wrap the active task status message and current task line in the plain widget fallback instead of hard-truncating them.
19
+
5
20
  ## 0.3.14 - 2026-08-20
6
21
 
7
22
  ### Documentation and metadata
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-long-task",
3
- "version": "0.3.14",
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
@@ -275,7 +275,10 @@ function renderSidebarWidgetLines(update: CoordinatorProgressUpdate): string[] {
275
275
  const progress = update.taskProgress;
276
276
  const summary = progress?.summary;
277
277
  const statusDetails = sidebarUpdateStateDetails(update);
278
- const lines = ["Pi Long Task", `${statusDetails.icon} ${statusDetails.label} · ${update.message}`];
278
+ const lines = [
279
+ "Pi Long Task",
280
+ `${statusDetails.icon} ${statusDetails.label} · ${update.activeStatus ?? update.message}`,
281
+ ];
279
282
  if (summary) {
280
283
  lines.push(
281
284
  `Tasks: ${summary.completedTasks}/${summary.totalTasks} · ${summary.completedPercent}%` +
@@ -346,11 +349,6 @@ function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme:
346
349
  }
347
350
  rows.push(renderSidebarStateLine(update, theme));
348
351
 
349
- const message = normalizeMessageForSidebar(update.message, update);
350
- if (message) {
351
- rows.push(...wrapPlainText(message, width, 2).map((line) => theme.fg("dim", line)));
352
- }
353
-
354
352
  if (!progress || progress.tasks.length === 0) {
355
353
  rows.push("", sidebarHeading("Context", theme), theme.fg("muted", "Waiting for TODO plan"));
356
354
  if (update.workerCostTotal > 0) {
@@ -397,6 +395,12 @@ function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme:
397
395
  rows.push(theme.fg("success", "No active task"));
398
396
  }
399
397
 
398
+ const activeStatus = update.activeStatus ?? normalizeMessageForSidebar(update.message, update);
399
+ if (currentTask && activeStatus) {
400
+ rows.push("", sidebarHeading("Active status", theme));
401
+ rows.push(...wrapPlainText(activeStatus, width, 6).map((line) => theme.fg("accent", line)));
402
+ }
403
+
400
404
  rows.push("", sidebarHeading("Task timeline", theme));
401
405
  const taskIndexes = centeredTaskIndexes(progress.tasks.length, currentIndex, 9);
402
406
  const first = taskIndexes[0] ?? 0;
@@ -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;