@narumitw/pi-subagents 0.1.36 → 0.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/README.md CHANGED
@@ -13,6 +13,7 @@ Use it to split research, planning, implementation, and review work across focus
13
13
  - Supports built-in `scout`, `planner`, `reviewer`, and `worker` agents.
14
14
  - Loads custom user agents from `~/.pi/agent/agents/*.md`.
15
15
  - Optionally loads project agents from `.pi/agents/*.md` with confirmation.
16
+ - Provides `/subagents:config` to persist per-agent tool allow-lists.
16
17
  - Supports per-task `cwd`, hard subprocess `timeoutMs`, abort propagation, and streaming progress.
17
18
  - Publishes transient runtime status through Pi's generic extension status API while subagents are running.
18
19
  - Returns complete worker output in tool details and a concise result for the main agent.
@@ -167,6 +168,19 @@ Built-in agents are available without setup and can be overridden by user or pro
167
168
 
168
169
  Built-in agents inherit the active/default Pi model instead of forcing a provider-specific model alias, which keeps subprocesses usable across different Pi setups.
169
170
 
171
+ ## ⚙️ Configure agent tools
172
+
173
+ Run `/subagents:config` in an interactive Pi session to edit the tools each subagent may use.
174
+ The command stores settings in `~/.pi/agent/pi-subagents-config.json`.
175
+
176
+ - Select an agent, then press Enter or Space to toggle tools.
177
+ - Press `S` to save, or Esc to cancel and return to agent selection.
178
+ - Save the default selection to remove a custom override and use the agent defaults again.
179
+ - Deselect every tool and save to run that agent with no tools.
180
+
181
+ Configured tool names that are not currently registered are preserved, so settings for tools from
182
+ other extension sessions are not silently dropped.
183
+
170
184
  ## 🧩 Custom agents
171
185
 
172
186
  Create markdown agent definitions in either location:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "0.1.36",
3
+ "version": "0.2.0",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/agents.ts CHANGED
@@ -15,11 +15,22 @@ export interface AgentConfig {
15
15
  description: string;
16
16
  tools?: string[];
17
17
  model?: string;
18
+ timeoutMs?: number;
18
19
  systemPrompt: string;
19
20
  source: AgentSource;
20
21
  filePath: string;
21
22
  }
22
23
 
24
+ export interface SubagentAgentConfig {
25
+ tools?: string[];
26
+ model?: string | null;
27
+ timeoutMs?: number | null;
28
+ }
29
+
30
+ export interface SubagentSettings {
31
+ agents?: Record<string, SubagentAgentConfig>;
32
+ }
33
+
23
34
  const BUILT_IN_AGENTS: AgentConfig[] = [
24
35
  {
25
36
  name: "scout",
@@ -164,7 +175,15 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
164
175
  }
165
176
  }
166
177
 
167
- export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
178
+ function hasOwn(obj: object, key: PropertyKey): boolean {
179
+ return Object.hasOwn(obj, key);
180
+ }
181
+
182
+ export function discoverAgents(
183
+ cwd: string,
184
+ scope: AgentScope,
185
+ config?: SubagentSettings,
186
+ ): AgentDiscoveryResult {
168
187
  const userDir = path.join(getAgentDir(), "agents");
169
188
  const projectAgentsDir = findNearestProjectAgentsDir(cwd);
170
189
 
@@ -187,6 +206,23 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
187
206
  for (const agent of projectAgents) agentMap.set(agent.name, agent);
188
207
  }
189
208
 
209
+ // Apply user-configured overrides (from /subagents:config) on top of
210
+ // the final resolved agent map, regardless of agent source.
211
+ for (const [name, override] of Object.entries(config?.agents ?? {})) {
212
+ const agent = agentMap.get(name);
213
+ if (!agent) continue;
214
+
215
+ const nextAgent: AgentConfig = { ...agent };
216
+ if (hasOwn(override, "tools")) nextAgent.tools = override.tools;
217
+ if (hasOwn(override, "model")) {
218
+ nextAgent.model = override.model === null ? undefined : override.model;
219
+ }
220
+ if (hasOwn(override, "timeoutMs")) {
221
+ nextAgent.timeoutMs = override.timeoutMs === null ? undefined : override.timeoutMs;
222
+ }
223
+ agentMap.set(name, nextAgent);
224
+ }
225
+
190
226
  return { agents: Array.from(agentMap.values()), projectAgentsDir };
