@narumitw/pi-subagents 3.2.0 → 3.3.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/dist/index.ts CHANGED
@@ -29,12 +29,29 @@ import {
29
29
  serializeChildBootstrap,
30
30
  validateMessage
31
31
  } from "./chunks/chunk-YMVSJ5GE.ts";
32
+ import {
33
+ CHILD_CORE_TOOL_NAMES,
34
+ DEFAULT_SUBAGENT_TOOLS,
35
+ SUBAGENT_THINKING_LEVELS,
36
+ TERMINAL_JOB_STATES
37
+ } from "./chunks/chunk-FY5HIU6Y.ts";
32
38
 
33
- // src/completion-renderer.ts
39
+ // src/display-text.ts
40
+ function sanitizeSubagentDisplayText(value) {
41
+ const text = value.replace(
42
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Strip untrusted terminal sequences before display.
43
+ /(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]|(?:\u001b[\]PX^_]|[\u0090\u0098\u009d-\u009f])[^\u0007\u001b\u0080-\u009f]*(?:\u0007|\u001b\\|\u009c)|\u001b[ -/]*[0-~]/gu,
44
+ ""
45
+ );
46
+ return sanitizeTerminalText(text.replace(/\p{Bidi_Control}/gu, "")).replace(/\s+/gu, " ").trim();
47
+ }
48
+
49
+ // src/message-renderers.ts
34
50
  import { getMarkdownTheme, keyText } from "@earendil-works/pi-coding-agent";
35
51
  import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
36
52
  var COMPLETION_MESSAGE_TYPE = "pi-subagents-completion";
