@ferris1225/pi-subagents 4.3.2 → 4.3.4
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 +18 -0
- package/README.md +89 -49
- package/agents/artisan.md +7 -5
- package/agents/scout.md +5 -3
- package/agents/sentinel.md +16 -0
- package/agents/steward.md +9 -8
- package/package.json +2 -2
- package/src/agents.ts +1 -1
- package/src/announcements.ts +1 -1
- package/src/config.ts +63 -14
- package/src/dispatch.ts +9 -8
- package/src/prompt.ts +15 -9
- package/src/setup.ts +277 -280
- package/src/thread-lifecycle.ts +4 -2
- package/src/ui.ts +66 -154
- package/src/widget.ts +14 -13
package/src/setup.ts
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive configuration wizard for /subagents-setup.
|
|
3
|
-
*
|
|
4
|
-
* The wizard stays one level deep: which agents run, then a model and an
|
|
5
|
-
* optional thinking override per agent. Role thinking defaults apply until
|
|
6
|
-
* the user picks a level. Everything else (agent scope, idle timeout, result
|
|
7
|
-
* lines) is config-file-only.
|
|
8
|
-
*/
|
|
1
|
+
/** Single-overlay editor for the UI-configurable pi-subagents settings. */
|
|
9
2
|
|
|
10
|
-
import { stat } from "node:fs/promises";
|
|
11
3
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
12
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
type Component,
|
|
8
|
+
type Focusable,
|
|
9
|
+
type KeybindingsManager,
|
|
10
|
+
type TUI,
|
|
11
|
+
} from "@earendil-works/pi-tui";
|
|
13
12
|
import {
|
|
14
|
-
AGENT_PROFILES,
|
|
15
13
|
BUILTIN_AGENT_NAMES,
|
|
16
|
-
DEFAULT_CONFIG,
|
|
17
|
-
DEFAULT_ENABLED_AGENTS,
|
|
18
14
|
agentProfile,
|
|
19
15
|
errorMessage,
|
|
20
16
|
getConfigPath,
|
|
@@ -29,302 +25,284 @@ import {
|
|
|
29
25
|
applyAgentModelChoice,
|
|
30
26
|
availableModelsInScope,
|
|
31
27
|
buildModelPickerItems,
|
|
32
|
-
currentModelRef,
|
|
33
28
|
findModelByRef,
|
|
34
|
-
modelRef,
|
|
35
29
|
resolveThinkingLevel,
|
|
36
30
|
supportedThinkingLevels,
|
|
37
31
|
} from "./models.ts";
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
|
|
41
|
-
off: "no reasoning tokens",
|
|
42
|
-
minimal: "minimal reasoning",
|
|
43
|
-
low: "light reasoning",
|
|
44
|
-
medium: "balanced reasoning",
|
|
45
|
-
high: "deep reasoning",
|
|
46
|
-
xhigh: "extra-deep reasoning",
|
|
47
|
-
max: "strongest reasoning",
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
function agentPickerItems(): Array<{ value: string; label: string; description: string }> {
|
|
51
|
-
return BUILTIN_AGENT_NAMES.map((name) => {
|
|
52
|
-
const profile = AGENT_PROFILES[name];
|
|
53
|
-
return {
|
|
54
|
-
value: name,
|
|
55
|
-
label: name,
|
|
56
|
-
description: `${profile.summary} — ${profile.remark}`,
|
|
57
|
-
};
|
|
58
|
-
});
|
|
59
|
-
}
|
|
32
|
+
import { makePickerStyles, Picker } from "./ui.ts";
|
|
60
33
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return profile ? `${name} — ${profile.summary}` : name;
|
|
64
|
-
}
|
|
34
|
+
const FIELD_COUNT = 3;
|
|
35
|
+
const WIDE_LAYOUT_MIN_WIDTH = 112;
|
|
65
36
|
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
await stat(configPath);
|
|
69
|
-
return true;
|
|
70
|
-
} catch {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
37
|
+
type SetupResult = SubagentsConfig | undefined;
|
|
74
38
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
agentPickerItems(),
|
|
84
|
-
current,
|
|
85
|
-
);
|
|
86
|
-
return picked;
|
|
39
|
+
export interface SetupOverlayOptions {
|
|
40
|
+
config: SubagentsConfig;
|
|
41
|
+
models: readonly Model<Api>[];
|
|
42
|
+
mainModel: Model<Api> | undefined;
|
|
43
|
+
theme: Theme;
|
|
44
|
+
tui: TUI;
|
|
45
|
+
keybindings: KeybindingsManager;
|
|
46
|
+
onDone: (result: SetupResult) => void;
|
|
87
47
|
}
|
|
88
48
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
models,
|
|
98
|
-
configuredRef,
|
|
99
|
-
mainRef: currentModelRef(ctx),
|
|
100
|
-
});
|
|
101
|
-
return promptSelectOne(
|
|
102
|
-
ctx,
|
|
103
|
-
title,
|
|
104
|
-
`Type to filter by provider, model, or capability • ↑/↓ • Enter selects • Esc ${escNote}`,
|
|
105
|
-
items,
|
|
106
|
-
configuredRef ?? CURRENT_MAIN_MODEL,
|
|
107
|
-
);
|
|
49
|
+
function cloneConfig(config: SubagentsConfig): SubagentsConfig {
|
|
50
|
+
return {
|
|
51
|
+
...config,
|
|
52
|
+
enabledAgents: [...config.enabledAgents],
|
|
53
|
+
knownAgents: [...config.knownAgents],
|
|
54
|
+
agentModels: { ...config.agentModels },
|
|
55
|
+
agentThinkingLevels: { ...config.agentThinkingLevels },
|
|
56
|
+
};
|
|
108
57
|
}
|
|
109
58
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
59
|
+
function setupAgentNames(config: SubagentsConfig): string[] {
|
|
60
|
+
return [
|
|
61
|
+
...new Set([
|
|
62
|
+
...BUILTIN_AGENT_NAMES,
|
|
63
|
+
...config.knownAgents,
|
|
64
|
+
...config.enabledAgents,
|
|
65
|
+
...Object.keys(config.agentModels),
|
|
66
|
+
...Object.keys(config.agentThinkingLevels),
|
|
67
|
+
]),
|
|
68
|
+
];
|
|
119
69
|
}
|
|
120
70
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Transactional settings component. It mutates only its private draft and
|
|
73
|
+
* returns that draft exclusively from the explicit Save & Exit row.
|
|
74
|
+
*/
|
|
75
|
+
export class SetupOverlay implements Component, Focusable {
|
|
76
|
+
private _focused = false;
|
|
77
|
+
private readonly state: SubagentsConfig;
|
|
78
|
+
private readonly agents: string[];
|
|
79
|
+
private row = 0;
|
|
80
|
+
private field = 0;
|
|
81
|
+
private modelPicker: Picker | undefined;
|
|
128
82
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
current: ThinkingLevel | undefined,
|
|
135
|
-
escNote = "cancels this setup pass",
|
|
136
|
-
): Promise<ThinkingLevel | undefined> {
|
|
137
|
-
const supported = supportedThinkingLevels(model);
|
|
138
|
-
const roleDefault = resolveThinkingLevel(model, roleThinkingLevel(agentName));
|
|
139
|
-
if (supported.length <= 1) return roleDefault;
|
|
140
|
-
|
|
141
|
-
const currentEffective = current ? resolveThinkingLevel(model, current) : roleDefault;
|
|
142
|
-
const modelName = model ? modelRef(model) : "current main model";
|
|
143
|
-
const options = supported.map((level) => {
|
|
144
|
-
const tags = [
|
|
145
|
-
level === roleDefault ? "role default" : "",
|
|
146
|
-
current !== undefined && currentEffective === level ? "current" : "",
|
|
147
|
-
].filter(Boolean);
|
|
148
|
-
return {
|
|
149
|
-
value: level,
|
|
150
|
-
label: `${level} — ${THINKING_LEVEL_HINTS[level]}${tags.length ? ` (${tags.join(", ")})` : ""}`,
|
|
151
|
-
};
|
|
152
|
-
});
|
|
153
|
-
return promptSelectOne(
|
|
154
|
-
ctx,
|
|
155
|
-
`Thinking for ${agentName}?`,
|
|
156
|
-
`${agentName} defaults to ${roleDefault} on ${modelName} • Enter selects • Esc ${escNote}`,
|
|
157
|
-
options,
|
|
158
|
-
currentEffective,
|
|
159
|
-
) as Promise<ThinkingLevel | undefined>;
|
|
160
|
-
}
|
|
83
|
+
constructor(private readonly options: SetupOverlayOptions) {
|
|
84
|
+
this.state = cloneConfig(options.config);
|
|
85
|
+
this.agents = setupAgentNames(this.state);
|
|
86
|
+
this.state.knownAgents = [...new Set([...this.state.knownAgents, ...this.agents])];
|
|
87
|
+
}
|
|
161
88
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
enabledAgents: readonly string[],
|
|
165
|
-
): Promise<string | undefined> {
|
|
166
|
-
if (enabledAgents.length === 0) {
|
|
167
|
-
ctx.ui.notify("No agents are enabled. Enable agents first.", "warning");
|
|
168
|
-
return undefined;
|
|
89
|
+
get focused(): boolean {
|
|
90
|
+
return this._focused;
|
|
169
91
|
}
|
|
170
|
-
return promptSelectOne(
|
|
171
|
-
ctx,
|
|
172
|
-
"Configure which agent?",
|
|
173
|
-
"Name, then what it owns • ↑/↓ • Enter selects • Esc returns to settings",
|
|
174
|
-
enabledAgents.map((name) => {
|
|
175
|
-
const profile = agentProfile(name);
|
|
176
|
-
return {
|
|
177
|
-
value: name,
|
|
178
|
-
label: moduleLabel(name),
|
|
179
|
-
description: profile?.remark,
|
|
180
|
-
};
|
|
181
|
-
}),
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
92
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
93
|
+
set focused(value: boolean) {
|
|
94
|
+
this._focused = value;
|
|
95
|
+
if (this.modelPicker) this.modelPicker.focused = value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
render(width: number): string[] {
|
|
99
|
+
if (this.modelPicker) return this.modelPicker.render(width);
|
|
100
|
+
|
|
101
|
+
const { theme } = this.options;
|
|
102
|
+
const safeWidth = Math.max(0, width);
|
|
103
|
+
const fit = (line: string): string => truncateToWidth(line, safeWidth, "", true);
|
|
104
|
+
const border = theme.fg("border", "─".repeat(Math.max(1, safeWidth)));
|
|
105
|
+
const lines = [
|
|
106
|
+
border,
|
|
107
|
+
theme.fg("accent", theme.bold("pi-subagents setup")),
|
|
108
|
+
theme.fg("dim", "↑/↓ agent or action • ←/→ field • Enter/Space edit • Esc cancels without saving"),
|
|
109
|
+
border,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const maxVisibleAgents = safeWidth >= WIDE_LAYOUT_MIN_WIDTH ? 8 : 3;
|
|
113
|
+
const activeAgentRow = Math.min(this.row, this.agents.length - 1);
|
|
114
|
+
const start = Math.max(
|
|
115
|
+
0,
|
|
116
|
+
Math.min(activeAgentRow - Math.floor(maxVisibleAgents / 2), this.agents.length - maxVisibleAgents),
|
|
117
|
+
);
|
|
118
|
+
const end = Math.min(start + maxVisibleAgents, this.agents.length);
|
|
119
|
+
for (let index = start; index < end; index++) {
|
|
120
|
+
const name = this.agents[index]!;
|
|
121
|
+
const active = index === this.row;
|
|
122
|
+
const cursor = active ? theme.fg("accent", "❯ ") : " ";
|
|
123
|
+
const enabled = this.state.enabledAgents.includes(name);
|
|
124
|
+
const enabledText = this.cell(enabled ? "[x]" : "[ ]", active && this.field === 0);
|
|
125
|
+
const profile = agentProfile(name);
|
|
126
|
+
const nameText = profile ? `${name} — ${profile.summary}` : `${name} (custom)`;
|
|
127
|
+
const modelText = this.cell(`model: ${this.modelDisplay(name)}`, active && this.field === 1);
|
|
128
|
+
const thinkingText = this.cell(`thinking: ${this.thinkingDisplay(name)}`, active && this.field === 2);
|
|
190
129
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const profile = agentProfile(name);
|
|
199
|
-
if (profile) ctx.ui.notify(`${name}: ${profile.remark}`, "info");
|
|
200
|
-
|
|
201
|
-
while (true) {
|
|
202
|
-
const modelChoice = await pickAgentModel(
|
|
203
|
-
ctx,
|
|
204
|
-
name,
|
|
205
|
-
config.agentModels[name],
|
|
206
|
-
"returns to agent selection",
|
|
207
|
-
);
|
|
208
|
-
if (modelChoice === undefined) break;
|
|
209
|
-
const model = effectiveModelForChoice(ctx, modelChoice);
|
|
210
|
-
const strength = await pickAgentStrength(
|
|
211
|
-
ctx,
|
|
212
|
-
name,
|
|
213
|
-
model,
|
|
214
|
-
config.agentThinkingLevels[name],
|
|
215
|
-
"returns to model selection",
|
|
216
|
-
);
|
|
217
|
-
if (strength === undefined) continue;
|
|
218
|
-
return { name, model: modelChoice, strength };
|
|
130
|
+
if (safeWidth >= WIDE_LAYOUT_MIN_WIDTH) {
|
|
131
|
+
lines.push(`${cursor}${enabledText} ${nameText} │ ${modelText} │ ${thinkingText}`);
|
|
132
|
+
} else {
|
|
133
|
+
lines.push(`${cursor}${enabledText} ${nameText}`);
|
|
134
|
+
lines.push(` ${modelText}`);
|
|
135
|
+
lines.push(` ${thinkingText}`);
|
|
136
|
+
}
|
|
219
137
|
}
|
|
138
|
+
const range = start > 0 || end < this.agents.length
|
|
139
|
+
? `agents ${start + 1}-${end} of ${this.agents.length}`
|
|
140
|
+
: undefined;
|
|
141
|
+
const selectedName = this.agents[this.row];
|
|
142
|
+
const selectedProfile = selectedName ? agentProfile(selectedName) : undefined;
|
|
143
|
+
const context = [range, selectedProfile?.remark].filter(Boolean).join(" · ");
|
|
144
|
+
if (context) lines.push(theme.fg("dim", ` ${context}`));
|
|
145
|
+
lines.push(border);
|
|
146
|
+
lines.push(this.actionLine(this.agents.length, "Save & Exit", "persist all changes"));
|
|
147
|
+
lines.push(this.actionLine(this.agents.length + 1, "Cancel", "discard this session"));
|
|
148
|
+
lines.push(border);
|
|
149
|
+
return lines.map(fit);
|
|
220
150
|
}
|
|
221
|
-
}
|
|
222
151
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
152
|
+
handleInput(data: string): void {
|
|
153
|
+
if (this.modelPicker) {
|
|
154
|
+
this.modelPicker.handleInput(data);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
227
157
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
158
|
+
const { keybindings } = this.options;
|
|
159
|
+
const rowCount = this.agents.length + 2;
|
|
160
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
161
|
+
this.options.onDone(undefined);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (keybindings.matches(data, "tui.select.up")) {
|
|
165
|
+
this.row = (this.row - 1 + rowCount) % rowCount;
|
|
166
|
+
} else if (keybindings.matches(data, "tui.select.down")) {
|
|
167
|
+
this.row = (this.row + 1) % rowCount;
|
|
168
|
+
} else if (this.row < this.agents.length && keybindings.matches(data, "tui.editor.cursorLeft")) {
|
|
169
|
+
this.field = (this.field - 1 + FIELD_COUNT) % FIELD_COUNT;
|
|
170
|
+
} else if (this.row < this.agents.length && keybindings.matches(data, "tui.editor.cursorRight")) {
|
|
171
|
+
this.field = (this.field + 1) % FIELD_COUNT;
|
|
172
|
+
} else if (keybindings.matches(data, "tui.select.confirm") || data === " ") {
|
|
173
|
+
this.activateCurrent();
|
|
174
|
+
}
|
|
175
|
+
this.options.tui.requestRender();
|
|
176
|
+
}
|
|
238
177
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
);
|
|
248
|
-
const choice = await ctx.ui.select("How to configure pi-subagents", [
|
|
249
|
-
"Continue — pick a model for each role (thinking has a role default you can change later)",
|
|
250
|
-
]);
|
|
251
|
-
return choice !== undefined;
|
|
252
|
-
}
|
|
178
|
+
invalidate(): void {
|
|
179
|
+
this.modelPicker?.invalidate();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private cell(text: string, selected: boolean): string {
|
|
183
|
+
if (!selected) return text;
|
|
184
|
+
return this.options.theme.bg("selectedBg", this.options.theme.fg("accent", text));
|
|
185
|
+
}
|
|
253
186
|
|
|
254
|
-
|
|
255
|
-
|
|
187
|
+
private actionLine(row: number, label: string, description: string): string {
|
|
188
|
+
const active = this.row === row;
|
|
189
|
+
const cursor = active ? this.options.theme.fg("accent", "❯ ") : " ";
|
|
190
|
+
const text = active ? this.cell(label, true) : label;
|
|
191
|
+
return `${cursor}${text} ${this.options.theme.fg("dim", `— ${description}`)}`;
|
|
192
|
+
}
|
|
256
193
|
|
|
257
|
-
|
|
258
|
-
|
|
194
|
+
private activateCurrent(): void {
|
|
195
|
+
if (this.row === this.agents.length) {
|
|
196
|
+
this.options.onDone(cloneConfig(this.state));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (this.row === this.agents.length + 1) {
|
|
200
|
+
this.options.onDone(undefined);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
259
203
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
if (choice === undefined) return false;
|
|
266
|
-
agentModels = applyAgentModelChoice(agentModels, agentName, choice);
|
|
204
|
+
const name = this.agents[this.row];
|
|
205
|
+
if (!name) return;
|
|
206
|
+
if (this.field === 0) this.toggleEnabled(name);
|
|
207
|
+
else if (this.field === 1) this.openModelPicker(name);
|
|
208
|
+
else this.cycleThinking(name);
|
|
267
209
|
}
|
|
268
210
|
|
|
269
|
-
|
|
270
|
-
enabledAgents
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
};
|
|
277
|
-
await saveConfig(next, configPath);
|
|
278
|
-
ctx.ui.notify(
|
|
279
|
-
`pi-subagents saved to ${configPath}. Role thinking defaults apply; open Configure an agent to change one.`,
|
|
280
|
-
"info",
|
|
281
|
-
);
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
211
|
+
private toggleEnabled(name: string): void {
|
|
212
|
+
if (this.state.enabledAgents.includes(name)) {
|
|
213
|
+
this.state.enabledAgents = this.state.enabledAgents.filter((candidate) => candidate !== name);
|
|
214
|
+
} else {
|
|
215
|
+
this.state.enabledAgents = [...this.state.enabledAgents, name];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
284
218
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
219
|
+
private openModelPicker(name: string): void {
|
|
220
|
+
const explicitRef = this.state.agentModels[name];
|
|
221
|
+
const items = buildModelPickerItems({
|
|
222
|
+
models: this.options.models,
|
|
223
|
+
configuredRef: explicitRef,
|
|
224
|
+
mainRef: this.options.mainModel ? `${this.options.mainModel.provider}/${this.options.mainModel.id}` : undefined,
|
|
225
|
+
});
|
|
226
|
+
if (name === "sentinel") {
|
|
227
|
+
const inherited = this.state.agentModels.artisan ?? "current main model";
|
|
228
|
+
items[0] = {
|
|
229
|
+
value: CURRENT_MAIN_MODEL,
|
|
230
|
+
label: "Follow artisan (role default)",
|
|
231
|
+
description: `Clear the sentinel override; currently follows ${inherited}`,
|
|
232
|
+
};
|
|
296
233
|
}
|
|
297
234
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
235
|
+
const styles = makePickerStyles(this.options.theme);
|
|
236
|
+
this.modelPicker = new Picker(
|
|
237
|
+
items,
|
|
238
|
+
styles,
|
|
239
|
+
[
|
|
240
|
+
styles.title(`Model for ${name}`),
|
|
241
|
+
styles.hint("Type to filter • ↑/↓ move • Enter select • Esc return to table"),
|
|
242
|
+
],
|
|
243
|
+
this.options.tui,
|
|
244
|
+
this.options.keybindings,
|
|
245
|
+
{
|
|
246
|
+
onSelect: (choice) => {
|
|
247
|
+
this.state.agentModels = applyAgentModelChoice(this.state.agentModels, name, choice);
|
|
248
|
+
this.clampThinkingOverride(name);
|
|
249
|
+
if (name === "artisan" && this.state.agentModels.sentinel === undefined) {
|
|
250
|
+
this.clampThinkingOverride("sentinel");
|
|
251
|
+
}
|
|
252
|
+
this.closeModelPicker();
|
|
253
|
+
},
|
|
254
|
+
onCancel: () => this.closeModelPicker(),
|
|
255
|
+
},
|
|
256
|
+
explicitRef ?? CURRENT_MAIN_MODEL,
|
|
257
|
+
);
|
|
258
|
+
this.modelPicker.focused = this.focused;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private closeModelPicker(): void {
|
|
262
|
+
this.modelPicker = undefined;
|
|
263
|
+
this.options.tui.requestRender();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private effectiveModel(name: string): Model<Api> | undefined {
|
|
267
|
+
const explicitRef = this.state.agentModels[name];
|
|
268
|
+
const inheritedRef = name === "sentinel" && explicitRef === undefined ? this.state.agentModels.artisan : undefined;
|
|
269
|
+
return findModelByRef(this.options.models, explicitRef ?? inheritedRef) ?? this.options.mainModel;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private clampThinkingOverride(name: string): void {
|
|
273
|
+
const current = this.state.agentThinkingLevels[name];
|
|
274
|
+
if (current === undefined) return;
|
|
275
|
+
this.state.agentThinkingLevels[name] = resolveThinkingLevel(this.effectiveModel(name), current);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private cycleThinking(name: string): void {
|
|
279
|
+
const model = this.effectiveModel(name);
|
|
280
|
+
const roleDefault = resolveThinkingLevel(model, roleThinkingLevel(name));
|
|
281
|
+
const overrides = supportedThinkingLevels(model).filter((level) => level !== roleDefault);
|
|
282
|
+
const current = this.state.agentThinkingLevels[name];
|
|
283
|
+
let next: ThinkingLevel | undefined;
|
|
284
|
+
if (current === undefined) {
|
|
285
|
+
next = overrides[0];
|
|
309
286
|
} else {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const picked = await configureOneAgent(ctx, next);
|
|
313
|
-
if (!picked) break;
|
|
314
|
-
configuredAny = true;
|
|
315
|
-
next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
|
|
316
|
-
next.agentThinkingLevels = applyThinkingChoice(next.agentThinkingLevels, picked.name, picked.strength);
|
|
317
|
-
}
|
|
318
|
-
if (!configuredAny) continue;
|
|
319
|
-
await saveConfig(next, configPath);
|
|
320
|
-
ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
|
|
321
|
-
config = next;
|
|
322
|
-
continue;
|
|
287
|
+
const index = overrides.indexOf(current);
|
|
288
|
+
next = index < 0 || index === overrides.length - 1 ? undefined : overrides[index + 1];
|
|
323
289
|
}
|
|
290
|
+
if (next === undefined) delete this.state.agentThinkingLevels[name];
|
|
291
|
+
else this.state.agentThinkingLevels[name] = next;
|
|
292
|
+
}
|
|
324
293
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return;
|
|
294
|
+
private modelDisplay(name: string): string {
|
|
295
|
+
const explicitRef = this.state.agentModels[name];
|
|
296
|
+
if (explicitRef) return explicitRef;
|
|
297
|
+
if (name === "sentinel") return `follow artisan → ${this.state.agentModels.artisan ?? "current main"}`;
|
|
298
|
+
return "current main (dynamic)";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private thinkingDisplay(name: string): string {
|
|
302
|
+
const override = this.state.agentThinkingLevels[name];
|
|
303
|
+
if (override !== undefined) return `${resolveThinkingLevel(this.effectiveModel(name), override)} (override)`;
|
|
304
|
+
const roleDefault = resolveThinkingLevel(this.effectiveModel(name), roleThinkingLevel(name));
|
|
305
|
+
return `${roleDefault} (role default)`;
|
|
328
306
|
}
|
|
329
307
|
}
|
|
330
308
|
|
|
@@ -334,13 +312,32 @@ export async function runSetup(ctx: ExtensionCommandContext, configPath: string
|
|
|
334
312
|
ctx.ui.notify("/subagents-setup requires Pi's interactive TUI.", "error");
|
|
335
313
|
return;
|
|
336
314
|
}
|
|
315
|
+
|
|
337
316
|
try {
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
317
|
+
const config = await loadConfig(configPath, { persistNormalization: false });
|
|
318
|
+
const models = availableModelsInScope(ctx);
|
|
319
|
+
const result = await ctx.ui.custom<SetupResult>(
|
|
320
|
+
(tui, theme, keybindings, done) =>
|
|
321
|
+
new SetupOverlay({
|
|
322
|
+
config,
|
|
323
|
+
models,
|
|
324
|
+
mainModel: ctx.model,
|
|
325
|
+
theme,
|
|
326
|
+
tui,
|
|
327
|
+
keybindings,
|
|
328
|
+
onDone: done,
|
|
329
|
+
}),
|
|
330
|
+
{
|
|
331
|
+
overlay: true,
|
|
332
|
+
overlayOptions: { anchor: "center", width: "90%", minWidth: 36, maxHeight: "90%", margin: 1 },
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
if (result === undefined) {
|
|
336
|
+
ctx.ui.notify("pi-subagents setup cancelled; no changes saved.", "info");
|
|
337
|
+
return;
|
|
343
338
|
}
|
|
339
|
+
await saveConfig(result, configPath);
|
|
340
|
+
ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
|
|
344
341
|
} catch (error) {
|
|
345
342
|
ctx.ui.notify(`pi-subagents setup failed: ${errorMessage(error)}`, "error");
|
|
346
343
|
}
|
package/src/thread-lifecycle.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./agents.ts";
|
|
21
21
|
import { type CompletionMessageItem } from "./completion.ts";
|
|
22
22
|
import {
|
|
23
|
+
configuredModelForAgent,
|
|
23
24
|
loadConfig,
|
|
24
25
|
roleThinkingLevel,
|
|
25
26
|
type SubagentsConfig,
|
|
@@ -241,8 +242,9 @@ export function withWorktreeSystemPrompt(agent: AgentConfig): AgentConfig {
|
|
|
241
242
|
};
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
/** Sentinel must inspect the caller's uncommitted diff, never a detached copy. */
|
|
244
246
|
export function isWorktreeCapableAgent(agent: AgentConfig): boolean {
|
|
245
|
-
return isWriteCapableAgent(agent);
|
|
247
|
+
return agent.name !== "sentinel" && isWriteCapableAgent(agent);
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
export interface DispatchEnvironment {
|
|
@@ -311,7 +313,7 @@ export function resolveDispatchModelRoute(
|
|
|
311
313
|
const availableModels = availableModelsInScope(ctx);
|
|
312
314
|
const mainRef = currentModelRef(ctx);
|
|
313
315
|
const route = resolveAgentModelRoute({
|
|
314
|
-
selectedRef: config.agentModels
|
|
316
|
+
selectedRef: configuredModelForAgent(config.agentModels, agent.name),
|
|
315
317
|
mainRef,
|
|
316
318
|
availableRefs: availableModels.map(modelRef),
|
|
317
319
|
});
|