@bacnh85/pi-subagent 0.16.1 → 0.17.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 +17 -0
- package/README.md +5 -3
- package/extensions/index.ts +16 -5
- package/extensions/roles-panel.ts +119 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0 (2026-08-31)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Add/remove model roles from the `/subagent` panel** — new `+ Add role` /
|
|
8
|
+
`− Remove role` action rows (two-prompt flow: name → chain). Custom roles are
|
|
9
|
+
deletable; built-in roles (`fast`/`coder`/`smart`) reset to their bundled
|
|
10
|
+
default. New names validate against `[A-Za-z0-9._-]{1,64}` with
|
|
11
|
+
case-insensitive collision checks.
|
|
12
|
+
- **Per-agent default display** — each override row now shows the agent's
|
|
13
|
+
default (no-override) chain, e.g. `scout (default: @fast → zai-coding-cn/
|
|
14
|
+
glm-5-turbo, …)`, so blank = inherit is meaningful. Labels track live role
|
|
15
|
+
edits and freshly added roles appear in `@role` completions immediately.
|
|
16
|
+
- Panel save is now guarded by a working-copy content diff (action-only
|
|
17
|
+
sessions — add/remove without row edits — previously left `editedKeys` empty
|
|
18
|
+
and silently skipped persistence).
|
|
19
|
+
|
|
3
20
|
## 0.16.1 (2026-08-29)
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -34,9 +34,11 @@ in `~/.pi/agent/settings.json` under `subagent.roles`:
|
|
|
34
34
|
|
|
35
35
|
`/subagent` opens the interactive role editor (TUI panel via the shared
|
|
36
36
|
`@bacnh85/pi-config-panel` kernel; prints the effective mapping headless).
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
The panel supports `+ Add role` / `− Remove role` (custom roles are deletable,
|
|
38
|
+
built-ins reset to their bundled default) and shows each agent's default
|
|
39
|
+
(no-override) chain on its row. `/subagent list` lists agents,
|
|
40
|
+
`/subagent <name>` shows an agent's resolved chain, and `/subagent @role`
|
|
41
|
+
(or `/subagent fast`) shows a role's chain and the agents using it.
|
|
40
42
|
|
|
41
43
|
## Live progress widget
|
|
42
44
|
|
package/extensions/index.ts
CHANGED
|
@@ -378,7 +378,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
378
|
|
|
379
379
|
const openRolesEditor = async (): Promise<void> => {
|
|
380
380
|
// Role mapping editor: panel in TUI, plain text otherwise.
|
|
381
|
-
const [{ openConfigPanel }, { buildRows, buildRolesPanelCfg, cfgToPatch, preserveUnknownAgentModels, writeSubagentSection }] = await Promise.all([
|
|
381
|
+
const [{ openConfigPanel }, { buildRows, buildRolesPanelCfg, cfgToPatch, makeAddRoleAction, makeRemoveRoleAction, preserveUnknownAgentModels, writeSubagentSection }] = await Promise.all([
|
|
382
382
|
import("@bacnh85/pi-config-panel"),
|
|
383
383
|
import("./roles-panel.ts"),
|
|
384
384
|
]);
|
|
@@ -401,20 +401,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
401
401
|
}
|
|
402
402
|
const current = readSubagentRolesGlobal();
|
|
403
403
|
const working = buildRolesPanelCfg(discovery.agents, current);
|
|
404
|
+
// Actions (add/remove role) set model.dirty but not editedKeys — guard
|
|
405
|
+
// the save on a working-copy diff instead (pi-a2a pattern).
|
|
406
|
+
const before = JSON.stringify([working.roles, working.agentModels]);
|
|
407
|
+
const notify = (message: string, kind?: "info" | "warning" | "error") => ctx.ui.notify(message, kind ?? "info");
|
|
404
408
|
const panelOptions = {
|
|
405
409
|
models: () => {
|
|
406
410
|
try { return ctx.modelRegistry.getAvailable().map((m) => `${m.provider}/${m.id}`); }
|
|
407
411
|
catch { return []; }
|
|
408
412
|
},
|
|
409
|
-
|
|
413
|
+
// Working copy: tracks live edits + freshly added (unsaved) roles.
|
|
414
|
+
roles: () => Object.keys(working.roles),
|
|
415
|
+
effectiveRoles: working.roles,
|
|
416
|
+
};
|
|
417
|
+
const actions = {
|
|
418
|
+
addRole: makeAddRoleAction(working, { notify }),
|
|
419
|
+
removeRole: makeRemoveRoleAction(working, { notify }),
|
|
410
420
|
};
|
|
411
421
|
await openConfigPanel({
|
|
412
422
|
ctx,
|
|
413
423
|
cfg: working,
|
|
414
|
-
|
|
424
|
+
actions,
|
|
425
|
+
build: (cfg, panelActions) => buildRows(cfg, discovery.agents, panelOptions, panelActions),
|
|
415
426
|
title: "Subagent model roles",
|
|
416
|
-
onSave: (saved
|
|
417
|
-
if (!
|
|
427
|
+
onSave: (saved) => {
|
|
428
|
+
if (!saved || JSON.stringify([working.roles, working.agentModels]) === before) return;
|
|
418
429
|
const patch = cfgToPatch(working);
|
|
419
430
|
patch.agentModels = preserveUnknownAgentModels(
|
|
420
431
|
patch.agentModels,
|
|
@@ -13,7 +13,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
15
|
import { row } from "@bacnh85/pi-config-panel";
|
|
16
|
-
import type { PanelGroup } from "@bacnh85/pi-config-panel";
|
|
16
|
+
import type { PanelAction, PanelGroup, PanelRow } from "@bacnh85/pi-config-panel";
|
|
17
17
|
|
|
18
18
|
// ponytail: local structural type — the kernel only reads value/label/
|
|
19
19
|
// description, so this stays compatible with the published 0.1.0 range while
|
|
@@ -24,7 +24,8 @@ interface CompletionItem {
|
|
|
24
24
|
description?: string;
|
|
25
25
|
}
|
|
26
26
|
import type { AgentConfig } from "./agents.ts";
|
|
27
|
-
import {
|
|
27
|
+
import { getModelCandidates } from "./agents.ts";
|
|
28
|
+
import { DEFAULT_ROLES, readSubagentRoles, resolveAgentModelChain, type RoleMap, type RolesConfig } from "./roles.ts";
|
|
28
29
|
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
30
31
|
// Settings persistence (global only — repo .pi/settings.json is read-only)
|
|
@@ -98,6 +99,9 @@ export interface RolesPanelOptions {
|
|
|
98
99
|
models: () => string[];
|
|
99
100
|
/** Known role names (defaults + configured), offered as `@role` on agent rows. */
|
|
100
101
|
roles: () => string[];
|
|
102
|
+
/** Effective role chains used to render each agent's default (falls back to
|
|
103
|
+
* DEFAULT_ROLES). Pass the working copy's roles so labels track live edits. */
|
|
104
|
+
effectiveRoles?: RoleMap;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
/** Seed a working config from current effective settings + bundled agents. */
|
|
@@ -115,10 +119,29 @@ export function buildRolesPanelCfg(agents: AgentConfig[], current: RolesConfig):
|
|
|
115
119
|
return cfg;
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
/** Human-readable default (no-override) chain for an agent's panel row label. */
|
|
123
|
+
export function defaultChainLabel(
|
|
124
|
+
agent: Pick<AgentConfig, "name" | "model" | "models">,
|
|
125
|
+
roles: RoleMap,
|
|
126
|
+
): string {
|
|
127
|
+
const raw = getModelCandidates(agent);
|
|
128
|
+
const alias = raw.filter((m) => m.startsWith("@")).join(", ");
|
|
129
|
+
const { candidates } = resolveAgentModelChain(agent, { roles, agentModels: {} });
|
|
130
|
+
const chain = candidates.length > 0 ? candidates.join(", ") : "parent fallback";
|
|
131
|
+
// alias is @-prefixed, candidates are expanded model ids — never equal.
|
|
132
|
+
return alias ? `default: ${alias} → ${chain}` : `default: ${chain}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
118
135
|
/** Build panel groups. Role rows first, then one override row per agent.
|
|
119
136
|
* `options` adds inline model/@role completions when provided (optional so
|
|
120
|
-
* existing unit tests and non-TUI callers stay unchanged).
|
|
121
|
-
|
|
137
|
+
* existing unit tests and non-TUI callers stay unchanged). `actions` appends
|
|
138
|
+
* the + Add role / − Remove role action rows when provided. */
|
|
139
|
+
export function buildRows(
|
|
140
|
+
cfg: RolesPanelCfg,
|
|
141
|
+
agents: AgentConfig[],
|
|
142
|
+
options?: RolesPanelOptions,
|
|
143
|
+
actions?: Record<string, PanelAction>,
|
|
144
|
+
): PanelGroup[] {
|
|
122
145
|
const defaultChain = (name: string) => Array.isArray(DEFAULT_ROLES[name]) ? (DEFAULT_ROLES[name] as string[]).join(", ") : String(DEFAULT_ROLES[name] ?? "");
|
|
123
146
|
const modelItems = (): CompletionItem[] =>
|
|
124
147
|
(options?.models() ?? []).sort().map((ref) => ({ value: ref }));
|
|
@@ -135,15 +158,23 @@ export function buildRows(cfg: RolesPanelCfg, agents: AgentConfig[], options?: R
|
|
|
135
158
|
cfg.roles[name] = String(v ?? "").trim();
|
|
136
159
|
}, withCompletions(modelItems));
|
|
137
160
|
});
|
|
161
|
+
const effective = options?.effectiveRoles ?? DEFAULT_ROLES;
|
|
138
162
|
const agentRows = agents.map((agent) =>
|
|
139
|
-
row(`agent.${agent.name}`, agent.name, "string", cfg.agentModels[agent.name] ?? "", (v) => {
|
|
163
|
+
row(`agent.${agent.name}`, `${agent.name} (${defaultChainLabel(agent, effective)})`, "string", cfg.agentModels[agent.name] ?? "", (v) => {
|
|
140
164
|
const value = String(v ?? "").trim();
|
|
141
165
|
if (value) cfg.agentModels[agent.name] = value;
|
|
142
166
|
else delete cfg.agentModels[agent.name];
|
|
143
167
|
}, withCompletions(() => [...modelItems(), ...roleItems()])),
|
|
144
168
|
);
|
|
169
|
+
const actionRows: PanelRow[] = [];
|
|
170
|
+
if (actions?.addRole) {
|
|
171
|
+
actionRows.push({ key: "action.addRole", label: "+ Add role", kind: "action", value: undefined, set: (p) => actions.addRole!.run(p as never) });
|
|
172
|
+
}
|
|
173
|
+
if (actions?.removeRole) {
|
|
174
|
+
actionRows.push({ key: "action.removeRole", label: "− Remove role", kind: "action", value: undefined, set: (p) => actions.removeRole!.run(p as never) });
|
|
175
|
+
}
|
|
145
176
|
return [
|
|
146
|
-
{ key: "roles", label: "Model roles (chain, blank = default)", rows: roleRows },
|
|
177
|
+
{ key: "roles", label: "Model roles (chain, blank = default)", rows: [...roleRows, ...actionRows] },
|
|
147
178
|
{ key: "agents", label: "Per-agent overrides (blank = inherit)", rows: agentRows },
|
|
148
179
|
];
|
|
149
180
|
}
|
|
@@ -165,6 +196,88 @@ export function cfgToPatch(cfg: RolesPanelCfg): { roles: RolesConfig["roles"]; a
|
|
|
165
196
|
return { roles, agentModels };
|
|
166
197
|
}
|
|
167
198
|
|
|
199
|
+
/** Validate a new role name; returns an error message or null when OK. */
|
|
200
|
+
export function validateNewRoleName(name: string, known: readonly string[]): string | null {
|
|
201
|
+
const trimmed = name.trim();
|
|
202
|
+
if (!trimmed) return "Role name is empty.";
|
|
203
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(trimmed)) {
|
|
204
|
+
return `Invalid role name "${trimmed}" — use letters, digits, dot, dash, underscore (max 64).`;
|
|
205
|
+
}
|
|
206
|
+
const clash = known.find((k) => k.toLowerCase() === trimmed.toLowerCase());
|
|
207
|
+
if (clash) return `Role @${clash} already exists.`;
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Apply a role removal to the working config. Built-in roles (DEFAULT_ROLES)
|
|
212
|
+
* can only be reset to their bundled chain (blank); custom roles are deleted
|
|
213
|
+
* (row disappears on rebuild, key dropped by cfgToPatch). Unknown → null. */
|
|
214
|
+
export function removeRoleFromCfg(cfg: RolesPanelCfg, name: string): "reset" | "deleted" | null {
|
|
215
|
+
const key = Object.keys(cfg.roles).find((k) => k.toLowerCase() === name.trim().toLowerCase());
|
|
216
|
+
if (!key) return null;
|
|
217
|
+
if (DEFAULT_ROLES[key] !== undefined) {
|
|
218
|
+
cfg.roles[key] = "";
|
|
219
|
+
return "reset";
|
|
220
|
+
}
|
|
221
|
+
delete cfg.roles[key];
|
|
222
|
+
return "deleted";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface RoleActionOpts {
|
|
226
|
+
/** User feedback (defaults to no-op so tests stay quiet). */
|
|
227
|
+
notify?: (message: string, kind?: "info" | "warning" | "error") => void;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** "+ Add role" panel action: prompt name → validate → prompt chain → mutate
|
|
231
|
+
* the working config (kernel rebuilds rows + marks dirty after the action). */
|
|
232
|
+
export function makeAddRoleAction(cfg: RolesPanelCfg, opts: RoleActionOpts = {}): PanelAction {
|
|
233
|
+
return {
|
|
234
|
+
label: "Add role",
|
|
235
|
+
run: (prompt) => new Promise<void>((resolve) => {
|
|
236
|
+
prompt("Role name (e.g. writer)", (name) => {
|
|
237
|
+
const trimmed = (name ?? "").trim();
|
|
238
|
+
if (!trimmed) return resolve();
|
|
239
|
+
const err = validateNewRoleName(trimmed, Object.keys(cfg.roles));
|
|
240
|
+
if (err) {
|
|
241
|
+
opts.notify?.(err, "warning");
|
|
242
|
+
return resolve();
|
|
243
|
+
}
|
|
244
|
+
prompt("Model chain (comma-separated; @role or * allowed)", (chain) => {
|
|
245
|
+
const value = (chain ?? "").trim();
|
|
246
|
+
if (!value) {
|
|
247
|
+
opts.notify?.("Chain required — role not added.", "warning");
|
|
248
|
+
return resolve();
|
|
249
|
+
}
|
|
250
|
+
cfg.roles[trimmed] = value;
|
|
251
|
+
resolve();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** "− Remove role" panel action: prompt pick from known roles, then reset
|
|
259
|
+
* (built-in) or delete (custom) via removeRoleFromCfg. */
|
|
260
|
+
export function makeRemoveRoleAction(cfg: RolesPanelCfg, opts: RoleActionOpts = {}): PanelAction {
|
|
261
|
+
return {
|
|
262
|
+
label: "Remove role",
|
|
263
|
+
run: (prompt) => new Promise<void>((resolve) => {
|
|
264
|
+
const names = Object.keys(cfg.roles).sort();
|
|
265
|
+
if (names.length === 0) {
|
|
266
|
+
opts.notify?.("No roles to remove.", "warning");
|
|
267
|
+
return resolve();
|
|
268
|
+
}
|
|
269
|
+
prompt(`Remove role (${names.join(", ")})`, (pick) => {
|
|
270
|
+
if (!pick) return resolve();
|
|
271
|
+
const result = removeRoleFromCfg(cfg, pick);
|
|
272
|
+
if (result === "reset") opts.notify?.(`@${pick.trim()} reset to bundled default`, "info");
|
|
273
|
+
else if (result === "deleted") opts.notify?.(`@${pick.trim()} removed`, "info");
|
|
274
|
+
else opts.notify?.(`No role named "${pick.trim()}".`, "warning");
|
|
275
|
+
resolve();
|
|
276
|
+
});
|
|
277
|
+
}),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
168
281
|
/** Keep overrides for agents NOT shown in the panel (e.g. overrides for
|
|
169
282
|
* project-local agents saved globally from another project) so a panel save
|
|
170
283
|
* doesn't wipe them. Discovered-agent entries always follow the panel. */
|
package/package.json
CHANGED