@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 +14 -0
- package/package.json +1 -1
- package/src/agents.ts +37 -1
- package/src/subagents.ts +381 -10
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
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
|
-
|
|
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 {
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|