@gotgenes/pi-subagents 5.2.0 → 5.4.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/CHANGELOG.md +27 -0
- package/docs/architecture/architecture.md +20 -13
- package/docs/plans/0071-extract-session-config-assembler.md +362 -0
- package/docs/plans/0080-consolidate-agent-config-lookup.md +247 -0
- package/docs/retro/0069-create-subagent-runtime.md +43 -0
- package/docs/retro/0071-extract-session-config-assembler.md +60 -0
- package/package.json +1 -1
- package/src/agent-runner.ts +39 -164
- package/src/agent-types.ts +10 -36
- package/src/index.ts +8 -8
- package/src/session-config.ts +243 -0
- package/src/tools/agent-tool.ts +3 -3
- package/src/ui/agent-menu.ts +11 -10
- package/src/ui/agent-widget.ts +4 -3
package/src/agent-types.ts
CHANGED
|
@@ -48,12 +48,6 @@ export function resolveType(name: string): string | undefined {
|
|
|
48
48
|
return resolveKey(name);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Get the agent config for a type (case-insensitive). */
|
|
52
|
-
export function getAgentConfig(name: string): AgentConfig | undefined {
|
|
53
|
-
const key = resolveKey(name);
|
|
54
|
-
return key ? agents.get(key) : undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
51
|
/** Get all enabled type names (for spawning and tool descriptions). */
|
|
58
52
|
export function getAvailableTypes(): string[] {
|
|
59
53
|
return [...agents.entries()]
|
|
@@ -116,49 +110,29 @@ export function getToolNamesForType(type: string): string[] {
|
|
|
116
110
|
return names;
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
/**
|
|
120
|
-
export function
|
|
121
|
-
displayName: string;
|
|
122
|
-
description: string;
|
|
123
|
-
builtinToolNames: string[];
|
|
124
|
-
extensions: true | string[] | false;
|
|
125
|
-
skills: true | string[] | false;
|
|
126
|
-
promptMode: "replace" | "append";
|
|
127
|
-
} {
|
|
113
|
+
/** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */
|
|
114
|
+
export function resolveAgentConfig(type: string): AgentConfig {
|
|
128
115
|
const key = resolveKey(type);
|
|
129
116
|
const config = key ? agents.get(key) : undefined;
|
|
130
|
-
if (config
|
|
131
|
-
return
|
|
132
|
-
displayName: config.displayName ?? config.name,
|
|
133
|
-
description: config.description,
|
|
134
|
-
builtinToolNames: config.builtinToolNames ?? BUILTIN_TOOL_NAMES,
|
|
135
|
-
extensions: config.extensions,
|
|
136
|
-
skills: config.skills,
|
|
137
|
-
promptMode: config.promptMode,
|
|
138
|
-
};
|
|
117
|
+
if (config) {
|
|
118
|
+
return config;
|
|
139
119
|
}
|
|
140
120
|
|
|
141
|
-
// Fallback for unknown
|
|
121
|
+
// Fallback to general-purpose for unknown types
|
|
142
122
|
const gp = agents.get("general-purpose");
|
|
143
|
-
if (gp
|
|
144
|
-
return
|
|
145
|
-
displayName: gp.displayName ?? gp.name,
|
|
146
|
-
description: gp.description,
|
|
147
|
-
builtinToolNames: gp.builtinToolNames ?? BUILTIN_TOOL_NAMES,
|
|
148
|
-
extensions: gp.extensions,
|
|
149
|
-
skills: gp.skills,
|
|
150
|
-
promptMode: gp.promptMode,
|
|
151
|
-
};
|
|
123
|
+
if (gp) {
|
|
124
|
+
return gp;
|
|
152
125
|
}
|
|
153
126
|
|
|
154
|
-
// Absolute fallback (should never happen)
|
|
127
|
+
// Absolute fallback (should never happen in practice)
|
|
155
128
|
return {
|
|
129
|
+
name: type,
|
|
156
130
|
displayName: "Agent",
|
|
157
131
|
description: "General-purpose agent for complex, multi-step tasks",
|
|
158
132
|
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
159
133
|
extensions: true,
|
|
160
134
|
skills: true,
|
|
135
|
+
systemPrompt: "",
|
|
161
136
|
promptMode: "append",
|
|
162
137
|
};
|
|
163
138
|
}
|
|
164
|
-
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import { defineTool, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { AgentManager } from "./agent-manager.js";
|
|
16
16
|
import { getAgentConversation, normalizeMaxTurns, steerAgent } from "./agent-runner.js";
|
|
17
|
-
import {
|
|
17
|
+
import { getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveAgentConfig, } from "./agent-types.js";
|
|
18
18
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
19
19
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
20
20
|
import { buildEventData, createNotificationSystem } from "./notification.js";
|
|
@@ -149,14 +149,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
149
149
|
const userNames = getUserAgentNames();
|
|
150
150
|
|
|
151
151
|
const defaultDescs = defaultNames.map((name) => {
|
|
152
|
-
const cfg =
|
|
153
|
-
const modelSuffix = cfg
|
|
154
|
-
return `- ${name}: ${cfg
|
|
152
|
+
const cfg = resolveAgentConfig(name);
|
|
153
|
+
const modelSuffix = cfg.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
154
|
+
return `- ${name}: ${cfg.description}${modelSuffix}`;
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
const customDescs = userNames.map((name) => {
|
|
158
|
-
const cfg =
|
|
159
|
-
return `- ${name}: ${cfg
|
|
158
|
+
const cfg = resolveAgentConfig(name);
|
|
159
|
+
return `- ${name}: ${cfg.description}`;
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
return [
|
|
@@ -237,8 +237,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
237
237
|
reloadCustomAgents,
|
|
238
238
|
agentActivity: runtime.agentActivity,
|
|
239
239
|
getModelLabel: (type, registry) => {
|
|
240
|
-
const cfg =
|
|
241
|
-
if (!cfg
|
|
240
|
+
const cfg = resolveAgentConfig(type);
|
|
241
|
+
if (!cfg.model) return 'inherit';
|
|
242
242
|
if (registry) {
|
|
243
243
|
const resolved = resolveModel(cfg.model, registry as any);
|
|
244
244
|
if (typeof resolved === 'string') return 'inherit';
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-config.ts — Pure configuration assembler for agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* `assembleSessionConfig()` is the pure core extracted from `runAgent()`.
|
|
5
|
+
* It accepts resolved inputs (agent type, narrow context, run options, env info)
|
|
6
|
+
* and returns everything `runAgent()` needs to create the SDK session — without
|
|
7
|
+
* importing or constructing any Pi SDK types.
|
|
8
|
+
*
|
|
9
|
+
* The only async IO in the assembly phase (`detectEnv`) is handled by the caller
|
|
10
|
+
* before invoking this function, keeping the assembler synchronous.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getMemoryToolNames,
|
|
15
|
+
getReadOnlyMemoryToolNames,
|
|
16
|
+
getToolNamesForType,
|
|
17
|
+
resolveAgentConfig,
|
|
18
|
+
} from "./agent-types.js";
|
|
19
|
+
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
20
|
+
import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
|
|
21
|
+
import { preloadSkills } from "./skill-loader.js";
|
|
22
|
+
import type { EnvInfo, SubagentType, ThinkingLevel } from "./types.js";
|
|
23
|
+
|
|
24
|
+
// ── Public interfaces ────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Narrow context the assembler reads from the parent session.
|
|
28
|
+
* Tests construct plain objects satisfying this interface — no SDK mocking needed.
|
|
29
|
+
*
|
|
30
|
+
* Models are treated as opaque handles: the assembler never inspects their
|
|
31
|
+
* internals, only passes them through. `getAvailable` returns just enough
|
|
32
|
+
* structural information ({ provider, id }) for the availability check in
|
|
33
|
+
* `resolveDefaultModel`.
|
|
34
|
+
*/
|
|
35
|
+
export interface AssemblerContext {
|
|
36
|
+
/** Parent working directory (overridable via options.cwd). */
|
|
37
|
+
cwd: string;
|
|
38
|
+
/** Parent's effective system prompt (for append-mode agents). */
|
|
39
|
+
parentSystemPrompt: string;
|
|
40
|
+
/** Parent's current model instance (fallback when agent config has no model). */
|
|
41
|
+
parentModel?: unknown;
|
|
42
|
+
/** Model registry for resolving config.model strings. */
|
|
43
|
+
modelRegistry: {
|
|
44
|
+
find(provider: string, modelId: string): unknown;
|
|
45
|
+
getAvailable?(): Array<{ provider: string; id: string }>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Narrow slice of RunOptions consumed by the assembler.
|
|
51
|
+
* All fields are optional — callers pass only what they have.
|
|
52
|
+
*/
|
|
53
|
+
export interface AssemblerOptions {
|
|
54
|
+
/** Override working directory (e.g. for worktree isolation). */
|
|
55
|
+
cwd?: string;
|
|
56
|
+
/** When true, forces extensions and skills to false. */
|
|
57
|
+
isolated?: boolean;
|
|
58
|
+
/** Explicit model override — wins over agentConfig.model and parent model. */
|
|
59
|
+
model?: unknown;
|
|
60
|
+
/** Explicit thinking level — wins over agentConfig.thinking. */
|
|
61
|
+
thinkingLevel?: ThinkingLevel;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Assembled configuration returned to `runAgent()`.
|
|
66
|
+
* Contains everything needed to create the SDK session and filter tools —
|
|
67
|
+
* with no SDK object references.
|
|
68
|
+
*/
|
|
69
|
+
export interface SessionConfig {
|
|
70
|
+
/** Resolved working directory (`options.cwd ?? ctx.cwd`). */
|
|
71
|
+
effectiveCwd: string;
|
|
72
|
+
/** Fully-assembled system prompt string (ready for `systemPromptOverride`). */
|
|
73
|
+
systemPrompt: string;
|
|
74
|
+
/** Built-in tool names for session creation, filtering, and memory augmentation. */
|
|
75
|
+
toolNames: string[];
|
|
76
|
+
/** Disallowed tool set from agentConfig (for `filterActiveTools`). undefined when empty. */
|
|
77
|
+
disallowedSet: Set<string> | undefined;
|
|
78
|
+
/** Resolved extensions setting for resource loader and tool filtering. */
|
|
79
|
+
extensions: boolean | string[];
|
|
80
|
+
/**
|
|
81
|
+
* Resolved model instance (undefined → use parent model as passed to SDK).
|
|
82
|
+
* Opaque handle — the assembler passes it through without inspection.
|
|
83
|
+
* Caller casts to the SDK’s Model<any> at the session-creation boundary.
|
|
84
|
+
*/
|
|
85
|
+
model: unknown;
|
|
86
|
+
/** Resolved thinking level (undefined → inherit from session). */
|
|
87
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
88
|
+
/** Whether to skip skill loading in the resource loader (`noSkills` flag). */
|
|
89
|
+
noSkills: boolean;
|
|
90
|
+
/** Prompt extras (memory block, preloaded skill blocks) — for transparency. */
|
|
91
|
+
extras: PromptExtras;
|
|
92
|
+
/** Per-agent configured max turns (from agentConfig.maxTurns). */
|
|
93
|
+
agentMaxTurns: number | undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the default model from the agent config's model string.
|
|
100
|
+
*
|
|
101
|
+
* Priority: parentModel is the fallback; if `configModel` is a "provider/modelId"
|
|
102
|
+
* string that resolves against the registry AND is in the available set, return
|
|
103
|
+
* that model instead.
|
|
104
|
+
*/
|
|
105
|
+
function resolveDefaultModel(
|
|
106
|
+
parentModel: unknown,
|
|
107
|
+
registry: AssemblerContext["modelRegistry"],
|
|
108
|
+
configModel?: string,
|
|
109
|
+
): unknown {
|
|
110
|
+
if (configModel) {
|
|
111
|
+
const slashIdx = configModel.indexOf("/");
|
|
112
|
+
if (slashIdx !== -1) {
|
|
113
|
+
const provider = configModel.slice(0, slashIdx);
|
|
114
|
+
const modelId = configModel.slice(slashIdx + 1);
|
|
115
|
+
|
|
116
|
+
const available = registry.getAvailable?.();
|
|
117
|
+
const availableKeys = available
|
|
118
|
+
? new Set(available.map((m) => `${m.provider}/${m.id}`))
|
|
119
|
+
: undefined;
|
|
120
|
+
const isAvailable = (p: string, id: string) =>
|
|
121
|
+
!availableKeys || availableKeys.has(`${p}/${id}`);
|
|
122
|
+
|
|
123
|
+
const found = registry.find(provider, modelId);
|
|
124
|
+
if (found && isAvailable(provider, modelId)) return found;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return parentModel;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Public function ──────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Assemble all configuration needed to create an agent session.
|
|
134
|
+
*
|
|
135
|
+
* Synchronous and side-effect-free (beyond calling `preloadSkills` which reads
|
|
136
|
+
* the filesystem). The caller is responsible for resolving `EnvInfo` beforehand
|
|
137
|
+
* via `detectEnv()`.
|
|
138
|
+
*
|
|
139
|
+
* @param type The subagent type name (case-insensitive registry lookup).
|
|
140
|
+
* @param ctx Narrow context from the parent session.
|
|
141
|
+
* @param options Per-call overrides (cwd, isolated, model, thinkingLevel).
|
|
142
|
+
* @param env Pre-resolved environment info from `detectEnv()`.
|
|
143
|
+
*/
|
|
144
|
+
export function assembleSessionConfig(
|
|
145
|
+
type: SubagentType,
|
|
146
|
+
ctx: AssemblerContext,
|
|
147
|
+
options: AssemblerOptions,
|
|
148
|
+
env: EnvInfo,
|
|
149
|
+
): SessionConfig {
|
|
150
|
+
const agentConfig = resolveAgentConfig(type);
|
|
151
|
+
|
|
152
|
+
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
153
|
+
|
|
154
|
+
// Resolve extensions/skills: isolated overrides to false
|
|
155
|
+
const extensions = options.isolated ? false : agentConfig.extensions;
|
|
156
|
+
const skills = options.isolated ? false : agentConfig.skills;
|
|
157
|
+
|
|
158
|
+
// Build prompt extras (memory, preloaded skills)
|
|
159
|
+
const extras: PromptExtras = {};
|
|
160
|
+
|
|
161
|
+
// Skill preloading: when skills is string[], preload their content into the prompt
|
|
162
|
+
if (Array.isArray(skills)) {
|
|
163
|
+
const loaded = preloadSkills(skills, effectiveCwd);
|
|
164
|
+
if (loaded.length > 0) {
|
|
165
|
+
extras.skillBlocks = loaded;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let toolNames = getToolNamesForType(type);
|
|
170
|
+
|
|
171
|
+
// Persistent memory: detect write capability and branch accordingly.
|
|
172
|
+
// Account for disallowedTools — a tool in the base set but on the denylist
|
|
173
|
+
// is not truly available.
|
|
174
|
+
if (agentConfig.memory) {
|
|
175
|
+
const existingNames = new Set(toolNames);
|
|
176
|
+
const denied = agentConfig.disallowedTools
|
|
177
|
+
? new Set(agentConfig.disallowedTools)
|
|
178
|
+
: undefined;
|
|
179
|
+
const effectivelyHas = (name: string) =>
|
|
180
|
+
existingNames.has(name) && !denied?.has(name);
|
|
181
|
+
const hasWriteTools = effectivelyHas("write") || effectivelyHas("edit");
|
|
182
|
+
|
|
183
|
+
if (hasWriteTools) {
|
|
184
|
+
const extraNames = getMemoryToolNames(existingNames);
|
|
185
|
+
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
186
|
+
extras.memoryBlock = buildMemoryBlock(
|
|
187
|
+
agentConfig.name,
|
|
188
|
+
agentConfig.memory,
|
|
189
|
+
effectiveCwd,
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
const extraNames = getReadOnlyMemoryToolNames(existingNames);
|
|
193
|
+
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
194
|
+
extras.memoryBlock = buildReadOnlyMemoryBlock(
|
|
195
|
+
agentConfig.name,
|
|
196
|
+
agentConfig.memory,
|
|
197
|
+
effectiveCwd,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build system prompt from the resolved agent config
|
|
203
|
+
const systemPrompt = buildAgentPrompt(
|
|
204
|
+
agentConfig,
|
|
205
|
+
effectiveCwd,
|
|
206
|
+
env,
|
|
207
|
+
ctx.parentSystemPrompt,
|
|
208
|
+
extras,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// noSkills: when we've already preloaded skills into the prompt, or skills = false,
|
|
212
|
+
// tell the resource loader not to load them again.
|
|
213
|
+
const noSkills = skills === false || Array.isArray(skills);
|
|
214
|
+
|
|
215
|
+
// Disallowed tools set (for filterActiveTools in runAgent)
|
|
216
|
+
const disallowedSet = agentConfig.disallowedTools
|
|
217
|
+
? new Set(agentConfig.disallowedTools)
|
|
218
|
+
: undefined;
|
|
219
|
+
|
|
220
|
+
// Model resolution: explicit option > config model string > parent model
|
|
221
|
+
const model =
|
|
222
|
+
options.model ??
|
|
223
|
+
resolveDefaultModel(ctx.parentModel, ctx.modelRegistry, agentConfig.model);
|
|
224
|
+
|
|
225
|
+
// Thinking level: explicit option > agent config > undefined (inherit)
|
|
226
|
+
const thinkingLevel = options.thinkingLevel ?? agentConfig.thinking;
|
|
227
|
+
|
|
228
|
+
// Per-agent max turns (combined with options.maxTurns and defaultMaxTurns by runAgent)
|
|
229
|
+
const agentMaxTurns = agentConfig.maxTurns;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
effectiveCwd,
|
|
233
|
+
systemPrompt,
|
|
234
|
+
toolNames,
|
|
235
|
+
disallowedSet,
|
|
236
|
+
extensions,
|
|
237
|
+
model,
|
|
238
|
+
thinkingLevel,
|
|
239
|
+
noSkills,
|
|
240
|
+
extras,
|
|
241
|
+
agentMaxTurns,
|
|
242
|
+
};
|
|
243
|
+
}
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import { normalizeMaxTurns } from "../agent-runner.js";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveAgentConfig, resolveType } from "../agent-types.js";
|
|
5
5
|
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
6
6
|
import { resolveInvocationModel } from "../model-resolver.js";
|
|
7
7
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "../output-file.js";
|
|
@@ -370,8 +370,8 @@ Guidelines:
|
|
|
370
370
|
|
|
371
371
|
const displayName = getDisplayName(subagentType);
|
|
372
372
|
|
|
373
|
-
// Get agent config
|
|
374
|
-
const customConfig =
|
|
373
|
+
// Get agent config for invocation resolution
|
|
374
|
+
const customConfig = resolveAgentConfig(subagentType);
|
|
375
375
|
|
|
376
376
|
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
377
377
|
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
BUILTIN_TOOL_NAMES,
|
|
6
|
-
getAgentConfig,
|
|
7
6
|
getAllTypes,
|
|
7
|
+
resolveAgentConfig,
|
|
8
|
+
resolveType,
|
|
8
9
|
} from "../agent-types.js";
|
|
9
10
|
import type { AgentConfig, AgentRecord } from "../types.js";
|
|
10
11
|
import type { AgentActivity } from "./agent-widget.js";
|
|
@@ -152,21 +153,21 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
152
153
|
};
|
|
153
154
|
|
|
154
155
|
const entries = allNames.map((name) => {
|
|
155
|
-
const cfg =
|
|
156
|
-
const disabled = cfg
|
|
156
|
+
const cfg = resolveAgentConfig(name);
|
|
157
|
+
const disabled = cfg.enabled === false;
|
|
157
158
|
const model = deps.getModelLabel(name, ctx.modelRegistry);
|
|
158
159
|
const indicator = sourceIndicator(cfg);
|
|
159
160
|
const prefix = `${indicator}${name} · ${model}`;
|
|
160
|
-
const desc = disabled ? "(disabled)" :
|
|
161
|
+
const desc = disabled ? "(disabled)" : cfg.description;
|
|
161
162
|
return { name, prefix, desc };
|
|
162
163
|
});
|
|
163
164
|
const maxPrefix = Math.max(...entries.map((e) => e.prefix.length));
|
|
164
165
|
|
|
165
166
|
const hasCustom = allNames.some((n) => {
|
|
166
|
-
const c =
|
|
167
|
-
return
|
|
167
|
+
const c = resolveAgentConfig(n);
|
|
168
|
+
return !c.isDefault && c.enabled !== false;
|
|
168
169
|
});
|
|
169
|
-
const hasDisabled = allNames.some((n) =>
|
|
170
|
+
const hasDisabled = allNames.some((n) => resolveAgentConfig(n).enabled === false);
|
|
170
171
|
const legendParts: string[] = [];
|
|
171
172
|
if (hasCustom) legendParts.push("• = project ◦ = global");
|
|
172
173
|
if (hasDisabled) legendParts.push("✕ = disabled");
|
|
@@ -184,7 +185,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
184
185
|
.split(" · ")[0]
|
|
185
186
|
.replace(/^[•◦✕\s]+/, "")
|
|
186
187
|
.trim();
|
|
187
|
-
if (
|
|
188
|
+
if (resolveType(agentName) != null) {
|
|
188
189
|
await showAgentDetail(ctx, agentName);
|
|
189
190
|
await showAllAgentsList(ctx);
|
|
190
191
|
}
|
|
@@ -245,11 +246,11 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
async function showAgentDetail(ctx: MenuContext, name: string) {
|
|
248
|
-
|
|
249
|
-
if (!cfg) {
|
|
249
|
+
if (resolveType(name) == null) {
|
|
250
250
|
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
|
+
const cfg = resolveAgentConfig(name);
|
|
253
254
|
|
|
254
255
|
const file = findAgentFile(name);
|
|
255
256
|
const isDefault = cfg.isDefault === true;
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { AgentManager } from "../agent-manager.js";
|
|
10
|
-
import {
|
|
10
|
+
import { resolveAgentConfig } from "../agent-types.js";
|
|
11
11
|
import type { AgentInvocation, SubagentType } from "../types.js";
|
|
12
12
|
import { getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, type SessionLike } from "../usage.js";
|
|
13
13
|
|
|
@@ -144,12 +144,13 @@ export function formatDuration(startedAt: number, completedAt?: number): string
|
|
|
144
144
|
|
|
145
145
|
/** Get display name for any agent type (built-in or custom). */
|
|
146
146
|
export function getDisplayName(type: SubagentType): string {
|
|
147
|
-
|
|
147
|
+
const config = resolveAgentConfig(type);
|
|
148
|
+
return config.displayName ?? config.name;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
|
|
151
152
|
export function getPromptModeLabel(type: SubagentType): string | undefined {
|
|
152
|
-
const config =
|
|
153
|
+
const config = resolveAgentConfig(type);
|
|
153
154
|
return config.promptMode === "append" ? "twin" : undefined;
|
|
154
155
|
}
|
|
155
156
|
|