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 +15 -0
- package/package.json +1 -1
- package/src/coordinator.ts +99 -8
- package/src/index.ts +10 -6
- 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.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.
|
|
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": [
|
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,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,
|
|
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 = [
|
|
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;
|
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;
|