@tintinweb/pi-subagents 0.2.7 → 0.3.1
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/CHANGELOG.md +43 -0
- package/README.md +32 -26
- package/package.json +4 -4
- package/src/agent-runner.ts +69 -39
- package/src/agent-types.ts +124 -84
- package/src/custom-agents.ts +10 -7
- package/src/default-agents.ts +164 -0
- package/src/index.ts +283 -175
- package/src/model-resolver.ts +81 -0
- package/src/prompts.ts +26 -141
- package/src/types.ts +14 -34
- package/src/ui/conversation-viewer.ts +241 -0
package/src/index.ts
CHANGED
|
@@ -18,10 +18,11 @@ import { Text } from "@mariozechner/pi-tui";
|
|
|
18
18
|
import { Type } from "@sinclair/typebox";
|
|
19
19
|
import { AgentManager } from "./agent-manager.js";
|
|
20
20
|
import { steerAgent, getAgentConversation, getDefaultMaxTurns, setDefaultMaxTurns, getGraceTurns, setGraceTurns } from "./agent-runner.js";
|
|
21
|
-
import {
|
|
21
|
+
import { DEFAULT_AGENT_NAMES, type SubagentType, type ThinkingLevel, type AgentConfig, type JoinMode, type AgentRecord } from "./types.js";
|
|
22
22
|
import { GroupJoinManager } from "./group-join.js";
|
|
23
|
-
import { getAvailableTypes,
|
|
23
|
+
import { getAvailableTypes, getAllTypes, getDefaultAgentNames, getUserAgentNames, getAgentConfig, resolveType, registerAgents, BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
24
24
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
25
|
+
import { resolveModel, type ModelRegistry } from "./model-resolver.js";
|
|
25
26
|
import {
|
|
26
27
|
AgentWidget,
|
|
27
28
|
SPINNER,
|
|
@@ -119,82 +120,25 @@ function buildDetails(
|
|
|
119
120
|
};
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
/** Resolve system prompt overrides from
|
|
123
|
-
function resolveCustomPrompt(config:
|
|
123
|
+
/** Resolve system prompt overrides from an agent config. */
|
|
124
|
+
function resolveCustomPrompt(config: AgentConfig | undefined): {
|
|
124
125
|
systemPromptOverride?: string;
|
|
125
126
|
systemPromptAppend?: string;
|
|
126
127
|
} {
|
|
127
128
|
if (!config?.systemPrompt) return {};
|
|
129
|
+
// Default agents use their systemPrompt via buildAgentPrompt in agent-runner,
|
|
130
|
+
// not via override/append. Only non-default agents use this path.
|
|
131
|
+
if (config.isDefault) return {};
|
|
128
132
|
if (config.promptMode === "append") return { systemPromptAppend: config.systemPrompt };
|
|
129
133
|
return { systemPromptOverride: config.systemPrompt };
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
/**
|
|
133
|
-
* Resolve a model string to a Model instance.
|
|
134
|
-
* Tries exact match first ("provider/modelId"), then fuzzy match against all available models.
|
|
135
|
-
* Returns the Model on success, or an error message string on failure.
|
|
136
|
-
*/
|
|
137
|
-
function resolveModel(
|
|
138
|
-
input: string,
|
|
139
|
-
registry: { find(provider: string, modelId: string): any; getAll(): any[]; getAvailable?(): any[] },
|
|
140
|
-
): any | string {
|
|
141
|
-
// 1. Exact match: "provider/modelId"
|
|
142
|
-
const slashIdx = input.indexOf("/");
|
|
143
|
-
if (slashIdx !== -1) {
|
|
144
|
-
const provider = input.slice(0, slashIdx);
|
|
145
|
-
const modelId = input.slice(slashIdx + 1);
|
|
146
|
-
const found = registry.find(provider, modelId);
|
|
147
|
-
if (found) return found;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 2. Fuzzy match against available models (those with auth configured)
|
|
151
|
-
const all = (registry.getAvailable?.() ?? registry.getAll()) as { id: string; name: string; provider: string }[];
|
|
152
|
-
const query = input.toLowerCase();
|
|
153
|
-
|
|
154
|
-
// Score each model: prefer exact id match > id contains > name contains > provider+id contains
|
|
155
|
-
let bestMatch: typeof all[number] | undefined;
|
|
156
|
-
let bestScore = 0;
|
|
157
|
-
|
|
158
|
-
for (const m of all) {
|
|
159
|
-
const id = m.id.toLowerCase();
|
|
160
|
-
const name = m.name.toLowerCase();
|
|
161
|
-
const full = `${m.provider}/${m.id}`.toLowerCase();
|
|
162
|
-
|
|
163
|
-
let score = 0;
|
|
164
|
-
if (id === query || full === query) {
|
|
165
|
-
score = 100; // exact
|
|
166
|
-
} else if (id.includes(query) || full.includes(query)) {
|
|
167
|
-
score = 60 + (query.length / id.length) * 30; // substring, prefer tighter matches
|
|
168
|
-
} else if (name.includes(query)) {
|
|
169
|
-
score = 40 + (query.length / name.length) * 20;
|
|
170
|
-
} else if (query.split(/[\s\-/]+/).every(part => id.includes(part) || name.includes(part) || m.provider.toLowerCase().includes(part))) {
|
|
171
|
-
score = 20; // all parts present somewhere
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (score > bestScore) {
|
|
175
|
-
bestScore = score;
|
|
176
|
-
bestMatch = m;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (bestMatch && bestScore >= 20) {
|
|
181
|
-
const found = registry.find(bestMatch.provider, bestMatch.id);
|
|
182
|
-
if (found) return found;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// 3. No match — list available models
|
|
186
|
-
const modelList = all
|
|
187
|
-
.map(m => ` ${m.provider}/${m.id}`)
|
|
188
|
-
.sort()
|
|
189
|
-
.join("\n");
|
|
190
|
-
return `Model not found: "${input}".\n\nAvailable models:\n${modelList}`;
|
|
191
|
-
}
|
|
192
136
|
|
|
193
137
|
export default function (pi: ExtensionAPI) {
|
|
194
|
-
/** Reload
|
|
138
|
+
/** Reload agents from .pi/agents/*.md and merge with defaults (called on init and each Agent invocation). */
|
|
195
139
|
const reloadCustomAgents = () => {
|
|
196
|
-
const
|
|
197
|
-
|
|
140
|
+
const userAgents = loadCustomAgents(process.cwd());
|
|
141
|
+
registerAgents(userAgents);
|
|
198
142
|
};
|
|
199
143
|
|
|
200
144
|
// Initial load
|
|
@@ -352,31 +296,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
352
296
|
widget.onTurnStart();
|
|
353
297
|
});
|
|
354
298
|
|
|
355
|
-
|
|
356
|
-
const builtinDescs = [
|
|
357
|
-
"- general-purpose: Full tool access for complex multi-step tasks.",
|
|
358
|
-
"- Explore: Fast codebase exploration (read-only, defaults to haiku).",
|
|
359
|
-
"- Plan: Software architect for implementation planning (read-only).",
|
|
360
|
-
"- statusline-setup: Configuration editor (read + edit only).",
|
|
361
|
-
"- claude-code-guide: Documentation and help queries (read-only).",
|
|
362
|
-
];
|
|
363
|
-
|
|
364
|
-
/** Build the full type list text, including any currently loaded custom agents. */
|
|
299
|
+
/** Build the full type list text dynamically from the unified registry. */
|
|
365
300
|
const buildTypeListText = () => {
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
|
|
301
|
+
const defaultNames = getDefaultAgentNames();
|
|
302
|
+
const userNames = getUserAgentNames();
|
|
303
|
+
|
|
304
|
+
const defaultDescs = defaultNames.map((name) => {
|
|
305
|
+
const cfg = getAgentConfig(name);
|
|
306
|
+
const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
307
|
+
return `- ${name}: ${cfg?.description ?? name}${modelSuffix}`;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const customDescs = userNames.map((name) => {
|
|
311
|
+
const cfg = getAgentConfig(name);
|
|
369
312
|
return `- ${name}: ${cfg?.description ?? name}`;
|
|
370
313
|
});
|
|
314
|
+
|
|
371
315
|
return [
|
|
372
|
-
"
|
|
373
|
-
...
|
|
374
|
-
...(customDescs.length > 0 ? ["", "Custom
|
|
316
|
+
"Default agents:",
|
|
317
|
+
...defaultDescs,
|
|
318
|
+
...(customDescs.length > 0 ? ["", "Custom agents:", ...customDescs] : []),
|
|
375
319
|
"",
|
|
376
|
-
"Custom agents can be defined in .pi/agents/<name>.md (project) or ~/.pi/agent/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones.",
|
|
320
|
+
"Custom agents can be defined in .pi/agents/<name>.md (project) or ~/.pi/agent/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.",
|
|
377
321
|
].join("\n");
|
|
378
322
|
};
|
|
379
323
|
|
|
324
|
+
/** Derive a short model label from a model string. */
|
|
325
|
+
function getModelLabelFromConfig(model: string): string {
|
|
326
|
+
// Strip provider prefix (e.g. "anthropic/claude-sonnet-4-6" → "claude-sonnet-4-6")
|
|
327
|
+
const name = model.includes("/") ? model.split("/").pop()! : model;
|
|
328
|
+
// Strip trailing date suffix (e.g. "claude-haiku-4-5-20251001" → "claude-haiku-4-5")
|
|
329
|
+
return name.replace(/-\d{8}$/, "");
|
|
330
|
+
}
|
|
331
|
+
|
|
380
332
|
const typeListText = buildTypeListText();
|
|
381
333
|
|
|
382
334
|
// ---- Agent tool ----
|
|
@@ -413,12 +365,12 @@ Guidelines:
|
|
|
413
365
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
414
366
|
}),
|
|
415
367
|
subagent_type: Type.String({
|
|
416
|
-
description: `The type of specialized agent to use.
|
|
368
|
+
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ~/.pi/agent/agents/*.md (global) are also available.`,
|
|
417
369
|
}),
|
|
418
370
|
model: Type.Optional(
|
|
419
371
|
Type.String({
|
|
420
372
|
description:
|
|
421
|
-
'Optional model
|
|
373
|
+
'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
|
|
422
374
|
}),
|
|
423
375
|
),
|
|
424
376
|
thinking: Type.Optional(
|
|
@@ -556,26 +508,27 @@ Guidelines:
|
|
|
556
508
|
// Reload custom agents so new .pi/agents/*.md files are picked up without restart
|
|
557
509
|
reloadCustomAgents();
|
|
558
510
|
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
return textResult(`Unknown agent type: "${params.subagent_type}". Valid types: ${getAvailableTypes().join(", ")}`);
|
|
564
|
-
}
|
|
511
|
+
const rawType = params.subagent_type as SubagentType;
|
|
512
|
+
const resolved = resolveType(rawType);
|
|
513
|
+
const subagentType = resolved ?? "general-purpose";
|
|
514
|
+
const fellBack = resolved === undefined;
|
|
565
515
|
|
|
566
516
|
const displayName = getDisplayName(subagentType);
|
|
567
517
|
|
|
568
|
-
// Get
|
|
569
|
-
const customConfig =
|
|
518
|
+
// Get agent config (if any)
|
|
519
|
+
const customConfig = getAgentConfig(subagentType);
|
|
570
520
|
|
|
571
521
|
// Resolve model if specified (supports exact "provider/modelId" or fuzzy match)
|
|
572
522
|
let model = ctx.model;
|
|
573
|
-
|
|
574
|
-
|
|
523
|
+
const modelInput = params.model ?? customConfig?.model;
|
|
524
|
+
if (modelInput) {
|
|
525
|
+
const resolved = resolveModel(modelInput, ctx.modelRegistry);
|
|
575
526
|
if (typeof resolved === "string") {
|
|
576
|
-
return textResult(resolved);
|
|
527
|
+
if (params.model) return textResult(resolved); // user-specified: error
|
|
528
|
+
// config-specified: silent fallback to parent
|
|
529
|
+
} else {
|
|
530
|
+
model = resolved;
|
|
577
531
|
}
|
|
578
|
-
model = resolved;
|
|
579
532
|
}
|
|
580
533
|
|
|
581
534
|
// Resolve thinking: explicit param > custom config > undefined
|
|
@@ -745,13 +698,17 @@ Guidelines:
|
|
|
745
698
|
|
|
746
699
|
const details = buildDetails(detailBase, record, { tokens: tokenText });
|
|
747
700
|
|
|
701
|
+
const fallbackNote = fellBack
|
|
702
|
+
? `Note: Unknown agent type "${rawType}" — using general-purpose.\n\n`
|
|
703
|
+
: "";
|
|
704
|
+
|
|
748
705
|
if (record.status === "error") {
|
|
749
|
-
return textResult(
|
|
706
|
+
return textResult(`${fallbackNote}Agent failed: ${record.error}`, details);
|
|
750
707
|
}
|
|
751
708
|
|
|
752
709
|
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
753
710
|
return textResult(
|
|
754
|
-
|
|
711
|
+
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${record.toolUses} tool uses)${getStatusNote(record.status)}.\n\n` +
|
|
755
712
|
(record.result ?? "No output."),
|
|
756
713
|
details,
|
|
757
714
|
);
|
|
@@ -875,33 +832,20 @@ Guidelines:
|
|
|
875
832
|
return undefined;
|
|
876
833
|
}
|
|
877
834
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
};
|
|
886
|
-
|
|
887
|
-
function getModelLabel(type: string): string {
|
|
888
|
-
const builtin = BUILTIN_MODEL_LABELS[type];
|
|
889
|
-
if (builtin) return builtin;
|
|
890
|
-
const custom = getCustomAgentConfig(type);
|
|
891
|
-
if (custom?.model) {
|
|
892
|
-
// Show short form: "anthropic/claude-haiku-4-5-20251001" → "haiku"
|
|
893
|
-
const id = custom.model.toLowerCase();
|
|
894
|
-
if (id.includes("haiku")) return "haiku";
|
|
895
|
-
if (id.includes("sonnet")) return "sonnet";
|
|
896
|
-
if (id.includes("opus")) return "opus";
|
|
897
|
-
return custom.model;
|
|
835
|
+
function getModelLabel(type: string, registry?: ModelRegistry): string {
|
|
836
|
+
const cfg = getAgentConfig(type);
|
|
837
|
+
if (!cfg?.model) return "inherit";
|
|
838
|
+
// If registry provided, check if the model actually resolves
|
|
839
|
+
if (registry) {
|
|
840
|
+
const resolved = resolveModel(cfg.model, registry);
|
|
841
|
+
if (typeof resolved === "string") return "inherit"; // model not available
|
|
898
842
|
}
|
|
899
|
-
return
|
|
843
|
+
return getModelLabelFromConfig(cfg.model);
|
|
900
844
|
}
|
|
901
845
|
|
|
902
846
|
async function showAgentsMenu(ctx: ExtensionCommandContext) {
|
|
903
847
|
reloadCustomAgents();
|
|
904
|
-
const
|
|
848
|
+
const allNames = getAllTypes();
|
|
905
849
|
|
|
906
850
|
// Build select options
|
|
907
851
|
const options: string[] = [];
|
|
@@ -914,32 +858,24 @@ Guidelines:
|
|
|
914
858
|
options.push(`Running agents (${agents.length}) — ${running} running, ${done} done`);
|
|
915
859
|
}
|
|
916
860
|
|
|
917
|
-
//
|
|
918
|
-
if (
|
|
919
|
-
options.push(`
|
|
861
|
+
// Agent types list
|
|
862
|
+
if (allNames.length > 0) {
|
|
863
|
+
options.push(`Agent types (${allNames.length})`);
|
|
920
864
|
}
|
|
921
865
|
|
|
922
866
|
// Actions
|
|
923
867
|
options.push("Create new agent");
|
|
924
868
|
options.push("Settings");
|
|
925
869
|
|
|
926
|
-
|
|
927
|
-
const maxBuiltin = Math.max(...SUBAGENT_TYPES.map(t => t.length));
|
|
928
|
-
const builtinLines = SUBAGENT_TYPES.map(t => {
|
|
929
|
-
const model = BUILTIN_MODEL_LABELS[t] ?? "inherit";
|
|
930
|
-
return ` ${t.padEnd(maxBuiltin)} · ${model}`;
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
const noAgentsMsg = customNames.length === 0 && agents.length === 0
|
|
870
|
+
const noAgentsMsg = allNames.length === 0 && agents.length === 0
|
|
934
871
|
? "No agents found. Create specialized subagents that can be delegated to.\n\n" +
|
|
935
872
|
"Each subagent has its own context window, custom system prompt, and specific tools.\n\n" +
|
|
936
873
|
"Try creating: Code Reviewer, Security Auditor, Test Writer, or Documentation Writer.\n\n"
|
|
937
874
|
: "";
|
|
938
875
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
);
|
|
876
|
+
if (noAgentsMsg) {
|
|
877
|
+
ctx.ui.notify(noAgentsMsg, "info");
|
|
878
|
+
}
|
|
943
879
|
|
|
944
880
|
const choice = await ctx.ui.select("Agents", options);
|
|
945
881
|
if (!choice) return;
|
|
@@ -947,8 +883,8 @@ Guidelines:
|
|
|
947
883
|
if (choice.startsWith("Running agents (")) {
|
|
948
884
|
await showRunningAgents(ctx);
|
|
949
885
|
await showAgentsMenu(ctx);
|
|
950
|
-
} else if (choice.startsWith("
|
|
951
|
-
await
|
|
886
|
+
} else if (choice.startsWith("Agent types (")) {
|
|
887
|
+
await showAllAgentsList(ctx);
|
|
952
888
|
await showAgentsMenu(ctx);
|
|
953
889
|
} else if (choice === "Create new agent") {
|
|
954
890
|
await showCreateWizard(ctx);
|
|
@@ -958,32 +894,53 @@ Guidelines:
|
|
|
958
894
|
}
|
|
959
895
|
}
|
|
960
896
|
|
|
961
|
-
async function
|
|
962
|
-
const
|
|
963
|
-
if (
|
|
964
|
-
ctx.ui.notify("No
|
|
897
|
+
async function showAllAgentsList(ctx: ExtensionCommandContext) {
|
|
898
|
+
const allNames = getAllTypes();
|
|
899
|
+
if (allNames.length === 0) {
|
|
900
|
+
ctx.ui.notify("No agents.", "info");
|
|
965
901
|
return;
|
|
966
902
|
}
|
|
967
903
|
|
|
968
|
-
//
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
return
|
|
904
|
+
// Source indicators: defaults unmarked, custom agents get • (project) or ◦ (global)
|
|
905
|
+
// Disabled agents get ✕ prefix
|
|
906
|
+
const sourceIndicator = (cfg: AgentConfig | undefined) => {
|
|
907
|
+
const disabled = cfg?.enabled === false;
|
|
908
|
+
if (cfg?.source === "project") return disabled ? "✕• " : "• ";
|
|
909
|
+
if (cfg?.source === "global") return disabled ? "✕◦ " : "◦ ";
|
|
910
|
+
if (disabled) return "✕ ";
|
|
911
|
+
return " ";
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
const entries = allNames.map(name => {
|
|
915
|
+
const cfg = getAgentConfig(name);
|
|
916
|
+
const disabled = cfg?.enabled === false;
|
|
917
|
+
const model = getModelLabel(name, ctx.modelRegistry);
|
|
918
|
+
const indicator = sourceIndicator(cfg);
|
|
919
|
+
const prefix = `${indicator}${name} · ${model}`;
|
|
920
|
+
const desc = disabled ? "(disabled)" : (cfg?.description ?? name);
|
|
921
|
+
return { name, prefix, desc };
|
|
974
922
|
});
|
|
975
923
|
const maxPrefix = Math.max(...entries.map(e => e.prefix.length));
|
|
976
924
|
|
|
925
|
+
const hasCustom = allNames.some(n => { const c = getAgentConfig(n); return c && !c.isDefault && c.enabled !== false; });
|
|
926
|
+
const hasDisabled = allNames.some(n => getAgentConfig(n)?.enabled === false);
|
|
927
|
+
const legendParts: string[] = [];
|
|
928
|
+
if (hasCustom) legendParts.push("• = project ◦ = global");
|
|
929
|
+
if (hasDisabled) legendParts.push("✕ = disabled");
|
|
930
|
+
const legend = legendParts.length ? "\n" + legendParts.join(" ") : "";
|
|
931
|
+
|
|
977
932
|
const options = entries.map(({ prefix, desc }) =>
|
|
978
933
|
`${prefix.padEnd(maxPrefix)} — ${desc}`,
|
|
979
934
|
);
|
|
935
|
+
if (legend) options.push(legend);
|
|
980
936
|
|
|
981
|
-
const choice = await ctx.ui.select("
|
|
937
|
+
const choice = await ctx.ui.select("Agent types", options);
|
|
982
938
|
if (!choice) return;
|
|
983
939
|
|
|
984
|
-
const agentName = choice.split(" · ")[0];
|
|
985
|
-
if (
|
|
940
|
+
const agentName = choice.split(" · ")[0].replace(/^[•◦✕\s]+/, "").trim();
|
|
941
|
+
if (getAgentConfig(agentName)) {
|
|
986
942
|
await showAgentDetail(ctx, agentName);
|
|
943
|
+
await showAllAgentsList(ctx);
|
|
987
944
|
}
|
|
988
945
|
}
|
|
989
946
|
|
|
@@ -994,27 +951,78 @@ Guidelines:
|
|
|
994
951
|
return;
|
|
995
952
|
}
|
|
996
953
|
|
|
997
|
-
// Show as a selectable list for potential future actions
|
|
998
954
|
const options = agents.map(a => {
|
|
999
955
|
const dn = getDisplayName(a.type);
|
|
1000
956
|
const dur = formatDuration(a.startedAt, a.completedAt);
|
|
1001
957
|
return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
|
|
1002
958
|
});
|
|
1003
959
|
|
|
1004
|
-
await ctx.ui.select("Running agents", options);
|
|
960
|
+
const choice = await ctx.ui.select("Running agents", options);
|
|
961
|
+
if (!choice) return;
|
|
962
|
+
|
|
963
|
+
// Find the selected agent by matching the option index
|
|
964
|
+
const idx = options.indexOf(choice);
|
|
965
|
+
if (idx < 0) return;
|
|
966
|
+
const record = agents[idx];
|
|
967
|
+
|
|
968
|
+
await viewAgentConversation(ctx, record);
|
|
969
|
+
// Back-navigation: re-show the list
|
|
970
|
+
await showRunningAgents(ctx);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function viewAgentConversation(ctx: ExtensionCommandContext, record: AgentRecord) {
|
|
974
|
+
if (!record.session) {
|
|
975
|
+
ctx.ui.notify(`Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`, "info");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const { ConversationViewer } = await import("./ui/conversation-viewer.js");
|
|
980
|
+
const session = record.session;
|
|
981
|
+
const activity = agentActivity.get(record.id);
|
|
982
|
+
|
|
983
|
+
await ctx.ui.custom<undefined>(
|
|
984
|
+
(tui, theme, _keybindings, done) => {
|
|
985
|
+
return new ConversationViewer(tui, session, record, activity, theme, done);
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
overlay: true,
|
|
989
|
+
overlayOptions: { anchor: "center", width: "90%" },
|
|
990
|
+
},
|
|
991
|
+
);
|
|
1005
992
|
}
|
|
1006
993
|
|
|
1007
994
|
async function showAgentDetail(ctx: ExtensionCommandContext, name: string) {
|
|
1008
|
-
const
|
|
1009
|
-
if (!
|
|
1010
|
-
ctx.ui.notify(`Agent
|
|
995
|
+
const cfg = getAgentConfig(name);
|
|
996
|
+
if (!cfg) {
|
|
997
|
+
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
1011
998
|
return;
|
|
1012
999
|
}
|
|
1013
1000
|
|
|
1014
|
-
const
|
|
1001
|
+
const file = findAgentFile(name);
|
|
1002
|
+
const isDefault = cfg.isDefault === true;
|
|
1003
|
+
const disabled = cfg.enabled === false;
|
|
1004
|
+
|
|
1005
|
+
let menuOptions: string[];
|
|
1006
|
+
if (disabled && file) {
|
|
1007
|
+
// Disabled agent with a file — offer Enable
|
|
1008
|
+
menuOptions = isDefault
|
|
1009
|
+
? ["Enable", "Edit", "Reset to default", "Delete", "Back"]
|
|
1010
|
+
: ["Enable", "Edit", "Delete", "Back"];
|
|
1011
|
+
} else if (isDefault && !file) {
|
|
1012
|
+
// Default agent with no .md override
|
|
1013
|
+
menuOptions = ["Eject (export as .md)", "Disable", "Back"];
|
|
1014
|
+
} else if (isDefault && file) {
|
|
1015
|
+
// Default agent with .md override (ejected)
|
|
1016
|
+
menuOptions = ["Edit", "Disable", "Reset to default", "Delete", "Back"];
|
|
1017
|
+
} else {
|
|
1018
|
+
// User-defined agent
|
|
1019
|
+
menuOptions = ["Edit", "Disable", "Delete", "Back"];
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const choice = await ctx.ui.select(name, menuOptions);
|
|
1015
1023
|
if (!choice || choice === "Back") return;
|
|
1016
1024
|
|
|
1017
|
-
if (choice === "Edit") {
|
|
1025
|
+
if (choice === "Edit" && file) {
|
|
1018
1026
|
const content = readFileSync(file.path, "utf-8");
|
|
1019
1027
|
const edited = await ctx.ui.editor(`Edit ${name}`, content);
|
|
1020
1028
|
if (edited !== undefined && edited !== content) {
|
|
@@ -1024,12 +1032,125 @@ Guidelines:
|
|
|
1024
1032
|
ctx.ui.notify(`Updated ${file.path}`, "info");
|
|
1025
1033
|
}
|
|
1026
1034
|
} else if (choice === "Delete") {
|
|
1027
|
-
|
|
1035
|
+
if (file) {
|
|
1036
|
+
const confirmed = await ctx.ui.confirm("Delete agent", `Delete ${name} from ${file.location} (${file.path})?`);
|
|
1037
|
+
if (confirmed) {
|
|
1038
|
+
unlinkSync(file.path);
|
|
1039
|
+
reloadCustomAgents();
|
|
1040
|
+
ctx.ui.notify(`Deleted ${file.path}`, "info");
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
} else if (choice === "Reset to default" && file) {
|
|
1044
|
+
const confirmed = await ctx.ui.confirm("Reset to default", `Delete override ${file.path} and restore embedded default?`);
|
|
1028
1045
|
if (confirmed) {
|
|
1029
1046
|
unlinkSync(file.path);
|
|
1030
1047
|
reloadCustomAgents();
|
|
1031
|
-
ctx.ui.notify(`
|
|
1048
|
+
ctx.ui.notify(`Restored default ${name}`, "info");
|
|
1049
|
+
}
|
|
1050
|
+
} else if (choice.startsWith("Eject")) {
|
|
1051
|
+
await ejectAgent(ctx, name, cfg);
|
|
1052
|
+
} else if (choice === "Disable") {
|
|
1053
|
+
await disableAgent(ctx, name);
|
|
1054
|
+
} else if (choice === "Enable") {
|
|
1055
|
+
await enableAgent(ctx, name);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** Eject a default agent: write its embedded config as a .md file. */
|
|
1060
|
+
async function ejectAgent(ctx: ExtensionCommandContext, name: string, cfg: AgentConfig) {
|
|
1061
|
+
const location = await ctx.ui.select("Choose location", [
|
|
1062
|
+
"Project (.pi/agents/)",
|
|
1063
|
+
"Personal (~/.pi/agent/agents/)",
|
|
1064
|
+
]);
|
|
1065
|
+
if (!location) return;
|
|
1066
|
+
|
|
1067
|
+
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
1068
|
+
mkdirSync(targetDir, { recursive: true });
|
|
1069
|
+
|
|
1070
|
+
const targetPath = join(targetDir, `${name}.md`);
|
|
1071
|
+
if (existsSync(targetPath)) {
|
|
1072
|
+
const overwrite = await ctx.ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
1073
|
+
if (!overwrite) return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Build the .md file content
|
|
1077
|
+
const fmFields: string[] = [];
|
|
1078
|
+
fmFields.push(`description: ${cfg.description}`);
|
|
1079
|
+
if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
|
|
1080
|
+
fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") || "all"}`);
|
|
1081
|
+
if (cfg.model) fmFields.push(`model: ${cfg.model}`);
|
|
1082
|
+
if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
|
|
1083
|
+
if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
|
|
1084
|
+
fmFields.push(`prompt_mode: ${cfg.promptMode}`);
|
|
1085
|
+
if (cfg.extensions === false) fmFields.push("extensions: false");
|
|
1086
|
+
else if (Array.isArray(cfg.extensions)) fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
|
|
1087
|
+
if (cfg.skills === false) fmFields.push("skills: false");
|
|
1088
|
+
else if (Array.isArray(cfg.skills)) fmFields.push(`skills: ${cfg.skills.join(", ")}`);
|
|
1089
|
+
if (cfg.inheritContext) fmFields.push("inherit_context: true");
|
|
1090
|
+
if (cfg.runInBackground) fmFields.push("run_in_background: true");
|
|
1091
|
+
if (cfg.isolated) fmFields.push("isolated: true");
|
|
1092
|
+
|
|
1093
|
+
const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
1094
|
+
|
|
1095
|
+
const { writeFileSync } = await import("node:fs");
|
|
1096
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
1097
|
+
reloadCustomAgents();
|
|
1098
|
+
ctx.ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** Disable an agent: set enabled: false in its .md file, or create a stub for built-in defaults. */
|
|
1102
|
+
async function disableAgent(ctx: ExtensionCommandContext, name: string) {
|
|
1103
|
+
const file = findAgentFile(name);
|
|
1104
|
+
if (file) {
|
|
1105
|
+
// Existing file — set enabled: false in frontmatter (idempotent)
|
|
1106
|
+
const content = readFileSync(file.path, "utf-8");
|
|
1107
|
+
if (content.includes("\nenabled: false\n")) {
|
|
1108
|
+
ctx.ui.notify(`${name} is already disabled.`, "info");
|
|
1109
|
+
return;
|
|
1032
1110
|
}
|
|
1111
|
+
const updated = content.replace(/^---\n/, "---\nenabled: false\n");
|
|
1112
|
+
const { writeFileSync } = await import("node:fs");
|
|
1113
|
+
writeFileSync(file.path, updated, "utf-8");
|
|
1114
|
+
reloadCustomAgents();
|
|
1115
|
+
ctx.ui.notify(`Disabled ${name} (${file.path})`, "info");
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// No file (built-in default) — create a stub
|
|
1120
|
+
const location = await ctx.ui.select("Choose location", [
|
|
1121
|
+
"Project (.pi/agents/)",
|
|
1122
|
+
"Personal (~/.pi/agent/agents/)",
|
|
1123
|
+
]);
|
|
1124
|
+
if (!location) return;
|
|
1125
|
+
|
|
1126
|
+
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
1127
|
+
mkdirSync(targetDir, { recursive: true });
|
|
1128
|
+
|
|
1129
|
+
const targetPath = join(targetDir, `${name}.md`);
|
|
1130
|
+
const { writeFileSync } = await import("node:fs");
|
|
1131
|
+
writeFileSync(targetPath, "---\nenabled: false\n---\n", "utf-8");
|
|
1132
|
+
reloadCustomAgents();
|
|
1133
|
+
ctx.ui.notify(`Disabled ${name} (${targetPath})`, "info");
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/** Enable a disabled agent by removing enabled: false from its frontmatter. */
|
|
1137
|
+
async function enableAgent(ctx: ExtensionCommandContext, name: string) {
|
|
1138
|
+
const file = findAgentFile(name);
|
|
1139
|
+
if (!file) return;
|
|
1140
|
+
|
|
1141
|
+
const content = readFileSync(file.path, "utf-8");
|
|
1142
|
+
const updated = content.replace(/^(---\n)enabled: false\n/, "$1");
|
|
1143
|
+
const { writeFileSync } = await import("node:fs");
|
|
1144
|
+
|
|
1145
|
+
// If the file was just a stub ("---\n---\n"), delete it to restore the built-in default
|
|
1146
|
+
if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
|
|
1147
|
+
unlinkSync(file.path);
|
|
1148
|
+
reloadCustomAgents();
|
|
1149
|
+
ctx.ui.notify(`Enabled ${name} (removed ${file.path})`, "info");
|
|
1150
|
+
} else {
|
|
1151
|
+
writeFileSync(file.path, updated, "utf-8");
|
|
1152
|
+
reloadCustomAgents();
|
|
1153
|
+
ctx.ui.notify(`Enabled ${name} (${file.path})`, "info");
|
|
1033
1154
|
}
|
|
1034
1155
|
}
|
|
1035
1156
|
|
|
@@ -1062,15 +1183,7 @@ Guidelines:
|
|
|
1062
1183
|
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
1063
1184
|
if (!name) return;
|
|
1064
1185
|
|
|
1065
|
-
|
|
1066
|
-
if (isValidType(name) && !getCustomAgentConfig(name)) {
|
|
1067
|
-
ctx.ui.notify(`"${name}" conflicts with a built-in agent type.`, "warning");
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
if (!mkdirSync(targetDir, { recursive: true }) && !existsSync(targetDir)) {
|
|
1072
|
-
mkdirSync(targetDir, { recursive: true });
|
|
1073
|
-
}
|
|
1186
|
+
mkdirSync(targetDir, { recursive: true });
|
|
1074
1187
|
|
|
1075
1188
|
const targetPath = join(targetDir, `${name}.md`);
|
|
1076
1189
|
if (existsSync(targetPath)) {
|
|
@@ -1139,11 +1252,6 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
1139
1252
|
const name = await ctx.ui.input("Agent name (filename, no spaces)");
|
|
1140
1253
|
if (!name) return;
|
|
1141
1254
|
|
|
1142
|
-
if (isValidType(name) && !getCustomAgentConfig(name)) {
|
|
1143
|
-
ctx.ui.notify(`"${name}" conflicts with a built-in agent type.`, "warning");
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
1255
|
// 2. Description
|
|
1148
1256
|
const description = await ctx.ui.input("Description (one line)");
|
|
1149
1257
|
if (!description) return;
|