@gotgenes/pi-subagents 6.3.1 → 6.5.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 +32 -0
- package/docs/architecture/architecture.md +244 -260
- package/docs/plans/0108-extract-agent-type-registry.md +322 -0
- package/docs/plans/0109-extract-settings-manager.md +276 -0
- package/docs/retro/0108-extract-agent-type-registry.md +41 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +16 -13
- package/src/agent-runner.ts +4 -0
- package/src/agent-types.ts +108 -91
- package/src/index.ts +31 -58
- package/src/runtime.ts +0 -6
- package/src/session-config.ts +5 -4
- package/src/settings.ts +94 -46
- package/src/tools/agent-tool.ts +11 -11
- package/src/tools/get-result-tool.ts +3 -1
- package/src/types.ts +0 -3
- package/src/ui/agent-menu.ts +47 -53
- package/src/ui/agent-widget.ts +10 -9
- package/src/ui/conversation-viewer.ts +4 -2
package/src/settings.ts
CHANGED
|
@@ -16,16 +16,104 @@ export interface SubagentsSettings {
|
|
|
16
16
|
graceTurns?: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
20
|
-
export interface SettingsAppliers {
|
|
21
|
-
setMaxConcurrent: (n: number) => void;
|
|
22
|
-
setDefaultMaxTurns: (n: number) => void;
|
|
23
|
-
setGraceTurns: (n: number) => void;
|
|
24
|
-
}
|
|
25
19
|
|
|
26
20
|
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
27
21
|
export type SettingsEmit = (event: string, payload: unknown) => void;
|
|
28
22
|
|
|
23
|
+
const DEFAULT_MAX_CONCURRENT = 4;
|
|
24
|
+
const DEFAULT_GRACE_TURNS = 5;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Owns all three in-memory settings values and their load/save/persist cycle.
|
|
28
|
+
* Replaces the scattered free-function + SettingsAppliers callback pattern.
|
|
29
|
+
*/
|
|
30
|
+
export class SettingsManager {
|
|
31
|
+
private _defaultMaxTurns: number | undefined = undefined;
|
|
32
|
+
private _graceTurns: number = DEFAULT_GRACE_TURNS;
|
|
33
|
+
private _maxConcurrent: number = DEFAULT_MAX_CONCURRENT;
|
|
34
|
+
|
|
35
|
+
private readonly emit: SettingsEmit;
|
|
36
|
+
private readonly cwd: string;
|
|
37
|
+
|
|
38
|
+
constructor(deps: { emit: SettingsEmit; cwd: string }) {
|
|
39
|
+
this.emit = deps.emit;
|
|
40
|
+
this.cwd = deps.cwd;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── defaultMaxTurns: 0 or undefined → unlimited (undefined); else max(1, n) ──
|
|
44
|
+
|
|
45
|
+
get defaultMaxTurns(): number | undefined {
|
|
46
|
+
return this._defaultMaxTurns;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
set defaultMaxTurns(n: number | undefined) {
|
|
50
|
+
if (n == null || n === 0) {
|
|
51
|
+
this._defaultMaxTurns = undefined;
|
|
52
|
+
} else {
|
|
53
|
+
this._defaultMaxTurns = Math.max(1, n);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── graceTurns: minimum 1 ──
|
|
58
|
+
|
|
59
|
+
get graceTurns(): number {
|
|
60
|
+
return this._graceTurns;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
set graceTurns(n: number) {
|
|
64
|
+
this._graceTurns = Math.max(1, n);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── maxConcurrent: minimum 1 ──
|
|
68
|
+
|
|
69
|
+
get maxConcurrent(): number {
|
|
70
|
+
return this._maxConcurrent;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
set maxConcurrent(n: number) {
|
|
74
|
+
this._maxConcurrent = Math.max(1, n);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Lifecycle methods ──
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Load merged settings (global + project), apply to in-memory values,
|
|
81
|
+
* and emit the `subagents:settings_loaded` lifecycle event.
|
|
82
|
+
* Returns the raw loaded settings object.
|
|
83
|
+
*/
|
|
84
|
+
load(): SubagentsSettings {
|
|
85
|
+
const settings = loadSettings(this.cwd);
|
|
86
|
+
if (typeof settings.maxConcurrent === "number") this.maxConcurrent = settings.maxConcurrent;
|
|
87
|
+
if (typeof settings.defaultMaxTurns === "number") this.defaultMaxTurns = settings.defaultMaxTurns;
|
|
88
|
+
if (typeof settings.graceTurns === "number") this.graceTurns = settings.graceTurns;
|
|
89
|
+
this.emit("subagents:settings_loaded", { settings });
|
|
90
|
+
return settings;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Snapshot current in-memory values for persistence.
|
|
95
|
+
* `defaultMaxTurns` uses 0 as the on-disk marker for unlimited (undefined).
|
|
96
|
+
*/
|
|
97
|
+
snapshot(): { maxConcurrent: number; defaultMaxTurns: number; graceTurns: number } {
|
|
98
|
+
return {
|
|
99
|
+
maxConcurrent: this._maxConcurrent,
|
|
100
|
+
defaultMaxTurns: this._defaultMaxTurns ?? 0,
|
|
101
|
+
graceTurns: this._graceTurns,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Persist the current snapshot, emit `subagents:settings_changed`,
|
|
107
|
+
* and return the toast the UI should display.
|
|
108
|
+
*/
|
|
109
|
+
saveAndNotify(successMsg: string): { message: string; level: "info" | "warning" } {
|
|
110
|
+
const snap = this.snapshot();
|
|
111
|
+
const persisted = saveSettings(snap, this.cwd);
|
|
112
|
+
this.emit("subagents:settings_changed", { settings: snap, persisted });
|
|
113
|
+
return persistToastFor(successMsg, persisted);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
29
117
|
// Sanity ceilings — prevent hand-edited configs from asking for values that
|
|
30
118
|
// make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
|
|
31
119
|
// that any realistic power-user setting passes through.
|
|
@@ -107,13 +195,6 @@ export function saveSettings(s: SubagentsSettings, cwd: string = process.cwd()):
|
|
|
107
195
|
}
|
|
108
196
|
}
|
|
109
197
|
|
|
110
|
-
/** Apply persisted settings to the in-memory state via caller-supplied setters. */
|
|
111
|
-
export function applySettings(s: SubagentsSettings, appliers: SettingsAppliers): void {
|
|
112
|
-
if (typeof s.maxConcurrent === "number") appliers.setMaxConcurrent(s.maxConcurrent);
|
|
113
|
-
if (typeof s.defaultMaxTurns === "number") appliers.setDefaultMaxTurns(s.defaultMaxTurns);
|
|
114
|
-
if (typeof s.graceTurns === "number") appliers.setGraceTurns(s.graceTurns);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
198
|
/**
|
|
118
199
|
* Format the user-facing toast for a settings mutation. Pure function —
|
|
119
200
|
* routes the success/failure of `saveSettings` into the right message + level
|
|
@@ -127,36 +208,3 @@ export function persistToastFor(
|
|
|
127
208
|
? { message: successMsg, level: "info" }
|
|
128
209
|
: { message: `${successMsg} (session only; failed to persist)`, level: "warning" };
|
|
129
210
|
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Load merged settings, apply them to in-memory state, and emit the
|
|
133
|
-
* `subagents:settings_loaded` lifecycle event. Returns the loaded settings so
|
|
134
|
-
* callers can log/inspect. Extension init wires this once.
|
|
135
|
-
*/
|
|
136
|
-
export function applyAndEmitLoaded(
|
|
137
|
-
appliers: SettingsAppliers,
|
|
138
|
-
emit: SettingsEmit,
|
|
139
|
-
cwd: string = process.cwd(),
|
|
140
|
-
): SubagentsSettings {
|
|
141
|
-
const settings = loadSettings(cwd);
|
|
142
|
-
applySettings(settings, appliers);
|
|
143
|
-
emit("subagents:settings_loaded", { settings });
|
|
144
|
-
return settings;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Persist a settings snapshot, emit the `subagents:settings_changed` event
|
|
149
|
-
* (regardless of persist outcome so listeners see the in-memory change), and
|
|
150
|
-
* return the toast the UI should display. Event payload carries the `persisted`
|
|
151
|
-
* flag so listeners can react to write failures.
|
|
152
|
-
*/
|
|
153
|
-
export function saveAndEmitChanged(
|
|
154
|
-
snapshot: SubagentsSettings,
|
|
155
|
-
successMsg: string,
|
|
156
|
-
emit: SettingsEmit,
|
|
157
|
-
cwd: string = process.cwd(),
|
|
158
|
-
): { message: string; level: "info" | "warning" } {
|
|
159
|
-
const persisted = saveSettings(snapshot, cwd);
|
|
160
|
-
emit("subagents:settings_changed", { settings: snapshot, persisted });
|
|
161
|
-
return persistToastFor(successMsg, persisted);
|
|
162
|
-
}
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import type { SpawnOptions } from "../agent-manager.js";
|
|
5
5
|
import { normalizeMaxTurns } from "../agent-runner.js";
|
|
6
|
-
import {
|
|
6
|
+
import { AgentTypeRegistry } from "../agent-types.js";
|
|
7
7
|
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
8
8
|
import { resolveInvocationModel } from "../model-resolver.js";
|
|
9
9
|
|
|
@@ -108,12 +108,12 @@ export interface AgentToolDeps {
|
|
|
108
108
|
widget: AgentToolWidget;
|
|
109
109
|
agentActivity: Map<string, AgentActivity>;
|
|
110
110
|
emitEvent: (name: string, data: unknown) => void;
|
|
111
|
-
|
|
111
|
+
registry: AgentTypeRegistry;
|
|
112
112
|
typeListText: string;
|
|
113
113
|
availableTypesText: string;
|
|
114
114
|
agentDir: string;
|
|
115
|
-
/**
|
|
116
|
-
|
|
115
|
+
/** Narrow settings accessor — only the default max turns is needed here. */
|
|
116
|
+
settings: { readonly defaultMaxTurns: number | undefined };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// ---- Factory ----
|
|
@@ -207,7 +207,7 @@ Guidelines:
|
|
|
207
207
|
|
|
208
208
|
renderCall(args: Record<string, unknown>, theme: any) {
|
|
209
209
|
const displayName = args.subagent_type
|
|
210
|
-
? getDisplayName(args.subagent_type as string)
|
|
210
|
+
? getDisplayName(args.subagent_type as string, deps.registry)
|
|
211
211
|
: "Agent";
|
|
212
212
|
const desc = (args.description as string) ?? "";
|
|
213
213
|
return new Text(
|
|
@@ -327,17 +327,17 @@ Guidelines:
|
|
|
327
327
|
deps.widget.setUICtx(ctx.ui as UICtx);
|
|
328
328
|
|
|
329
329
|
// Reload custom agents so new .pi/agents/*.md files are picked up without restart
|
|
330
|
-
deps.
|
|
330
|
+
deps.registry.reload();
|
|
331
331
|
|
|
332
332
|
const rawType = params.subagent_type as SubagentType;
|
|
333
|
-
const resolved = resolveType(rawType);
|
|
333
|
+
const resolved = deps.registry.resolveType(rawType);
|
|
334
334
|
const subagentType = resolved ?? "general-purpose";
|
|
335
335
|
const fellBack = resolved === undefined;
|
|
336
336
|
|
|
337
|
-
const displayName = getDisplayName(subagentType);
|
|
337
|
+
const displayName = getDisplayName(subagentType, deps.registry);
|
|
338
338
|
|
|
339
339
|
// Get agent config for invocation resolution
|
|
340
|
-
const customConfig = resolveAgentConfig(subagentType);
|
|
340
|
+
const customConfig = deps.registry.resolveAgentConfig(subagentType);
|
|
341
341
|
|
|
342
342
|
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
343
343
|
|
|
@@ -364,7 +364,7 @@ Guidelines:
|
|
|
364
364
|
? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
|
|
365
365
|
: undefined;
|
|
366
366
|
const effectiveMaxTurns = normalizeMaxTurns(
|
|
367
|
-
resolvedConfig.maxTurns ?? deps.
|
|
367
|
+
resolvedConfig.maxTurns ?? deps.settings.defaultMaxTurns,
|
|
368
368
|
);
|
|
369
369
|
const agentInvocation: AgentInvocation = {
|
|
370
370
|
modelName,
|
|
@@ -375,7 +375,7 @@ Guidelines:
|
|
|
375
375
|
runInBackground,
|
|
376
376
|
isolation,
|
|
377
377
|
};
|
|
378
|
-
const modeLabel = getPromptModeLabel(subagentType);
|
|
378
|
+
const modeLabel = getPromptModeLabel(subagentType, deps.registry);
|
|
379
379
|
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
380
380
|
const agentTags = modeLabel ? [modeLabel, ...invocationTags] : invocationTags;
|
|
381
381
|
const detailBase = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { AgentConfigLookup } from "../agent-types.js";
|
|
3
4
|
import type { AgentRecord } from "../types.js";
|
|
4
5
|
import { formatDuration, getDisplayName } from "../ui/agent-widget.js";
|
|
5
6
|
import { getSessionContextPercent } from "../usage.js";
|
|
@@ -10,6 +11,7 @@ export interface GetResultDeps {
|
|
|
10
11
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
11
12
|
cancelNudge: (key: string) => void;
|
|
12
13
|
getConversation: (session: AgentSession) => string | undefined;
|
|
14
|
+
registry: AgentConfigLookup;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/** Create the get_subagent_result tool definition (without Pi SDK wrapper). */
|
|
@@ -57,7 +59,7 @@ export function createGetResultTool(deps: GetResultDeps) {
|
|
|
57
59
|
await record.promise;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
const displayName = getDisplayName(record.type);
|
|
62
|
+
const displayName = getDisplayName(record.type, deps.registry);
|
|
61
63
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
62
64
|
const tokens = formatLifetimeTokens(record);
|
|
63
65
|
const contextPercent = getSessionContextPercent(record.session);
|
package/src/types.ts
CHANGED
|
@@ -12,9 +12,6 @@ export type { ThinkingLevel };
|
|
|
12
12
|
/** Agent type: any string name (built-in defaults or user-defined). */
|
|
13
13
|
export type SubagentType = string;
|
|
14
14
|
|
|
15
|
-
/** Names of the three embedded default agents. */
|
|
16
|
-
export const DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as const;
|
|
17
|
-
|
|
18
15
|
/** Memory scope for persistent agent memory. */
|
|
19
16
|
export type MemoryScope = "user" | "project" | "local";
|
|
20
17
|
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -4,10 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { SpawnOptions } from "../agent-manager.js";
|
|
6
6
|
import {
|
|
7
|
+
AgentTypeRegistry,
|
|
7
8
|
BUILTIN_TOOL_NAMES,
|
|
8
|
-
getAllTypes,
|
|
9
|
-
resolveAgentConfig,
|
|
10
|
-
resolveType,
|
|
11
9
|
} from "../agent-types.js";
|
|
12
10
|
import type { ModelRegistry } from "../model-resolver.js";
|
|
13
11
|
import type { AgentConfig, AgentRecord } from "../types.js";
|
|
@@ -22,34 +20,29 @@ export interface AgentMenuManager {
|
|
|
22
20
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
23
21
|
/** Used by generate wizard to spawn an agent that writes the .md file. */
|
|
24
22
|
spawnAndWait: (ctx: ExtensionContext, type: string, prompt: string, opts: Omit<SpawnOptions, "isBackground">) => Promise<AgentRecord>;
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
/** Drain the concurrency queue after maxConcurrent has been updated on SettingsManager. */
|
|
24
|
+
notifyConcurrencyChanged: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Narrow settings interface required by the agent menu. */
|
|
28
|
+
export interface AgentMenuSettings {
|
|
29
|
+
maxConcurrent: number;
|
|
30
|
+
defaultMaxTurns: number | undefined;
|
|
31
|
+
graceTurns: number;
|
|
32
|
+
saveAndNotify(msg: string): { message: string; level: "info" | "warning" };
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export interface AgentMenuDeps {
|
|
30
36
|
manager: AgentMenuManager;
|
|
31
|
-
|
|
37
|
+
registry: AgentTypeRegistry;
|
|
32
38
|
agentActivity: Map<string, AgentActivity>;
|
|
33
39
|
/** Resolve model label for a given agent type + registry. */
|
|
34
40
|
getModelLabel: (type: string, registry?: ModelRegistry) => string;
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/** Save settings and return a notification result. */
|
|
38
|
-
saveSettings: (
|
|
39
|
-
settings: { maxConcurrent: number; defaultMaxTurns: number; graceTurns: number },
|
|
40
|
-
successMsg: string,
|
|
41
|
-
) => { message: string; level: string };
|
|
41
|
+
/** Settings manager — owns in-memory values and persistence. */
|
|
42
|
+
settings: AgentMenuSettings;
|
|
42
43
|
emitEvent: (name: string, data: unknown) => void;
|
|
43
44
|
personalAgentsDir: string;
|
|
44
45
|
projectAgentsDir: string;
|
|
45
|
-
/** Returns the runtime default max turns (undefined = unlimited). */
|
|
46
|
-
getDefaultMaxTurns: () => number | undefined;
|
|
47
|
-
/** Returns the runtime grace turns value. */
|
|
48
|
-
getGraceTurns: () => number;
|
|
49
|
-
/** Updates the runtime default max turns (undefined = unlimited). */
|
|
50
|
-
setDefaultMaxTurns: (n: number | undefined) => void;
|
|
51
|
-
/** Updates the runtime grace turns value (minimum 1). */
|
|
52
|
-
setGraceTurns: (n: number) => void;
|
|
53
46
|
}
|
|
54
47
|
|
|
55
48
|
// ---- Narrow UI context types ----
|
|
@@ -72,8 +65,8 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
72
65
|
}
|
|
73
66
|
|
|
74
67
|
async function showAgentsMenu(ctx: ExtensionContext) {
|
|
75
|
-
deps.
|
|
76
|
-
const allNames = getAllTypes();
|
|
68
|
+
deps.registry.reload();
|
|
69
|
+
const allNames = deps.registry.getAllTypes();
|
|
77
70
|
|
|
78
71
|
const options: string[] = [];
|
|
79
72
|
|
|
@@ -126,7 +119,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
126
119
|
}
|
|
127
120
|
|
|
128
121
|
async function showAllAgentsList(ctx: ExtensionContext) {
|
|
129
|
-
const allNames = getAllTypes();
|
|
122
|
+
const allNames = deps.registry.getAllTypes();
|
|
130
123
|
if (allNames.length === 0) {
|
|
131
124
|
ctx.ui.notify("No agents.", "info");
|
|
132
125
|
return;
|
|
@@ -141,7 +134,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
141
134
|
};
|
|
142
135
|
|
|
143
136
|
const entries = allNames.map((name) => {
|
|
144
|
-
const cfg = resolveAgentConfig(name);
|
|
137
|
+
const cfg = deps.registry.resolveAgentConfig(name);
|
|
145
138
|
const disabled = cfg.enabled === false;
|
|
146
139
|
const model = deps.getModelLabel(name, ctx.modelRegistry);
|
|
147
140
|
const indicator = sourceIndicator(cfg);
|
|
@@ -152,10 +145,10 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
152
145
|
const maxPrefix = Math.max(...entries.map((e) => e.prefix.length));
|
|
153
146
|
|
|
154
147
|
const hasCustom = allNames.some((n) => {
|
|
155
|
-
const c = resolveAgentConfig(n);
|
|
148
|
+
const c = deps.registry.resolveAgentConfig(n);
|
|
156
149
|
return !c.isDefault && c.enabled !== false;
|
|
157
150
|
});
|
|
158
|
-
const hasDisabled = allNames.some((n) => resolveAgentConfig(n).enabled === false);
|
|
151
|
+
const hasDisabled = allNames.some((n) => deps.registry.resolveAgentConfig(n).enabled === false);
|
|
159
152
|
const legendParts: string[] = [];
|
|
160
153
|
if (hasCustom) legendParts.push("• = project ◦ = global");
|
|
161
154
|
if (hasDisabled) legendParts.push("✕ = disabled");
|
|
@@ -173,7 +166,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
173
166
|
.split(" · ")[0]
|
|
174
167
|
.replace(/^[•◦✕\s]+/, "")
|
|
175
168
|
.trim();
|
|
176
|
-
if (resolveType(agentName) != null) {
|
|
169
|
+
if (deps.registry.resolveType(agentName) != null) {
|
|
177
170
|
await showAgentDetail(ctx, agentName);
|
|
178
171
|
await showAllAgentsList(ctx);
|
|
179
172
|
}
|
|
@@ -187,7 +180,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
187
180
|
}
|
|
188
181
|
|
|
189
182
|
const options = agents.map((a) => {
|
|
190
|
-
const dn = getDisplayName(a.type);
|
|
183
|
+
const dn = getDisplayName(a.type, deps.registry);
|
|
191
184
|
const dur = formatDuration(a.startedAt, a.completedAt);
|
|
192
185
|
return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
|
|
193
186
|
});
|
|
@@ -220,7 +213,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
220
213
|
|
|
221
214
|
await ctx.ui.custom<undefined>(
|
|
222
215
|
(tui: any, theme: any, _keybindings: any, done: any) => {
|
|
223
|
-
return new ConversationViewer(tui, session, record, activity, theme, done);
|
|
216
|
+
return new ConversationViewer(tui, session, record, activity, theme, done, deps.registry);
|
|
224
217
|
},
|
|
225
218
|
{
|
|
226
219
|
overlay: true,
|
|
@@ -234,11 +227,11 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
234
227
|
}
|
|
235
228
|
|
|
236
229
|
async function showAgentDetail(ctx: ExtensionContext, name: string) {
|
|
237
|
-
if (resolveType(name) == null) {
|
|
230
|
+
if (deps.registry.resolveType(name) == null) {
|
|
238
231
|
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
239
232
|
return;
|
|
240
233
|
}
|
|
241
|
-
const cfg = resolveAgentConfig(name);
|
|
234
|
+
const cfg = deps.registry.resolveAgentConfig(name);
|
|
242
235
|
|
|
243
236
|
const file = findAgentFile(name);
|
|
244
237
|
const isDefault = cfg.isDefault === true;
|
|
@@ -266,7 +259,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
266
259
|
if (edited !== undefined && edited !== content) {
|
|
267
260
|
const { writeFileSync } = await import("node:fs");
|
|
268
261
|
writeFileSync(file.path, edited, "utf-8");
|
|
269
|
-
deps.
|
|
262
|
+
deps.registry.reload();
|
|
270
263
|
ctx.ui.notify(`Updated ${file.path}`, "info");
|
|
271
264
|
}
|
|
272
265
|
} else if (choice === "Delete") {
|
|
@@ -277,7 +270,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
277
270
|
);
|
|
278
271
|
if (confirmed) {
|
|
279
272
|
unlinkSync(file.path);
|
|
280
|
-
deps.
|
|
273
|
+
deps.registry.reload();
|
|
281
274
|
ctx.ui.notify(`Deleted ${file.path}`, "info");
|
|
282
275
|
}
|
|
283
276
|
}
|
|
@@ -288,7 +281,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
288
281
|
);
|
|
289
282
|
if (confirmed) {
|
|
290
283
|
unlinkSync(file.path);
|
|
291
|
-
deps.
|
|
284
|
+
deps.registry.reload();
|
|
292
285
|
ctx.ui.notify(`Restored default ${name}`, "info");
|
|
293
286
|
}
|
|
294
287
|
} else if (choice.startsWith("Eject")) {
|
|
@@ -347,7 +340,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
347
340
|
|
|
348
341
|
const { writeFileSync } = await import("node:fs");
|
|
349
342
|
writeFileSync(targetPath, content, "utf-8");
|
|
350
|
-
deps.
|
|
343
|
+
deps.registry.reload();
|
|
351
344
|
ctx.ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
352
345
|
}
|
|
353
346
|
|
|
@@ -362,7 +355,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
362
355
|
const updated = content.replace(/^---\n/, "---\nenabled: false\n");
|
|
363
356
|
const { writeFileSync } = await import("node:fs");
|
|
364
357
|
writeFileSync(file.path, updated, "utf-8");
|
|
365
|
-
deps.
|
|
358
|
+
deps.registry.reload();
|
|
366
359
|
ctx.ui.notify(`Disabled ${name} (${file.path})`, "info");
|
|
367
360
|
return;
|
|
368
361
|
}
|
|
@@ -381,7 +374,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
381
374
|
const targetPath = join(targetDir, `${name}.md`);
|
|
382
375
|
const { writeFileSync } = await import("node:fs");
|
|
383
376
|
writeFileSync(targetPath, "---\nenabled: false\n---\n", "utf-8");
|
|
384
|
-
deps.
|
|
377
|
+
deps.registry.reload();
|
|
385
378
|
ctx.ui.notify(`Disabled ${name} (${targetPath})`, "info");
|
|
386
379
|
}
|
|
387
380
|
|
|
@@ -395,11 +388,11 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
395
388
|
|
|
396
389
|
if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
|
|
397
390
|
unlinkSync(file.path);
|
|
398
|
-
deps.
|
|
391
|
+
deps.registry.reload();
|
|
399
392
|
ctx.ui.notify(`Enabled ${name} (removed ${file.path})`, "info");
|
|
400
393
|
} else {
|
|
401
394
|
writeFileSync(file.path, updated, "utf-8");
|
|
402
|
-
deps.
|
|
395
|
+
deps.registry.reload();
|
|
403
396
|
ctx.ui.notify(`Enabled ${name} (${file.path})`, "info");
|
|
404
397
|
}
|
|
405
398
|
}
|
|
@@ -501,7 +494,7 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
501
494
|
return;
|
|
502
495
|
}
|
|
503
496
|
|
|
504
|
-
deps.
|
|
497
|
+
deps.registry.reload();
|
|
505
498
|
|
|
506
499
|
if (existsSync(targetPath)) {
|
|
507
500
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
@@ -604,27 +597,28 @@ ${systemPrompt}
|
|
|
604
597
|
|
|
605
598
|
const { writeFileSync } = await import("node:fs");
|
|
606
599
|
writeFileSync(targetPath, content, "utf-8");
|
|
607
|
-
deps.
|
|
600
|
+
deps.registry.reload();
|
|
608
601
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
609
602
|
}
|
|
610
603
|
|
|
611
604
|
async function showSettings(ctx: ExtensionContext) {
|
|
612
605
|
const choice = await ctx.ui.select("Settings", [
|
|
613
|
-
`Max concurrency (current: ${deps.
|
|
614
|
-
`Default max turns (current: ${deps.
|
|
615
|
-
`Grace turns (current: ${deps.
|
|
606
|
+
`Max concurrency (current: ${deps.settings.maxConcurrent})`,
|
|
607
|
+
`Default max turns (current: ${deps.settings.defaultMaxTurns ?? "unlimited"})`,
|
|
608
|
+
`Grace turns (current: ${deps.settings.graceTurns})`,
|
|
616
609
|
]);
|
|
617
610
|
if (!choice) return;
|
|
618
611
|
|
|
619
612
|
if (choice.startsWith("Max concurrency")) {
|
|
620
613
|
const val = await ctx.ui.input(
|
|
621
614
|
"Max concurrent background agents",
|
|
622
|
-
String(deps.
|
|
615
|
+
String(deps.settings.maxConcurrent),
|
|
623
616
|
);
|
|
624
617
|
if (val) {
|
|
625
618
|
const n = parseInt(val, 10);
|
|
626
619
|
if (n >= 1) {
|
|
627
|
-
deps.
|
|
620
|
+
deps.settings.maxConcurrent = n;
|
|
621
|
+
deps.manager.notifyConcurrencyChanged();
|
|
628
622
|
notifyApplied(ctx, `Max concurrency set to ${n}`);
|
|
629
623
|
} else {
|
|
630
624
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
@@ -633,15 +627,15 @@ ${systemPrompt}
|
|
|
633
627
|
} else if (choice.startsWith("Default max turns")) {
|
|
634
628
|
const val = await ctx.ui.input(
|
|
635
629
|
"Default max turns before wrap-up (0 = unlimited)",
|
|
636
|
-
String(deps.
|
|
630
|
+
String(deps.settings.defaultMaxTurns ?? 0),
|
|
637
631
|
);
|
|
638
632
|
if (val) {
|
|
639
633
|
const n = parseInt(val, 10);
|
|
640
634
|
if (n === 0) {
|
|
641
|
-
deps.
|
|
635
|
+
deps.settings.defaultMaxTurns = undefined;
|
|
642
636
|
notifyApplied(ctx, "Default max turns set to unlimited");
|
|
643
637
|
} else if (n >= 1) {
|
|
644
|
-
deps.
|
|
638
|
+
deps.settings.defaultMaxTurns = n;
|
|
645
639
|
notifyApplied(ctx, `Default max turns set to ${n}`);
|
|
646
640
|
} else {
|
|
647
641
|
ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
@@ -650,12 +644,12 @@ ${systemPrompt}
|
|
|
650
644
|
} else if (choice.startsWith("Grace turns")) {
|
|
651
645
|
const val = await ctx.ui.input(
|
|
652
646
|
"Grace turns after wrap-up steer",
|
|
653
|
-
String(deps.
|
|
647
|
+
String(deps.settings.graceTurns),
|
|
654
648
|
);
|
|
655
649
|
if (val) {
|
|
656
650
|
const n = parseInt(val, 10);
|
|
657
651
|
if (n >= 1) {
|
|
658
|
-
deps.
|
|
652
|
+
deps.settings.graceTurns = n;
|
|
659
653
|
notifyApplied(ctx, `Grace turns set to ${n}`);
|
|
660
654
|
} else {
|
|
661
655
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
@@ -665,7 +659,7 @@ ${systemPrompt}
|
|
|
665
659
|
}
|
|
666
660
|
|
|
667
661
|
function notifyApplied(ctx: ExtensionContext, successMsg: string) {
|
|
668
|
-
const { message, level } = deps.
|
|
662
|
+
const { message, level } = deps.settings.saveAndNotify(successMsg);
|
|
669
663
|
ctx.ui.notify(message, level as "info" | "warning" | "error");
|
|
670
664
|
}
|
|
671
665
|
|
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 { type AgentConfigLookup, AgentTypeRegistry } 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
|
|
|
@@ -143,14 +143,14 @@ export function formatDuration(startedAt: number, completedAt?: number): string
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/** Get display name for any agent type (built-in or custom). */
|
|
146
|
-
export function getDisplayName(type: SubagentType): string {
|
|
147
|
-
const config = resolveAgentConfig(type);
|
|
146
|
+
export function getDisplayName(type: SubagentType, registry: AgentConfigLookup): string {
|
|
147
|
+
const config = registry.resolveAgentConfig(type);
|
|
148
148
|
return config.displayName ?? config.name;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
|
|
152
|
-
export function getPromptModeLabel(type: SubagentType): string | undefined {
|
|
153
|
-
const config = resolveAgentConfig(type);
|
|
152
|
+
export function getPromptModeLabel(type: SubagentType, registry: AgentConfigLookup): string | undefined {
|
|
153
|
+
const config = registry.resolveAgentConfig(type);
|
|
154
154
|
return config.promptMode === "append" ? "twin" : undefined;
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -225,6 +225,7 @@ export class AgentWidget {
|
|
|
225
225
|
constructor(
|
|
226
226
|
private manager: AgentManager,
|
|
227
227
|
private agentActivity: Map<string, AgentActivity>,
|
|
228
|
+
private registry: AgentTypeRegistry,
|
|
228
229
|
) {}
|
|
229
230
|
|
|
230
231
|
/** Set the UI context (grabbed from first tool execution). */
|
|
@@ -275,8 +276,8 @@ export class AgentWidget {
|
|
|
275
276
|
|
|
276
277
|
/** Render a finished agent line. */
|
|
277
278
|
private renderFinishedLine(a: { id: string; type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string }, theme: Theme): string {
|
|
278
|
-
const name = getDisplayName(a.type);
|
|
279
|
-
const modeLabel = getPromptModeLabel(a.type);
|
|
279
|
+
const name = getDisplayName(a.type, this.registry);
|
|
280
|
+
const modeLabel = getPromptModeLabel(a.type, this.registry);
|
|
280
281
|
const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
|
|
281
282
|
|
|
282
283
|
let icon: string;
|
|
@@ -345,8 +346,8 @@ export class AgentWidget {
|
|
|
345
346
|
|
|
346
347
|
const runningLines: string[][] = []; // each entry is [header, activity]
|
|
347
348
|
for (const a of running) {
|
|
348
|
-
const name = getDisplayName(a.type);
|
|
349
|
-
const modeLabel = getPromptModeLabel(a.type);
|
|
349
|
+
const name = getDisplayName(a.type, this.registry);
|
|
350
|
+
const modeLabel = getPromptModeLabel(a.type, this.registry);
|
|
350
351
|
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
351
352
|
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
352
353
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
10
|
+
import type { AgentConfigLookup } from "../agent-types.js";
|
|
10
11
|
import { extractText } from "../context.js";
|
|
11
12
|
import type { AgentRecord } from "../types.js";
|
|
12
13
|
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
@@ -33,6 +34,7 @@ export class ConversationViewer implements Component {
|
|
|
33
34
|
private activity: AgentActivity | undefined,
|
|
34
35
|
private theme: Theme,
|
|
35
36
|
private done: (result: undefined) => void,
|
|
37
|
+
private registry: AgentConfigLookup,
|
|
36
38
|
) {
|
|
37
39
|
this.unsubscribe = session.subscribe(() => {
|
|
38
40
|
if (this.closed) return;
|
|
@@ -91,8 +93,8 @@ export class ConversationViewer implements Component {
|
|
|
91
93
|
|
|
92
94
|
// Header
|
|
93
95
|
lines.push(hrTop);
|
|
94
|
-
const name = getDisplayName(this.record.type);
|
|
95
|
-
const modeLabel = getPromptModeLabel(this.record.type);
|
|
96
|
+
const name = getDisplayName(this.record.type, this.registry);
|
|
97
|
+
const modeLabel = getPromptModeLabel(this.record.type, this.registry);
|
|
96
98
|
const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
|
|
97
99
|
const statusIcon = this.record.status === "running"
|
|
98
100
|
? th.fg("accent", "●")
|