pi-subagents-lite 1.0.2 → 1.2.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.
package/src/format.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * format.ts — Consolidated display formatting helpers.
3
+ *
4
+ * Single source of truth for all display-formatting functions used across
5
+ * the UI layer. Previously scattered across agent-widget.ts, output-file.ts,
6
+ * and agent-types.ts by historical accident.
7
+ *
8
+ * Pure functions — no module-level state, no side effects.
9
+ */
10
+
11
+ import { getConfig } from "./agent-types.js";
12
+ import type { SubagentType, Theme } from "./types.js";
13
+ import { formatTokens, formatCost } from "./usage.js";
14
+
15
+ /** Max length for a truncated command in tool arg summaries. */
16
+ const MAX_COMMAND_DISPLAY_LENGTH = 100;
17
+
18
+ /** Max length for a truncated string value in default tool arg summaries. */
19
+ const MAX_DEFAULT_STRING_DISPLAY_LENGTH = 200;
20
+
21
+ // ---- Internal helpers (used by buildStatsParts) ----
22
+
23
+ /**
24
+ * Token count with optional context-fill % and compaction-count annotations.
25
+ * Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
26
+ * Compaction count rendered as `↻ N` in dim.
27
+ *
28
+ * "12.3k" — no annotations
29
+ * "12.3k(45%)" — percent only
30
+ * "12.3k(↻ 2)" — compactions only (e.g. right after compact)
31
+ * "12.3k(45%·↻ 2)" — both
32
+ */
33
+ function formatSessionTokens(
34
+ tokens: number,
35
+ percent: number | null,
36
+ theme: Theme,
37
+ compactions = 0,
38
+ ): string {
39
+ const tokenStr = formatTokens(tokens);
40
+ const annot: string[] = [];
41
+ if (percent !== null) {
42
+ const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
43
+ annot.push(theme.fg(color, `${Math.round(percent)}%`));
44
+ }
45
+ if (compactions > 0) {
46
+ annot.push(theme.fg("dim", `↻ ${compactions}`));
47
+ }
48
+ if (annot.length === 0) return tokenStr;
49
+ // Include closing paren in the last annotation's color span to prevent
50
+ // ANSI reset from leaving `)` in default color when wrapped in outer dim.
51
+ const lastIdx = annot.length - 1;
52
+ annot[lastIdx] += ")";
53
+ return `${tokenStr}(${annot.join("·")}`;
54
+ }
55
+
56
+ /** Format turn count with optional max limit: "5≤30⟳" or "5⟳". */
57
+ function formatTurns(turnCount: number, maxTurns?: number | null): string {
58
+ return maxTurns != null ? `${turnCount}≤${maxTurns}⟳ ` : `${turnCount}⟳ `;
59
+ }
60
+
61
+ // ---- Exported formatting functions ----
62
+
63
+ /** Format milliseconds as a compact human-readable duration: "1h 1m 1s", "5m 37s", "10s", "<1s". */
64
+ export function formatMs(ms: number): string {
65
+ if (!Number.isFinite(ms) || ms < 1000) return "<1s";
66
+
67
+ const totalSeconds = Math.floor(ms / 1000);
68
+ const hours = Math.floor(totalSeconds / 3600);
69
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
70
+ const seconds = totalSeconds % 60;
71
+
72
+ const parts: string[] = [];
73
+ if (hours > 0) parts.push(`${hours}h`);
74
+ if (minutes > 0) parts.push(`${minutes}m`);
75
+ if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
76
+
77
+ return parts.join(" ");
78
+ }
79
+
80
+ /**
81
+ * Build common stats parts: toolUses · turns · tokens with context % · cost.
82
+ * Shared by AgentWidget and index.ts for consistent stats display.
83
+ */
84
+ export function buildStatsParts(
85
+ args: {
86
+ toolUses: number;
87
+ turnCount?: number;
88
+ maxTurns?: number;
89
+ tokens: number;
90
+ contextPercent: number | null;
91
+ compactions: number;
92
+ cost?: number;
93
+ },
94
+ theme: Theme,
95
+ ): string[] {
96
+ const parts: string[] = [];
97
+ if (args.toolUses > 0) parts.push(`${args.toolUses}🛠 `);
98
+ if (args.turnCount != null) parts.push(formatTurns(args.turnCount, args.maxTurns));
99
+ if (args.tokens > 0) {
100
+ parts.push(formatSessionTokens(
101
+ args.tokens, args.contextPercent, theme, args.compactions,
102
+ ));
103
+ }
104
+ if (args.cost != null && args.cost > 0) parts.push(formatCost(args.cost));
105
+ return parts;
106
+ }
107
+
108
+ /** Get display name for any agent type (built-in or custom). */
109
+ export function getDisplayName(type: SubagentType): string {
110
+ return getConfig(type).displayName;
111
+ }
112
+
113
+ /**
114
+ * Summarize tool arguments for log-friendly display.
115
+ *
116
+ * Heavy tools (read, write, edit, bash, grep, rg) get compact summaries.
117
+ * Other tools fall back to the default JSON formatting.
118
+ */
119
+ export function summarizeToolArgs(name: string, rawArgs: Record<string, unknown> | undefined): string {
120
+ if (!rawArgs || typeof rawArgs !== "object" || Object.keys(rawArgs).length === 0) return "";
121
+
122
+ switch (name) {
123
+ case "read": {
124
+ // read("/path/to/file") — just the path
125
+ const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
126
+ return `(${JSON.stringify(path)})`;
127
+ }
128
+ case "write": {
129
+ // write("/path/to/file", <N> chars) — path + content size
130
+ const path = typeof rawArgs.file_path === "string" ? rawArgs.file_path : "";
131
+ const content = rawArgs.content;
132
+ const size = typeof content === "string" ? content.length : 0;
133
+ return `(${JSON.stringify(path)}, ${size} chars)`;
134
+ }
135
+ case "edit": {
136
+ // edit("/path/to/file", <N> edits) — path + edit count
137
+ const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
138
+ const edits = rawArgs.edits;
139
+ const editCount = Array.isArray(edits) ? edits.length : 0;
140
+ return `(${JSON.stringify(path)}, ${editCount} edits)`;
141
+ }
142
+ case "bash": {
143
+ // bash("command") — just the command, strip heredoc, truncate long
144
+ const cmd = typeof rawArgs.command === "string" ? rawArgs.command : "";
145
+ // Strip heredoc: truncate at << followed by delimiter
146
+ const heredocIdx = cmd.search(/<<\s*['"]?\w+['"]?/);
147
+ const cleanCmd = heredocIdx >= 0 ? cmd.slice(0, heredocIdx).trim() : cmd.trim();
148
+ // Truncate long commands
149
+ const display = cleanCmd.length > MAX_COMMAND_DISPLAY_LENGTH
150
+ ? cleanCmd.slice(0, MAX_COMMAND_DISPLAY_LENGTH) + "…" : cleanCmd;
151
+ return `(${JSON.stringify(display)})`;
152
+ }
153
+ case "grep":
154
+ case "rg": {
155
+ // grep("pattern", "/path") — pattern + path
156
+ const pattern = typeof rawArgs.pattern === "string" ? rawArgs.pattern : "";
157
+ const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
158
+ return `(${JSON.stringify(pattern)}, ${JSON.stringify(path)})`;
159
+ }
160
+ default: {
161
+ // Default behavior for other tools: single-arg shorthand or JSON dump
162
+ const keys = Object.keys(rawArgs);
163
+ if (keys.length === 1) {
164
+ const val = rawArgs[keys[0]];
165
+ const display = typeof val === "string" && val.length > MAX_DEFAULT_STRING_DISPLAY_LENGTH
166
+ ? JSON.stringify(val.slice(0, MAX_DEFAULT_STRING_DISPLAY_LENGTH) + "...")
167
+ : JSON.stringify(val);
168
+ return `(${display})`;
169
+ }
170
+ return ` ${JSON.stringify(rawArgs)}`;
171
+ }
172
+ }
173
+ }
package/src/index.ts CHANGED
@@ -23,7 +23,6 @@
23
23
  * - session_shutdown: Abort all, dispose manager
24
24
  */
25
25
 
26
- import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
27
26
  import { Type } from "@sinclair/typebox";
28
27
  import * as path from "node:path";
29
28
  import type {
@@ -31,38 +30,44 @@ import type {
31
30
  ExtensionCommandContext,
32
31
  ExtensionContext,
33
32
  } from "@earendil-works/pi-coding-agent";
34
- import type { SessionModelOverrides, SubagentsConfig } from "./model-precedence.js";
35
33
  import { DEFAULT_AGENTS } from "./default-agents.js";
36
34
  import { registerAgents, getAvailableTypes, setAgentScanDirs } from "./agent-types.js";
37
35
  import { scanAgentFilesInDir, mergeAgents } from "./agent-discovery.js";
38
36
  import { AgentManager } from "./agent-manager.js";
39
- import { AgentWidget, buildStatsParts, formatMs, getDisplayName, type AgentActivity, type Theme, type UICtx } from "./ui/agent-widget.js";
37
+ import { AgentWidget, type UICtx } from "./ui/agent-widget.js";
40
38
  import { showAgentsMainMenu } from "./menus.js";
41
- import { loadConfig, DEFAULT_CONFIG } from "./config-io.js";
42
- import { executeAgentTool, toolCallListener, backgroundAgentIds, scheduleNudge } from "./tool-execution.js";
43
- import { executeStopAgentTool } from "./stop-agent-tool.js";
44
-
45
- // ============================================================================
46
- // Module-level state
47
- // ============================================================================
48
-
49
- /** Session-only model overrides — not persisted, cleared on session_start. */
50
- export let sessionOverrides: SessionModelOverrides = { default: null };
51
-
52
- /** Config cache — loaded at session_start, updated by /agents menu mutations. */
53
- export let __config: SubagentsConfig = { ...DEFAULT_CONFIG, agent: { ...DEFAULT_CONFIG.agent }, concurrency: { ...DEFAULT_CONFIG.concurrency } };
54
-
55
- /** Agent manager singleton — module-level, no globalThis access. */
56
- export let manager: AgentManager;
57
-
58
- /** Live activity state per agent, keyed by agent ID. Read by AgentWidget and tool-execution. */
59
- export const agentActivity = new Map<string, AgentActivity>();
60
-
61
- /** Live TUI widget showing running/completed agents above the editor. Used by tool-execution. */
62
- export let widget: AgentWidget | undefined;
63
-
64
- /** ExtensionAPI reference — stored at init for execute callbacks. */
65
- export let piInstance: ExtensionAPI;
39
+ import { loadConfig } from "./config-io.js";
40
+ import { executeAgentTool, executeStopAgentTool, toolCallListener, backgroundAgentIds, scheduleNudge } from "./tool-execution.js";
41
+ import { renderAgentToolCall, renderAgentToolResult, renderSubagentResult } from "./renderer.js";
42
+ import {
43
+ __config,
44
+ sessionOverrides,
45
+ agentActivity,
46
+ piInstance,
47
+ setConfig,
48
+ setManager,
49
+ clearManager,
50
+ setWidget,
51
+ setPiInstance,
52
+ setSessionCtx,
53
+ resetSessionOverrides,
54
+ resetLastToolsExpanded,
55
+ syncWidgetSettings,
56
+ syncCompactFromToolsExpanded,
57
+ getManager,
58
+ getWidget,
59
+ } from "./state.js";
60
+
61
+ // Re-exports for backward compatibility
62
+ export {
63
+ __config,
64
+ sessionOverrides,
65
+ agentActivity,
66
+ piInstance,
67
+ setShowCostEnabled,
68
+ syncWidgetSettings,
69
+ syncCompactFromToolsExpanded,
70
+ } from "./state.js";
66
71
 
67
72
 
68
73
 
@@ -75,30 +80,37 @@ export let piInstance: ExtensionAPI;
75
80
  * Idempotent — safe to call on every session_start.
76
81
  */
77
82
  function ensureManagerAndWidget(): void {
78
- if (manager) return;
79
-
80
- manager = new AgentManager(
81
- (record) => {
82
- // Only nudge for background (async) agents — sync agents already returned via tool result
83
- if (backgroundAgentIds.has(record.id)) {
84
- scheduleNudge(record.id);
85
- backgroundAgentIds.delete(record.id);
86
- }
87
-
88
- // Mark finished and update widget BEFORE deleting activity —
89
- // renderFinishedLine reads activity for turn count, tokens, etc.
90
- widget?.markFinished(record.id);
91
- widget?.update();
83
+ const currentManager = getManager();
84
+ const currentWidget = getWidget();
85
+ // Create manager if missing
86
+ if (!currentManager) {
87
+ const newManager = new AgentManager(
88
+ (record) => {
89
+ // Only nudge for background (async) agents — sync agents already returned via tool result
90
+ if (backgroundAgentIds.has(record.id)) {
91
+ scheduleNudge(record.id);
92
+ backgroundAgentIds.delete(record.id);
93
+ }
92
94
 
93
- // Remove from live activity tracking
94
- agentActivity.delete(record.id);
95
- },
96
- __config.concurrency,
97
- );
95
+ // Mark finished and update widget BEFORE deleting activity
96
+ // renderFinishedLine reads activity for turn count, tokens, etc.
97
+ getWidget()?.markFinished(record.id);
98
+ getWidget()?.update();
99
+
100
+ // Remove from live activity tracking
101
+ agentActivity.delete(record.id);
102
+ },
103
+ __config.concurrency,
104
+ );
105
+ setManager(newManager);
106
+ }
98
107
 
99
- // Create/replace widget tied to this manager instance
100
- if (!widget) {
101
- widget = new AgentWidget(manager, agentActivity);
108
+ // Create widget if missing (uses existing or newly created manager)
109
+ if (!currentWidget) {
110
+ const newWidget = new AgentWidget(getManager(), agentActivity);
111
+ newWidget.setShowCost(__config.agent.showCost === true);
112
+ setWidget(newWidget);
113
+ syncWidgetSettings();
102
114
  }
103
115
  }
104
116
 
@@ -127,35 +139,12 @@ async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
127
139
  }
128
140
 
129
141
  async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
130
- __config = loadConfig();
142
+ setConfig(loadConfig());
131
143
  ensureManagerAndWidget();
132
144
  await scanAndRegisterAgents(ctx);
133
145
  }
134
146
 
135
- // ============================================================================
136
- // UI helpers — stats card rendering (shared by renderResult and message renderer)
137
- // ============================================================================
138
-
139
- /** Format agent display name with optional model: "Agent (mimo-v2.5-pro)" or "Agent". */
140
- function agentNameLabel(d: Record<string, unknown>, theme: Theme): string {
141
- const typeName = getDisplayName((d.type as string) || "");
142
- const modelName = d.modelName as string | undefined;
143
- return modelName ? `${theme.bold(typeName)} (${modelName})` : theme.bold(typeName);
144
- }
145
147
 
146
- /** Build the stats line for an agent result card. Used by both renderers. */
147
- function buildStatsLine(d: Record<string, unknown>, theme: Theme): string {
148
- const parts = buildStatsParts({
149
- toolUses: (d.toolUses as number) ?? 0,
150
- turnCount: d.turnCount as number | undefined,
151
- maxTurns: d.maxTurns as number | undefined,
152
- tokens: (d.tokens as number) ?? 0,
153
- contextPercent: d.contextPercent as number | null,
154
- compactions: (d.compactions as number) ?? 0,
155
- }, theme);
156
- parts.push(formatMs(d.durationMs as number));
157
- return parts.join("·");
158
- }
159
148
 
160
149
  // ============================================================================
161
150
  // Agent tool registration helper — dynamic enum for agent types
@@ -182,55 +171,17 @@ function registerAgentTool(pi: ExtensionAPI): void {
182
171
  description: Type.String(),
183
172
  agent: agentParam,
184
173
  run_in_background: Type.Optional(Type.Boolean()),
185
-
174
+ worktree_path: Type.Optional(Type.String()),
186
175
  }),
187
176
  execute: executeAgentTool,
188
177
 
189
- renderCall(args, theme) {
190
- const typeName = getDisplayName((args.agent as string) || "");
191
- const label = typeName || "Agent";
192
- let text = `▸ ${theme.fg("accent", theme.bold(label))}`;
193
-
194
- // Show model in parens when it differs from the parent model
195
- // _modelOverride is injected by toolCallListener when the resolved
196
- // model differs from the session's parent model
197
- const a = args as Record<string, unknown>;
198
- const modelOverride = a._modelOverride as string | undefined;
199
- if (modelOverride) {
200
- text += ` (${modelOverride})`;
201
- }
202
-
203
- return new Text(text, 0, 0);
204
- },
205
-
206
- renderResult(result, options, theme) {
207
- const { expanded } = options as { expanded?: boolean };
208
- const text = result.content[0]?.type === "text" ? result.content[0].text : "";
209
- const d = result.details as Record<string, unknown> | undefined;
210
- const isError = !!(result as any).isError;
211
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
212
- const desc = (d?.description as string) || "";
213
-
214
- if (d && d.turnCount != null) {
215
- const namePart = agentNameLabel(d, theme);
216
- const statsLine = buildStatsLine(d, theme);
217
- let lines = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
218
- if (expanded && text) {
219
- lines += "\n" + text.split("\n").map(l => ` ${l}`).join("\n");
220
- }
221
- return new Text(lines, 0, 0);
222
- }
223
-
224
- // Minimal card — type name already shown by renderCall
225
- // For background spawns (no stats), use space placeholder — agent isn't done yet
226
- const isBackground = text.includes("running in background") || text.includes("queued");
227
- const prefix = isBackground ? " " : `${icon} `;
228
- if (desc) {
229
- return new Text(`${prefix}${theme.fg("text", desc)}`, 0, 0);
230
- }
178
+ renderCall: (args, theme) => renderAgentToolCall(args as Record<string, unknown>, theme),
231
179
 
232
- return new Text(`${prefix}${theme.fg("dim", text)}`, 0, 0);
233
- },
180
+ renderResult: (result, options, theme) => renderAgentToolResult(
181
+ result as { content: Array<{ type: string; text?: string }>; details?: Record<string, unknown>; isError?: boolean },
182
+ options as { expanded?: boolean },
183
+ theme,
184
+ ),
234
185
  });
