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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squad",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
5
5
  "type": "module",
6
6
  "scripts": {
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: { requestUpdate: () => void; dispose: () => void } | null = null;
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
- if (activeSquadId === cancelledId || (activeSquadId !== null && !store.loadSquad(activeSquadId))) {
201
- activeSquadId = null;
202
- }
203
- widgetState.squadId = activeSquadId;
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
- activeSquadId = squadId;
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
- activeSquadId = null;
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
- activeSquadId = squadId;
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
- activeSquadId = squad.id;
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
- activeSquadId = null;
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
- activeSquadId = null;
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
- activeSquadId = null;
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
- widgetControls?.requestUpdate();
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
- activeSquadId = squadId;
1637
-
1638
- // Update widget to show the new squad. The widget reads squadId on each
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
- activeSquadId = squadId;
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: () => void;
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
- render();
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
- render();
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
  },