pi-orchestrator-config 4.2.0 → 4.3.2
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/README.md +76 -67
- package/agents/code-reviewer-docs.md +11 -1
- package/agents/code-reviewer-guidelines.md +11 -1
- package/agents/code-reviewer-quality.md +11 -1
- package/agents/code-reviewer-security.md +11 -1
- package/agents/code-reviewer-spec.md +11 -1
- package/agents/git-expert.md +42 -3
- package/agents/github-expert.md +11 -0
- package/extensions/acpx-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/agents/claude.ts +3 -2
- package/extensions/cli-provider/agents/cursor.ts +3 -5
- package/extensions/cli-provider/agents/gemini.ts +3 -2
- package/extensions/cli-provider/discover.ts +7 -7
- package/extensions/cli-provider/index.ts +5 -0
- package/extensions/cli-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/session-reaper.ts +5 -7
- package/extensions/cli-provider/sessions.ts +4 -0
- package/extensions/cli-provider/shared/discover-cache.ts +17 -176
- package/extensions/coms/coms-p2p.ts +1719 -423
- package/extensions/coms/coms-shared.ts +261 -39
- package/extensions/coms/coms-wrapper.ts +100 -62
- package/extensions/coms/index.ts +8 -4
- package/extensions/coms/mixed-turn.ts +26 -0
- package/extensions/coms/probe-socket.ts +42 -0
- package/extensions/coms/themeMap.ts +0 -1
- package/extensions/image-gen/image-gen.ts +19 -1
- package/extensions/orchestrator/async-agents.ts +563 -102
- package/extensions/orchestrator/async-capability.ts +8 -8
- package/extensions/orchestrator/cron.ts +15 -9
- package/extensions/orchestrator/dreaming.ts +32 -25
- package/extensions/orchestrator/enforcement-helpers.ts +459 -17
- package/extensions/orchestrator/enforcement-rules.ts +5 -4
- package/extensions/orchestrator/enforcement.ts +80 -27
- package/extensions/orchestrator/extended-autocomplete.ts +7 -0
- package/extensions/orchestrator/git-helpers.ts +2 -2
- package/extensions/orchestrator/index.ts +78 -10
- package/extensions/orchestrator/markdown-transformer.ts +313 -0
- package/extensions/orchestrator/memory-embeddings.ts +36 -34
- package/extensions/orchestrator/memory-provenance.ts +29 -25
- package/extensions/orchestrator/memory-scoring.ts +23 -18
- package/extensions/orchestrator/memory-tools.ts +10 -3
- package/extensions/orchestrator/overlay-dashboard.ts +17 -5
- package/extensions/orchestrator/parse-model-override.ts +29 -0
- package/extensions/orchestrator/pi-config-review-state.ts +57 -33
- package/extensions/orchestrator/project-settings.ts +333 -127
- package/extensions/orchestrator/resolve-agent-model.ts +24 -13
- package/extensions/orchestrator/review-porcelain.ts +4 -0
- package/extensions/orchestrator/review-ui.ts +63 -15
- package/extensions/orchestrator/rule-placeholders.ts +411 -2
- package/extensions/orchestrator/rules.ts +80 -132
- package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
- package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
- package/extensions/orchestrator/settings-tui.ts +636 -0
- package/extensions/orchestrator/state-jsonl.ts +431 -0
- package/extensions/orchestrator/subagent-tool.ts +157 -59
- package/extensions/orchestrator/sync-limit.ts +23 -0
- package/extensions/orchestrator/task-lifecycle.ts +77 -67
- package/extensions/orchestrator/utils.ts +8 -5
- package/extensions/pidash/pidash.ts +193 -156
- package/extensions/pidiff/pidiff.ts +96 -38
- package/extensions/pidiff/spawn-lock.ts +62 -0
- package/extensions/pitasks/index.ts +430 -0
- package/extensions/pitasks/reminders.ts +37 -0
- package/extensions/pitasks/task-store.ts +467 -0
- package/extensions/pitasks/task-tools.ts +375 -0
- package/extensions/pitasks/task-widget.ts +181 -0
- package/extensions/pitasks/types.ts +38 -0
- package/extensions/providers/acpx-driver.ts +12 -2
- package/extensions/providers/built-in-drivers.ts +6 -6
- package/extensions/providers/claude-driver.ts +1 -1
- package/extensions/providers/cursor-cli-driver.ts +1 -1
- package/extensions/providers/gemini-driver.ts +1 -1
- package/extensions/providers/index.ts +144 -26
- package/extensions/providers/initialized-guard.ts +27 -0
- package/extensions/providers/restore-default-model.ts +596 -0
- package/extensions/providers/session-shutdown.ts +39 -0
- package/extensions/shared/coms-active.ts +23 -0
- package/extensions/shared/create-runtime-provider.ts +2 -0
- package/extensions/shared/daemon-manager.ts +0 -8
- package/extensions/shared/file-logger.ts +132 -25
- package/extensions/shared/logger.ts +50 -0
- package/extensions/shared/models-dev.ts +421 -0
- package/extensions/shared/oneshot.ts +116 -0
- package/extensions/shared/resolve-binary.ts +191 -0
- package/extensions/shared/types.ts +3 -0
- package/package.json +9 -3
- package/prompts/coderabbit-rate-limit.md +2 -0
- package/rules/00-orchestrator-core.md +3 -0
- package/rules/10-agent-routing.md +12 -1
- package/rules/20-code-review-loop.md +52 -25
- package/rules/30-prompt-templates.md +4 -0
- package/rules/35-memory.md +3 -3
- package/rules/40-critical-rules.md +3 -0
- package/rules/55-coms-protocol.md +84 -18
- package/rules/60-task-tracking.md +2 -0
- package/settings-keys.json +563 -0
- package/templates/coms-feature-manager-prompt.md +4 -6
- package/extensions/coms/coms-net-server.ts +0 -1642
- package/extensions/coms/coms-net-wrapper.ts +0 -521
- package/extensions/coms/coms-net.ts +0 -1589
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings TUI submenu components — input, picker, multi-select, agent overrides.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
Input,
|
|
9
|
+
type SelectItem,
|
|
10
|
+
SelectList,
|
|
11
|
+
matchesKey,
|
|
12
|
+
Key,
|
|
13
|
+
truncateToWidth,
|
|
14
|
+
fuzzyFilter,
|
|
15
|
+
} from "@earendil-works/pi-tui";
|
|
16
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
17
|
+
import type { SettingsKeyDef } from "./project-settings.js";
|
|
18
|
+
|
|
19
|
+
// ── Input submenu component ─────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export class InputSubmenu implements Component {
|
|
22
|
+
private input: Input;
|
|
23
|
+
private label: string;
|
|
24
|
+
private hint: string;
|
|
25
|
+
private done: (value?: string) => void;
|
|
26
|
+
private theme: Theme;
|
|
27
|
+
|
|
28
|
+
constructor(label: string, currentValue: string, hint: string, theme: Theme, done: (value?: string) => void) {
|
|
29
|
+
this.label = label;
|
|
30
|
+
this.hint = hint;
|
|
31
|
+
this.done = done;
|
|
32
|
+
this.theme = theme;
|
|
33
|
+
this.input = new Input();
|
|
34
|
+
this.input.setValue(currentValue === "(empty)" ? "" : currentValue);
|
|
35
|
+
this.input.onSubmit = (val: string) => this.done(val);
|
|
36
|
+
this.input.onEscape = () => this.done(undefined);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render(width: number): string[] {
|
|
40
|
+
const t = this.theme;
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
lines.push(truncateToWidth(` ${t.fg("accent", t.bold(this.label))}`, width));
|
|
43
|
+
lines.push(truncateToWidth(` ${t.fg("dim", this.hint)}`, width));
|
|
44
|
+
lines.push("");
|
|
45
|
+
const inputLines = this.input.render(Math.max(10, width - 4));
|
|
46
|
+
for (const line of inputLines) {
|
|
47
|
+
lines.push(truncateToWidth(` ${line}`, width));
|
|
48
|
+
}
|
|
49
|
+
lines.push("");
|
|
50
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "Enter: save · Esc: cancel")}`, width));
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
handleInput(data: string): void {
|
|
55
|
+
this.input.handleInput(data);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
invalidate(): void {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Number input submenu ────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export class NumberInputSubmenu implements Component {
|
|
64
|
+
private input: Input;
|
|
65
|
+
private label: string;
|
|
66
|
+
private min?: number;
|
|
67
|
+
private max?: number;
|
|
68
|
+
private isInt: boolean;
|
|
69
|
+
private error: string;
|
|
70
|
+
private done: (value?: string) => void;
|
|
71
|
+
private theme: Theme;
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
label: string,
|
|
75
|
+
currentValue: string,
|
|
76
|
+
def: SettingsKeyDef,
|
|
77
|
+
theme: Theme,
|
|
78
|
+
done: (value?: string) => void,
|
|
79
|
+
) {
|
|
80
|
+
this.label = label;
|
|
81
|
+
this.min = def.min;
|
|
82
|
+
this.max = def.max;
|
|
83
|
+
this.isInt = def.type === "int" || def.type === "port";
|
|
84
|
+
this.error = "";
|
|
85
|
+
this.done = done;
|
|
86
|
+
this.theme = theme;
|
|
87
|
+
this.input = new Input();
|
|
88
|
+
this.input.setValue(currentValue);
|
|
89
|
+
this.input.onSubmit = (val: string) => this.validate(val);
|
|
90
|
+
this.input.onEscape = () => this.done(undefined);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private validate(val: string): void {
|
|
94
|
+
const trimmed = val.trim();
|
|
95
|
+
if (trimmed === "") {
|
|
96
|
+
this.done(trimmed);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const num = Number(trimmed);
|
|
100
|
+
if (!Number.isFinite(num)) {
|
|
101
|
+
this.error = "Invalid number";
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (this.isInt && !Number.isInteger(num)) {
|
|
105
|
+
this.error = "Must be an integer";
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (this.min !== undefined && num < this.min) {
|
|
109
|
+
this.error = `Minimum: ${this.min}`;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (this.max !== undefined && num > this.max) {
|
|
113
|
+
this.error = `Maximum: ${this.max}`;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.done(trimmed);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
render(width: number): string[] {
|
|
120
|
+
const t = this.theme;
|
|
121
|
+
const lines: string[] = [];
|
|
122
|
+
lines.push(truncateToWidth(` ${t.fg("accent", t.bold(this.label))}`, width));
|
|
123
|
+
|
|
124
|
+
const constraints: string[] = [];
|
|
125
|
+
if (this.min !== undefined) constraints.push(`min: ${this.min}`);
|
|
126
|
+
if (this.max !== undefined) constraints.push(`max: ${this.max}`);
|
|
127
|
+
if (this.isInt) constraints.push("integer");
|
|
128
|
+
if (constraints.length > 0) {
|
|
129
|
+
lines.push(truncateToWidth(` ${t.fg("dim", `(${constraints.join(", ")})`)}`, width));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.push("");
|
|
133
|
+
const inputLines = this.input.render(Math.max(10, width - 4));
|
|
134
|
+
for (const line of inputLines) {
|
|
135
|
+
lines.push(truncateToWidth(` ${line}`, width));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this.error) {
|
|
139
|
+
lines.push(truncateToWidth(` ${t.fg("error", `⚠ ${this.error}`)}`, width));
|
|
140
|
+
}
|
|
141
|
+
lines.push("");
|
|
142
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "Enter: save · Esc: cancel")}`, width));
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
handleInput(data: string): void {
|
|
147
|
+
this.error = "";
|
|
148
|
+
this.input.handleInput(data);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
invalidate(): void {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Picker submenu (SelectList + search Input) ──────────────────────
|
|
155
|
+
|
|
156
|
+
export class PickerSubmenu implements Component {
|
|
157
|
+
private searchInput: Input;
|
|
158
|
+
private selectList: SelectList;
|
|
159
|
+
private allItems: SelectItem[];
|
|
160
|
+
private label: string;
|
|
161
|
+
private done: (value?: string) => void;
|
|
162
|
+
private theme: Theme;
|
|
163
|
+
|
|
164
|
+
constructor(label: string, items: SelectItem[], theme: Theme, done: (value?: string) => void) {
|
|
165
|
+
this.label = label;
|
|
166
|
+
this.allItems = items;
|
|
167
|
+
this.done = done;
|
|
168
|
+
this.theme = theme;
|
|
169
|
+
|
|
170
|
+
this.searchInput = new Input();
|
|
171
|
+
this.selectList = new SelectList(items, Math.max(1, Math.min(items.length, 15)), getSelectListTheme());
|
|
172
|
+
this.selectList.onSelect = (item) => this.done(item.value);
|
|
173
|
+
this.selectList.onCancel = () => this.done(undefined);
|
|
174
|
+
|
|
175
|
+
const origHandleInput = this.searchInput.handleInput.bind(this.searchInput);
|
|
176
|
+
this.searchInput.handleInput = (data: string) => {
|
|
177
|
+
if (matchesKey(data, Key.escape)) {
|
|
178
|
+
this.done(undefined);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (matchesKey(data, Key.up) || matchesKey(data, Key.down) || matchesKey(data, Key.enter)) {
|
|
182
|
+
this.selectList.handleInput(data);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
origHandleInput(data);
|
|
186
|
+
this.applyFilter();
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private applyFilter(): void {
|
|
191
|
+
const query = this.searchInput.getValue().trim();
|
|
192
|
+
if (!query) {
|
|
193
|
+
this.selectList = new SelectList(this.allItems, Math.max(1, Math.min(this.allItems.length, 15)), getSelectListTheme());
|
|
194
|
+
} else {
|
|
195
|
+
const filtered = fuzzyFilter(this.allItems, query, (item) => `${item.label} ${item.description || ""}`);
|
|
196
|
+
this.selectList = new SelectList(filtered, Math.max(1, Math.min(filtered.length, 15)), getSelectListTheme());
|
|
197
|
+
}
|
|
198
|
+
this.selectList.onSelect = (item) => this.done(item.value);
|
|
199
|
+
this.selectList.onCancel = () => this.done(undefined);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
render(width: number): string[] {
|
|
203
|
+
const t = this.theme;
|
|
204
|
+
const lines: string[] = [];
|
|
205
|
+
lines.push(truncateToWidth(` ${t.fg("accent", t.bold(this.label))}`, width));
|
|
206
|
+
lines.push("");
|
|
207
|
+
const inputLines = this.searchInput.render(Math.max(10, width - 6));
|
|
208
|
+
for (const line of inputLines) {
|
|
209
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "🔍")} ${line}`, width));
|
|
210
|
+
}
|
|
211
|
+
lines.push("");
|
|
212
|
+
const listLines = this.selectList.render(Math.max(10, width - 4));
|
|
213
|
+
for (const line of listLines) {
|
|
214
|
+
lines.push(truncateToWidth(` ${line}`, width));
|
|
215
|
+
}
|
|
216
|
+
lines.push("");
|
|
217
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "↑↓ navigate · Enter select · Esc cancel · Type to filter")}`, width));
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
handleInput(data: string): void {
|
|
222
|
+
this.searchInput.handleInput(data);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
invalidate(): void {}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Multi-select submenu (agent list) ───────────────────────────────
|
|
229
|
+
|
|
230
|
+
export class MultiSelectSubmenu implements Component {
|
|
231
|
+
private agents: string[];
|
|
232
|
+
private selected: Set<string>;
|
|
233
|
+
private selectedIndex: number;
|
|
234
|
+
private done: (value?: string) => void;
|
|
235
|
+
private label: string;
|
|
236
|
+
private theme: Theme;
|
|
237
|
+
|
|
238
|
+
constructor(label: string, available: string[], current: string[], theme: Theme, done: (value?: string) => void) {
|
|
239
|
+
this.label = label;
|
|
240
|
+
this.agents = available;
|
|
241
|
+
this.selected = new Set(current);
|
|
242
|
+
this.selectedIndex = 0;
|
|
243
|
+
this.done = done;
|
|
244
|
+
this.theme = theme;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
render(width: number): string[] {
|
|
248
|
+
const t = this.theme;
|
|
249
|
+
const lines: string[] = [];
|
|
250
|
+
lines.push(truncateToWidth(` ${t.fg("accent", t.bold(this.label))}`, width));
|
|
251
|
+
lines.push("");
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < this.agents.length; i++) {
|
|
254
|
+
const agent = this.agents[i];
|
|
255
|
+
const checked = this.selected.has(agent)
|
|
256
|
+
? t.fg("success", "☑")
|
|
257
|
+
: t.fg("dim", "☐");
|
|
258
|
+
const cursor = i === this.selectedIndex
|
|
259
|
+
? t.fg("accent", "❯")
|
|
260
|
+
: " ";
|
|
261
|
+
const name = i === this.selectedIndex
|
|
262
|
+
? t.fg("accent", agent)
|
|
263
|
+
: t.fg("text", agent);
|
|
264
|
+
lines.push(truncateToWidth(` ${cursor} ${checked} ${name}`, width));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (this.agents.length === 0) {
|
|
268
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "(no agents found)")}`, width));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
lines.push("");
|
|
272
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "↑↓ navigate · Space toggle · Enter save · Esc cancel")}`, width));
|
|
273
|
+
return lines;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
handleInput(data: string): void {
|
|
277
|
+
if (matchesKey(data, Key.escape)) {
|
|
278
|
+
this.done(undefined);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (matchesKey(data, Key.enter)) {
|
|
282
|
+
const result = [...this.selected].sort().join(", ");
|
|
283
|
+
this.done(result || "(empty)");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (matchesKey(data, Key.up) && this.selectedIndex > 0) {
|
|
287
|
+
this.selectedIndex--;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (matchesKey(data, Key.down) && this.selectedIndex < this.agents.length - 1) {
|
|
291
|
+
this.selectedIndex++;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (matchesKey(data, Key.space) || data === " ") {
|
|
295
|
+
const agent = this.agents[this.selectedIndex];
|
|
296
|
+
if (agent) {
|
|
297
|
+
if (this.selected.has(agent)) {
|
|
298
|
+
this.selected.delete(agent);
|
|
299
|
+
} else {
|
|
300
|
+
this.selected.add(agent);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
invalidate(): void {}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Select submenu (enum values) ────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
export class SelectSubmenu implements Component {
|
|
312
|
+
private options: string[];
|
|
313
|
+
private selectedIndex: number;
|
|
314
|
+
private label: string;
|
|
315
|
+
private done: (value?: string) => void;
|
|
316
|
+
private theme: Theme;
|
|
317
|
+
|
|
318
|
+
constructor(label: string, options: string[], currentValue: string, theme: Theme, done: (value?: string) => void) {
|
|
319
|
+
this.label = label;
|
|
320
|
+
this.options = options;
|
|
321
|
+
this.selectedIndex = Math.max(0, options.indexOf(currentValue));
|
|
322
|
+
this.done = done;
|
|
323
|
+
this.theme = theme;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
render(width: number): string[] {
|
|
327
|
+
const t = this.theme;
|
|
328
|
+
const lines: string[] = [];
|
|
329
|
+
lines.push(truncateToWidth(` ${t.fg("accent", t.bold(this.label))}`, width));
|
|
330
|
+
lines.push("");
|
|
331
|
+
|
|
332
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
333
|
+
const opt = this.options[i];
|
|
334
|
+
const cursor = i === this.selectedIndex ? t.fg("accent", "❯") : " ";
|
|
335
|
+
const label = i === this.selectedIndex ? t.fg("accent", opt) : t.fg("text", opt);
|
|
336
|
+
lines.push(truncateToWidth(` ${cursor} ${label}`, width));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
lines.push("");
|
|
340
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "↑↓ navigate · Enter select · Esc cancel")}`, width));
|
|
341
|
+
return lines;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
handleInput(data: string): void {
|
|
345
|
+
if (matchesKey(data, Key.escape)) {
|
|
346
|
+
this.done(undefined);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (matchesKey(data, Key.enter)) {
|
|
350
|
+
this.done(this.options[this.selectedIndex]);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (matchesKey(data, Key.up) && this.selectedIndex > 0) {
|
|
354
|
+
this.selectedIndex--;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (matchesKey(data, Key.down) && this.selectedIndex < this.options.length - 1) {
|
|
358
|
+
this.selectedIndex++;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
invalidate(): void {}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Agent overrides editor ──────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
export class AgentOverridesSubmenu implements Component {
|
|
368
|
+
private overrides: Record<string, { provider?: string | null; model?: string | null }>;
|
|
369
|
+
private agentNames: string[];
|
|
370
|
+
private selectedIndex: number;
|
|
371
|
+
private done: (value?: string) => void;
|
|
372
|
+
private label: string;
|
|
373
|
+
private theme: Theme;
|
|
374
|
+
private mode: "list" | "edit-agent" | "edit-field" | "edit-picker";
|
|
375
|
+
private editingAgent: string;
|
|
376
|
+
private editFieldIndex: number; // 0=provider, 1=model
|
|
377
|
+
private editInput: Input;
|
|
378
|
+
private pickerSubmenu: PickerSubmenu | null;
|
|
379
|
+
private providerItems: SelectItem[];
|
|
380
|
+
private modelItems: SelectItem[];
|
|
381
|
+
private searchInput: Input;
|
|
382
|
+
private filteredAgents: string[];
|
|
383
|
+
|
|
384
|
+
constructor(
|
|
385
|
+
label: string,
|
|
386
|
+
currentOverrides: Record<string, { provider?: string | null; model?: string | null }>,
|
|
387
|
+
availableAgents: string[],
|
|
388
|
+
theme: Theme,
|
|
389
|
+
done: (value?: string) => void,
|
|
390
|
+
providerItems: SelectItem[],
|
|
391
|
+
modelItems: SelectItem[],
|
|
392
|
+
) {
|
|
393
|
+
this.label = label;
|
|
394
|
+
this.overrides = JSON.parse(JSON.stringify(currentOverrides || {}));
|
|
395
|
+
this.agentNames = availableAgents;
|
|
396
|
+
this.selectedIndex = 0;
|
|
397
|
+
this.done = done;
|
|
398
|
+
this.theme = theme;
|
|
399
|
+
this.mode = "list";
|
|
400
|
+
this.editingAgent = "";
|
|
401
|
+
this.editFieldIndex = 0;
|
|
402
|
+
this.editInput = new Input();
|
|
403
|
+
this.pickerSubmenu = null;
|
|
404
|
+
this.providerItems = providerItems;
|
|
405
|
+
this.modelItems = modelItems;
|
|
406
|
+
this.searchInput = new Input();
|
|
407
|
+
this.filteredAgents = availableAgents;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
render(width: number): string[] {
|
|
411
|
+
const t = this.theme;
|
|
412
|
+
const lines: string[] = [];
|
|
413
|
+
lines.push(truncateToWidth(` ${t.fg("accent", t.bold(this.label))}`, width));
|
|
414
|
+
lines.push("");
|
|
415
|
+
|
|
416
|
+
if (this.mode === "list") {
|
|
417
|
+
// Search input
|
|
418
|
+
const searchLines = this.searchInput.render(Math.max(10, width - 6));
|
|
419
|
+
for (const sl of searchLines) {
|
|
420
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "🔍")} ${sl}`, width));
|
|
421
|
+
}
|
|
422
|
+
lines.push("");
|
|
423
|
+
|
|
424
|
+
for (let i = 0; i < this.filteredAgents.length; i++) {
|
|
425
|
+
const agent = this.filteredAgents[i];
|
|
426
|
+
const override = this.overrides[agent];
|
|
427
|
+
const cursor = i === this.selectedIndex ? t.fg("accent", "❯") : " ";
|
|
428
|
+
const name = i === this.selectedIndex ? t.fg("accent", agent) : t.fg("text", agent);
|
|
429
|
+
let info = t.fg("dim", "(default)");
|
|
430
|
+
if (override) {
|
|
431
|
+
const parts: string[] = [];
|
|
432
|
+
if (override.provider !== undefined) {
|
|
433
|
+
parts.push(`provider: ${t.fg("success", String(override.provider ?? "parent"))}`);
|
|
434
|
+
}
|
|
435
|
+
if (override.model !== undefined) {
|
|
436
|
+
parts.push(`model: ${t.fg("success", String(override.model ?? "parent"))}`);
|
|
437
|
+
}
|
|
438
|
+
info = parts.join(t.fg("dim", ", ")) || t.fg("dim", "(default)");
|
|
439
|
+
}
|
|
440
|
+
lines.push(truncateToWidth(` ${cursor} ${name}${t.fg("dim", ":")} ${info}`, width));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (this.filteredAgents.length === 0) {
|
|
444
|
+
lines.push(truncateToWidth(` ${t.fg("dim", this.agentNames.length === 0 ? "(no agents found)" : "(no matches)")}`, width));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
lines.push("");
|
|
448
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "Type to filter · ↑↓ navigate · Enter edit · d delete · Esc close")}`, width));
|
|
449
|
+
|
|
450
|
+
} else if (this.mode === "edit-agent") {
|
|
451
|
+
const agent = this.editingAgent;
|
|
452
|
+
const override = this.overrides[agent] || {};
|
|
453
|
+
const providerVal = override.provider === null ? "null (use parent)" : override.provider || "(not set)";
|
|
454
|
+
const modelVal = override.model === null ? "null (use parent)" : override.model || "(not set)";
|
|
455
|
+
|
|
456
|
+
lines.push(truncateToWidth(` ${t.fg("text", "Agent:")} ${t.fg("accent", agent)}`, width));
|
|
457
|
+
lines.push("");
|
|
458
|
+
|
|
459
|
+
const fields = [
|
|
460
|
+
{ label: "Provider", value: providerVal },
|
|
461
|
+
{ label: "Model", value: modelVal },
|
|
462
|
+
];
|
|
463
|
+
for (let i = 0; i < fields.length; i++) {
|
|
464
|
+
const cursor = i === this.editFieldIndex ? t.fg("accent", "❯") : " ";
|
|
465
|
+
const label = i === this.editFieldIndex ? t.fg("accent", fields[i].label) : t.fg("text", fields[i].label);
|
|
466
|
+
const value = t.fg("muted", fields[i].value);
|
|
467
|
+
lines.push(truncateToWidth(` ${cursor} ${label}${t.fg("dim", ":")} ${value}`, width));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
lines.push("");
|
|
471
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "↑↓ select field · Enter edit · d delete both · Esc back")}`, width));
|
|
472
|
+
|
|
473
|
+
} else if (this.mode === "edit-picker" && this.pickerSubmenu) {
|
|
474
|
+
return this.pickerSubmenu.render(width);
|
|
475
|
+
|
|
476
|
+
} else {
|
|
477
|
+
// edit-field mode
|
|
478
|
+
const field = this.editFieldIndex === 0 ? "provider" : "model";
|
|
479
|
+
lines.push(truncateToWidth(` ${t.fg("text", `Set ${field} for:`)} ${t.fg("accent", this.editingAgent)}`, width));
|
|
480
|
+
const fieldName = this.editFieldIndex === 0 ? "provider" : "model";
|
|
481
|
+
lines.push(truncateToWidth(` ${t.fg("dim", `Enter ${fieldName} (empty to clear, "null" to use parent)`)}`, width));
|
|
482
|
+
lines.push("");
|
|
483
|
+
const inputLines = this.editInput.render(Math.max(10, width - 4));
|
|
484
|
+
for (const line of inputLines) {
|
|
485
|
+
lines.push(truncateToWidth(` ${line}`, width));
|
|
486
|
+
}
|
|
487
|
+
lines.push("");
|
|
488
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "Enter: save · Esc: cancel")}`, width));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return lines;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
handleInput(data: string): void {
|
|
495
|
+
if (this.mode === "edit-picker" && this.pickerSubmenu) {
|
|
496
|
+
this.pickerSubmenu.handleInput(data);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (this.mode === "edit-field") {
|
|
501
|
+
if (matchesKey(data, Key.escape)) {
|
|
502
|
+
this.mode = "edit-agent";
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.editInput.handleInput(data);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (this.mode === "edit-agent") {
|
|
510
|
+
if (matchesKey(data, Key.escape)) {
|
|
511
|
+
this.mode = "list";
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (matchesKey(data, Key.up)) {
|
|
515
|
+
this.editFieldIndex = 0;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (matchesKey(data, Key.down)) {
|
|
519
|
+
this.editFieldIndex = 1;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (matchesKey(data, Key.enter)) {
|
|
523
|
+
const field = this.editFieldIndex === 0 ? "provider" : "model";
|
|
524
|
+
let items = this.editFieldIndex === 0 ? this.providerItems : this.modelItems;
|
|
525
|
+
// When editing model, filter by agent's provider override if set
|
|
526
|
+
if (this.editFieldIndex === 1) {
|
|
527
|
+
const agentProvider = this.overrides[this.editingAgent]?.provider;
|
|
528
|
+
if (agentProvider && typeof agentProvider === "string") {
|
|
529
|
+
const filtered = this.modelItems.filter((m) => m.description === agentProvider);
|
|
530
|
+
if (filtered.length > 0) items = filtered;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (items.length > 0) {
|
|
534
|
+
// Use fuzzy picker for provider/model
|
|
535
|
+
this.pickerSubmenu = new PickerSubmenu(
|
|
536
|
+
`Select ${field} for ${this.editingAgent}`,
|
|
537
|
+
items,
|
|
538
|
+
this.theme,
|
|
539
|
+
(val?: string) => {
|
|
540
|
+
if (val !== undefined) {
|
|
541
|
+
this.applyOverride(this.editingAgent, field, val);
|
|
542
|
+
}
|
|
543
|
+
this.pickerSubmenu = null;
|
|
544
|
+
this.mode = "edit-agent";
|
|
545
|
+
},
|
|
546
|
+
);
|
|
547
|
+
this.mode = "edit-picker";
|
|
548
|
+
} else {
|
|
549
|
+
// Fallback to text input if no items available
|
|
550
|
+
const current = this.overrides[this.editingAgent]?.[field];
|
|
551
|
+
this.editInput = new Input();
|
|
552
|
+
this.editInput.setValue(current === null ? "null" : current ?? "");
|
|
553
|
+
this.editInput.onSubmit = (val: string) => {
|
|
554
|
+
this.applyOverride(this.editingAgent, field, val);
|
|
555
|
+
this.mode = "edit-agent";
|
|
556
|
+
};
|
|
557
|
+
this.editInput.onEscape = () => { this.mode = "edit-agent"; };
|
|
558
|
+
this.mode = "edit-field";
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (data === "d") {
|
|
563
|
+
delete this.overrides[this.editingAgent];
|
|
564
|
+
this.mode = "list";
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// list mode
|
|
571
|
+
if (matchesKey(data, Key.escape)) {
|
|
572
|
+
if (this.searchInput.getValue()) {
|
|
573
|
+
// Clear search first
|
|
574
|
+
this.searchInput.setValue("");
|
|
575
|
+
this.filteredAgents = this.agentNames;
|
|
576
|
+
this.selectedIndex = 0;
|
|
577
|
+
} else {
|
|
578
|
+
// Save & close
|
|
579
|
+
this.done(JSON.stringify(this.overrides));
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (matchesKey(data, Key.enter)) {
|
|
584
|
+
const agent = this.filteredAgents[this.selectedIndex];
|
|
585
|
+
if (agent) {
|
|
586
|
+
this.editingAgent = agent;
|
|
587
|
+
this.editFieldIndex = 0;
|
|
588
|
+
this.mode = "edit-agent";
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (matchesKey(data, Key.up)) {
|
|
593
|
+
if (this.filteredAgents.length > 0) {
|
|
594
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.filteredAgents.length - 1 : this.selectedIndex - 1;
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (matchesKey(data, Key.down)) {
|
|
599
|
+
if (this.filteredAgents.length > 0) {
|
|
600
|
+
this.selectedIndex = this.selectedIndex === this.filteredAgents.length - 1 ? 0 : this.selectedIndex + 1;
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (data === "d") {
|
|
605
|
+
const agent = this.filteredAgents[this.selectedIndex];
|
|
606
|
+
if (agent) delete this.overrides[agent];
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// All other keys go to search input
|
|
610
|
+
this.searchInput.handleInput(data);
|
|
611
|
+
const query = this.searchInput.getValue().trim();
|
|
612
|
+
if (!query) {
|
|
613
|
+
this.filteredAgents = this.agentNames;
|
|
614
|
+
} else {
|
|
615
|
+
this.filteredAgents = this.agentNames.filter((name) =>
|
|
616
|
+
name.toLowerCase().includes(query.toLowerCase()),
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
this.selectedIndex = 0;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private applyOverride(agent: string, field: "provider" | "model", val: string): void {
|
|
623
|
+
const trimmed = val.trim();
|
|
624
|
+
if (!this.overrides[agent]) this.overrides[agent] = {};
|
|
625
|
+
|
|
626
|
+
if (trimmed === "") {
|
|
627
|
+
delete this.overrides[agent]![field];
|
|
628
|
+
if (Object.keys(this.overrides[agent]!).length === 0) {
|
|
629
|
+
delete this.overrides[agent];
|
|
630
|
+
}
|
|
631
|
+
} else if (trimmed === "null") {
|
|
632
|
+
this.overrides[agent]![field] = null;
|
|
633
|
+
} else {
|
|
634
|
+
this.overrides[agent]![field] = trimmed;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Ensure provider/model consistency
|
|
638
|
+
if (this.overrides[agent]) {
|
|
639
|
+
if (field === "provider" && trimmed !== "" && trimmed !== "null") {
|
|
640
|
+
// Provider changed to a real value — clear model to prevent stale cross-provider model
|
|
641
|
+
this.overrides[agent]!.model = null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
invalidate(): void {}
|
|
647
|
+
}
|