37
- function createCompletionBox(children, outputPad, background) {
53
+ var BROKER_MESSAGE_TYPE = "pi-subagents-message";
54
+ function createMessageBox(children, outputPad, background) {
38
55
  return {
39
56
  render(width) {
40
57
  if (width <= 0) return [];
@@ -48,30 +65,157 @@ function createCompletionBox(children, outputPad, background) {
48
65
  }
49
66
  };
50
67
  }
51
- function registerCompletionRenderer(pi) {
52
- pi.registerMessageRenderer(COMPLETION_MESSAGE_TYPE, (message, options, theme) => {
53
- const children = [
54
- new Text(theme.fg("customMessageLabel", theme.bold(`[${COMPLETION_MESSAGE_TYPE}]`)), 0, 0),
55
- new Spacer(1)
56
- ];
57
- if (!options.expanded) {
58
- children.push(
59
- new Text(
60
- theme.fg("customMessageText", "Subagent job completion (") + theme.fg("dim", keyText("app.tools.expand")) + theme.fg("customMessageText", " to expand)"),
61
- 0,
62
- 0
63
- )
64
- );
68
+ function registerMessageRenderers(pi) {
69
+ for (const [messageType, summary] of [
70
+ [COMPLETION_MESSAGE_TYPE, "Subagent job completion"],
71
+ [BROKER_MESSAGE_TYPE, "Subagent message"]
72
+ ]) {
73
+ pi.registerMessageRenderer(messageType, (message, options, theme) => {
74
+ const children = [
75
+ new Text(theme.fg("customMessageLabel", theme.bold(`[${messageType}]`)), 0, 0),
76
+ new Spacer(1)
77
+ ];
78
+ if (!options.expanded) {
79
+ children.push(
80
+ new Text(
81
+ theme.fg("customMessageText", `${summary} (`) + theme.fg("dim", keyText("app.tools.expand")) + theme.fg("customMessageText", " to expand)"),
82
+ 0,
83
+ 0
84
+ )
85
+ );
86
+ } else {
87
+ const content = typeof message.content === "string" ? message.content : message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
88
+ children.push(
89
+ new Markdown(sanitizeTerminalText(content), 0, 0, getMarkdownTheme(), {
90
+ color: (text) => theme.fg("customMessageText", text)
91
+ })
92
+ );
93
+ }
94
+ return createMessageBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
95
+ });
96
+ }
97
+ }
98
+
99
+ // src/settings.ts
100
+ import fs from "node:fs/promises";
101
+ import { dirname, join } from "node:path";
102
+ import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
103
+ var DEFAULT_SUBAGENT_SETTINGS = Object.freeze({
104
+ showWidget: true,
105
+ showJobId: true,
106
+ showTools: true,
107
+ showTiming: true
108
+ });
109
+ var SETTINGS_KEYS = Object.keys(DEFAULT_SUBAGENT_SETTINGS);
110
+ function subagentSettingsPath() {
111
+ return join(getAgentDir(), "pi-subagents.json");
112
+ }
113
+ var SubagentSettingsStore = class {
114
+ constructor(userPath) {
115
+ this.userPath = userPath;
116
+ }
117
+ userPath;
118
+ effective = { ...DEFAULT_SUBAGENT_SETTINGS };
119
+ tail = Promise.resolve();
120
+ get settings() {
121
+ return { ...this.effective };
122
+ }
123
+ load() {
124
+ return this.enqueue(async () => {
125
+ const loaded = await readDocument(this.userPath);
126
+ const settings = normalizeSettings(loaded.document ?? {}, loaded.diagnostics);
127
+ this.effective = settings;
128
+ return { settings: this.settings, diagnostics: loaded.diagnostics };
129
+ }).catch((error) => ({
130
+ settings: this.settings,
131
+ diagnostics: [readDiagnostic(this.userPath, error)]
132
+ }));
133
+ }
134
+ save(key, value, signal) {
135
+ return this.enqueue(async () => {
136
+ signal.throwIfAborted();
137
+ if (!SETTINGS_KEYS.includes(key) || typeof value !== "boolean") {
138
+ throw new Error("Invalid subagent settings update");
139
+ }
140
+ const loaded = await readDocument(this.userPath);
141
+ signal.throwIfAborted();
142
+ if (!loaded.document) {
143
+ throw new Error(`Cannot save subagent settings: ${loaded.diagnostics.join("; ")}`);
144
+ }
145
+ const document = { ...loaded.document, [key]: value };
146
+ const settings = normalizeSettings(document, []);
147
+ const parent = dirname(this.userPath);
148
+ await fs.mkdir(parent, { recursive: true });
149
+ signal.throwIfAborted();
150
+ const temporary = await fs.mkdtemp(join(parent, ".pi-subagents-"));
151
+ try {
152
+ signal.throwIfAborted();
153
+ const staged = join(temporary, "settings.json");
154
+ await fs.writeFile(staged, `${JSON.stringify(document, null, 2)}
155
+ `, { flag: "wx", signal });
156
+ signal.throwIfAborted();
157
+ await fs.rename(staged, this.userPath);
158
+ this.effective = settings;
159
+ return this.settings;
160
+ } finally {
161
+ await fs.rm(temporary, { recursive: true, force: true }).catch(() => {
162
+ });
163
+ }
164
+ });
165
+ }
166
+ flush() {
167
+ return this.tail;
168
+ }
169
+ enqueue(operation) {
170
+ const previous = this.tail;
171
+ const result = withFileMutationQueue(this.userPath, async () => {
172
+ await previous;
173
+ return operation();
174
+ });
175
+ this.tail = result.then(
176
+ () => {
177
+ },
178
+ () => {
179
+ }
180
+ );
181
+ return result;
182
+ }
183
+ };
184
+ async function readDocument(path4) {
185
+ let text;
186
+ try {
187
+ text = await fs.readFile(path4, "utf8");
188
+ } catch (error) {
189
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
190
+ return { document: {}, diagnostics: [] };
191
+ }
192
+ return { diagnostics: [readDiagnostic(path4, error)] };
193
+ }
194
+ let document;
195
+ try {
196
+ document = JSON.parse(text);
197
+ } catch {
198
+ return { diagnostics: [`Cannot load subagent settings at ${path4}: invalid JSON`] };
199
+ }
200
+ if (typeof document !== "object" || document === null || Array.isArray(document)) {
201
+ return { diagnostics: [`Cannot load subagent settings at ${path4}: expected a JSON object`] };
202
+ }
203
+ return { document, diagnostics: [] };
204
+ }
205
+ function normalizeSettings(document, diagnostics) {
206
+ const settings = { ...DEFAULT_SUBAGENT_SETTINGS };
207
+ for (const key of SETTINGS_KEYS) {
208
+ if (!Object.hasOwn(document, key)) continue;
209
+ if (typeof document[key] === "boolean") {
210
+ settings[key] = document[key];
65
211
  } else {
66
- const content = typeof message.content === "string" ? message.content : message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
67
- children.push(
68
- new Markdown(sanitizeTerminalText(content), 0, 0, getMarkdownTheme(), {
69
- color: (text) => theme.fg("customMessageText", text)
70
- })
71
- );
212
+ diagnostics.push(`Ignored invalid subagent setting ${key}: expected a boolean`);
72
213
  }
73
- return createCompletionBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
74
- });
214
+ }
215
+ return settings;
216
+ }
217
+ function readDiagnostic(path4, error) {
218
+ return `Cannot load subagent settings at ${path4}: ${error instanceof Error ? error.message : String(error)}`;
75
219
  }
76
220
 
77
221
  // src/tools.ts
@@ -114,7 +258,7 @@ function modelVisibleJson(value, options = {}) {
114
258
 
115
259
  // src/process.ts
116
260
  import { spawn } from "node:child_process";
117
- import * as fs from "node:fs";
261
+ import * as fs2 from "node:fs";
118
262
  import * as path from "node:path";
119
263
  import { StringDecoder } from "node:string_decoder";
120
264
  import { fileURLToPath } from "node:url";
@@ -696,23 +840,23 @@ async function executeProcess(invocation, request) {
696
840
  };
697
841
  }
698
842
  function resolvePiInvocation(args) {
699
- const packageDirectory = fs.realpathSync(getPackageDir());
843
+ const packageDirectory = fs2.realpathSync(getPackageDir());
700
844
  const manifestPath = path.join(packageDirectory, "package.json");
701
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
845
+ const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
702
846
  if (manifest.name !== CORE_PACKAGE_NAME || typeof manifest.bin?.pi !== "string") {
703
847
  throw new Error("Loaded Pi core package does not declare a valid bin.pi entry.");
704
848
  }
705
849
  const declared = manifest.bin.pi;
706
850
  if (path.isAbsolute(declared)) throw new Error("Pi core bin.pi must be package-relative.");
707
- if (globalThis.process.versions.bun && /^pi(?:\.exe)?$/iu.test(path.basename(globalThis.process.execPath)) && path.dirname(fs.realpathSync(globalThis.process.execPath)) === packageDirectory) {
851
+ if (globalThis.process.versions.bun && /^pi(?:\.exe)?$/iu.test(path.basename(globalThis.process.execPath)) && path.dirname(fs2.realpathSync(globalThis.process.execPath)) === packageDirectory) {
708
852
  return { command: globalThis.process.execPath, args };
709
853
  }
710
- const cliPath = fs.realpathSync(path.resolve(packageDirectory, declared));
854
+ const cliPath = fs2.realpathSync(path.resolve(packageDirectory, declared));
711
855
  const relative4 = path.relative(packageDirectory, cliPath);
712
856
  if (relative4 === ".." || relative4.startsWith(`..${path.sep}`) || path.isAbsolute(relative4)) {
713
857
  throw new Error("Pi core bin.pi escapes its package directory.");
714
858
  }
715
- if (!fs.statSync(cliPath).isFile()) throw new Error("Pi core bin.pi is not a file.");
859
+ if (!fs2.statSync(cliPath).isFile()) throw new Error("Pi core bin.pi is not a file.");
716
860
  return { command: globalThis.process.execPath, args: [cliPath, ...args] };
717
861
  }
718
862
  function signalPosixProcess(process2, signal) {
@@ -783,7 +927,7 @@ function assertTrustedChildResources(value, cwd) {
783
927
  if (!Array.isArray(commands)) throw invalid();
784
928
  let canonicalCwd;
785
929
  try {
786
- canonicalCwd = fs.realpathSync(cwd);
930
+ canonicalCwd = fs2.realpathSync(cwd);
787
931
  } catch {
788
932
  throw invalid();
789
933
  }
@@ -796,7 +940,7 @@ function assertTrustedChildResources(value, cwd) {
796
940
  const lexicalPath = path.resolve(cwd, resourcePath);
797
941
  let canonicalPath;
798
942
  try {
799
- canonicalPath = fs.realpathSync(lexicalPath);
943
+ canonicalPath = fs2.realpathSync(lexicalPath);
800
944
  } catch {
801
945
  throw invalid();
802
946
  }
@@ -1193,36 +1337,6 @@ function deduplicatePackageSkillPaths(packageSkillPaths, explicitSkillPaths) {
1193
1337
  return uniquePaths;
1194
1338
  }
1195
1339
 
1196
- // src/types.ts
1197
- var TERMINAL_JOB_STATES = /* @__PURE__ */ new Set([
1198
- "completed",
1199
- "partial",
1200
- "failed",
1201
- "timed_out",
1202
- "cancelled"
1203
- ]);
1204
- var CHILD_CORE_TOOL_NAMES = [
1205
- "read",
1206
- "bash",
1207
- "powershell",
1208
- "edit",
1209
- "write",
1210
- "grep",
1211
- "find",
1212
- "ls",
1213
- "codemode"
1214
- ];
1215
- var DEFAULT_SUBAGENT_TOOLS = ["read", "grep", "find", "ls"];
1216
- var SUBAGENT_THINKING_LEVELS = [
1217
- "off",
1218
- "minimal",
1219
- "low",
1220
- "medium",
1221
- "high",
1222
- "xhigh",
1223
- "max"
1224
- ];
1225
-
1226
1340
  // src/resource-attachments.ts
1227
1341
  var MAX_ATTACHED_SKILLS = 16;
1228
1342
  var MAX_ATTACHED_EXTENSIONS = 16;
@@ -1773,6 +1887,7 @@ var SubagentRuntime = class {
1773
1887
  const now = this.now();
1774
1888
  return [...this.jobs.values()].filter((job) => !isTerminal(job.state)).sort((left, right) => left.createdAt - right.createdAt).map((job) => ({
1775
1889
  jobId: job.jobId,
1890
+ task: job.taskText,
1776
1891
  state: job.state,
1777
1892
  elapsedMs: Math.max(0, now - (job.startedAt ?? job.createdAt)),
1778
1893
  ...job.timeout !== void 0 ? { timeout: job.timeout } : {},
@@ -1814,6 +1929,7 @@ var SubagentRuntime = class {
1814
1929
  ...input.timeout !== void 0 ? { timeout: input.timeout } : {},
1815
1930
  controller,
1816
1931
  tools: effectiveTools,
1932
+ taskText: input.task,
1817
1933
  terminal,
1818
1934
  resolveTerminal,
1819
1935
  controlReady,
@@ -2150,7 +2266,6 @@ function abortError2(message) {
2150
2266
  // src/tools.ts
2151
2267
  var MAX_TASK_BYTES = 50 * 1024;
2152
2268
  var MAX_TOOLS = MAX_SELECTED_TOOLS;
2153
- var MESSAGE_TYPE = "pi-subagents-message";
2154
2269
  var CHILD_CORE_TOOL_SET = new Set(CHILD_CORE_TOOL_NAMES);
2155
2270
  var THINKING_LEVEL_SET = new Set(SUBAGENT_THINKING_LEVELS);
2156
2271
  var SpawnParameters = Type.Object(
@@ -2418,7 +2533,7 @@ function deliverMessage(pi, message) {
2418
2533
  );
2419
2534
  pi.sendMessage(
2420
2535
  {
2421
- customType: MESSAGE_TYPE,
2536
+ customType: BROKER_MESSAGE_TYPE,
2422
2537
  content,
2423
2538
  display: true,
2424
2539
  details: {
@@ -2529,15 +2644,19 @@ function toolResult(value) {
2529
2644
  }
2530
2645
 
2531
2646
  // src/widget.ts
2532
- import { truncateToWidth } from "@earendil-works/pi-tui";
2647
+ import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2648
+ import { EditorStatusWidget } from "@narumitw/pi-tui-kit/editor-status-widget";
2533
2649
  var SUBAGENT_WIDGET_KEY = "subagents";
2534
2650
  var SUBAGENT_WIDGET_REFRESH_INTERVAL_MS = 1e3;
2535
2651
  var WIDGET_OPTIONS = { placement: "aboveEditor" };
2652
+ var TASK_PREVIEW_WIDTH = 40;
2536
2653
  function createSubagentWidgetController(runtime) {
2537
2654
  let activeSession;
2538
2655
  let refreshTimer;
2539
2656
  let unsubscribeJobs;
2540
2657
  let publishedValue;
2658
+ let settings = { ...DEFAULT_SUBAGENT_SETTINGS };
2659
+ let clearWidget;
2541
2660
  const ownsSession = (ctx) => ctx.sessionManager === activeSession;
2542
2661
  const stop = () => {
2543
2662
  if (refreshTimer) clearInterval(refreshTimer);
@@ -2547,76 +2666,92 @@ function createSubagentWidgetController(runtime) {
2547
2666
  };
2548
2667
  const publish = (ctx) => {
2549
2668
  if (ctx.mode !== "tui" || !ownsSession(ctx)) return;
2550
- const jobs = runtime.activeJobsForDisplay();
2551
- const value = widgetValue(jobs);
2669
+ const jobs = settings.showWidget ? runtime.activeJobsForDisplay() : [];
2670
+ const value = `${JSON.stringify(settings)}
2671
+ ${widgetValue(jobs)}`;
2552
2672
  if (value === publishedValue) return;
2553
2673
  if (jobs.length === 0) {
2554
2674
  ctx.ui.setWidget(SUBAGENT_WIDGET_KEY, void 0);
2555
2675
  } else {
2556
2676
  const snapshot = jobs.map(cloneDisplayJob);
2677
+ const preferences = { ...settings };
2557
2678
  ctx.ui.setWidget(
2558
2679
  SUBAGENT_WIDGET_KEY,
2559
- (_tui, theme) => ({
2560
- render: (width) => renderSubagentWidget(snapshot, theme, width),
2561
- invalidate: () => {
2562
- }
2563
- }),
2680
+ (_tui, theme) => createSubagentWidget(snapshot, theme, preferences),
2564
2681
  WIDGET_OPTIONS
2565
2682
  );
2566
2683
  }
2567
2684
  publishedValue = value;
2568
2685
  };
2569
2686
  return {
2570
- start(ctx) {
2687
+ start(ctx, preferences = DEFAULT_SUBAGENT_SETTINGS) {
2571
2688
  stop();
2689
+ clearWidget?.();
2690
+ clearWidget = void 0;
2691
+ settings = { ...preferences };
2572
2692
  activeSession = ctx.sessionManager;
2573
2693
  publishedValue = void 0;
2574
2694
  if (ctx.mode !== "tui") return;
2575
- ctx.ui.setWidget(SUBAGENT_WIDGET_KEY, void 0);
2695
+ clearWidget = () => ctx.ui.setWidget(SUBAGENT_WIDGET_KEY, void 0);
2696
+ clearWidget();
2576
2697
  unsubscribeJobs = runtime.subscribeJobs(() => publish(ctx));
2577
2698
  refreshTimer = setInterval(() => publish(ctx), SUBAGENT_WIDGET_REFRESH_INTERVAL_MS);
2578
2699
  refreshTimer.unref();
2579
2700
  publish(ctx);
2580
2701
  },
2702
+ update(ctx, preferences) {
2703
+ if (!ownsSession(ctx)) return;
2704
+ settings = { ...preferences };
2705
+ publish(ctx);
2706
+ },
2581
2707
  shutdown(ctx) {
2582
2708
  if (!ownsSession(ctx)) return;
2583
2709
  stop();
2584
- if (ctx.mode === "tui") ctx.ui.setWidget(SUBAGENT_WIDGET_KEY, void 0);
2710
+ clearWidget?.();
2711
+ clearWidget = void 0;
2585
2712
  publishedValue = void 0;
2586
2713
  activeSession = void 0;
2587
2714
  }
2588
2715
  };
2589
2716
  }
2590
- function renderSubagentWidget(jobs, theme, width) {
2591
- const renderWidth = Math.max(0, width);
2592
- const lines = [
2593
- theme.fg("borderMuted", "\u2500".repeat(renderWidth)),
2594
- theme.fg("muted", `Subagents \xB7 ${jobs.length} active`),
2595
- ...jobs.map((job) => renderJob(job, theme))
2596
- ];
2597
- return lines.map((line) => truncateToWidth(line, renderWidth, ""));
2717
+ function createSubagentWidget(jobs, theme, settings) {
2718
+ return new EditorStatusWidget({
2719
+ theme,
2720
+ renderBody: () => [
2721
+ theme.fg("muted", `Subagents \xB7 ${jobs.length} active`),
2722
+ ...jobs.map((job) => renderJob(job, theme, settings))
2723
+ ]
2724
+ });
2598
2725
  }
2599
- function renderJob(job, theme) {
2726
+ function renderJob(job, theme, settings) {
2600
2727
  const running = job.state === "running";
2601
2728
  const symbol = theme.fg(running ? "accent" : "dim", running ? "\u25B6 " : "\u25CB ");
2602
2729
  const state = theme.fg(running ? "accent" : "muted", job.state);
2603
- const jobId = sanitizeLabel(job.jobId);
2604
- const tools = job.tools.length > 0 ? job.tools.map(sanitizeLabel).join(", ") : "none";
2730
+ const jobId = sanitizeSubagentDisplayText(job.jobId);
2731
+ const taskText = sanitizeSubagentDisplayText(job.task);
2732
+ const task = stripTerminalSequences(truncateToWidth(taskText, TASK_PREVIEW_WIDTH, "\u2026"));
2733
+ const labels = [
2734
+ ...settings.showJobId ? [theme.fg("text", jobId)] : [],
2735
+ ...visibleWidth(task) > 0 ? [theme.fg("text", task)] : [],
2736
+ state
2737
+ ];
2738
+ const tools = job.tools.length > 0 ? job.tools.map(sanitizeSubagentDisplayText).join(", ") : "none";
2605
2739
  const timeout = job.timeout === void 0 ? "no timeout" : formatSeconds(job.timeout);
2606
- const detail = ` \xB7 ${formatSeconds(Math.floor(job.elapsedMs / 1e3))} / ${timeout} \xB7 tools: ${tools}`;
2607
- return `${symbol}${theme.fg("text", jobId)} \xB7 ${state}${theme.fg("muted", detail)}`;
2740
+ const details = [
2741
+ ...settings.showTiming ? [`${formatSeconds(Math.floor(job.elapsedMs / 1e3))} / ${timeout}`] : [],
2742
+ ...settings.showTools ? [`tools: ${tools}`] : []
2743
+ ];
2744
+ const detail = details.length > 0 ? theme.fg("muted", ` \xB7 ${details.join(" \xB7 ")}`) : "";
2745
+ return `${symbol}${labels.join(" \xB7 ")}${detail}`;
2608
2746
  }
2609
2747
  function widgetValue(jobs) {
2610
2748
  return jobs.map(
2611
- (job) => `${job.jobId}\0${job.state}\0${Math.floor(job.elapsedMs / 1e3)}\0${job.timeout ?? ""}\0${job.tools.join(",")}`
2749
+ (job) => `${job.jobId}\0${job.task}\0${job.state}\0${Math.floor(job.elapsedMs / 1e3)}\0${job.timeout ?? ""}\0${job.tools.join(",")}`
2612
2750
  ).join("\n");
2613
2751
  }
2614
2752
  function cloneDisplayJob(job) {
2615
2753
  return { ...job, tools: [...job.tools] };
2616
2754
  }
2617
- function sanitizeLabel(value) {
2618
- return sanitizeTerminalText(value).replace(/\s+/gu, " ").trim();
2619
- }
2620
2755
  function formatSeconds(value) {
2621
2756
  if (value < 60) return `${formatNumber(value)}s`;
2622
2757
  const wholeSeconds = Math.floor(value);
@@ -2632,24 +2767,133 @@ function formatNumber(value) {
2632
2767
 
2633
2768
  // src/subagents.ts
2634
2769
  function subagents(pi, dependencies = {}) {
2635
- registerCompletionRenderer(pi);
2770
+ registerMessageRenderers(pi);
2636
2771
  const tools = registerSubagentTools(pi, dependencies);
2637
2772
  const widget = createSubagentWidgetController(tools.runtime);
2638
- let activeSession;
2773
+ const store = dependencies.settingsStore ?? new SubagentSettingsStore(subagentSettingsPath());
2774
+ let activeContext;
2639
2775
  let sessionGeneration = 0;
2776
+ let owner = new AbortController();
2777
+ let ready = false;
2778
+ let menuOpen = false;
2779
+ let settings = { ...DEFAULT_SUBAGENT_SETTINGS };
2780
+ const menuTasks = /* @__PURE__ */ new Set();
2781
+ const ownsSession = (ctx) => ctx.sessionManager === activeContext?.sessionManager;
2782
+ const openSettings = async (ctx, signal, isCurrent) => {
2783
+ if (signal.aborted || !isCurrent()) return "stale";
2784
+ if (ctx.mode !== "tui") {
2785
+ ctx.ui.notify(sanitizeSubagentDisplayText(`Edit user settings: ${store.userPath}. Then run /reload.`), "info");
2786
+ return "back";
2787
+ }
2788
+ const { showSubagentSettings } = await import("./chunks/settings-ui-PSEYB6JX.ts");
2789
+ if (signal.aborted || !isCurrent()) return "stale";
2790
+ return showSubagentSettings(ctx, {
2791
+ signal,
2792
+ isCurrent,
2793
+ get: () => ({ ...settings }),
2794
+ save: (key, value, actionSignal) => {
2795
+ actionSignal.throwIfAborted();
2796
+ if (!isCurrent()) throw new Error("The subagents session changed.");
2797
+ return store.save(key, value, actionSignal);
2798
+ },
2799
+ apply: (next) => {
2800
+ if (!isCurrent()) return;
2801
+ settings = { ...next };
2802
+ widget.update(ctx, settings);
2803
+ }
2804
+ });
2805
+ };
2806
+ pi.registerCommand("subagents", {
2807
+ description: "Manage subagent jobs and widget settings",
2808
+ getArgumentCompletions: (prefix) => "settings".startsWith(prefix) ? [{ value: "settings", label: "settings", description: "Configure the subagent widget" }] : null,
2809
+ handler: async (args, ctx) => {
2810
+ if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") {
2811
+ throw new Error("/subagents requires TUI or RPC mode.");
2812
+ }
2813
+ const route = args.trim();
2814
+ if (route !== "" && route !== "settings") {
2815
+ ctx.ui.notify("Usage: /subagents | /subagents settings", "warning");
2816
+ return;
2817
+ }
2818
+ if (!ownsSession(ctx) || !ready || menuOpen) {
2819
+ ctx.ui.notify("Subagent controls are loading, unavailable, or already open.", "warning");
2820
+ return;
2821
+ }
2822
+ menuOpen = true;
2823
+ const generation = sessionGeneration;
2824
+ const signal = owner.signal;
2825
+ const isCurrent = () => generation === sessionGeneration && ownsSession(ctx) && !signal.aborted;
2826
+ const task = (async () => {
2827
+ if (route === "settings") {
2828
+ await openSettings(ctx, signal, isCurrent);
2829
+ return;
2830
+ }
2831
+ const { showSubagentsMenu } = await import("./chunks/menu-KAIUEWL7.ts");
2832
+ if (!isCurrent()) return;
2833
+ await showSubagentsMenu(ctx, {
2834
+ signal,
2835
+ isCurrent,
2836
+ getState: () => ({
2837
+ settings: { ...settings },
2838
+ settingsPath: store.userPath,
2839
+ ...tools.runtime.inspectJobs()
2840
+ }),
2841
+ cancelJob: async (jobId, actionSignal) => {
2842
+ actionSignal.throwIfAborted();
2843
+ if (!isCurrent()) throw new Error("The subagents session changed.");
2844
+ await tools.runtime.cancel(jobId);
2845
+ },
2846
+ showSettings: (actionSignal) => openSettings(ctx, actionSignal, isCurrent)
2847
+ });
2848
+ })();
2849
+ menuTasks.add(task);
2850
+ try {
2851
+ await task;
2852
+ } catch (error) {
2853
+ if (isCurrent()) {
2854
+ ctx.ui.notify(sanitizeSubagentDisplayText(error instanceof Error ? error.message : String(error)), "error");
2855
+ }
2856
+ } finally {
2857
+ menuTasks.delete(task);
2858
+ if (isCurrent()) menuOpen = false;
2859
+ }
2860
+ }
2861
+ });
2640
2862
  pi.on("session_start", async (_event, ctx) => {
2641
- activeSession = ctx.sessionManager;
2863
+ owner.abort();
2864
+ if (activeContext) widget.shutdown(activeContext);
2865
+ owner = new AbortController();
2866
+ activeContext = ctx;
2867
+ ready = false;
2868
+ menuOpen = false;
2642
2869
  const generation = ++sessionGeneration;
2870
+ const isCurrent = () => generation === sessionGeneration && ownsSession(ctx) && !owner.signal.aborted;
2643
2871
  await tools.startSession();
2644
- if (generation !== sessionGeneration) return;
2645
- widget.start(ctx);
2872
+ if (!isCurrent()) return;
2873
+ await Promise.allSettled([...menuTasks]);
2874
+ if (!isCurrent()) return;
2875
+ const loaded = await store.load();
2876
+ if (!isCurrent()) return;
2877
+ settings = { ...loaded.settings };
2878
+ ready = true;
2879
+ widget.start(ctx, settings);
2880
+ if (loaded.diagnostics.length > 0) {
2881
+ const message = sanitizeSubagentDisplayText(`Subagent settings: ${loaded.diagnostics.join(" ")}`);
2882
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
2883
+ else throw new Error(message);
2884
+ }
2646
2885
  });
2647
2886
  pi.on("session_shutdown", async (_event, ctx) => {
2648
- if (ctx.sessionManager !== activeSession) return;
2887
+ if (!ownsSession(ctx)) return;
2649
2888
  sessionGeneration++;
2650
- activeSession = void 0;
2889
+ owner.abort();
2890
+ ready = false;
2891
+ menuOpen = false;
2892
+ activeContext = void 0;
2651
2893
  widget.shutdown(ctx);
2652
- await tools.shutdown();
2894
+ const results = await Promise.allSettled([tools.shutdown(), ...menuTasks, store.flush()]);
2895
+ const failed = results.find((result) => result.status === "rejected");
2896
+ if (failed?.status === "rejected") throw failed.reason;
2653
2897
  });
2654
2898
  }
2655
2899
  export {