235
186
  }
236
187
 
@@ -240,7 +191,7 @@ function registerAgentTool(pi: ExtensionAPI): void {
240
191
 
241
192
  export default function (pi: ExtensionAPI) {
242
193
  // Store pi for execute callbacks
243
- piInstance = pi;
194
+ setPiInstance(pi);
244
195
 
245
196
  // ========================================================================
246
197
  // Tool registration (stealth schemas — at init time)
@@ -261,55 +212,13 @@ export default function (pi: ExtensionAPI) {
261
212
  });
262
213
 
263
214
  // Message renderer — subagent-result (background agent completion)
264
- pi.registerMessageRenderer("subagent-result", (message, options, theme) => {
265
- const { expanded } = options as { expanded?: boolean };
266
- const d = message.details as Record<string, unknown> | undefined;
267
- const text = (message.content as string)?.trim() || "";
268
-
269
- const inner = new Container();
270
- inner.addChild(new Text(theme.fg("customMessageLabel", "Subagent Result"), 0, 0));
271
- inner.addChild(new Spacer(1));
272
-
273
- if (d && d.turnCount != null) {
274
- const isError = d.status === "error" || d.status === "aborted" || d.status === "stopped";
275
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
276
- const desc = (d.description as string) || "";
277
-
278
- const namePart = agentNameLabel(d, theme);
279
- const statsLine = buildStatsLine(d, theme);
280
- let headerLine = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
281
- if ((d.outputFile as string)) {
282
- headerLine += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
283
- }
284
- inner.addChild(new Text(headerLine, 0, 0));
285
-
286
- if (expanded && text) {
287
- inner.addChild(new Spacer(1));
288
- const resultLines = text.split("\n").map(l => ` ${l}`).join("\n");
289
- inner.addChild(new Text(resultLines, 0, 0));
290
- }
291
- } else {
292
- const desc = (d?.description as string) || "";
293
- let line = `${theme.fg("success", "✓")}`;
294
- if (d?.type) {
295
- line += ` ${agentNameLabel(d, theme)}`;
296
- }
297
- if (desc) line += `\n ${theme.fg("text", desc)}`;
298
- if (d?.outputFile) {
299
- line += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
300
- }
301
- inner.addChild(new Text(line, 0, 0));
302
- }
303
-
304
- const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
305
- box.addChild(inner);
306
-
307
- const outer = new Container();
308
- outer.addChild(new Spacer(1));
309
- outer.addChild(box);
310
- outer.addChild(new Spacer(1));
311
- return outer;
312
- });
215
+ pi.registerMessageRenderer("subagent-result", (message, options, theme) =>
216
+ renderSubagentResult(
217
+ message as { content?: string; details?: Record<string, unknown> },
218
+ options as { expanded?: boolean },
219
+ theme,
220
+ ),
221
+ );
313
222
 
314
223
  // Command registration
315
224
  pi.registerCommand("agents", {
@@ -324,25 +233,66 @@ export default function (pi: ExtensionAPI) {
324
233
  pi.on("tool_call", toolCallListener);
325
234
 
326
235
  pi.on("tool_execution_start", async (_event, ctx) => {
327
- widget?.setUICtx(ctx.ui as unknown as UICtx);
328
- widget?.onTurnStart();
236
+ // Set UI context on first tool execution
237
+ if (!getWidget()) {
238
+ ensureManagerAndWidget();
239
+ }
240
+ getWidget()?.setUICtx(ctx.ui as unknown as UICtx);
241
+ getWidget()?.onTurnStart();
329
242
  });
330
243
 
244
+
245
+
331
246
  // session_start — load config, scan agents, register into registry,
332
247
  // then re-register Agent tool with dynamic agent type enum
248
+ // Listen for ctrl+o keypress to sync compact mode (push-based, no polling)
249
+ let unregisterTerminalInput: (() => void) | undefined;
250
+
333
251
  pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
334
- sessionOverrides = { default: null };
252
+ setSessionCtx(ctx);
253
+ resetSessionOverrides();
335
254
  agentActivity.clear();
255
+ resetLastToolsExpanded();
336
256
  await loadConfigAndRegisterAgents(ctx);
337
257
  // Re-register with updated agent type list (now includes user/project agents)
338
258
  registerAgentTool(pi);
259
+ // Register ctrl+o listener
260
+ if (ctx.hasUI && !unregisterTerminalInput) {
261
+ unregisterTerminalInput = ctx.ui.onTerminalInput((data: string) => {
262
+ // ctrl+o = 0x0F (15) — toggles tool expansion
263
+ if (data === "\u000f") {
264
+ // Read state after a tick to let the built-in handler process it first
265
+ setTimeout(() => {
266
+ const ui = ctx.ui as unknown as { getToolsExpanded?: () => boolean };
267
+ const expanded = ui.getToolsExpanded?.();
268
+ if (expanded !== undefined) {
269
+ getWidget()?.notifyToolsExpansionChanged(expanded);
270
+ }
271
+ }, 0);
272
+ }
273
+ return undefined; // Don't consume the input
274
+ });
275
+ }
276
+ // Sync compact mode with initial tool expansion state
277
+ syncCompactFromToolsExpanded(false);
339
278
  });
340
279
 
341
- pi.on("session_shutdown", async (_event: unknown) => {
342
- widget?.dispose();
343
- widget = undefined;
344
- if (manager) {
345
- await manager.dispose();
280
+ pi.on("session_shutdown", async (_event: unknown, ctx: ExtensionContext) => {
281
+ // Warn if agents were killed
282
+ const currentManager = getManager();
283
+ if (currentManager) {
284
+ const records = currentManager.listAgents();
285
+ const active = records.filter(r => r.lifecycle.status === "running" || r.lifecycle.status === "queued");
286
+ if (active.length > 0 && ctx.hasUI) {
287
+ ctx.ui.notify(`${active.length} agent(s) killed by reload`, "warning");
288
+ }
289
+ }
290
+ getWidget()?.dispose();
291
+ setWidget(undefined);
292
+ const mgr = getManager();
293
+ if (mgr) {
294
+ await mgr.dispose();
295
+ clearManager();
346
296
  }
347
297
  });
348
298
  }