191
227
  }
192
228
 
package/src/subagents.ts CHANGED
@@ -21,12 +21,31 @@ import type { Message } from "@earendil-works/pi-ai";
21
21
  import { StringEnum } from "@earendil-works/pi-ai";
22
22
  import {
23
23
  type ExtensionAPI,
24
+ getAgentDir,
24
25
  getMarkdownTheme,
25
26
  withFileMutationQueue,
27
+ DynamicBorder,
26
28
  } from "@earendil-works/pi-coding-agent";
27
- import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
29
+ import {
30
+ Container,
31
+ Markdown,
32
+ Spacer,
33
+ Text,
34
+ type SelectItem,
35
+ SelectList,
36
+ Key,
37
+ matchesKey,
38
+ truncateToWidth,
39
+ } from "@earendil-works/pi-tui";
28
40
  import { Type } from "typebox";
29
- import { type AgentConfig, type AgentScope, type AgentSource, discoverAgents } from "./agents.js";
41
+ import {
42
+ type AgentConfig,
43
+ type AgentScope,
44
+ type AgentSource,
45
+ type SubagentAgentConfig,
46
+ type SubagentSettings,
47
+ discoverAgents,
48
+ } from "./agents.js";
30
49
 
31
50
  const MAX_PARALLEL_TASKS = 8;
32
51
  const MAX_CONCURRENCY = 4;
