@ferris1225/pi-subagents 4.3.2 → 4.3.3

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/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 { promptSelectMany, promptSelectOne } from "./ui.ts";
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
- function moduleLabel(name: string): string {
62
- const profile = agentProfile(name);
63
- return profile ? `${name} — ${profile.summary}` : name;
64
- }
34
+ const FIELD_COUNT = 3;
35
+ const WIDE_LAYOUT_MIN_WIDTH = 112;
65
36
 
66
- async function configExists(configPath: string): Promise<boolean> {
67
- try {
68
- await stat(configPath);
69
- return true;
70
- } catch {
71
- return false;
72
- }
73
- }
37
+ type SetupResult = SubagentsConfig | undefined;
74
38
 
75
- async function pickEnabledAgents(
76
- ctx: ExtensionCommandContext,
77
- current: readonly string[],
78
- ): Promise<string[] | undefined> {
79
- const picked = await promptSelectMany(
80
- ctx,
81
- "Which agents should run?",
82
- "Each line is a role and its job. Space toggles • Enter confirms • Esc back",
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
- async function pickConfiguredModel(
90
- ctx: ExtensionCommandContext,
91
- title: string,
92
- configuredRef: string | undefined,
93
- escNote: string,
94
- ): Promise<string | undefined> {
95
- const models = availableModelsInScope(ctx);
96
- const items = buildModelPickerItems({
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
- async function pickAgentModel(
111
- ctx: ExtensionCommandContext,
112
- agentName: string,
113
- currentRef: string | undefined,
114
- escNote = "cancels this setup pass",
115
- ): Promise<string | undefined> {
116
- const profile = agentProfile(agentName);
117
- const duty = profile ? ` — ${profile.summary}` : "";
118
- return pickConfiguredModel(ctx, `Model for ${agentName}${duty}?`, currentRef, escNote);
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
- function effectiveModelForChoice(
122
- ctx: ExtensionCommandContext,
123
- choice: string,
124
- ): Model<Api> | undefined {
125
- if (choice === CURRENT_MAIN_MODEL) return ctx.model;
126
- return findModelByRef(availableModelsInScope(ctx), choice);
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
- /** Role default is the first row. Picking it clears a stored override. */
130
- async function pickAgentStrength(
131
- ctx: ExtensionCommandContext,
132
- agentName: string,
133
- model: Model<Api> | undefined,
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
- async function pickAgentToConfigure(
163
- ctx: ExtensionCommandContext,
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
- interface ConfiguredAgentChoice {
186
- name: string;
187
- model: string;
188
- strength: ThinkingLevel;
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
- async function configureOneAgent(
192
- ctx: ExtensionCommandContext,
193
- config: SubagentsConfig,
194
- ): Promise<ConfiguredAgentChoice | undefined> {
195
- while (true) {
196
- const name = await pickAgentToConfigure(ctx, config.enabledAgents);
197
- if (name === undefined) return undefined;
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
- function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string[]): Record<string, T> {
224
- const keep = new Set(enabled);
225
- return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
226
- }
152
+ handleInput(data: string): void {
153
+ if (this.modelPicker) {
154
+ this.modelPicker.handleInput(data);
155
+ return;
156
+ }
227
157
 
228
- function applyThinkingChoice(
229
- levels: Record<string, ThinkingLevel>,
230
- agentName: string,
231
- strength: ThinkingLevel,
232
- ): Record<string, ThinkingLevel> {
233
- const next = { ...levels };
234
- if (strength === roleThinkingLevel(agentName)) delete next[agentName];
235
- else next[agentName] = strength;
236
- return next;
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
- async function introduceSetup(ctx: ExtensionCommandContext): Promise<boolean> {
240
- const lines = BUILTIN_AGENT_NAMES.map((name) => {
241
- const profile = AGENT_PROFILES[name];
242
- return `${name} — ${profile.summary}. ${profile.remark}`;
243
- });
244
- ctx.ui.notify(
245
- `pi-subagents: ${lines.join(" ")} Pick a model for each role next. Thinking defaults per role (scout low, artisan high, steward medium); change it on a role when you want.`,
246
- "info",
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
- async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<boolean> {
255
- if (!(await introduceSetup(ctx))) return false;
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
- const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
258
- if (enabled === undefined) return false;
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
- let agentModels = keepAgentEntries(base.agentModels, enabled);
261
- for (const agentName of enabled) {
262
- const profile = agentProfile(agentName);
263
- if (profile) ctx.ui.notify(`${agentName}: ${profile.remark}`, "info");
264
- const choice = await pickAgentModel(ctx, agentName, agentModels[agentName]);
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
- const next: SubagentsConfig = {
270
- enabledAgents: enabled,
271
- agentModels,
272
- agentThinkingLevels: keepAgentEntries(base.agentThinkingLevels, enabled),
273
- maxResultLines: base.maxResultLines,
274
- agentScope: base.agentScope,
275
- idleTimeoutSec: base.idleTimeoutSec,
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
- async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
286
- while (true) {
287
- const choice = await ctx.ui.select("pi-subagents settings", [
288
- "Enable/disable agents — choose which roles are available",
289
- "Configure an agent — model and thinking, with its job on the row",
290
- "Full re-setup walk through the team and pick models again",
291
- ]);
292
- if (choice === undefined) return;
293
- if (choice.startsWith("Full")) {
294
- if (await runFullSetup(ctx, configPath, config)) return;
295
- continue;
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
- let next: SubagentsConfig = {
299
- ...config,
300
- agentModels: { ...config.agentModels },
301
- agentThinkingLevels: { ...config.agentThinkingLevels },
302
- };
303
- if (choice.startsWith("Enable")) {
304
- const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
305
- if (enabled === undefined) continue;
306
- next.enabledAgents = enabled;
307
- next.agentModels = keepAgentEntries(next.agentModels, enabled);
308
- next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
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
- let configuredAny = false;
311
- while (true) {
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
- await saveConfig(next, configPath);
326
- ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
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 exists = await configExists(configPath);
339
- const config = await loadConfig(configPath);
340
- if (exists) await runMenu(ctx, configPath, config);
341
- else if (!(await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] }))) {
342
- ctx.ui.notify("pi-subagents setup cancelled.", "info");
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
  }
@@ -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[agent.name],
316
+ selectedRef: configuredModelForAgent(config.agentModels, agent.name),
315
317
  mainRef,
316
318
  availableRefs: availableModels.map(modelRef),
317
319
  });