pi-squad 0.17.0 → 0.17.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 +40 -54
- package/src/panel/squad-widget.ts +20 -9
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { validatePlan, PLAN_STRUCTURE_RULES } from "./plan-rules.js";
|
|
|
22
22
|
import { ADVISOR_SYSTEM_PROMPT, buildAdvisorConsultText, type AdvisorConsultInput } from "./advisor.js";
|
|
23
23
|
import { completeSimple, type Message, type TextContent } from "@earendil-works/pi-ai";
|
|
24
24
|
import { SquadPanel, type SquadPanelResult } from "./panel/squad-panel.js";
|
|
25
|
-
import { setupSquadWidget, type SquadWidgetState } from "./panel/squad-widget.js";
|
|
25
|
+
import { setupSquadWidget, type SquadWidgetControls, type SquadWidgetState } from "./panel/squad-widget.js";
|
|
26
26
|
import * as store from "./store.js";
|
|
27
27
|
import { debug, logError } from "./logger.js";
|
|
28
28
|
import { buildCompletionSummary, buildFailureSummary } from "./report.js";
|
|
@@ -53,7 +53,15 @@ let overlayOpen = false;
|
|
|
53
53
|
let uiCtx: import("@earendil-works/pi-coding-agent").ExtensionContext | null = null;
|
|
54
54
|
/** Component-based widget state + controls */
|
|
55
55
|
const widgetState: SquadWidgetState = { squadId: null, enabled: true };
|
|
56
|
-
let widgetControls:
|
|
56
|
+
let widgetControls: SquadWidgetControls | null = null;
|
|
57
|
+
|
|
58
|
+
/** Keep main-session focus, compact widget, and detail panel targeting identical. */
|
|
59
|
+
function focusSquad(squadId: string | null): void {
|
|
60
|
+
activeSquadId = squadId;
|
|
61
|
+
widgetState.squadId = squadId;
|
|
62
|
+
if (squadId) widgetState.enabled = true;
|
|
63
|
+
widgetControls?.refreshNow();
|
|
64
|
+
}
|
|
57
65
|
|
|
58
66
|
/** Format completion against active work while retaining cancelled history. */
|
|
59
67
|
function formatTaskProgress(tasks: Task[]): string {
|
|
@@ -197,11 +205,10 @@ function getActiveScheduler(): Scheduler | null {
|
|
|
197
205
|
|
|
198
206
|
/** Restore the single focus/widget invariant after destructive exact cancellation. */
|
|
199
207
|
function repairFocusAfterCancellation(cancelledId: string): void {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
widgetControls?.requestUpdate();
|
|
208
|
+
const nextFocus = activeSquadId === cancelledId || (activeSquadId !== null && !store.loadSquad(activeSquadId))
|
|
209
|
+
? null
|
|
210
|
+
: activeSquadId;
|
|
211
|
+
focusSquad(nextFocus);
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
function activeSuspendedAttentionForProject(cwd: string): Array<{ squadId: string; attention: SuspendedStallAttention }> {
|
|
@@ -218,10 +225,7 @@ function ensureScheduler(pi: ExtensionAPI, squadId: string, skillPaths: string[]
|
|
|
218
225
|
schedulers.set(squadId, scheduler);
|
|
219
226
|
wireSchedulerEvents(pi, scheduler, squadId);
|
|
220
227
|
}
|
|
221
|
-
|
|
222
|
-
widgetState.squadId = squadId;
|
|
223
|
-
widgetState.enabled = true;
|
|
224
|
-
widgetControls?.requestUpdate();
|
|
228
|
+
focusSquad(squadId);
|
|
225
229
|
return scheduler;
|
|
226
230
|
}
|
|
227
231
|
|
|
@@ -309,8 +313,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
309
313
|
if (activeSquadId) {
|
|
310
314
|
const squad = store.loadSquad(activeSquadId);
|
|
311
315
|
if (!squad) {
|
|
312
|
-
|
|
313
|
-
widgetState.squadId = null;
|
|
316
|
+
focusSquad(null);
|
|
314
317
|
if (durablePrompts.length > 0) {
|
|
315
318
|
return { systemPrompt: event.systemPrompt + "\n\n" + durablePrompts.join("\n\n") };
|
|
316
319
|
}
|
|
@@ -756,7 +759,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
756
759
|
if (!activeScheduler) {
|
|
757
760
|
return { content: [{ type: "text" as const, text: `Squad '${squadId}' has no active scheduler. Use resume, add_task, or resume_task to reconstruct it.` }], details: undefined };
|
|
758
761
|
}
|
|
759
|
-
|
|
762
|
+
focusSquad(squadId);
|
|
760
763
|
|
|
761
764
|
switch (params.action) {
|
|
762
765
|
case "add_task": {
|
|
@@ -882,6 +885,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
882
885
|
pi.on("session_start", async (_event, ctx) => {
|
|
883
886
|
uiCtx = ctx;
|
|
884
887
|
|
|
888
|
+
// A session replacement may not deliver shutdown first. Never carry a
|
|
889
|
+
// focused squad across projects, and never seed a new widget from stale state.
|
|
890
|
+
widgetControls?.dispose();
|
|
891
|
+
widgetControls = null;
|
|
892
|
+
const focused = activeSquadId ? store.loadSquad(activeSquadId) : null;
|
|
893
|
+
focusSquad(focused?.cwd === ctx.cwd ? activeSquadId : null);
|
|
894
|
+
|
|
885
895
|
// Install component-based widget
|
|
886
896
|
if (ctx.hasUI) {
|
|
887
897
|
widgetControls = setupSquadWidget(ctx, widgetState);
|
|
@@ -948,12 +958,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
948
958
|
}
|
|
949
959
|
await scheduler.start();
|
|
950
960
|
}
|
|
951
|
-
if (pendingMailSquads.length > 0)
|
|
952
|
-
activeSquadId = pendingMailSquads[0].id;
|
|
953
|
-
widgetState.squadId = activeSquadId;
|
|
954
|
-
widgetState.enabled = true;
|
|
955
|
-
widgetControls?.requestUpdate();
|
|
956
|
-
}
|
|
961
|
+
if (pendingMailSquads.length > 0) focusSquad(pendingMailSquads[0].id);
|
|
957
962
|
|
|
958
963
|
// Notify about paused squads only if they have real completed work
|
|
959
964
|
const paused = store.findActiveSquads()
|
|
@@ -979,10 +984,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
979
984
|
.filter((s) => s.cwd === ctx.cwd && s.status === "review");
|
|
980
985
|
if (pendingReviews.length > 0) {
|
|
981
986
|
const squad = pendingReviews.sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
982
|
-
|
|
983
|
-
widgetState.squadId = squad.id;
|
|
984
|
-
widgetState.enabled = true;
|
|
985
|
-
widgetControls?.requestUpdate();
|
|
987
|
+
focusSquad(squad.id);
|
|
986
988
|
const tasks = store.loadAllTasks(squad.id);
|
|
987
989
|
pi.sendMessage({
|
|
988
990
|
customType: "squad-review-required",
|
|
@@ -1021,13 +1023,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1021
1023
|
});
|
|
1022
1024
|
|
|
1023
1025
|
pi.on("session_shutdown", async () => {
|
|
1026
|
+
focusSquad(null);
|
|
1024
1027
|
widgetControls?.dispose();
|
|
1025
1028
|
widgetControls = null;
|
|
1026
1029
|
for (const [id, sched] of schedulers) {
|
|
1027
1030
|
await sched.stop();
|
|
1028
1031
|
}
|
|
1029
1032
|
schedulers.clear();
|
|
1030
|
-
activeSquadId = null;
|
|
1031
1033
|
uiCtx = null;
|
|
1032
1034
|
});
|
|
1033
1035
|
|
|
@@ -1252,8 +1254,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1252
1254
|
|
|
1253
1255
|
case "clear": {
|
|
1254
1256
|
if (activeSquadId) schedulers.delete(activeSquadId);
|
|
1255
|
-
|
|
1256
|
-
widgetState.squadId = null;
|
|
1257
|
+
focusSquad(null);
|
|
1257
1258
|
widgetControls?.dispose();
|
|
1258
1259
|
ctx.ui.notify("Squad view cleared", "info");
|
|
1259
1260
|
return;
|
|
@@ -1274,9 +1275,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1274
1275
|
await sched.stop();
|
|
1275
1276
|
}
|
|
1276
1277
|
schedulers.clear();
|
|
1277
|
-
|
|
1278
|
-
widgetState.squadId = null;
|
|
1279
|
-
widgetControls?.requestUpdate();
|
|
1278
|
+
focusSquad(null);
|
|
1280
1279
|
|
|
1281
1280
|
let count = 0;
|
|
1282
1281
|
for (const id of allSquadIds) {
|
|
@@ -1312,9 +1311,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1312
1311
|
await sched.stop();
|
|
1313
1312
|
}
|
|
1314
1313
|
schedulers.clear();
|
|
1315
|
-
|
|
1316
|
-
widgetState.squadId = null;
|
|
1317
|
-
widgetControls?.requestUpdate();
|
|
1314
|
+
focusSquad(null);
|
|
1318
1315
|
let count = 0;
|
|
1319
1316
|
for (const id of allSquadIds) {
|
|
1320
1317
|
fs.rmSync(store.getSquadDir(id), { recursive: true, force: true });
|
|
@@ -1332,11 +1329,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1332
1329
|
await sched.stop();
|
|
1333
1330
|
schedulers.delete(squad.id);
|
|
1334
1331
|
}
|
|
1335
|
-
if (activeSquadId === squad.id)
|
|
1336
|
-
activeSquadId = null;
|
|
1337
|
-
widgetState.squadId = null;
|
|
1338
|
-
widgetControls?.requestUpdate();
|
|
1339
|
-
}
|
|
1332
|
+
if (activeSquadId === squad.id) focusSquad(null);
|
|
1340
1333
|
fs.rmSync(store.getSquadDir(squad.id), { recursive: true, force: true });
|
|
1341
1334
|
ctx.ui.notify(`Deleted: ${squad.id}`, "info");
|
|
1342
1335
|
}
|
|
@@ -1358,10 +1351,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1358
1351
|
await sched.stop();
|
|
1359
1352
|
}
|
|
1360
1353
|
schedulers.clear();
|
|
1361
|
-
activeSquadId = null;
|
|
1362
|
-
widgetState.squadId = null;
|
|
1363
1354
|
widgetState.enabled = false;
|
|
1364
|
-
|
|
1355
|
+
focusSquad(null);
|
|
1365
1356
|
ctx.ui.notify("pi-squad disabled — all tools, widget, and system prompt injection stopped", "info");
|
|
1366
1357
|
return;
|
|
1367
1358
|
}
|
|
@@ -1633,13 +1624,9 @@ function activateSquadView(squadId: string, ctx: import("@earendil-works/pi-codi
|
|
|
1633
1624
|
return;
|
|
1634
1625
|
}
|
|
1635
1626
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
// render, so just updating the state and requesting a render is enough.
|
|
1640
|
-
widgetState.squadId = squadId;
|
|
1641
|
-
widgetState.enabled = true;
|
|
1642
|
-
widgetControls?.requestUpdate();
|
|
1627
|
+
// Selection is one atomic focus operation: panel, widget, status, and tools
|
|
1628
|
+
// must all target this exact squad before control returns to the caller.
|
|
1629
|
+
focusSquad(squadId);
|
|
1643
1630
|
|
|
1644
1631
|
// Compact notification — widget already shows full task details.
|
|
1645
1632
|
// Avoid large multi-line notifications that can break TUI layout.
|
|
@@ -1743,6 +1730,9 @@ function openPanel(
|
|
|
1743
1730
|
squadId: string,
|
|
1744
1731
|
): void {
|
|
1745
1732
|
if (overlayOpen) return;
|
|
1733
|
+
// Opening details also establishes authoritative focus. This covers callers
|
|
1734
|
+
// that reconstructed or targeted a scheduler without going through selector UI.
|
|
1735
|
+
focusSquad(squadId);
|
|
1746
1736
|
overlayOpen = true;
|
|
1747
1737
|
|
|
1748
1738
|
// The promise resolves when the panel calls done()
|
|
@@ -1914,12 +1904,8 @@ async function startSquad(
|
|
|
1914
1904
|
// Start scheduler
|
|
1915
1905
|
const scheduler = new Scheduler(squadId, skillPaths, schedulerSpawnContext);
|
|
1916
1906
|
schedulers.set(squadId, scheduler);
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
// Activate widget for this squad
|
|
1920
|
-
widgetState.squadId = squadId;
|
|
1921
|
-
widgetState.enabled = true;
|
|
1922
|
-
widgetControls?.requestUpdate();
|
|
1907
|
+
// Activate panel/widget/tool focus as one invariant.
|
|
1908
|
+
focusSquad(squadId);
|
|
1923
1909
|
|
|
1924
1910
|
// Wire up completion/escalation notifications to main agent.
|
|
1925
1911
|
wireSchedulerEvents(pi, scheduler, squadId);
|
|
@@ -43,6 +43,12 @@ export interface SquadWidgetState {
|
|
|
43
43
|
enabled: boolean;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface SquadWidgetControls {
|
|
47
|
+
requestUpdate: () => void;
|
|
48
|
+
refreshNow: () => void;
|
|
49
|
+
dispose: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
/**
|
|
47
53
|
* Set up the squad widget. Returns control functions.
|
|
48
54
|
*
|
|
@@ -53,11 +59,8 @@ export interface SquadWidgetState {
|
|
|
53
59
|
export function setupSquadWidget(
|
|
54
60
|
ctx: { ui: { setWidget: Function; setStatus: Function; [key: string]: any }; hasUI?: boolean },
|
|
55
61
|
state: SquadWidgetState,
|
|
56
|
-
): {
|
|
57
|
-
requestUpdate: () =>
|
|
58
|
-
dispose: () => void;
|
|
59
|
-
} {
|
|
60
|
-
if (!ctx.hasUI) return { requestUpdate: () => {}, dispose: () => {} };
|
|
62
|
+
): SquadWidgetControls {
|
|
63
|
+
if (!ctx.hasUI) return { requestUpdate: () => {}, refreshNow: () => {}, dispose: () => {} };
|
|
61
64
|
|
|
62
65
|
let durationTimer: ReturnType<typeof setInterval> | null = null;
|
|
63
66
|
let renderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -269,9 +272,14 @@ export function setupSquadWidget(
|
|
|
269
272
|
}
|
|
270
273
|
}
|
|
271
274
|
|
|
275
|
+
function refreshNow(): void {
|
|
276
|
+
if (renderTimer) { clearTimeout(renderTimer); renderTimer = null; }
|
|
277
|
+
render();
|
|
278
|
+
manageDurationTimer();
|
|
279
|
+
}
|
|
280
|
+
|
|
272
281
|
// Initial render
|
|
273
|
-
|
|
274
|
-
manageDurationTimer();
|
|
282
|
+
refreshNow();
|
|
275
283
|
|
|
276
284
|
return {
|
|
277
285
|
requestUpdate(): void {
|
|
@@ -279,14 +287,17 @@ export function setupSquadWidget(
|
|
|
279
287
|
if (renderTimer) return;
|
|
280
288
|
renderTimer = setTimeout(() => {
|
|
281
289
|
renderTimer = null;
|
|
282
|
-
|
|
283
|
-
manageDurationTimer();
|
|
290
|
+
refreshNow();
|
|
284
291
|
}, 50);
|
|
285
292
|
},
|
|
293
|
+
refreshNow,
|
|
286
294
|
dispose(): void {
|
|
287
295
|
if (durationTimer) { clearInterval(durationTimer); durationTimer = null; }
|
|
288
296
|
if (renderTimer) { clearTimeout(renderTimer); renderTimer = null; }
|
|
289
297
|
lastCacheKey = "";
|
|
298
|
+
currentLines = [];
|
|
299
|
+
widgetInstalled = false;
|
|
300
|
+
widgetComponent = null;
|
|
290
301
|
ctx.ui.setWidget("squad-tasks", undefined);
|
|
291
302
|
ctx.ui.setStatus("squad", undefined);
|
|
292
303
|
},
|