@pi-unipi/subagents 2.4.0 → 2.5.0

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.
Files changed (66) hide show
  1. package/README.md +3 -1
  2. package/dist/agent-manager.d.ts +81 -0
  3. package/dist/agent-manager.d.ts.map +1 -0
  4. package/dist/agent-manager.js +292 -0
  5. package/dist/agent-manager.js.map +1 -0
  6. package/dist/agent-runner.d.ts +51 -0
  7. package/dist/agent-runner.d.ts.map +1 -0
  8. package/dist/agent-runner.js +262 -0
  9. package/dist/agent-runner.js.map +1 -0
  10. package/dist/config.d.ts +24 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +132 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/conversation-viewer.d.ts +40 -0
  15. package/dist/conversation-viewer.d.ts.map +1 -0
  16. package/dist/conversation-viewer.js +276 -0
  17. package/dist/conversation-viewer.js.map +1 -0
  18. package/dist/core-compat.d.ts +14 -0
  19. package/dist/core-compat.d.ts.map +1 -0
  20. package/dist/core-compat.js +24 -0
  21. package/dist/core-compat.js.map +1 -0
  22. package/dist/custom-agents.d.ts +14 -0
  23. package/dist/custom-agents.d.ts.map +1 -0
  24. package/dist/custom-agents.js +106 -0
  25. package/dist/custom-agents.js.map +1 -0
  26. package/dist/file-lock.d.ts +42 -0
  27. package/dist/file-lock.d.ts.map +1 -0
  28. package/dist/file-lock.js +91 -0
  29. package/dist/file-lock.js.map +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +751 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/model-resolver.d.ts +19 -0
  35. package/dist/model-resolver.d.ts.map +1 -0
  36. package/dist/model-resolver.js +61 -0
  37. package/dist/model-resolver.js.map +1 -0
  38. package/dist/types.d.ts +96 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +47 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/widget.d.ts +56 -0
  43. package/dist/widget.d.ts.map +1 -0
  44. package/dist/widget.js +396 -0
  45. package/dist/widget.js.map +1 -0
  46. package/package.json +10 -6
  47. package/src/__tests__/badge-generation.test.ts +0 -315
  48. package/src/__tests__/config.test.ts +0 -240
  49. package/src/__tests__/esc-propagation.test.ts +0 -162
  50. package/src/__tests__/file-lock.test.ts +0 -244
  51. package/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
  52. package/src/__tests__/workflow-integration.test.ts +0 -334
  53. package/src/agent-manager.ts +0 -334
  54. package/src/agent-runner.ts +0 -329
  55. package/src/config.ts +0 -147
  56. package/src/conversation-viewer.ts +0 -299
  57. package/src/custom-agents.ts +0 -118
  58. package/src/file-lock.ts +0 -102
  59. package/src/index.ts +0 -862
  60. package/src/model-resolver.ts +0 -79
  61. package/src/prompts.ts +0 -39
  62. package/src/skills/explore/SKILL.md +0 -32
  63. package/src/skills/work/SKILL.md +0 -40
  64. package/src/types.ts +0 -146
  65. package/src/widget.ts +0 -454
  66. package/tsconfig.json +0 -19