@@ -382,7 +401,10 @@ async function runSingleAgent(
382
401
 
383
402
  const args: string[] = ["--mode", "json", "-p", "--no-session"];
384
403
  if (agent.model) args.push("--model", agent.model);
385
- if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
404
+ if (Array.isArray(agent.tools)) {
405
+ if (agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
406
+ else args.push("--no-tools");
407
+ }
386
408
 
387
409
  let tmpPromptDir: string | null = null;
388
410
  let tmpPromptPath: string | null = null;
@@ -395,7 +417,7 @@ async function runSingleAgent(
395
417
  messages: [],
396
418
  stderr: "",
397
419
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
398
- model: agent.model,
420
+ model: agent.model ?? undefined,
399
421
  step,
400
422
  timeoutMs,
401
423
  };
@@ -586,6 +608,156 @@ const SubagentParams = Type.Object({
586
608
  timeoutMs: Type.Optional(TimeoutMs),
587
609
  });
588
610
 
611
+ // ---- Settings helpers ----
612
+
613
+ function hasOwn(obj: object, key: PropertyKey): boolean {
614
+ return Object.hasOwn(obj, key);
615
+ }
616
+
617
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
618
+ return typeof value === "object" && value !== null && !Array.isArray(value);
619
+ }
620
+
621
+ function isStringArray(value: unknown): value is string[] {
622
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
623
+ }
624
+
625
+ function isPositiveNumber(value: unknown): value is number {
626
+ return typeof value === "number" && Number.isFinite(value) && value >= 1;
627
+ }
628
+
629
+ function normalizeAgentSettings(value: unknown): SubagentAgentConfig | undefined {
630
+ if (!isPlainObject(value)) return undefined;
631
+
632
+ const config: SubagentAgentConfig = {};
633
+ let hasKnownField = false;
634
+
635
+ if (hasOwn(value, "tools")) {
636
+ if (!isStringArray(value.tools)) return undefined;
637
+ config.tools = value.tools;
638
+ hasKnownField = true;
639
+ }
640
+
641
+ if (hasOwn(value, "model")) {
642
+ if (value.model !== null && typeof value.model !== "string") return undefined;
643
+ config.model = value.model;
644
+ hasKnownField = true;
645
+ }
646
+
647
+ if (hasOwn(value, "timeoutMs")) {
648
+ if (value.timeoutMs !== null && !isPositiveNumber(value.timeoutMs)) return undefined;
649
+ config.timeoutMs = value.timeoutMs;
650
+ hasKnownField = true;
651
+ }
652
+
653
+ return hasKnownField ? config : undefined;
654
+ }
655
+
656
+ function normalizeSubagentSettings(value: unknown): SubagentSettings | undefined {
657
+ if (!isPlainObject(value)) return undefined;
658
+ if (!hasOwn(value, "agents")) return {};
659
+ if (!isPlainObject(value.agents)) return undefined;
660
+
661
+ const agents: Record<string, SubagentAgentConfig> = {};
662
+ for (const [name, rawConfig] of Object.entries(value.agents)) {
663
+ const config = normalizeAgentSettings(rawConfig);
664
+ if (config) agents[name] = config;
665
+ }
666
+
667
+ return Object.keys(agents).length > 0 ? { agents } : {};
668
+ }
669
+
670
+ function readSubagentSettings(): SubagentSettings | undefined {
671
+ const configPath = path.join(getAgentDir(), "pi-subagents-config.json");
672
+ if (!fs.existsSync(configPath)) return undefined;
673
+ try {
674
+ return normalizeSubagentSettings(JSON.parse(fs.readFileSync(configPath, "utf-8")));
675
+ } catch {
676
+ return undefined;
677
+ }
678
+ }
679
+
680
+ function saveSubagentConfig(settings: SubagentSettings): void {
681
+ const agentDir = getAgentDir();
682
+ fs.mkdirSync(agentDir, { recursive: true });
683
+
684
+ const configPath = path.join(agentDir, "pi-subagents-config.json");
685
+ fs.writeFileSync(configPath, `${JSON.stringify(settings, null, "\t")}\n`, "utf-8");
686
+ }
687
+
688
+ function uniqueToolNames(tools: string[]): string[] {
689
+ return [...new Set(tools)];
690
+ }
691
+
692
+ function sameToolSet(left: string[], right: string[]): boolean {
693
+ const leftSet = new Set(left);
694
+ const rightSet = new Set(right);
695
+ if (leftSet.size !== rightSet.size) return false;
696
+ return [...leftSet].every((tool) => rightSet.has(tool));
697
+ }
698
+
699
+ function hasAnyAgentOverride(config: SubagentAgentConfig): boolean {
700
+ return hasOwn(config, "tools") || hasOwn(config, "model") || hasOwn(config, "timeoutMs");
701
+ }
702
+
703
+ // ---- Tool toggle component ----
704
+
705
+ class ToolToggleList {
706
+ private items: { name: string; selected: boolean }[];
707
+ private cursor = 0;
708
+ private cachedWidth?: number;
709
+ private cachedLines?: string[];
710
+ onDone?: (selected: string[]) => void;
711
+ onCancel?: () => void;
712
+
713
+ constructor(tools: string[], selected: Set<string>) {
714
+ this.items = tools.map((name) => ({ name, selected: selected.has(name) }));
715
+ }
716
+
717
+ private getSelectedNames(): string[] {
718
+ return this.items.filter((i) => i.selected).map((i) => i.name);
719
+ }
720
+
721
+ handleInput(data: string): void {
722
+ if (matchesKey(data, Key.escape)) {
723
+ this.onCancel?.();
724
+ return;
725
+ }
726
+ if (data === "s" || data === "S") {
727
+ this.onDone?.(this.getSelectedNames());
728
+ return;
729
+ }
730
+ if (this.items.length === 0) return;
731
+
732
+ if (matchesKey(data, Key.up) && this.cursor > 0) {
733
+ this.cursor--;
734
+ this.invalidate();
735
+ } else if (matchesKey(data, Key.down) && this.cursor < this.items.length - 1) {
736
+ this.cursor++;
737
+ this.invalidate();
738
+ } else if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
739
+ this.items[this.cursor].selected = !this.items[this.cursor].selected;
740
+ this.invalidate();
741
+ }
742
+ }
743
+
744
+ render(width: number): string[] {
745
+ if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
746
+ this.cachedWidth = width;
747
+ this.cachedLines = this.items.map((item, i) => {
748
+ const pointer = i === this.cursor ? ">" : " ";
749
+ const check = item.selected ? "✓" : "○";
750
+ return truncateToWidth(`${pointer} ${check} ${item.name}`, width);
751
+ });
752
+ return this.cachedLines;
753
+ }
754
+
755
+ invalidate(): void {
756
+ this.cachedWidth = undefined;
757
+ this.cachedLines = undefined;
758
+ }
759
+ }
760
+
589
761
  export default function (pi: ExtensionAPI) {
590
762
  pi.registerTool({
591
763
  name: "subagent",
@@ -610,10 +782,15 @@ export default function (pi: ExtensionAPI) {
610
782
 
611
783
  async execute(toolCallId, params, signal, onUpdate, ctx) {
612
784
  const agentScope: AgentScope = params.agentScope ?? "user";
613
- const discovery = discoverAgents(ctx.cwd, agentScope);
785
+ const config = readSubagentSettings();
786
+ const discovery = discoverAgents(ctx.cwd, agentScope, config);
614
787
  const agents = discovery.agents;
615
788
  const confirmProjectAgents = params.confirmProjectAgents ?? true;
616
- const defaultTimeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;
789
+ const resolveTimeoutMs = (agentName: string, localTimeoutMs?: number) =>
790
+ localTimeoutMs ??
791
+ params.timeoutMs ??
792
+ agents.find((agent) => agent.name === agentName)?.timeoutMs ??
793
+ DEFAULT_TIMEOUT_MS;
617
794
 
618
795
  const hasChain = (params.chain?.length ?? 0) > 0;
619
796
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -707,7 +884,7 @@ export default function (pi: ExtensionAPI) {
707
884
  step.cwd,
708
885
  i + 1,
709
886
  signal,
710
- step.timeoutMs ?? defaultTimeoutMs,
887
+ resolveTimeoutMs(step.agent, step.timeoutMs),
711
888
  chainUpdate,
712
889
  makeDetails("chain"),
713
890
  );
@@ -793,7 +970,7 @@ export default function (pi: ExtensionAPI) {
793
970
  t.cwd,
794
971
  undefined,
795
972
  signal,
796
- t.timeoutMs ?? defaultTimeoutMs,
973
+ resolveTimeoutMs(t.agent, t.timeoutMs),
797
974
  // Per-task update callback
798
975
  (partial) => {
799
976
  if (partial.details?.results[0]) {
@@ -826,7 +1003,7 @@ export default function (pi: ExtensionAPI) {
826
1003
  aggregator.cwd,
827
1004
  undefined,
828
1005
  signal,
829
- aggregator.timeoutMs ?? defaultTimeoutMs,
1006
+ resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
830
1007
  (partial) => {
831
1008
  status.update(fanInStatus(aggregator.agent));
832
1009
  if (onUpdate && partial.details?.results[0]) {
@@ -883,7 +1060,7 @@ export default function (pi: ExtensionAPI) {
883
1060
  params.cwd,
884
1061
  undefined,
885
1062
  signal,
886
- params.timeoutMs ?? defaultTimeoutMs,
1063
+ resolveTimeoutMs(params.agent, params.timeoutMs),
887
1064
  onUpdate,
888
1065
  makeDetails("single"),
889
1066
  );
@@ -1290,4 +1467,198 @@ export default function (pi: ExtensionAPI) {
1290
1467
  return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
1291
1468
  },
1292
1469
  });
1470
+
1471
+ // ---- Configuration command ----
1472
+
1473
+ pi.registerCommand("subagents:config", {
1474
+ description: "Configure which tools each subagent can use",
1475
+ handler: async (_args, ctx) => {
1476
+ if (!ctx.hasUI) {
1477
+ return;
1478
+ }
1479
+
1480
+ // Get current settings
1481
+ const currentSettings = readSubagentSettings() ?? {};
1482
+ const currentAgents = currentSettings.agents ?? {};
1483
+
1484
+ // Discover agents to show which ones are available
1485
+ const discovery = discoverAgents(ctx.cwd, "user", currentSettings);
1486
+ const agents = discovery.agents;
1487
+
1488
+ if (agents.length === 0) {
1489
+ ctx.ui.notify("No agents found", "warning");
1490
+ return;
1491
+ }
1492
+
1493
+ // Loop: agent selection → tool toggle (Esc in tools returns here)
1494
+ while (true) {
1495
+ // Step 1: pick an agent to configure
1496
+ const agentItems: SelectItem[] = agents.map((a) => {
1497
+ const cfg = currentAgents[a.name];
1498
+ const hasToolsOverride = cfg ? hasOwn(cfg, "tools") : false;
1499
+ const toolSummary = hasToolsOverride
1500
+ ? cfg?.tools && cfg.tools.length > 0
1501
+ ? cfg.tools.join(", ")
1502
+ : "none"
1503
+ : "defaults";
1504
+ return {
1505
+ value: a.name,
1506
+ label: a.name,
1507
+ description: `${a.source} · tools: ${toolSummary}`,
1508
+ };
1509
+ });
1510
+
1511
+ const agentName = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
1512
+ const container = new Container();
1513
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1514
+ container.addChild(
1515
+ new Text(theme.fg("accent", theme.bold("Subagent Tool Configuration")), 1, 0),
1516
+ );
1517
+ container.addChild(new Spacer(1));
1518
+ container.addChild(
1519
+ new Text(theme.fg("muted", "Select an agent to configure its allowed tools:"), 1, 0),
1520
+ );
1521
+ container.addChild(new Spacer(1));
1522
+ const selectList = new SelectList(agentItems, Math.min(agentItems.length + 2, 15), {
1523
+ selectedPrefix: (t: string) => theme.fg("accent", t),
1524
+ selectedText: (t: string) => theme.fg("accent", t),
1525
+ description: (t: string) => theme.fg("muted", t),
1526
+ scrollInfo: (t: string) => theme.fg("dim", t),
1527
+ noMatch: (t: string) => theme.fg("warning", t),
1528
+ });
1529
+ selectList.onSelect = (item) => done(item.value);
1530
+ selectList.onCancel = () => done(null);
1531
+ container.addChild(selectList);
1532
+ container.addChild(
1533
+ new Text(theme.fg("dim", "↑↓ navigate · enter select · esc cancel"), 1, 0),
1534
+ );
1535
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1536
+ return {
1537
+ render: (w: number) => container.render(w),
1538
+ invalidate: () => container.invalidate(),
1539
+ handleInput: (data: string) => {
1540
+ selectList.handleInput(data);
1541
+ tui.requestRender();
1542
+ },
1543
+ };
1544
+ });
1545
+
1546
+ if (!agentName) return;
1547
+
1548
+ const agent = agents.find((a) => a.name === agentName);
1549
+ if (!agent) return;
1550
+
1551
+ // Step 2: toggle tools for the selected agent
1552
+ // Discover without overrides to get original built-in/frontmatter defaults.
1553
+ // The main discovery above applies saved overrides, so agent.tools is already
1554
+ // overridden — using it for the reset-to-default comparison would match the
1555
+ // override against itself and silently delete it on a no-op save.
1556
+ const defaultDiscovery = discoverAgents(ctx.cwd, "user");
1557
+ const defaultTools = defaultDiscovery.agents.find((a) => a.name === agentName)?.tools;
1558
+ const currentAgentSettings = currentAgents[agentName];
1559
+ const configuredTools =
1560
+ currentAgentSettings && hasOwn(currentAgentSettings, "tools")
1561
+ ? (currentAgentSettings.tools ?? [])
1562
+ : undefined;
1563
+
1564
+ // Get all available tools from pi's registry
1565
+ const allTools = uniqueToolNames(pi.getAllTools().map((t) => t.name)).sort((a, b) =>
1566
+ a.localeCompare(b),
1567
+ );
1568
+ const currentTools = uniqueToolNames(configuredTools ?? defaultTools ?? allTools);
1569
+ // Sort: currently selected tools first, then rest alphabetically. Preserve
1570
+ // unavailable configured tools so saving does not silently drop them.
1571
+ const currentSet = new Set(currentTools);
1572
+ const selectedFirst = [...currentTools, ...allTools.filter((t) => !currentSet.has(t))];
1573
+
1574
+ const selectedTools = await ctx.ui.custom<string[] | null>((tui, theme, _kb, done) => {
1575
+ const toggleList = new ToolToggleList(selectedFirst, currentSet);
1576
+
1577
+ const container = new Container();
1578
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1579
+ container.addChild(
1580
+ new Text(
1581
+ theme.fg("accent", theme.bold(`${agentName} tools`)) +
1582
+ theme.fg("muted", ` (${agent.source})`),
1583
+ 1,
1584
+ 0,
1585
+ ),
1586
+ );
1587
+ container.addChild(new Spacer(1));
1588
+ container.addChild(
1589
+ new Text(theme.fg("muted", "Toggle tools with Enter/Space. S to save, Esc to cancel."), 1, 0),
1590
+ );
1591
+ container.addChild(new Spacer(1));
1592
+
1593
+ const listContainer = new Container();
1594
+ listContainer.addChild({
1595
+ render: (w: number) => toggleList.render(w),
1596
+ invalidate: () => toggleList.invalidate(),
1597
+ });
1598
+ container.addChild(listContainer);
1599
+
1600
+ container.addChild(new Spacer(1));
1601
+ container.addChild(
1602
+ new Text(theme.fg("dim", "↑↓ navigate · enter/space toggle · S save · esc cancel"), 1, 0),
1603
+ );
1604
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
1605
+
1606
+ toggleList.onDone = (tools) => done(tools);
1607
+ toggleList.onCancel = () => done(null);
1608
+
1609
+ return {
1610
+ render: (w: number) => container.render(w),
1611
+ invalidate: () => container.invalidate(),
1612
+ handleInput: (data: string) => {
1613
+ toggleList.handleInput(data);
1614
+ tui.requestRender();
1615
+ },
1616
+ };
1617
+ });
1618
+
1619
+ // null means user cancelled — loop back to agent selection
1620
+ if (selectedTools === null) continue;
1621
+
1622
+ // Save to global settings
1623
+ const updatedAgents = { ...currentAgents };
1624
+ let restoredDefaults = false;
1625
+
1626
+ const isSameAsDefault =
1627
+ defaultTools === undefined
1628
+ ? sameToolSet(selectedTools, allTools)
1629
+ : sameToolSet(selectedTools, defaultTools);
1630
+
1631
+ if (isSameAsDefault) {
1632
+ // Tools match defaults — remove only the tools override.
1633
+ // Keep other settings (model, timeoutMs) if present.
1634
+ const existing = updatedAgents[agentName];
1635
+ if (existing) {
1636
+ const nextConfig = { ...existing };
1637
+ delete nextConfig.tools;
1638
+ if (hasAnyAgentOverride(nextConfig)) updatedAgents[agentName] = nextConfig;
1639
+ else delete updatedAgents[agentName];
1640
+ }
1641
+ restoredDefaults = true;
1642
+ } else {
1643
+ updatedAgents[agentName] = {
1644
+ ...updatedAgents[agentName],
1645
+ tools: selectedTools,
1646
+ };
1647
+ }
1648
+
1649
+ const newSettings: SubagentSettings = {
1650
+ ...currentSettings,
1651
+ agents: Object.keys(updatedAgents).length > 0 ? updatedAgents : undefined,
1652
+ };
1653
+
1654
+ saveSubagentConfig(newSettings);
1655
+ const message = restoredDefaults
1656
+ ? `${agentName}: defaults restored`
1657
+ : `${agentName}: ${selectedTools.length} tool${selectedTools.length !== 1 ? "s" : ""} configured`;
1658
+ ctx.ui.notify(message, "info");
1659
+ // Saved — exit the loop
1660
+ break;
1661
+ }
1662
+ },
1663
+ });
1293
1664
  }