pi-squad 0.1.0 → 0.4.14
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/README.md +248 -70
- package/package.json +7 -3
- package/src/index.ts +559 -320
- package/src/panel/message-view.ts +100 -125
- package/src/panel/squad-panel.ts +94 -148
- package/src/panel/squad-widget.ts +192 -0
- package/src/panel/task-list.ts +38 -28
- package/src/planner.ts +1 -1
- package/src/protocol.ts +37 -0
- package/src/scheduler.ts +188 -3
- package/src/skills/supervisor/SKILL.md +111 -0
- package/src/skills/verification/SKILL.md +32 -0
- package/src/store.ts +29 -1
- package/src/types.ts +11 -0
package/src/index.ts
CHANGED
|
@@ -18,20 +18,34 @@ import type { Squad, Task, SquadConfig, PlannerOutput } from "./types.js";
|
|
|
18
18
|
import { DEFAULT_SQUAD_CONFIG } from "./types.js";
|
|
19
19
|
import { Scheduler, type SchedulerEvent } from "./scheduler.js";
|
|
20
20
|
import { runPlanner } from "./planner.js";
|
|
21
|
-
import { SquadPanel } from "./panel/squad-panel.js";
|
|
21
|
+
import { SquadPanel, type SquadPanelResult } from "./panel/squad-panel.js";
|
|
22
|
+
import { setupSquadWidget, type SquadWidgetState } from "./panel/squad-widget.js";
|
|
22
23
|
import * as store from "./store.js";
|
|
23
24
|
|
|
24
25
|
// ============================================================================
|
|
25
26
|
// State
|
|
26
27
|
// ============================================================================
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
/** Master switch — when false, all squad tools, hooks, and widget are disabled */
|
|
30
|
+
let squadEnabled = true;
|
|
31
|
+
/** Registry of all running schedulers — supports multiple concurrent squads */
|
|
32
|
+
const schedulers = new Map<string, Scheduler>();
|
|
33
|
+
/** The currently viewed/focused squad (for widget, panel, status) */
|
|
29
34
|
let activeSquadId: string | null = null;
|
|
30
|
-
|
|
35
|
+
/** Whether an overlay panel is currently open (prevents double-open) */
|
|
36
|
+
let overlayOpen = false;
|
|
31
37
|
/** Stored ExtensionContext for widget updates from background scheduler events */
|
|
32
38
|
let uiCtx: import("@mariozechner/pi-coding-agent").ExtensionContext | null = null;
|
|
33
|
-
/**
|
|
34
|
-
|
|
39
|
+
/** Component-based widget state + controls */
|
|
40
|
+
const widgetState: SquadWidgetState = { squadId: null, enabled: true };
|
|
41
|
+
let widgetControls: { requestUpdate: () => void; dispose: () => void } | null = null;
|
|
42
|
+
|
|
43
|
+
/** Get the active scheduler (for the focused squad) */
|
|
44
|
+
function getActiveScheduler(): Scheduler | null {
|
|
45
|
+
if (!activeSquadId) return null;
|
|
46
|
+
return schedulers.get(activeSquadId) || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
|
|
36
50
|
// ============================================================================
|
|
37
51
|
// Extension Entry
|
|
@@ -53,37 +67,55 @@ export default function (pi: ExtensionAPI) {
|
|
|
53
67
|
// Context Injection — give main agent awareness of squad state
|
|
54
68
|
// =========================================================================
|
|
55
69
|
|
|
56
|
-
// Inject squad
|
|
70
|
+
// Inject squad awareness before each LLM call
|
|
57
71
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
58
|
-
if (!
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
if (!squadEnabled) return;
|
|
73
|
+
|
|
74
|
+
// When a squad is active, inject its status
|
|
75
|
+
if (activeSquadId) {
|
|
76
|
+
const squad = store.loadSquad(activeSquadId);
|
|
77
|
+
if (!squad) return;
|
|
78
|
+
const tasks = store.loadAllTasks(activeSquadId);
|
|
79
|
+
if (tasks.length === 0) return;
|
|
80
|
+
|
|
81
|
+
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
82
|
+
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
83
|
+
|
|
84
|
+
const taskLines = tasks.map((t) => {
|
|
85
|
+
const icon = t.status === "done" ? "✓" : t.status === "in_progress" ? "⏳" : t.status === "failed" ? "✗" : t.status === "blocked" ? "◻" : "·";
|
|
86
|
+
let line = ` ${icon} ${t.id} (${t.agent}) [${t.status}]`;
|
|
87
|
+
if (t.output) line += ` — ${t.output.split("\n")[0].slice(0, 80)}`;
|
|
88
|
+
if (t.error) line += ` ERROR: ${t.error.slice(0, 60)}`;
|
|
89
|
+
return line;
|
|
90
|
+
}).join("\n");
|
|
91
|
+
|
|
92
|
+
const squadContext = [
|
|
93
|
+
`<squad_status>`,
|
|
94
|
+
`Squad: ${squad.id} — ${squad.goal}`,
|
|
95
|
+
`Status: ${squad.status} | ${doneCount}/${tasks.length} tasks | $${totalCost.toFixed(2)}`,
|
|
96
|
+
taskLines,
|
|
97
|
+
`</squad_status>`,
|
|
98
|
+
`You have an active squad. Use squad_message to talk to agents, squad_status for details, squad_modify to change tasks.`,
|
|
99
|
+
].join("\n");
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
systemPrompt: event.systemPrompt + "\n\n" + squadContext,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// When NO squad is active, nudge the agent to consider using squad for complex tasks
|
|
107
|
+
const squadNudge = [
|
|
108
|
+
`<squad_hint>`,
|
|
109
|
+
`You have the "squad" tool available for multi-agent collaboration.`,
|
|
110
|
+
`Use it when the user's request involves multiple concerns (e.g. backend + frontend + tests + docs),`,
|
|
111
|
+
`would benefit from parallel execution, or is too large for a single agent context.`,
|
|
112
|
+
`The squad tool decomposes work into tasks, assigns specialist agents, and runs them in parallel.`,
|
|
113
|
+
`When in doubt about whether a task is complex enough, prefer using squad — it handles the coordination for you.`,
|
|
114
|
+
`</squad_hint>`,
|
|
82
115
|
].join("\n");
|
|
83
116
|
|
|
84
|
-
// Append to system prompt so the agent always sees it
|
|
85
117
|
return {
|
|
86
|
-
systemPrompt: event.systemPrompt + "\n\n" +
|
|
118
|
+
systemPrompt: event.systemPrompt + "\n\n" + squadNudge,
|
|
87
119
|
};
|
|
88
120
|
});
|
|
89
121
|
|
|
@@ -96,9 +128,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
96
128
|
label: "Squad",
|
|
97
129
|
description: [
|
|
98
130
|
"Start a multi-agent squad for complex, multi-step tasks.",
|
|
99
|
-
"
|
|
100
|
-
"has natural parallelism, or would overflow a single agent's context.",
|
|
101
|
-
"
|
|
131
|
+
"ALWAYS use squad when a task involves 2+ of: backend, frontend, testing, docs, devops, security.",
|
|
132
|
+
"Use when a task has natural parallelism, touches multiple files/systems, or would overflow a single agent's context.",
|
|
133
|
+
"Examples that NEED squad: 'build a REST API with auth and tests', 'add a feature with frontend + backend + docs',",
|
|
134
|
+
"'refactor the auth system and update tests', 'set up CI/CD with Docker and deployment'.",
|
|
135
|
+
"Do NOT use for simple single-file changes, quick bug fixes, or tasks a single agent can handle in a few minutes.",
|
|
136
|
+
"When in doubt about complexity, use squad — it's better to parallelize than to do everything sequentially.",
|
|
102
137
|
"Non-blocking: returns immediately with the plan while agents work in background.",
|
|
103
138
|
].join(" "),
|
|
104
139
|
parameters: Type.Object({
|
|
@@ -132,18 +167,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
132
167
|
}),
|
|
133
168
|
|
|
134
169
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
170
|
+
if (!squadEnabled) return { content: [{ type: "text" as const, text: "Squad is disabled. Use /squad enable to re-enable." }] };
|
|
135
171
|
if (!uiCtx) uiCtx = ctx;
|
|
136
172
|
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
content: [
|
|
140
|
-
{
|
|
141
|
-
type: "text" as const,
|
|
142
|
-
text: `A squad is already running (${activeSquadId}). Use squad_status to check progress, or squad_modify to cancel it.`,
|
|
143
|
-
},
|
|
144
|
-
],
|
|
145
|
-
};
|
|
146
|
-
}
|
|
173
|
+
// Multiple squads can run concurrently — no guard needed
|
|
147
174
|
|
|
148
175
|
const squadId = store.makeTaskId(params.goal);
|
|
149
176
|
if (store.squadExists(squadId)) {
|
|
@@ -182,9 +209,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
182
209
|
}
|
|
183
210
|
|
|
184
211
|
// If scheduler is running, force a context refresh
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
212
|
+
const sched = schedulers.get(id!);
|
|
213
|
+
if (sched) sched.updateContext();
|
|
188
214
|
|
|
189
215
|
const context = store.loadContext(id);
|
|
190
216
|
if (!context) {
|
|
@@ -234,6 +260,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
234
260
|
}),
|
|
235
261
|
|
|
236
262
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
263
|
+
const activeScheduler = getActiveScheduler();
|
|
237
264
|
if (!activeScheduler || !activeSquadId) {
|
|
238
265
|
return { content: [{ type: "text" as const, text: "No active squad." }] };
|
|
239
266
|
}
|
|
@@ -249,7 +276,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
249
276
|
return { content: [{ type: "text" as const, text: "Could not determine target task. Provide taskId or an agent name that is currently running." }] };
|
|
250
277
|
}
|
|
251
278
|
|
|
252
|
-
const sent = await activeScheduler
|
|
279
|
+
const sent = await activeScheduler!.sendHumanMessage(taskId, params.message);
|
|
253
280
|
const status = sent ? "delivered" : "queued for when the agent starts";
|
|
254
281
|
|
|
255
282
|
return { content: [{ type: "text" as const, text: `Message ${status}: "${params.message}"` }] };
|
|
@@ -290,9 +317,86 @@ export default function (pi: ExtensionAPI) {
|
|
|
290
317
|
),
|
|
291
318
|
}),
|
|
292
319
|
|
|
293
|
-
async execute(_toolCallId, params, _signal, _onUpdate,
|
|
320
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
321
|
+
// Resume can work without an active scheduler — it recreates one from disk
|
|
322
|
+
if (params.action === "resume") {
|
|
323
|
+
// Find a squad to resume: use activeSquadId or find the latest paused one
|
|
324
|
+
const squadId = activeSquadId || store.findActiveSquads()
|
|
325
|
+
.filter((s) => s.cwd === ctx.cwd && s.status === "paused")
|
|
326
|
+
.sort((a, b) => b.created.localeCompare(a.created))[0]?.id;
|
|
327
|
+
|
|
328
|
+
if (!squadId) {
|
|
329
|
+
return { content: [{ type: "text" as const, text: "No paused squad found to resume." }] };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Create a fresh scheduler if needed
|
|
333
|
+
if (!schedulers.has(squadId)) {
|
|
334
|
+
const scheduler = new Scheduler(squadId, squadSkillPaths);
|
|
335
|
+
schedulers.set(squadId, scheduler);
|
|
336
|
+
activeSquadId = squadId;
|
|
337
|
+
|
|
338
|
+
// Activate widget
|
|
339
|
+
widgetState.squadId = squadId;
|
|
340
|
+
widgetState.enabled = true;
|
|
341
|
+
widgetControls?.requestUpdate();
|
|
342
|
+
|
|
343
|
+
// Wire up events (same as startSquad)
|
|
344
|
+
scheduler.onEvent((event: SchedulerEvent) => {
|
|
345
|
+
forceWidgetUpdate();
|
|
346
|
+
switch (event.type) {
|
|
347
|
+
case "squad_completed": {
|
|
348
|
+
const tasks = store.loadAllTasks(squadId);
|
|
349
|
+
const summary = tasks
|
|
350
|
+
.filter((t) => t.status === "done")
|
|
351
|
+
.map((t) => `- ${t.id} (${t.agent}): ${t.output?.slice(0, 150) || "done"}`)
|
|
352
|
+
.join("\n");
|
|
353
|
+
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
354
|
+
const s = schedulers.get(squadId); if (s) s.updateContext();
|
|
355
|
+
pi.sendMessage({
|
|
356
|
+
customType: "squad-completed",
|
|
357
|
+
content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\nSummary:\n${summary}\n\nTotal cost: $${totalCost.toFixed(4)}`,
|
|
358
|
+
display: true,
|
|
359
|
+
});
|
|
360
|
+
schedulers.delete(squadId);
|
|
361
|
+
forceWidgetUpdate();
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "squad_failed": {
|
|
365
|
+
const tasks = store.loadAllTasks(squadId);
|
|
366
|
+
const failed = tasks.filter((t) => t.status === "failed");
|
|
367
|
+
const done = tasks.filter((t) => t.status === "done");
|
|
368
|
+
pi.sendMessage({
|
|
369
|
+
customType: "squad-failed",
|
|
370
|
+
content: `[squad] Squad "${squadId}" has stalled. ${done.length}/${tasks.length} done, ${failed.length} failed.\nFailed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}`,
|
|
371
|
+
display: true,
|
|
372
|
+
}, { triggerTurn: true });
|
|
373
|
+
forceWidgetUpdate();
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case "escalation": {
|
|
377
|
+
pi.sendMessage({
|
|
378
|
+
customType: "squad-escalation",
|
|
379
|
+
content: `[squad] Agent '${event.agentName}' on task '${event.taskId}' needs attention:\n${event.message}`,
|
|
380
|
+
display: true,
|
|
381
|
+
}, { triggerTurn: true });
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const resumeSched = schedulers.get(squadId)!;
|
|
389
|
+
resumeSched.resume().catch((err) => {
|
|
390
|
+
console.error(`[squad] Resume error: ${(err as Error).message}`);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const tasks = store.loadAllTasks(squadId);
|
|
394
|
+
const done = tasks.filter(t => t.status === "done").length;
|
|
395
|
+
return { content: [{ type: "text" as const, text: `Squad "${squadId}" resumed (${done}/${tasks.length} done). Agents restarting in background.` }] };
|
|
396
|
+
}
|
|
397
|
+
|
|
294
398
|
if (!activeScheduler || !activeSquadId) {
|
|
295
|
-
return { content: [{ type: "text" as const, text: "No active squad." }] };
|
|
399
|
+
return { content: [{ type: "text" as const, text: "No active squad. Use squad_modify with action 'resume' to resume a paused squad, or start a new one with the squad tool." }] };
|
|
296
400
|
}
|
|
297
401
|
|
|
298
402
|
switch (params.action) {
|
|
@@ -333,7 +437,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
333
437
|
|
|
334
438
|
case "resume_task": {
|
|
335
439
|
if (!params.taskId) return { content: [{ type: "text" as const, text: "Provide taskId." }] };
|
|
336
|
-
|
|
440
|
+
activeScheduler.resumeTask(params.taskId).catch((err) => {
|
|
441
|
+
console.error(`[squad] Resume task error: ${(err as Error).message}`);
|
|
442
|
+
});
|
|
337
443
|
return { content: [{ type: "text" as const, text: `Task '${params.taskId}' resumed.` }] };
|
|
338
444
|
}
|
|
339
445
|
|
|
@@ -348,7 +454,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
348
454
|
}
|
|
349
455
|
|
|
350
456
|
case "resume": {
|
|
351
|
-
|
|
457
|
+
// Handled above (before the activeScheduler guard)
|
|
352
458
|
return { content: [{ type: "text" as const, text: "Squad resumed." }] };
|
|
353
459
|
}
|
|
354
460
|
|
|
@@ -359,7 +465,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
359
465
|
squad.status = "failed";
|
|
360
466
|
store.saveSquad(squad);
|
|
361
467
|
}
|
|
362
|
-
|
|
468
|
+
schedulers.delete(activeSquadId);
|
|
363
469
|
activeSquadId = null;
|
|
364
470
|
return { content: [{ type: "text" as const, text: "Squad cancelled." }] };
|
|
365
471
|
}
|
|
@@ -376,40 +482,72 @@ export default function (pi: ExtensionAPI) {
|
|
|
376
482
|
|
|
377
483
|
pi.on("session_start", async (_event, ctx) => {
|
|
378
484
|
uiCtx = ctx;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
485
|
+
|
|
486
|
+
// Install component-based widget
|
|
487
|
+
if (ctx.hasUI) {
|
|
488
|
+
widgetControls = setupSquadWidget(ctx, widgetState);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Clean up orphaned squads from crashed sessions:
|
|
492
|
+
// If a squad is "running" but has no live scheduler, its parent died.
|
|
493
|
+
// Suspend in-progress tasks and mark the squad as paused so it doesn't
|
|
494
|
+
// block new squads or trigger confusing followUp messages.
|
|
495
|
+
const orphaned = store.findActiveSquads()
|
|
496
|
+
.filter((s) => s.cwd === ctx.cwd && s.status === "running");
|
|
497
|
+
for (const squad of orphaned) {
|
|
498
|
+
const tasks = store.loadAllTasks(squad.id);
|
|
499
|
+
let hadInProgress = false;
|
|
500
|
+
for (const task of tasks) {
|
|
501
|
+
if (task.status === "in_progress") {
|
|
502
|
+
store.updateTaskStatus(squad.id, task.id, "suspended");
|
|
503
|
+
hadInProgress = true;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (hadInProgress) {
|
|
507
|
+
squad.status = "paused";
|
|
508
|
+
store.saveSquad(squad);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Notify about paused squads only if they have real completed work
|
|
513
|
+
const paused = store.findActiveSquads()
|
|
514
|
+
.filter((s) => s.cwd === ctx.cwd && s.status === "paused");
|
|
515
|
+
if (paused.length > 0) {
|
|
516
|
+
const squad = paused[0];
|
|
517
|
+
const tasks = store.loadAllTasks(squad.id);
|
|
518
|
+
const done = tasks.filter(t => t.status === "done").length;
|
|
519
|
+
// Only notify if at least 1 task completed — worth resuming
|
|
520
|
+
if (done > 0) {
|
|
521
|
+
pi.sendMessage({
|
|
522
|
+
customType: "squad-paused",
|
|
523
|
+
content: `[squad] Found paused squad "${squad.id}" (${squad.goal}) — ${done}/${tasks.length} done. ` +
|
|
524
|
+
`Use squad_modify with action "resume" to continue, or start a new squad.`,
|
|
525
|
+
display: true,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
389
528
|
}
|
|
390
529
|
|
|
391
530
|
// Register Ctrl+Q terminal input handler for panel toggle
|
|
392
531
|
if (ctx.hasUI) {
|
|
393
532
|
ctx.ui.onTerminalInput((data) => {
|
|
394
533
|
if (data === "\x11") {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (activeSquadId) {
|
|
408
|
-
const sched = activeScheduler || new Scheduler(activeSquadId, squadSkillPaths);
|
|
409
|
-
createPanel(ctx, sched, activeSquadId);
|
|
534
|
+
// If overlay is already open, let the panel's own handler deal with it
|
|
535
|
+
if (overlayOpen) return undefined;
|
|
536
|
+
|
|
537
|
+
// Auto-pick a squad if none active
|
|
538
|
+
if (!activeSquadId) {
|
|
539
|
+
const latest = store.findLatestSquad(ctx.cwd)
|
|
540
|
+
|| store.listSquads().map((id) => store.loadSquad(id)).filter((s): s is Squad => s !== null).sort((a, b) => b.created.localeCompare(a.created))[0];
|
|
541
|
+
if (latest) {
|
|
542
|
+
activateSquadView(latest.id, ctx);
|
|
543
|
+
} else {
|
|
544
|
+
ctx.ui.notify("No squads found. Use /squad or the squad tool.", "info");
|
|
545
|
+
return { consume: true };
|
|
410
546
|
}
|
|
411
|
-
}
|
|
412
|
-
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (activeSquadId) {
|
|
550
|
+
openPanel(ctx, schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths), activeSquadId);
|
|
413
551
|
}
|
|
414
552
|
return { consume: true };
|
|
415
553
|
}
|
|
@@ -419,16 +557,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
419
557
|
});
|
|
420
558
|
|
|
421
559
|
pi.on("session_shutdown", async () => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
if (activeScheduler) {
|
|
428
|
-
await activeScheduler.stop();
|
|
429
|
-
activeScheduler = null;
|
|
430
|
-
activeSquadId = null;
|
|
560
|
+
widgetControls?.dispose();
|
|
561
|
+
widgetControls = null;
|
|
562
|
+
for (const [id, sched] of schedulers) {
|
|
563
|
+
await sched.stop();
|
|
431
564
|
}
|
|
565
|
+
schedulers.clear();
|
|
566
|
+
activeSquadId = null;
|
|
432
567
|
uiCtx = null;
|
|
433
568
|
});
|
|
434
569
|
|
|
@@ -437,17 +572,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
437
572
|
// =========================================================================
|
|
438
573
|
|
|
439
574
|
pi.registerCommand("squad", {
|
|
440
|
-
description: "Browse, select, and manage squads. Usage: /squad [list|all|select|msg|widget|panel|cancel|clear]",
|
|
575
|
+
description: "Browse, select, and manage squads. Usage: /squad [list|all|select|agents|msg|widget|panel|cancel|clear]",
|
|
441
576
|
getArgumentCompletions: (prefix) => {
|
|
442
577
|
const subs = [
|
|
443
578
|
{ value: "list", label: "list", description: "List squads for current project" },
|
|
444
579
|
{ value: "all", label: "all", description: "List all squads, select to activate" },
|
|
445
580
|
{ value: "select", label: "select", description: "Pick a squad to view (interactive)" },
|
|
581
|
+
{ value: "agents", label: "agents", description: "List, view, or edit agent definitions" },
|
|
446
582
|
{ value: "msg", label: "msg", description: "Send message to agent: /squad msg [agent] text" },
|
|
447
583
|
{ value: "widget", label: "widget", description: "Toggle live widget" },
|
|
448
584
|
{ value: "panel", label: "panel", description: "Toggle overlay panel" },
|
|
449
585
|
{ value: "cancel", label: "cancel", description: "Cancel running squad" },
|
|
450
586
|
{ value: "clear", label: "clear", description: "Dismiss widget and deactivate squad" },
|
|
587
|
+
{ value: "cleanup", label: "cleanup", description: "Delete squad data (select or all)" },
|
|
588
|
+
{ value: "enable", label: "enable", description: "Enable pi-squad (tools, widget, system prompt)" },
|
|
589
|
+
{ value: "disable", label: "disable", description: "Disable pi-squad completely" },
|
|
451
590
|
];
|
|
452
591
|
return subs.filter((s) => s.value.startsWith(prefix));
|
|
453
592
|
},
|
|
@@ -507,21 +646,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
507
646
|
}
|
|
508
647
|
|
|
509
648
|
case "widget": {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
// If no active squad, try to pick one
|
|
649
|
+
widgetState.enabled = !widgetState.enabled;
|
|
650
|
+
if (widgetState.enabled) {
|
|
513
651
|
if (!activeSquadId) {
|
|
514
652
|
const latest = store.findLatestSquad(ctx.cwd);
|
|
515
653
|
if (latest) activateSquadView(latest.id, ctx);
|
|
516
654
|
}
|
|
517
|
-
updateWidget();
|
|
518
|
-
ctx.ui.notify("Squad widget enabled", "info");
|
|
519
|
-
} else {
|
|
520
|
-
widgetEnabled = false;
|
|
521
|
-
ctx.ui.setWidget("squad-tasks", undefined);
|
|
522
|
-
ctx.ui.setStatus("squad", undefined);
|
|
523
|
-
ctx.ui.notify("Squad widget disabled", "info");
|
|
524
655
|
}
|
|
656
|
+
// requestUpdate handles both enable (renders) and disable (clears)
|
|
657
|
+
widgetControls?.requestUpdate();
|
|
658
|
+
ctx.ui.notify(`Squad widget ${widgetState.enabled ? "enabled" : "disabled"}`, "info");
|
|
525
659
|
return;
|
|
526
660
|
}
|
|
527
661
|
|
|
@@ -536,12 +670,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
536
670
|
return;
|
|
537
671
|
}
|
|
538
672
|
}
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
createPanel(ctx, sched, activeSquadId);
|
|
543
|
-
} else if (activePanel) {
|
|
544
|
-
activePanel.toggleFocus();
|
|
673
|
+
if (activeSquadId) {
|
|
674
|
+
const sched = schedulers.get(activeSquadId) || new Scheduler(activeSquadId, squadSkillPaths);
|
|
675
|
+
openPanel(ctx, sched, activeSquadId);
|
|
545
676
|
}
|
|
546
677
|
return;
|
|
547
678
|
}
|
|
@@ -598,8 +729,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
598
729
|
}
|
|
599
730
|
}
|
|
600
731
|
|
|
601
|
-
|
|
602
|
-
|
|
732
|
+
const msgSched = getActiveScheduler();
|
|
733
|
+
if (msgSched) {
|
|
734
|
+
await msgSched.sendHumanMessage(targetTaskId, msgText);
|
|
603
735
|
ctx.ui.notify(`Sent to ${targetAgent}: "${msgText.slice(0, 50)}"`, "info");
|
|
604
736
|
} else {
|
|
605
737
|
store.appendMessage(activeSquadId, targetTaskId, {
|
|
@@ -610,36 +742,254 @@ export default function (pi: ExtensionAPI) {
|
|
|
610
742
|
});
|
|
611
743
|
ctx.ui.notify(`Logged to ${targetTaskId} (agent not running)`, "info");
|
|
612
744
|
}
|
|
613
|
-
|
|
745
|
+
forceWidgetUpdate();
|
|
614
746
|
return;
|
|
615
747
|
}
|
|
616
748
|
|
|
617
749
|
case "cancel": {
|
|
618
|
-
|
|
750
|
+
const cancelSched = getActiveScheduler();
|
|
751
|
+
if (!cancelSched) {
|
|
619
752
|
ctx.ui.notify("No running squad to cancel", "info");
|
|
620
753
|
return;
|
|
621
754
|
}
|
|
622
|
-
await
|
|
755
|
+
await cancelSched.stop();
|
|
623
756
|
const squad = store.loadSquad(activeSquadId!);
|
|
624
757
|
if (squad) { squad.status = "failed"; store.saveSquad(squad); }
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
updateWidget();
|
|
758
|
+
if (activeSquadId) schedulers.delete(activeSquadId);
|
|
759
|
+
forceWidgetUpdate();
|
|
628
760
|
ctx.ui.notify("Squad cancelled", "info");
|
|
629
761
|
return;
|
|
630
762
|
}
|
|
631
763
|
|
|
632
764
|
case "clear": {
|
|
765
|
+
if (activeSquadId) schedulers.delete(activeSquadId);
|
|
633
766
|
activeSquadId = null;
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (activePanel) { activePanel.dispose(); activePanel = null; }
|
|
637
|
-
ctx.ui.setWidget("squad-tasks", undefined);
|
|
638
|
-
ctx.ui.setStatus("squad", undefined);
|
|
767
|
+
widgetState.squadId = null;
|
|
768
|
+
widgetControls?.dispose();
|
|
639
769
|
ctx.ui.notify("Squad view cleared", "info");
|
|
640
770
|
return;
|
|
641
771
|
}
|
|
642
772
|
|
|
773
|
+
case "cleanup": {
|
|
774
|
+
const cleanupArg = parts[1];
|
|
775
|
+
const allSquadIds = store.listSquads();
|
|
776
|
+
|
|
777
|
+
if (allSquadIds.length === 0) {
|
|
778
|
+
ctx.ui.notify("No squads to clean up", "info");
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (cleanupArg === "all") {
|
|
783
|
+
// Stop any running schedulers first
|
|
784
|
+
for (const [id, sched] of schedulers) {
|
|
785
|
+
await sched.stop();
|
|
786
|
+
}
|
|
787
|
+
schedulers.clear();
|
|
788
|
+
activeSquadId = null;
|
|
789
|
+
widgetState.squadId = null;
|
|
790
|
+
widgetControls?.requestUpdate();
|
|
791
|
+
|
|
792
|
+
let count = 0;
|
|
793
|
+
for (const id of allSquadIds) {
|
|
794
|
+
fs.rmSync(store.getSquadDir(id), { recursive: true, force: true });
|
|
795
|
+
count++;
|
|
796
|
+
}
|
|
797
|
+
ctx.ui.notify(`Deleted ${count} squad(s)`, "info");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Interactive: pick squads to delete
|
|
802
|
+
const squads = allSquadIds
|
|
803
|
+
.map((id) => store.loadSquad(id))
|
|
804
|
+
.filter((s): s is Squad => s !== null)
|
|
805
|
+
.sort((a, b) => b.created.localeCompare(a.created));
|
|
806
|
+
|
|
807
|
+
const options = [
|
|
808
|
+
"🗑 Delete ALL squads",
|
|
809
|
+
...squads.map((s) => {
|
|
810
|
+
const tasks = store.loadAllTasks(s.id);
|
|
811
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
812
|
+
const cost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
813
|
+
const icon = s.status === "done" ? "✓" : s.status === "running" ? "⏳" : s.status === "failed" ? "✗" : "·";
|
|
814
|
+
return `${icon} ${s.id} [${s.status}] ${done}/${tasks.length} $${cost.toFixed(2)}`;
|
|
815
|
+
}),
|
|
816
|
+
];
|
|
817
|
+
|
|
818
|
+
const choice = await ctx.ui.select("Delete squad data", options);
|
|
819
|
+
if (!choice) return;
|
|
820
|
+
|
|
821
|
+
if (choice.startsWith("🗑")) {
|
|
822
|
+
// Delete all
|
|
823
|
+
for (const [id, sched] of schedulers) {
|
|
824
|
+
await sched.stop();
|
|
825
|
+
}
|
|
826
|
+
schedulers.clear();
|
|
827
|
+
activeSquadId = null;
|
|
828
|
+
widgetState.squadId = null;
|
|
829
|
+
widgetControls?.requestUpdate();
|
|
830
|
+
let count = 0;
|
|
831
|
+
for (const id of allSquadIds) {
|
|
832
|
+
fs.rmSync(store.getSquadDir(id), { recursive: true, force: true });
|
|
833
|
+
count++;
|
|
834
|
+
}
|
|
835
|
+
ctx.ui.notify(`Deleted ${count} squad(s)`, "info");
|
|
836
|
+
} else {
|
|
837
|
+
// Delete selected
|
|
838
|
+
const idx = options.indexOf(choice) - 1; // -1 for the "Delete ALL" option
|
|
839
|
+
if (idx >= 0 && idx < squads.length) {
|
|
840
|
+
const squad = squads[idx];
|
|
841
|
+
// Stop scheduler if running
|
|
842
|
+
const sched = schedulers.get(squad.id);
|
|
843
|
+
if (sched) {
|
|
844
|
+
await sched.stop();
|
|
845
|
+
schedulers.delete(squad.id);
|
|
846
|
+
}
|
|
847
|
+
if (activeSquadId === squad.id) {
|
|
848
|
+
activeSquadId = null;
|
|
849
|
+
widgetState.squadId = null;
|
|
850
|
+
widgetControls?.requestUpdate();
|
|
851
|
+
}
|
|
852
|
+
fs.rmSync(store.getSquadDir(squad.id), { recursive: true, force: true });
|
|
853
|
+
ctx.ui.notify(`Deleted: ${squad.id}`, "info");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
case "enable": {
|
|
860
|
+
squadEnabled = true;
|
|
861
|
+
widgetControls?.requestUpdate();
|
|
862
|
+
ctx.ui.notify("pi-squad enabled — tools, widget, and system prompt active", "info");
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
case "disable": {
|
|
867
|
+
squadEnabled = false;
|
|
868
|
+
// Stop all running schedulers
|
|
869
|
+
for (const [id, sched] of schedulers) {
|
|
870
|
+
await sched.stop();
|
|
871
|
+
}
|
|
872
|
+
schedulers.clear();
|
|
873
|
+
activeSquadId = null;
|
|
874
|
+
widgetState.squadId = null;
|
|
875
|
+
widgetState.enabled = false;
|
|
876
|
+
widgetControls?.requestUpdate();
|
|
877
|
+
ctx.ui.notify("pi-squad disabled — all tools, widget, and system prompt injection stopped", "info");
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
case "agents": {
|
|
882
|
+
const agentArg = parts[1];
|
|
883
|
+
const allAgents = store.loadAllAgentDefs(ctx.cwd);
|
|
884
|
+
|
|
885
|
+
if (!agentArg) {
|
|
886
|
+
// List all agents — interactive selector
|
|
887
|
+
if (allAgents.length === 0) {
|
|
888
|
+
ctx.ui.notify("No agents found", "info");
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const options = allAgents.map((a) => {
|
|
892
|
+
const model = a.model ? ` [${a.model}]` : " [default]";
|
|
893
|
+
const status = a.disabled ? " ✗ disabled" : "";
|
|
894
|
+
return `${a.name} — ${a.role}${model}${status}`;
|
|
895
|
+
});
|
|
896
|
+
const choice = await ctx.ui.select("Squad Agents (select to view/edit)", options);
|
|
897
|
+
if (!choice) return;
|
|
898
|
+
const selectedName = choice.split(" — ")[0];
|
|
899
|
+
const agent = allAgents.find((a) => a.name === selectedName);
|
|
900
|
+
if (!agent) return;
|
|
901
|
+
|
|
902
|
+
// Show agent details and offer actions
|
|
903
|
+
const disableLabel = agent.disabled ? "Enable agent" : "Disable agent";
|
|
904
|
+
const actions = [
|
|
905
|
+
"View details",
|
|
906
|
+
"Edit in editor",
|
|
907
|
+
"Change model",
|
|
908
|
+
"Toggle tools (restrict/unrestrict)",
|
|
909
|
+
disableLabel,
|
|
910
|
+
"Cancel",
|
|
911
|
+
];
|
|
912
|
+
const action = await ctx.ui.select(`${agent.name} (${agent.role})`, actions);
|
|
913
|
+
if (!action || action === "Cancel") return;
|
|
914
|
+
|
|
915
|
+
if (action === "View details") {
|
|
916
|
+
const details = [
|
|
917
|
+
`Name: ${agent.name}`,
|
|
918
|
+
`Role: ${agent.role}`,
|
|
919
|
+
`Description: ${agent.description}`,
|
|
920
|
+
`Model: ${agent.model || "(default)"}`,
|
|
921
|
+
`Tools: ${agent.tools ? agent.tools.join(", ") : "(all)"}`,
|
|
922
|
+
`Tags: ${agent.tags.join(", ")}`,
|
|
923
|
+
``,
|
|
924
|
+
`Prompt:`,
|
|
925
|
+
`${agent.prompt.slice(0, 300)}${agent.prompt.length > 300 ? "..." : ""}`,
|
|
926
|
+
``,
|
|
927
|
+
`File: ${store.getGlobalAgentsDir()}/${agent.name}.json`,
|
|
928
|
+
].join("\n");
|
|
929
|
+
ctx.ui.notify(details, "info");
|
|
930
|
+
} else if (action === "Edit in editor") {
|
|
931
|
+
// Check for local override first, fall back to global
|
|
932
|
+
const localPath = `${store.getLocalAgentsDir(ctx.cwd)}/${agent.name}.json`;
|
|
933
|
+
const globalPath = `${store.getGlobalAgentsDir()}/${agent.name}.json`;
|
|
934
|
+
const filePath = fs.existsSync(localPath) ? localPath : globalPath;
|
|
935
|
+
pi.sendMessage({
|
|
936
|
+
customType: "squad-edit-agent",
|
|
937
|
+
content: `Edit agent file: ${filePath}`,
|
|
938
|
+
display: true,
|
|
939
|
+
}, { triggerTurn: true });
|
|
940
|
+
} else if (action === "Change model") {
|
|
941
|
+
const newModel = await ctx.ui.input(
|
|
942
|
+
`Model for ${agent.name} (empty = default)`,
|
|
943
|
+
agent.model || "",
|
|
944
|
+
);
|
|
945
|
+
if (newModel !== undefined) {
|
|
946
|
+
agent.model = newModel.trim() || null;
|
|
947
|
+
store.saveAgentDef(agent);
|
|
948
|
+
ctx.ui.notify(`${agent.name} model → ${agent.model || "(default)"}`, "info");
|
|
949
|
+
}
|
|
950
|
+
} else if (action === disableLabel) {
|
|
951
|
+
agent.disabled = !agent.disabled;
|
|
952
|
+
store.saveAgentDef(agent);
|
|
953
|
+
const newState = agent.disabled ? "disabled — planner will not assign tasks to this agent" : "enabled";
|
|
954
|
+
ctx.ui.notify(`${agent.name}: ${newState}`, "info");
|
|
955
|
+
} else if (action === "Toggle tools") {
|
|
956
|
+
if (agent.tools) {
|
|
957
|
+
agent.tools = null;
|
|
958
|
+
store.saveAgentDef(agent);
|
|
959
|
+
ctx.ui.notify(`${agent.name}: all tools enabled`, "info");
|
|
960
|
+
} else {
|
|
961
|
+
const toolList = await ctx.ui.input(
|
|
962
|
+
`Tools for ${agent.name} (comma-separated)`,
|
|
963
|
+
"bash,read,write,edit",
|
|
964
|
+
);
|
|
965
|
+
if (toolList) {
|
|
966
|
+
agent.tools = toolList.split(",").map((t) => t.trim()).filter(Boolean);
|
|
967
|
+
store.saveAgentDef(agent);
|
|
968
|
+
ctx.ui.notify(`${agent.name}: tools = [${agent.tools.join(", ")}]`, "info");
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// /squad agents <name> — show specific agent
|
|
976
|
+
const agent = store.loadAgentDef(agentArg, ctx.cwd);
|
|
977
|
+
if (agent) {
|
|
978
|
+
const status = agent.disabled ? " ✗ DISABLED" : "";
|
|
979
|
+
const details = [
|
|
980
|
+
`${agent.name} — ${agent.role}${status}`,
|
|
981
|
+
`${agent.description}`,
|
|
982
|
+
`Model: ${agent.model || "(default)"}`,
|
|
983
|
+
`Tools: ${agent.tools ? agent.tools.join(", ") : "(all)"}`,
|
|
984
|
+
`Tags: ${agent.tags.join(", ")}`,
|
|
985
|
+
].join("\n");
|
|
986
|
+
ctx.ui.notify(details, "info");
|
|
987
|
+
} else {
|
|
988
|
+
ctx.ui.notify(`Agent '${agentArg}' not found`, "warning");
|
|
989
|
+
}
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
643
993
|
default:
|
|
644
994
|
// Treat as a squad ID — try to activate it directly
|
|
645
995
|
const direct = store.loadSquad(sub);
|
|
@@ -699,176 +1049,59 @@ function activateSquadView(squadId: string, ctx: import("@mariozechner/pi-coding
|
|
|
699
1049
|
|
|
700
1050
|
activeSquadId = squadId;
|
|
701
1051
|
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (squad.status === "running" || squad.status === "paused") {
|
|
708
|
-
startWidgetRefresh();
|
|
709
|
-
}
|
|
1052
|
+
// Update widget to show the new squad. The widget reads squadId on each
|
|
1053
|
+
// render, so just updating the state and requesting a render is enough.
|
|
1054
|
+
widgetState.squadId = squadId;
|
|
1055
|
+
widgetState.enabled = true;
|
|
1056
|
+
widgetControls?.requestUpdate();
|
|
710
1057
|
|
|
1058
|
+
// Compact notification — widget already shows full task details.
|
|
1059
|
+
// Avoid large multi-line notifications that can break TUI layout.
|
|
711
1060
|
const tasks = store.loadAllTasks(squadId);
|
|
712
1061
|
const done = tasks.filter((t) => t.status === "done").length;
|
|
713
1062
|
const cost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const taskLines = tasks.map((t) => {
|
|
717
|
-
const icon = t.status === "done" ? "✓" : t.status === "in_progress" ? "⏳" : t.status === "failed" ? "✗" : t.status === "blocked" ? "◻" : "·";
|
|
718
|
-
return ` ${icon} ${t.id} (${t.agent}) [${t.status}]`;
|
|
719
|
-
}).join("\n");
|
|
720
|
-
|
|
721
|
-
ctx.ui.notify(
|
|
722
|
-
`Viewing: ${squad.id} [${squad.status}]\n` +
|
|
723
|
-
`Project: ${project}\n` +
|
|
724
|
-
`Tasks: ${done}/${tasks.length} done · $${cost.toFixed(2)}\n` +
|
|
725
|
-
taskLines,
|
|
726
|
-
"info",
|
|
727
|
-
);
|
|
1063
|
+
ctx.ui.notify(`Viewing: ${squad.id} [${squad.status}] ${done}/${tasks.length} $${cost.toFixed(2)}`, "info");
|
|
728
1064
|
}
|
|
729
1065
|
|
|
730
1066
|
// ============================================================================
|
|
731
|
-
// Widget —
|
|
1067
|
+
// Widget — component-based, event-driven (inspired by pi-interactive-shell)
|
|
732
1068
|
// ============================================================================
|
|
733
1069
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const tasks = store.loadAllTasks(activeSquadId);
|
|
738
|
-
const squad = store.loadSquad(activeSquadId);
|
|
739
|
-
if (!squad || tasks.length === 0) return;
|
|
740
|
-
|
|
741
|
-
const th = uiCtx.ui.theme;
|
|
742
|
-
|
|
743
|
-
// Build widget lines
|
|
744
|
-
const lines: string[] = [];
|
|
745
|
-
|
|
746
|
-
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
747
|
-
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
748
|
-
const elapsed = Date.now() - new Date(squad.created).getTime();
|
|
749
|
-
const elapsedStr = formatElapsedShort(elapsed);
|
|
750
|
-
|
|
751
|
-
// Header line with shortcut hint
|
|
752
|
-
const statusIcon = squad.status === "done" ? th.fg("success", "✓")
|
|
753
|
-
: squad.status === "failed" ? th.fg("error", "✗")
|
|
754
|
-
: th.fg("warning", "⏳");
|
|
755
|
-
const hint = th.fg("dim", "ctrl+q panel · /squad");
|
|
756
|
-
lines.push(
|
|
757
|
-
`${statusIcon} ${th.fg("accent", "squad")} ${th.fg("dim", squad.goal.slice(0, 30))} ` +
|
|
758
|
-
`${th.fg("muted", `${doneCount}/${tasks.length}`)} ` +
|
|
759
|
-
`${th.fg("dim", `$${totalCost.toFixed(2)}`)} ` +
|
|
760
|
-
`${th.fg("dim", elapsedStr)} ` +
|
|
761
|
-
`${hint}`
|
|
762
|
-
);
|
|
763
|
-
|
|
764
|
-
// Task lines
|
|
765
|
-
for (const task of tasks) {
|
|
766
|
-
const icon = task.status === "done" ? th.fg("success", "✓")
|
|
767
|
-
: task.status === "in_progress" ? th.fg("warning", "⏳")
|
|
768
|
-
: task.status === "failed" ? th.fg("error", "✗")
|
|
769
|
-
: task.status === "blocked" ? th.fg("muted", "◻")
|
|
770
|
-
: th.fg("dim", "·");
|
|
771
|
-
|
|
772
|
-
let line = ` ${icon} ${th.fg("muted", task.id)} ${th.fg("dim", `(${task.agent})`)}`;
|
|
773
|
-
|
|
774
|
-
// Show live activity for in_progress tasks
|
|
775
|
-
if (task.status === "in_progress") {
|
|
776
|
-
const messages = store.loadMessages(activeSquadId, task.id);
|
|
777
|
-
const lastTool = [...messages].reverse().find((m) => m.type === "tool");
|
|
778
|
-
const lastText = [...messages].reverse().find((m) => m.type === "text" && m.from !== "system");
|
|
779
|
-
if (lastTool) {
|
|
780
|
-
const toolPreview = lastTool.name || lastTool.text;
|
|
781
|
-
const argPreview = lastTool.args?.path || lastTool.args?.command || "";
|
|
782
|
-
const preview = argPreview ? `${toolPreview} ${argPreview}` : toolPreview;
|
|
783
|
-
line += ` ${th.fg("dim", "→ " + preview.slice(0, 40))}`;
|
|
784
|
-
} else if (lastText) {
|
|
785
|
-
line += ` ${th.fg("dim", lastText.text.split("\n")[0].slice(0, 40))}`;
|
|
786
|
-
}
|
|
787
|
-
} else if (task.status === "done" && task.output) {
|
|
788
|
-
line += ` ${th.fg("dim", task.output.split("\n")[0].slice(0, 40))}`;
|
|
789
|
-
} else if (task.status === "failed" && task.error) {
|
|
790
|
-
line += ` ${th.fg("error", task.error.slice(0, 40))}`;
|
|
791
|
-
} else if (task.status === "blocked") {
|
|
792
|
-
const blockers = task.depends.filter((d) => {
|
|
793
|
-
const dep = tasks.find((t) => t.id === d);
|
|
794
|
-
return dep && dep.status !== "done";
|
|
795
|
-
});
|
|
796
|
-
if (blockers.length > 0) {
|
|
797
|
-
line += ` ${th.fg("dim", "← " + blockers.join(", "))}`;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
lines.push(line);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
uiCtx.ui.setWidget("squad-tasks", lines);
|
|
805
|
-
|
|
806
|
-
// Footer status
|
|
807
|
-
const statusText = squad.status === "done"
|
|
808
|
-
? th.fg("success", `✓ squad ${doneCount}/${tasks.length}`)
|
|
809
|
-
: squad.status === "failed"
|
|
810
|
-
? th.fg("error", `✗ squad ${doneCount}/${tasks.length}`)
|
|
811
|
-
: th.fg("accent", `⏳ squad ${doneCount}/${tasks.length} $${totalCost.toFixed(2)}`);
|
|
812
|
-
uiCtx.ui.setStatus("squad", statusText);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
function clearWidget(): void {
|
|
816
|
-
if (uiCtx?.hasUI) {
|
|
817
|
-
uiCtx.ui.setWidget("squad-tasks", undefined);
|
|
818
|
-
uiCtx.ui.setStatus("squad", undefined);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
function startWidgetRefresh(): void {
|
|
823
|
-
if (widgetInterval) return;
|
|
824
|
-
updateWidget();
|
|
825
|
-
widgetInterval = setInterval(() => updateWidget(), 2000);
|
|
1070
|
+
/** Trigger widget re-render from scheduler events */
|
|
1071
|
+
function forceWidgetUpdate(): void {
|
|
1072
|
+
widgetControls?.requestUpdate();
|
|
826
1073
|
}
|
|
827
1074
|
|
|
828
|
-
function clearWidgetRefresh(): void {
|
|
829
|
-
if (widgetInterval) {
|
|
830
|
-
clearInterval(widgetInterval);
|
|
831
|
-
widgetInterval = null;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function formatElapsedShort(ms: number): string {
|
|
836
|
-
const s = Math.floor(ms / 1000);
|
|
837
|
-
const m = Math.floor(s / 60);
|
|
838
|
-
const h = Math.floor(m / 60);
|
|
839
|
-
if (h > 0) return `${h}h${m % 60}m`;
|
|
840
|
-
if (m > 0) return `${m}m${s % 60}s`;
|
|
841
|
-
return `${s}s`;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
/** Shared widget-enabled flag — declared in the extension body, referenced here */
|
|
845
|
-
let widgetEnabled = true;
|
|
846
|
-
|
|
847
1075
|
// ============================================================================
|
|
848
|
-
// Panel
|
|
1076
|
+
// Panel — overlay via ctx.ui.custom() with proper done() lifecycle
|
|
849
1077
|
// ============================================================================
|
|
850
1078
|
|
|
851
|
-
|
|
1079
|
+
/**
|
|
1080
|
+
* Open the squad panel overlay.
|
|
1081
|
+
* Uses the pi-interactive-shell pattern: ctx.ui.custom() returns a Promise
|
|
1082
|
+
* that resolves when done() is called. The panel calls done() on close.
|
|
1083
|
+
*/
|
|
1084
|
+
function openPanel(
|
|
852
1085
|
ctx: import("@mariozechner/pi-coding-agent").ExtensionContext,
|
|
853
1086
|
scheduler: Scheduler,
|
|
854
1087
|
squadId: string,
|
|
855
1088
|
): void {
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1089
|
+
if (overlayOpen) return;
|
|
1090
|
+
overlayOpen = true;
|
|
1091
|
+
|
|
1092
|
+
// The promise resolves when the panel calls done()
|
|
1093
|
+
const panelPromise = ctx.ui.custom<SquadPanelResult>(
|
|
1094
|
+
(tui, theme, _kb, done) => {
|
|
1095
|
+
const panel = new SquadPanel(tui, theme, scheduler, squadId, done);
|
|
862
1096
|
|
|
863
1097
|
// Wire up message sending from panel
|
|
864
1098
|
panel.onSendMessage = async (taskId: string, _prefill: string) => {
|
|
865
|
-
// Temporarily release panel focus so input dialog works
|
|
866
|
-
(panel as any).handle?.unfocus();
|
|
867
1099
|
const task = store.loadTask(squadId, taskId);
|
|
868
1100
|
const agentName = task?.agent || taskId;
|
|
869
1101
|
const input = await ctx.ui.input(`Message to ${agentName}`, "Type your message...");
|
|
870
|
-
|
|
871
|
-
|
|
1102
|
+
const panelSched = schedulers.get(squadId);
|
|
1103
|
+
if (input && panelSched) {
|
|
1104
|
+
await panelSched.sendHumanMessage(taskId, input);
|
|
872
1105
|
ctx.ui.notify(`Sent to ${agentName}: "${input.slice(0, 50)}"`, "info");
|
|
873
1106
|
} else if (input) {
|
|
874
1107
|
store.appendMessage(squadId, taskId, {
|
|
@@ -879,8 +1112,6 @@ function createPanel(
|
|
|
879
1112
|
});
|
|
880
1113
|
ctx.ui.notify(`Logged to ${taskId}`, "info");
|
|
881
1114
|
}
|
|
882
|
-
// Re-focus panel after input
|
|
883
|
-
(panel as any).handle?.focus();
|
|
884
1115
|
tui.requestRender();
|
|
885
1116
|
};
|
|
886
1117
|
|
|
@@ -888,29 +1119,22 @@ function createPanel(
|
|
|
888
1119
|
},
|
|
889
1120
|
{
|
|
890
1121
|
overlay: true,
|
|
891
|
-
overlayOptions:
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
width: "35%" as const,
|
|
897
|
-
maxHeight: "100%" as const,
|
|
898
|
-
margin: { top: 0, right: 0, bottom: 1, left: 0 },
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
return {
|
|
902
|
-
anchor: "center" as const,
|
|
903
|
-
width: "90%" as const,
|
|
904
|
-
maxHeight: "85%" as const,
|
|
905
|
-
};
|
|
906
|
-
},
|
|
907
|
-
onHandle: (handle) => {
|
|
908
|
-
if (activePanel) {
|
|
909
|
-
(activePanel as any).handle = handle;
|
|
910
|
-
}
|
|
1122
|
+
overlayOptions: {
|
|
1123
|
+
anchor: "center" as const,
|
|
1124
|
+
width: "80%" as const,
|
|
1125
|
+
maxHeight: "80%" as const,
|
|
1126
|
+
margin: 2,
|
|
911
1127
|
},
|
|
912
1128
|
},
|
|
913
1129
|
);
|
|
1130
|
+
|
|
1131
|
+
// When panel closes (done() called), clean up
|
|
1132
|
+
panelPromise.then(() => {
|
|
1133
|
+
overlayOpen = false;
|
|
1134
|
+
forceWidgetUpdate();
|
|
1135
|
+
}).catch(() => {
|
|
1136
|
+
overlayOpen = false;
|
|
1137
|
+
});
|
|
914
1138
|
}
|
|
915
1139
|
|
|
916
1140
|
// ============================================================================
|
|
@@ -1019,16 +1243,18 @@ async function startSquad(
|
|
|
1019
1243
|
|
|
1020
1244
|
// Start scheduler
|
|
1021
1245
|
const scheduler = new Scheduler(squadId, skillPaths);
|
|
1022
|
-
|
|
1246
|
+
schedulers.set(squadId, scheduler);
|
|
1023
1247
|
activeSquadId = squadId;
|
|
1024
1248
|
|
|
1025
|
-
//
|
|
1026
|
-
|
|
1249
|
+
// Activate widget for this squad
|
|
1250
|
+
widgetState.squadId = squadId;
|
|
1251
|
+
widgetState.enabled = true;
|
|
1252
|
+
widgetControls?.requestUpdate();
|
|
1027
1253
|
|
|
1028
1254
|
// Wire up completion/escalation notifications to main agent
|
|
1029
1255
|
scheduler.onEvent((event: SchedulerEvent) => {
|
|
1030
1256
|
// Update widget on every scheduler event
|
|
1031
|
-
|
|
1257
|
+
forceWidgetUpdate();
|
|
1032
1258
|
switch (event.type) {
|
|
1033
1259
|
case "squad_completed": {
|
|
1034
1260
|
const tasks = store.loadAllTasks(squadId);
|
|
@@ -1039,21 +1265,24 @@ async function startSquad(
|
|
|
1039
1265
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
1040
1266
|
|
|
1041
1267
|
// Final context update before clearing scheduler
|
|
1042
|
-
|
|
1043
|
-
|
|
1268
|
+
const completedSched = schedulers.get(squadId);
|
|
1269
|
+
if (completedSched) {
|
|
1270
|
+
completedSched.updateContext();
|
|
1044
1271
|
}
|
|
1045
1272
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1273
|
+
// Use sendMessage with display:true — visible to user but doesn't
|
|
1274
|
+
// pollute LLM message history. No triggerTurn needed for completions.
|
|
1275
|
+
pi.sendMessage({
|
|
1276
|
+
customType: "squad-completed",
|
|
1277
|
+
content: `[squad] Squad "${squadId}" completed all ${tasks.length} tasks.\n\n` +
|
|
1278
|
+
`Summary:\n${summary}\n\n` +
|
|
1279
|
+
`Total cost: $${totalCost.toFixed(4)}`,
|
|
1280
|
+
display: true,
|
|
1281
|
+
});
|
|
1052
1282
|
|
|
1053
1283
|
// Clear scheduler but keep activeSquadId so squad_status still works
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
updateWidget(); // Final update showing done state
|
|
1284
|
+
schedulers.delete(squadId);
|
|
1285
|
+
forceWidgetUpdate(); // Final update showing done state
|
|
1057
1286
|
break;
|
|
1058
1287
|
}
|
|
1059
1288
|
|
|
@@ -1062,32 +1291,42 @@ async function startSquad(
|
|
|
1062
1291
|
const failed = tasks.filter((t) => t.status === "failed");
|
|
1063
1292
|
const done = tasks.filter((t) => t.status === "done");
|
|
1064
1293
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
`
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1294
|
+
// Squad failed — notify user. Use triggerTurn so the agent can
|
|
1295
|
+
// suggest next steps (retry, modify, cancel).
|
|
1296
|
+
pi.sendMessage({
|
|
1297
|
+
customType: "squad-failed",
|
|
1298
|
+
content: `[squad] Squad "${squadId}" has stalled. ` +
|
|
1299
|
+
`${done.length}/${tasks.length} tasks done, ${failed.length} failed.\n` +
|
|
1300
|
+
`Failed: ${failed.map((t) => `${t.id}: ${t.error?.slice(0, 100)}`).join("; ")}\n` +
|
|
1301
|
+
`Use squad_status for details or squad_modify to adjust.`,
|
|
1302
|
+
display: true,
|
|
1303
|
+
}, { triggerTurn: true });
|
|
1304
|
+
forceWidgetUpdate();
|
|
1074
1305
|
break;
|
|
1075
1306
|
}
|
|
1076
1307
|
|
|
1077
1308
|
case "escalation": {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
{
|
|
1083
|
-
|
|
1309
|
+
// Escalation — agent needs help. triggerTurn so the main agent
|
|
1310
|
+
// can respond and relay help.
|
|
1311
|
+
pi.sendMessage({
|
|
1312
|
+
customType: "squad-escalation",
|
|
1313
|
+
content: `[squad] Agent '${event.agentName}' on task '${event.taskId}' needs attention:\n` +
|
|
1314
|
+
`${event.message}\n\n` +
|
|
1315
|
+
`Reply to me and I'll forward your answer, or use the squad panel.`,
|
|
1316
|
+
display: true,
|
|
1317
|
+
}, { triggerTurn: true });
|
|
1084
1318
|
break;
|
|
1085
1319
|
}
|
|
1086
1320
|
}
|
|
1087
1321
|
});
|
|
1088
1322
|
|
|
1089
|
-
// Start scheduling
|
|
1090
|
-
|
|
1323
|
+
// Start scheduling — fire and forget, don't block the tool call.
|
|
1324
|
+
// scheduler.start() spawns agents which can take seconds per agent.
|
|
1325
|
+
// We must return immediately so the main agent's turn completes
|
|
1326
|
+
// and the user regains interactive control.
|
|
1327
|
+
scheduler.start().catch((err) => {
|
|
1328
|
+
console.error(`[squad] Scheduler start error: ${(err as Error).message}`);
|
|
1329
|
+
});
|
|
1091
1330
|
|
|
1092
1331
|
// Build response
|
|
1093
1332
|
const taskSummary = plan.tasks
|