package/src/widget.ts DELETED
@@ -1,454 +0,0 @@
1
- /**
2
- * @pi-unipi/subagents — Live widget
3
- *
4
- * Shows running/completed agents above the editor with:
5
- * - Animated braille spinners
6
- * - Finished agent lingering (1 turn success, 2 turns error)
7
- * - Priority-based overflow (running > queued > finished)
8
- * - Status bar integration
9
- * - Activity description grouping
10
- * - ANSI-aware truncation via pi-tui
11
- */
12
-
13
- import { truncateToWidth } from "@earendil-works/pi-tui";
14
- import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
15
- import type { AgentManager } from "./agent-manager.js";
16
- import type { AgentActivity } from "./types.js";
17
-
18
- // ---- Constants ----
19
-
20
- /** Maximum lines the widget may render. */
21
- const MAX_WIDGET_LINES = 12;
22
-
23
- /** Braille spinner frames. */
24
- const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
25
-
26
- /** Statuses that indicate error/non-success (for linger behavior). */
27
- const ERROR_STATUSES = new Set(["error", "aborted", "stopped"]);
28
-
29
- /** Tool name → human-readable action for activity descriptions. */
30
- const TOOL_DISPLAY: Record<string, string> = {
31
- read: "reading",
32
- bash: "running command",
33
- edit: "editing",
34
- write: "writing",
35
- grep: "searching",
36
- find: "finding files",
37
- ls: "listing",
38
- };
39
-
40
- // ---- Formatting helpers ----
41
-
42
- /** Format duration. */
43
- function formatMs(ms: number): string {
44
- if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
45
- if (ms >= 1_000) return `${(ms / 1_000).toFixed(1)}s`;
46
- return `${ms}ms`;
47
- }
48
-
49
- /** Format turns with optional max limit: "⟳5≤30" or "⟳5". */
50
- function formatTurns(turn: number, max?: number): string {
51
- return max != null ? `⟳${turn}≤${max}` : `⟳${turn}`;
52
- }
53
-
54
- /** Format token count compactly: "33.8k token", "1.2M token". */
55
- function formatTokens(count: number): string {
56
- if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
57
- if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
58
- return `${count} token`;
59
- }
60
-
61
- /**
62
- * Build a human-readable activity string from currently-running tools.
63
- * Groups by tool type with counts: "reading 3 files, searching 2 patterns".
64
- */
65
- function describeActivity(activeTools: Map<string, string>, responseText?: string): string {
66
- if (activeTools.size > 0) {
67
- const groups = new Map<string, number>();
68
- for (const toolName of activeTools.values()) {
69
- const action = TOOL_DISPLAY[toolName] ?? toolName;
70
- groups.set(action, (groups.get(action) ?? 0) + 1);
71
- }
72
-
73
- const parts: string[] = [];
74
- for (const [action, count] of groups) {
75
- if (count > 1) {
76
- parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
77
- } else {
78
- parts.push(action);
79
- }
80
- }
81
- return parts.join(", ") + "…";
82
- }
83
-
84
- if (responseText && responseText.trim().length > 0) {
85
- const lastLine = responseText.split("\n").find((l) => l.trim())?.trim() ?? "";
86
- if (lastLine.length > 60) return lastLine.slice(0, 60) + "…";
87
- if (lastLine.length > 0) return lastLine;
88
- }
89
-
90
- return "thinking…";
91
- }
92
-
93
- // ---- Widget ----
94
-
95
- export class AgentWidget {
96
- private spinnerFrame = 0;
97
- private timer?: ReturnType<typeof setInterval>;
98
- private uiCtx?: ExtensionUIContext;
99
- private tui?: import("@earendil-works/pi-tui").TUI;
100
- private widgetRegistered = false;
101
- /** Last content key — skips requestRender when only spinner changed. */
102
- private lastContentKey = "";
103
-
104
- /** Tracks how many turns each finished agent has survived. */
105
- private finishedTurnAge = new Map<string, number>();
106
- /** How many extra turns error/aborted agents linger. */
107
- private static readonly ERROR_LINGER_TURNS = 2;
108
- /** Last status bar text for dedup. */
109
- private lastStatusText: string | undefined;
110
-
111
- constructor(
112
- private manager: AgentManager,
113
- private activity: Map<string, AgentActivity>,
114
- ) {}
115
-
116
- setUICtx(ctx: ExtensionUIContext) {
117
- if (ctx !== this.uiCtx) {
118
- this.uiCtx = ctx;
119
- this.widgetRegistered = false;
120
- this.tui = undefined;
121
- this.lastStatusText = undefined;
122
- }
123
- }
124
-
125
- /**
126
- * Called on each new turn (tool_execution_start).
127
- * Ages finished agents and clears those that have lingered long enough.
128
- */
129
- onTurnStart() {
130
- for (const [id, age] of this.finishedTurnAge) {
131
- this.finishedTurnAge.set(id, age + 1);
132
- }
133
- this.update();
134
- }
135
-
136
- ensureTimer() {
137
- if (this.timer) return;
138
- this.timer = setInterval(() => {
139
- this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER.length;
140
- if (this.lastContentKey) this.triggerRender();
141
- }, 80);
142
- }
143
-
144
- /** Record an agent as finished (call when agent completes). */
145
- markFinished(agentId: string) {
146
- if (!this.finishedTurnAge.has(agentId)) {
147
- this.finishedTurnAge.set(agentId, 0);
148
- }
149
- }
150
-
151
- update() {
152
- this.triggerRender();
153
- }
154
-
155
- /** Check if a finished agent should still be shown. */
156
- private shouldShowFinished(agentId: string, status: string): boolean {
157
- const age = this.finishedTurnAge.get(agentId) ?? 0;
158
- const maxAge = ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_TURNS : 1;
159
- return age < maxAge;
160
- }
161
-
162
- /**
163
- * Build a content key capturing what's actually visible.
164
- * Excludes spinner — spinner-only changes skip requestRender.
165
- */
166
- private buildContentKey(): string {
167
- const allAgents = this.manager.listAgents();
168
- const parts: string[] = [];
169
- for (const a of allAgents) {
170
- if (a.status === "running" || a.status === "queued") {
171
- const act = this.activity.get(a.id);
172
- parts.push(
173
- `${a.id}:${a.status}:${a.toolUses}:${act?.turnCount ?? 0}:${act?.tokens ?? ""}:${describeActivity(act?.activeTools ?? new Map(), act?.responseText ?? "")}`,
174
- );
175
- } else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) {
176
- parts.push(`${a.id}:${a.status}:${a.toolUses}:finished`);
177
- }
178
- }
179
- return parts.join("|");
180
- }
181
-
182
- private triggerRender() {
183
- if (!this.uiCtx) return;
184
-
185
- const contentKey = this.buildContentKey();
186
- const hasContent = contentKey.length > 0;
187
-
188
- // Nothing to show — clear widget
189
- if (!hasContent) {
190
- if (this.widgetRegistered) {
191
- this.uiCtx.setWidget("unipi-agents", undefined);
192
- this.widgetRegistered = false;
193
- this.tui = undefined;
194
- this.lastContentKey = "";
195
- }
196
- if (this.lastStatusText !== undefined) {
197
- this.uiCtx.setStatus?.("subagents", undefined);
198
- this.lastStatusText = undefined;
199
- }
200
- // Clean up stale finished entries
201
- const allAgents = this.manager.listAgents();
202
- for (const [id] of this.finishedTurnAge) {
203
- if (!allAgents.some((a) => a.id === id)) this.finishedTurnAge.delete(id);
204
- }
205
- return;
206
- }
207
-
208
- // Status bar
209
- this.updateStatusBar();
210
-
211
- // Register widget callback once
212
- if (!this.widgetRegistered) {
213
- this.uiCtx.setWidget(
214
- "unipi-agents",
215
- (tui: any, theme: any) => {
216
- this.tui = tui;
217
- return {
218
- render: (width: number) => this.renderWidget(tui, theme, width),
219
- invalidate: () => {
220
- this.widgetRegistered = false;
221
- this.tui = undefined;
222
- },
223
- };
224
- },
225
- { placement: "aboveEditor" },
226
- );
227
- this.widgetRegistered = true;
228
- this.lastContentKey = "";
229
- }
230
-
231
- // Only request render when content actually changed
232
- if (contentKey !== this.lastContentKey) {
233
- this.lastContentKey = contentKey;
234
- this.tui?.requestRender?.();
235
- }
236
- }
237
-
238
- private updateStatusBar() {
239
- if (!this.uiCtx?.setStatus) return;
240
- const allAgents = this.manager.listAgents();
241
- let runningCount = 0;
242
- let queuedCount = 0;
243
- for (const a of allAgents) {
244
- if (a.status === "running") runningCount++;
245
- else if (a.status === "queued") queuedCount++;
246
- }
247
-
248
- let newStatusText: string | undefined;
249
- if (runningCount > 0 || queuedCount > 0) {
250
- const parts: string[] = [];
251
- if (runningCount > 0) parts.push(`${runningCount} running`);
252
- if (queuedCount > 0) parts.push(`${queuedCount} queued`);
253
- const total = runningCount + queuedCount;
254
- newStatusText = `${parts.join(", ")} agent${total === 1 ? "" : "s"}`;
255
- }
256
-
257
- if (newStatusText !== this.lastStatusText) {
258
- this.uiCtx.setStatus("subagents", newStatusText);
259
- this.lastStatusText = newStatusText;
260
- }
261
- }
262
-
263
- /** Render a finished agent line. */
264
- private renderFinishedLine(
265
- a: { id: string; type: string; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string },
266
- theme: any,
267
- w: number,
268
- ): string {
269
- const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
270
-
271
- let icon: string;
272
- let statusText: string;
273
- if (a.status === "completed") {
274
- icon = theme.fg("success", "✓");
275
- statusText = "";
276
- } else if (a.status === "stopped") {
277
- icon = theme.fg("dim", "■");
278
- statusText = theme.fg("dim", " stopped");
279
- } else if (a.status === "error") {
280
- icon = theme.fg("error", "✗");
281
- const errMsg = a.error ? `: ${a.error.slice(0, 40)}` : "";
282
- statusText = theme.fg("error", ` error${errMsg}`);
283
- } else {
284
- // aborted
285
- icon = theme.fg("error", "✗");
286
- statusText = theme.fg("warning", " aborted");
287
- }
288
-
289
- const parts: string[] = [];
290
- const act = this.activity.get(a.id);
291
- if (act) parts.push(formatTurns(act.turnCount, act.maxTurns));
292
- if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
293
- parts.push(duration);
294
-
295
- const line =
296
- theme.fg("dim", "├─") +
297
- " " +
298
- icon +
299
- " " +
300
- theme.fg("dim", a.type) +
301
- " " +
302
- theme.fg("dim", a.description) +
303
- " " +
304
- theme.fg("dim", "·") +
305
- " " +
306
- theme.fg("dim", parts.join(" · ")) +
307
- statusText;
308
-
309
- return truncateToWidth(line, w);
310
- }
311
-
312
- private renderWidget(tui: any, theme: any, width?: number): string[] {
313
- const allAgents = this.manager.listAgents();
314
- const running = allAgents.filter((a) => a.status === "running");
315
- const queued = allAgents.filter((a) => a.status === "queued");
316
- const finished = allAgents.filter(
317
- (a) =>
318
- a.status !== "running" &&
319
- a.status !== "queued" &&
320
- a.completedAt &&
321
- this.shouldShowFinished(a.id, a.status),
322
- );
323
-
324
- const hasActive = running.length > 0 || queued.length > 0;
325
- const hasFinished = finished.length > 0;
326
- if (!hasActive && !hasFinished) return [];
327
-
328
- const w = width ?? tui.terminal?.columns ?? 80;
329
- const frame = SPINNER[this.spinnerFrame % SPINNER.length];
330
- const headingColor = hasActive ? "accent" : "dim";
331
- const headingIcon = hasActive ? "●" : "○";
332
-
333
- // Build sections: finished (1 line each), running (2 lines each), queued (1 line)
334
- const finishedLines: string[] = finished.map((a) => this.renderFinishedLine(a, theme, w));
335
-
336
- const runningLines: string[][] = running.map((a) => {
337
- const act = this.activity.get(a.id);
338
- const toolCount = a.toolUses;
339
- const tokens = act?.tokens ?? "";
340
- const elapsed = formatMs(Date.now() - a.startedAt);
341
- const activity = act ? describeActivity(act.activeTools, act.responseText) : "starting…";
342
-
343
- const parts: string[] = [];
344
- if (act?.turnCount) parts.push(formatTurns(act.turnCount, act.maxTurns));
345
- if (toolCount > 0) parts.push(`${toolCount} tool use${toolCount === 1 ? "" : "s"}`);
346
- if (tokens) parts.push(tokens);
347
- parts.push(elapsed);
348
-
349
- return [
350
- truncateToWidth(
351
- theme.fg("dim", "├─") +
352
- ` ${theme.fg("accent", frame)} ${theme.bold(a.type)} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}`,
353
- w,
354
- ),
355
- truncateToWidth(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`), w),
356
- ];
357
- });
358
-
359
- const queuedLine =
360
- queued.length > 0
361
- ? truncateToWidth(
362
- theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`,
363
- w,
364
- )
365
- : undefined;
366
-
367
- // Assemble with overflow cap
368
- const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
369
- const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
370
-
371
- const lines: string[] = [truncateToWidth(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"), w)];
372
-
373
- if (totalBody <= maxBody) {
374
- // Everything fits
375
- lines.push(...finishedLines);
376
- for (const pair of runningLines) lines.push(...pair);
377
- if (queuedLine) lines.push(queuedLine);
378
-
379
- // Fix last connector: ├─ → └─
380
- if (lines.length > 1) {
381
- const last = lines.length - 1;
382
- lines[last] = lines[last].replace("├─", "└─");
383
- if (runningLines.length > 0 && !queuedLine && last >= 2) {
384
- lines[last - 1] = lines[last - 1].replace("├─", "└─");
385
- lines[last] = lines[last].replace("│ ", " ");
386
- }
387
- }
388
- } else {
389
- // Overflow — prioritize: running > queued > finished
390
- let budget = maxBody - 1; // reserve 1 for overflow indicator
391
- let hiddenRunning = 0;
392
- let hiddenFinished = 0;
393
-
394
- // 1. Running agents (2 lines each)
395
- for (const pair of runningLines) {
396
- if (budget >= 2) {
397
- lines.push(...pair);
398
- budget -= 2;
399
- } else {
400
- hiddenRunning++;
401
- }
402
- }
403
-
404
- // 2. Queued
405
- if (queuedLine && budget >= 1) {
406
- lines.push(queuedLine);
407
- budget--;
408
- }
409
-
410
- // 3. Finished
411
- for (const fl of finishedLines) {
412
- if (budget >= 1) {
413
- lines.push(fl);
414
- budget--;
415
- } else {
416
- hiddenFinished++;
417
- }
418
- }
419
-
420
- // Overflow summary
421
- const overflowParts: string[] = [];
422
- if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
423
- if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
424
- if (overflowParts.length > 0) {
425
- lines.push(
426
- truncateToWidth(
427
- theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowParts.join(", ")})`)}`,
428
- w,
429
- ),
430
- );
431
- }
432
- }
433
-
434
- return lines;
435
- }
436
-
437
- dispose() {
438
- if (this.timer) {
439
- clearInterval(this.timer);
440
- this.timer = undefined;
441
- }
442
- if (this.uiCtx) {
443
- if (this.widgetRegistered) {
444
- this.uiCtx.setWidget("unipi-agents", undefined);
445
- }
446
- if (this.lastStatusText !== undefined) {
447
- this.uiCtx.setStatus?.("subagents", undefined);
448
- }
449
- }
450
- this.widgetRegistered = false;
451
- this.tui = undefined;
452
- this.lastStatusText = undefined;
453
- }
454
- }
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "Node16",
5
- "moduleResolution": "Node16",
6
- "lib": ["ES2022"],
7
- "outDir": "dist",
8
- "rootDir": "src",
9
- "strict": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true,
12
- "forceConsistentCasingInFileNames": true,
13
- "declaration": true,
14
- "declarationMap": true,
15
- "sourceMap": true
16
- },
17
- "include": ["src/**/*"],
18
- "exclude": ["node_modules", "dist"]
19
- }