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 +15 -0
- package/package.json +1 -1
- package/src/coordinator.ts +107 -8
- package/src/index.ts +5 -7
- package/src/worker_session.ts +42 -4
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.
|
|
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": [
|
package/src/coordinator.ts
CHANGED
|
@@ -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: {
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
401
|
-
if (currentTask &&
|
|
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(
|
|
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));
|
package/src/worker_session.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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;
|