pi-better-goal 0.1.22 → 0.2.1
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/package.json +1 -1
- package/src/index.ts +52 -5
- package/src/subagents.ts +42 -4
- package/src/types.ts +1 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
2
3
|
import { Type } from "typebox";
|
|
3
4
|
|
|
4
5
|
import {
|
|
@@ -59,6 +60,19 @@ const MAX_NO_PROGRESS_RETRIES = parseRetryLimit(
|
|
|
59
60
|
DEFAULT_MAX_NO_PROGRESS_RETRIES,
|
|
60
61
|
);
|
|
61
62
|
|
|
63
|
+
const GOAL_ACTIONS: readonly AutocompleteItem[] = [
|
|
64
|
+
{ value: "pause", label: "pause", description: "Pause the active goal" },
|
|
65
|
+
{ value: "resume", label: "resume", description: "Resume the paused goal" },
|
|
66
|
+
{ value: "clear", label: "clear", description: "Remove the current goal" },
|
|
67
|
+
{ value: "complete", label: "complete", description: "Mark the current goal complete" },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
export function goalArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
|
|
71
|
+
const prefix = argumentPrefix.trimStart().toLowerCase();
|
|
72
|
+
const matches = GOAL_ACTIONS.filter((action) => action.value.startsWith(prefix));
|
|
73
|
+
return matches.length > 0 ? [...matches] : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
function parseDurationEnv(raw: string | undefined, fallback: number): number {
|
|
63
77
|
if (raw === undefined || raw.trim() === "") {
|
|
64
78
|
return fallback;
|
|
@@ -148,6 +162,7 @@ function formatSnapshot(snapshot: ActivitySnapshot): string {
|
|
|
148
162
|
|
|
149
163
|
export default function (pi: ExtensionAPI): void {
|
|
150
164
|
const providers = new Map<string, BackgroundActivityProvider>();
|
|
165
|
+
const providerUnsubscribers = new Map<string, () => void>();
|
|
151
166
|
providers.set("subagents", {
|
|
152
167
|
id: "subagents",
|
|
153
168
|
label: "Subagents",
|
|
@@ -158,6 +173,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
158
173
|
let foregroundRunning = false;
|
|
159
174
|
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
160
175
|
let collecting = false;
|
|
176
|
+
let collectionPending = false;
|
|
161
177
|
let latestSnapshot: ActivitySnapshot | null = null;
|
|
162
178
|
let backgroundDrainTracker: BackgroundDrainTracker | null = null;
|
|
163
179
|
let lastWakeSignature = "";
|
|
@@ -216,6 +232,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
216
232
|
continuationQueuedFor = null;
|
|
217
233
|
backgroundDrainTracker = null;
|
|
218
234
|
clearIdleContinuation();
|
|
235
|
+
syncPollingState();
|
|
219
236
|
// Force a full redraw when the dock height changes (absent ↔ visible clock).
|
|
220
237
|
refreshGoalWidget?.(!wasVisible || !isGoalClockVisible(goal));
|
|
221
238
|
};
|
|
@@ -227,6 +244,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
227
244
|
continuationQueuedFor = null;
|
|
228
245
|
backgroundDrainTracker = null;
|
|
229
246
|
clearIdleContinuation();
|
|
247
|
+
syncPollingState();
|
|
230
248
|
refreshGoalWidget?.(wasVisible);
|
|
231
249
|
};
|
|
232
250
|
|
|
@@ -395,12 +413,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
395
413
|
|
|
396
414
|
const collectIfPossible = (): void => {
|
|
397
415
|
const ctx = currentCtx;
|
|
398
|
-
if (!ctx
|
|
416
|
+
if (!ctx) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (collecting) {
|
|
420
|
+
collectionPending = true;
|
|
399
421
|
return;
|
|
400
422
|
}
|
|
401
423
|
collecting = true;
|
|
402
424
|
void publishSnapshot(ctx).finally(() => {
|
|
403
425
|
collecting = false;
|
|
426
|
+
if (collectionPending) {
|
|
427
|
+
collectionPending = false;
|
|
428
|
+
collectIfPossible();
|
|
429
|
+
} else {
|
|
430
|
+
syncPollingState();
|
|
431
|
+
}
|
|
404
432
|
});
|
|
405
433
|
};
|
|
406
434
|
|
|
@@ -410,7 +438,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
410
438
|
}
|
|
411
439
|
pollTimer = setInterval(collectIfPossible, POLL_INTERVAL_MS);
|
|
412
440
|
pollTimer.unref?.();
|
|
413
|
-
collectIfPossible();
|
|
414
441
|
};
|
|
415
442
|
|
|
416
443
|
const stopPolling = (): void => {
|
|
@@ -421,6 +448,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
421
448
|
pollTimer = undefined;
|
|
422
449
|
};
|
|
423
450
|
|
|
451
|
+
const syncPollingState = (): void => {
|
|
452
|
+
const ctx = currentCtx;
|
|
453
|
+
const goal = ctx ? currentGoalSnapshot(ctx) : null;
|
|
454
|
+
if (ctx && (isPokeable(goal) || latestSnapshot?.backgroundRunning === true)) {
|
|
455
|
+
startPolling();
|
|
456
|
+
} else {
|
|
457
|
+
stopPolling();
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
424
461
|
const installGoalWidget = (ctx: ExtensionContext): void => {
|
|
425
462
|
if (!ctx.hasUI) {
|
|
426
463
|
return;
|
|
@@ -480,12 +517,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
480
517
|
if (!candidate || typeof candidate.id !== "string" || typeof candidate.getActivity !== "function") {
|
|
481
518
|
return;
|
|
482
519
|
}
|
|
483
|
-
|
|
484
|
-
|
|
520
|
+
const accepted = candidate as BackgroundActivityProvider;
|
|
521
|
+
providerUnsubscribers.get(accepted.id)?.();
|
|
522
|
+
providerUnsubscribers.delete(accepted.id);
|
|
523
|
+
providers.set(accepted.id, accepted);
|
|
524
|
+
if (typeof accepted.onActivityChanged === "function") {
|
|
525
|
+
providerUnsubscribers.set(accepted.id, accepted.onActivityChanged(collectIfPossible));
|
|
526
|
+
}
|
|
527
|
+
if (latestSnapshot) collectIfPossible();
|
|
485
528
|
});
|
|
486
529
|
|
|
487
530
|
pi.registerCommand("goal", {
|
|
488
531
|
description: "Create, inspect, pause, resume, clear, or complete the active goal",
|
|
532
|
+
getArgumentCompletions: goalArgumentCompletions,
|
|
489
533
|
handler: async (args, ctx) => {
|
|
490
534
|
currentCtx = ctx;
|
|
491
535
|
const trimmed = args.trim();
|
|
@@ -646,7 +690,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
646
690
|
foregroundRunning = !ctx.isIdle();
|
|
647
691
|
pi.events.emit(EVENT_READY, { version: EXTENSION_VERSION });
|
|
648
692
|
installGoalWidget(ctx);
|
|
649
|
-
|
|
693
|
+
latestSnapshot = await publishSnapshot(ctx);
|
|
694
|
+
syncPollingState();
|
|
650
695
|
});
|
|
651
696
|
|
|
652
697
|
pi.on("input", async (event, ctx) => {
|
|
@@ -747,6 +792,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
747
792
|
foregroundRunning = false;
|
|
748
793
|
clearIdleContinuation();
|
|
749
794
|
currentCtx = undefined;
|
|
795
|
+
latestSnapshot = null;
|
|
796
|
+
collectionPending = false;
|
|
750
797
|
try {
|
|
751
798
|
ctx.ui.setStatus(EXTENSION_NAME, undefined);
|
|
752
799
|
ctx.ui.setWidget(EXTENSION_NAME, undefined);
|
package/src/subagents.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -82,6 +82,46 @@ export function readSubagentMetas(baseDir = defaultSubagentBaseDir()): SubagentR
|
|
|
82
82
|
return metas.sort((left, right) => right.startedAt - left.startedAt);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
export function readParentSubagentMetas(
|
|
86
|
+
baseDir = defaultSubagentBaseDir(),
|
|
87
|
+
parentPid = process.pid,
|
|
88
|
+
): SubagentRunMeta[] {
|
|
89
|
+
const directory = join(baseDir, "by-parent", String(parentPid));
|
|
90
|
+
ensureParentIndex(baseDir, directory, parentPid);
|
|
91
|
+
let ids: string[];
|
|
92
|
+
try {
|
|
93
|
+
ids = readdirSync(directory).filter((id) => id !== ".initialized");
|
|
94
|
+
} catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
return ids
|
|
98
|
+
.map((id) => readSubagentMeta(baseDir, id))
|
|
99
|
+
.filter((meta): meta is SubagentRunMeta => meta !== undefined && meta.spawnPid === parentPid)
|
|
100
|
+
.sort((left, right) => right.startedAt - left.startedAt);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readSubagentMeta(baseDir: string, id: string): SubagentRunMeta | undefined {
|
|
104
|
+
try {
|
|
105
|
+
const meta = JSON.parse(readFileSync(join(baseDir, "runs", id, "meta.json"), "utf8")) as SubagentRunMeta;
|
|
106
|
+
return typeof meta.id === "string" && typeof meta.status === "string" ? meta : undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ensureParentIndex(baseDir: string, directory: string, parentPid: number): void {
|
|
113
|
+
try {
|
|
114
|
+
readFileSync(join(directory, ".initialized"));
|
|
115
|
+
return;
|
|
116
|
+
} catch {
|
|
117
|
+
// Existing registries are backfilled once for this parent process.
|
|
118
|
+
}
|
|
119
|
+
const owned = readSubagentMetas(baseDir).filter((meta) => meta.spawnPid === parentPid);
|
|
120
|
+
mkdirSync(directory, { recursive: true });
|
|
121
|
+
for (const meta of owned) writeFileSync(join(directory, meta.id), "");
|
|
122
|
+
writeFileSync(join(directory, ".initialized"), "1");
|
|
123
|
+
}
|
|
124
|
+
|
|
85
125
|
export function subagentStatusToItem(
|
|
86
126
|
meta: SubagentRunMeta,
|
|
87
127
|
status: SubagentEffectiveStatus,
|
|
@@ -122,9 +162,7 @@ export function collectSubagentActivity(
|
|
|
122
162
|
): BackgroundProviderSnapshot {
|
|
123
163
|
const parentPid = options.parentPid ?? process.pid;
|
|
124
164
|
const processExists = options.processExists ?? defaultProcessExists;
|
|
125
|
-
const metas =
|
|
126
|
-
isCurrentParentSubagent(meta, parentPid),
|
|
127
|
-
);
|
|
165
|
+
const metas = readParentSubagentMetas(options.baseDir, parentPid);
|
|
128
166
|
|
|
129
167
|
return {
|
|
130
168
|
providerId: "subagents",
|
package/src/types.ts
CHANGED
|
@@ -47,6 +47,7 @@ export interface BackgroundActivityProvider {
|
|
|
47
47
|
id: string;
|
|
48
48
|
label?: string;
|
|
49
49
|
getActivity(ctx: ExtensionContext): BackgroundProviderSnapshot | Promise<BackgroundProviderSnapshot>;
|
|
50
|
+
onActivityChanged?(notify: () => void): () => void;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export interface ActivitySnapshot {
|